七大原則:
- 單一職責原則;
- 接口隔離原則;
- 依賴倒轉原則;
- 里氏替換原則;
- 開閉原則ocp;
- 迪米特法則;
- 合成復用原則。
設計模式其實包含了面向對象的精髓,封裝、繼承、多態。
一、單一職責原則
對於類來說,一個類應該只負責一項職責。
假設A負責兩個不同的職責1和2,如果1的內容需要改變,影響了2,那可能2會執行錯誤,所以需要將A分為兩個類。
1.1 示例
public class SingleResponsibility1 {
public static void main(String[] args) {
Vehicle vehicle = new Vehicle();
vehicle.run("汽車");
vehicle.run("摩托車");
vehicle.run("飛機");
}
}
class Vehicle{
public void run(String vehicle){
System.out.println(vehicle+"在地上跑");
}
}
對於一個完成交通工具的類Vehicle來說,顯然對不同的對象、汽車和飛機,提供的服務不應該都是在地上跑,並且修改之后,肯定會影響到其中一類對象的功能,所以按照單一職責,那就應該拆開,成兩個類。
1.2 改進版本 1
public class SingleResponsibility2 {
public static void main(String[] args) {
RoadVehicle roadVehicle = new RoadVehicle();
roadVehicle.run("摩托車");
roadVehicle.run("汽車");
AirVehicle airVehicle = new AirVehicle();
airVehicle.run("飛機");
}
}
class RoadVehicle{
public void run(String vehicle){
System.out.println(vehicle+"在地上跑");
}
}
class AirVehicle{
public void run(String vehicle){
System.out.println(vehicle+"在天上飛");
}
}
但是這么寫又有了新的問題:那就是類分解的時候,客戶端代碼也要改,調用方式改了。因此可以直接更改本來的Vehicle類,變成我們的第三種寫法.
1.3 改進版本 2
public class SingleResponsibility3 {
public static void main(String[] args) {
Vehicle2 vehicle2 = new Vehicle2();
vehicle2.runRoad("汽車");
vehicle2.runWater("輪船");
vehicle2.runAir("飛機");
}
}
class Vehicle2{
public void runRoad(String vehicle){
System.out.println(vehicle+"在公路上跑");
}
public void runAir(String vehicle){
System.out.println(vehicle+"在天上飛");
}
public void runWater(String vehicle){
System.out.println(vehicle+"在水里游");
}
}
這種寫法,顯然更加方便,相比第一種,沒有更改類的聲明方式,只是在類內部增加了方法的各司其職,可以看出來,雖然沒有在類級別上嚴格遵循單一職責原則,但是在方法級別上嚴格遵循了單一職責原則,相比之下比方法2更合適。
1.4 總結單一職責原則
- 降低類的復雜度,一個類只負責一項職責(上面的例子因為過於簡單,所以看起來第三個寫法更有效);
- 提高類的可讀性、可維護性,降低變更帶來的風險;
- 通常情況下,我們應該遵守這個職責,只有在邏輯足夠簡單的時候,才可以在代碼級別違反這個原則,也就是上面的,改為在方法級別保持單一職責原則。
二、接口隔離原則
接口隔離(Interface Segregation Principle)
意思是說,如果某一個類對另一個類的依賴通過的是接口,那么這個類對另一個類的依賴應該建立在最小的接口上,如果不是最小接口,則需要拆。
2.1 示例
例如如下的用例圖:
A 和 B 是 Interface1 的實現類,所以必須實現它的 4 個方法,C 和 D 分別依賴於這個接口,使用 A 和 B 里面對應的方法,但不是全部,C 使用前三個方法,D 使用后三個方法,如果按照類圖實現,代碼會如下所示:
interface Interface1{
void fuc1();
void fuc2();
void fuc3();
void fuc4();
}
class A implements Interface1{
public void fuc1() {System.out.println("A實現了fuc1");}
public void fuc2() {System.out.println("A實現了fuc2");}
public void fuc3() {System.out.println("A實現了fuc3");}
public void fuc4() {System.out.println("A實現了fuc4");}
}
class B implements Interface1{
public void fuc1() {System.out.println("B實現了fuc1");}
public void fuc2() {System.out.println("B實現了fuc2");}
public void fuc3() {System.out.println("B實現了fuc3");}
public void fuc4() {System.out.println("B實現了fuc4");}
}
//C通過接口Interface1依賴,使用A這個實現類,但是只用A的前兩個方法
class C {
public void depend1(Interface1 i){
i.fuc1();
}
public void depend2(Interface1 i){
i.fuc2();
}
public void depend3(Interface1 i){
i.fuc3();
}
}
//D通過接口Interface1依賴,使用B這個實現類,但是只用B的后兩個方法
class D {
public void depend2(Interface1 i){
i.fuc2();
}
public void depend3(Interface1 i){
i.fuc3();
}
public void depend4(Interface1 i){
i.fuc4();
}
}
顯然,A 和 B 兩個實現類都做了多余的工作,也就是 C 和 D 依賴的這個接口有些方法是他們不需要的,這個接口寫的不好。
2.2 改進
根據接口隔離原則,我們應該將其拆成滿足最小接口的類型,也就是說多余的我們全都不應要,所以接口 Interface1 應該拆分為三個接口,接口 1 里面有方法 1 ,接口 2 里面有方法 23,接口 3 里面有方法 4,這樣實現的時候A和B兩個類就更加清晰,修改后的類圖如下:
那么,這樣 A 和 B 實現的方法就是需要的方法,不會有多余的方法,C 和 D 的依賴也就更加清楚,滿足 最小接口。
interface Interface11{
void fuc1();
}
interface Interface12{
void fuc2();
void fuc3();
}
interface Interface13{
void fuc4();
}
class A1 implements Interface11,Interface12{
public void fuc1() {System.out.println("A實現了接口1的fuc1");}
public void fuc2() {System.out.println("A實現了接口2的fuc2");}
public void fuc3() {System.out.println("A實現了接口2的fuc3");}
}
class B1 implements Interface12,Interface13{
public void fuc2() {System.out.println("B實現了接口2的fuc2");}
public void fuc3() {System.out.println("B實現了接口2的fuc3");}
public void fuc4() {System.out.println("B實現了接口3的fuc4");}
}
class C1{
public void depend1(Interface11 i){
i.fuc1();
}
public void depend2(Interface12 i){
i.fuc2();
}
public void depend3(Interface12 i){
i.fuc3();
}
}
class D1{
public void depend2(Interface12 i){
i.fuc2();
}
public void depend3(Interface12 i){
i.fuc3();
}
public void depend4(Interface13 i){
i.fuc4();
}
}
這種寫法就是遵循了接口隔離原則
客戶端使用的時候:
C1 c = new C1();
c.depend1(new A1());//C類通過接口依賴(使用)的是A類
c.depend2(new A1());
c.depend3(new A1());
D1 d = new D1();
d.depend2(new B1());//D類通過接口依賴(使用)的是B類
d.depend3(new B1());
d.depend4(new B1());
三、依賴倒轉原則
依賴倒轉原則 ( Dependence Inversion Principle ) 指的是:
- 高層模塊不應該依賴低層模塊,二者都應該依賴其抽象;
- 抽象不應該依賴細節,細節應該依賴抽象;
- 依賴倒轉的核心思想是面向接口編程。
為什么要有依賴倒轉原則:主要是因為,相對於細節的多變,抽象的東西要穩定的多,以抽象為基礎搭建的架構比以細節為基礎的架構穩定的多,在java種,抽象指的就是接口或者抽象類,細節就是具體的實現類,抽象類制定好規范,展現細節的任務交給實現類去做。
依賴倒轉原則需要注意:
- 底層模塊盡量都要有抽象類或接口,或者兩者都有,程序的穩定性會更好;
- 變量的聲明類型盡量是抽象類或接口,這樣我們的變量引用和實際對象之間,就存在一個緩沖層,利於程序擴展和優化;
- 繼承時遵循里氏替換原則。
例如:有一個功能,一個Person類,要有一個接收消息的功能。
3.1 示例
public class DependencyInversion1 {
public static void main(String[] args) {
Person person = new Person();
person.receive(new Email());
}
}
class Email{
public String getInfo(){
return "電子郵件信息";
}
}
class Person{
public void receive(Email email){
System.out.println(email.getInfo());
}
}
因為Person類需要的接受是一個功能,消息應該是另一個類,所以還有一個Email類。那么這種寫法,顯然直接犯在Person類里依賴了Email類,也就是上面所說的”高層模塊依賴底層模塊“。
那么這么寫可能會帶來哪些問題呢?
對於Person類,接受的這個動作可以擴展,如果要接受的不僅僅是Email,是很多短信、微信等信息,那么在新增短信、微信類的同時,Person類里要新增對應的方法,改動基本是所有的都改動。
按照依賴倒轉原則,對於Email、短信這些不同的類,應該將他們抽象出一個接口,然后他們實現接口,這樣的話,對於Person類,他的接受方法,就是對這個抽象接口的依賴,而不是直接依賴這些不同的實現類。
3.2 改進
public class Dependency1 {
public static void main(String[] args) {
Person1 person1 = new Person1();
person1.receive(new Email1());
}
}
interface IReceiver{
public String getInfo();
}
class Email1 implements IReceiver{
public String getInfo() {
return "電子郵件信息:";
}
}
class Person1{
public void receive(IReceiver receiver){
System.out.println(receiver.getInfo());
}
}
在客戶端的調用方式也是幾乎一樣的,運行結果是一樣的,同時,因為Person依賴的是抽象的接口而不是具體的Email。
當我們想要增加一個接受的類的時候,平行增加,同時都去實現接口里面的方法就可以,Person類不用做改變,那么調用的時候傳入參數去變成具體的實現類就可以。
//擴展
class Wechat implements IReceiver{
public String getInfo() {
return "微信消息:";
}
}
那么main方法的客戶端只需要:
person1.receive(new Wechat());
就可以。
3.3 擴展:依賴關系傳遞的三種方式
依賴關系的傳遞一般有三種方式:
- 接口傳遞;
- 構造方法傳遞;
- setter方法傳遞。
第一種:
//方式1:通過接口傳遞依賴
interface IOpenandClose{
public void open(ITV itv);
}
interface ITV{
public void play();
}
class OpenandClose implements IOpenandClose{
public void open(ITV itv) {
itv.play();
}
}
可以看到,因為第一個接口依賴於第二個接口,那么實現第一個接口的時候,就需要實現對應的方法,把接口作為參數,實現了依賴的傳遞。
第二種:
//方式2,通過構造方法傳遞依賴
interface IOpenandClose2{
public void open();
}
interface ITV2{
public void play();
}
class OpenandClose2 implements IOpenandClose2{
public ITV2 itv2;
public OpenandClose2(ITV2 itv2){
this.itv2 = itv2;
}
public void open() {
this.itv2.play();
}
}
通第一個接口和第二個接口雖然沒有直接寫明依賴,但是依賴體現在實現類里,實現類里通過構造方法傳入一個參數,才能進行open方法的實現,所以在構造方法的部分體現了依賴關系。
第三種:
//方式3,通過set方法傳遞依賴
interface IOpenandClose3{
public void open();
public void setTV(ITV3 itv3);
}
interface ITV3{
public void play();
}
class OpenandClose3 implements IOpenandClose3{
private ITV3 itv3;
public void setTV(ITV3 itv3) {
this.itv3 = itv3;
}
public void open() {
this.itv3.play();
}
}
其實和第一種類似,不過沒有在open的地方直接依賴,而是分成兩個步驟,先給聲明的ITV初始化,再進行使用,這樣依賴也就傳遞成功了。
使用三種示例的方法,我們都用到一個ITV的實現類,實現一個play方法,然后調用開關這個類,體現開關接口的依賴性。比如第一種是
class Sumsang implements ITV{
public void play() {
System.out.println("三星電視開機啦");
}
}
然后在主方法里調用就是:
OpenandClose close = new OpenandClose();
close.open(new Sumsang());
因為使用開關機的時候,這個方法就是通過接口參數才能調用,所以這是第一種接口傳遞。
第二種就只用:
OpenandClose2 close2 = new OpenandClose2(new Sony());
close2.open();
雖然open沒有參數,但是依賴是通過構造方法傳遞的。
第三種情況:
OpenandClose3 close3 = new OpenandClose3();
close3.setTV(new Xiaomi());//如果不先set,就會報空指針close3.open();
四、里氏替換原則
面向對象中繼承的問題:
- 父類實現好的方法,實際上設定了規范,雖然不強制要求子類都要遵守,到那時如果子類對這些方法進行了修改,會對整個繼承體系造成破壞;
- 如果使用繼承會給程序帶來入侵性,使得移植性降低,增加對象之間的耦合性,如果一個類被其他的類所繼承,則當這個類需要修改時,會影響所有子類;
- 那么,如何在編程中正確使用繼承呢?=> 里氏替換原則
里氏替換:
- 里氏替換原則:如果每個類型 T1 的對象 o1 ,都有類型 T2 的對象 o2,使得以 T1 定義的所有程序 P 在所有的對象 o1 都換成 o2 時,程序 P 的行為沒有發生變化,那么類型 T2 是類型 T1 的子類型,也就是說,引用基類(父類)的地方必須能透明的使用其子類(派生類)的對象。
- 使用繼承的時候,遵循里氏替換原則,也就是盡量不要重寫父類的方法;
- 這個原則告訴我們,繼承讓兩個類的耦合性增強了,適當情況下,可以通過聚合、組合、依賴來解決問題。
如何理解這個原則呢,假如一個父類是 A,B extends A,結果把 A 類的所有方法都重寫了,那肯定A類的對象所有行為都變化了,另一方面,B 還要繼承 A 類純屬有病。適當的,A 和 B 更適合於,繼承同一個更加基礎的類 C,重新整理,這樣解決這個問題會比較合適。
4.1 示例
例如:
class A{
public int func1(int a, int b){
return a - b;
}
}
class B extends A{
public int func1(int a, int b){
return a + b;
}
public int func2(int a, int b){
return func1(a, b)+9;
}
}
不管是無意還是有意,B 繼承 A 的時候把 func1 重寫了。
顯然,這樣 B 以為被正常調用的時候,求a-b的 func1 ,卻輸出了和調用 a 的 func 1不一樣的結果。(可能例子不是很恰當,但是如果更復雜的情況下,調用一個子類的某一個方法,方法名是一樣的,肯定會認為功能是一樣的)
實際開發過程中,就是因為一些重寫父類方法來完成新功能的操作,讓整個繼承體系的復用性變差,特別是用到多態比較多的時候。
通用的做法就是:原來的父類和子類都繼承一個更通俗的基類,原有的繼承關系去掉,采用依賴、聚合、組合等關系來替代。
4.2 改進
//更加基礎的類
class Base{
}
class A1 extends Base{
public int func1(int a, int b){
return a - b;
}
}
class B1 extends Base{
public int func1(int a, int b){
return a + b;
}
public int func2(int a, int b){
return func1(a, b)+9;
}
//如果b要使用到A的方法,使用組合的關系
private A1 a1 = new A1();
//仍然使用A的方法
public int func3(int a, int b){
return this.a1.func1(a, b);
}
}
那么,這樣的話A和B已經沒有耦合的依賴關系了,那么調用的時候,想要減法的方法就可以調用fuc3,使用加法可以調用fuc1,此時不會和A的fuc1打架或者覆蓋。
五、開閉原則ocp
開閉原則(Open Closed Principle)是編程中最基礎、最重要的設計原則。
- 一個軟件實體、如類,模塊和函數應該對擴展開放(對提供方),對修改關閉(對使用方)。用抽象構建框架,用實現擴展細節;
- 當軟件需要變化時,盡量通過擴展軟件實體的行為來實現變化,而不是通過已有代碼實現變化;
- 編程中遵循其他原則的目的就是遵循開閉原則。
5.1 示例
//繪圖類,根據接受的shape不同來繪制圖形,【使用方】
class GraphicEidtor{
public void drawShape(Shape s){
if(s.type == 1) drawRec(s);
if (s.type == 2) drawCir(s);
}
public void drawRec(Shape r){
System.out.println("矩形");
}
public void drawCir(Shape c){
System.out.println("圓形");
}
}
//基類
class Shape{
int type;
}
//【提供方】
class Rectangle extends Shape{
Rectangle(){
super.type = 1;
}
}
class Circle extends Shape{
Circle(){
super.type = 2;
}
}
調用的時候,給draw方法傳入不同的參數,會根據類型畫出不同的圖形,我們調用一下:
GraphicEidtor graphicEidtor = new GraphicEidtor();
graphicEidtor.drawShape(new Rectangle());
graphicEidtor.drawShape(new Circle());
這種寫法還是比較好理解的,但是這種寫法很不好。
那么,這種寫法的問題在哪里呢?
違反了設計模式的 OCP 開閉原則,也就是不滿足對擴展開放,對修改關閉。這個原則希望當我們給類增加新功能的時候,盡量不修改代碼,或者盡可能少修改代碼。
比如我們想加一個圖形種類,那就要將這個圖形多寫一個類作為【提供方】,在畫圖類【使用方】里增加一個對應shape類型的調用,再增加一個方法。這就是違背原則了。
5.2 改進
思路就是:創建shape作為抽象類,提供一個抽線的draw方法,那么子類實現的時候去實現draw方法,這樣的話,【使用方】就不用修改代碼,滿足了開閉原則。
(其實說白了就是面向對象的意思,這個對象自己本身需要向外提供方法,而不是讓使用方去提供方法,同時,前面四個原則里也多多多少少有用到這個思路,就是讓使用方不要改動)
class GraphicEidtor1{
public void drawShape(Shape1 s){
s.draw();
}
}
abstract class Shape1{
int type;
public abstract void draw();
}
class Rectangle1 extends Shape1{
Rectangle1(){
super.type = 1;
}
public void draw(){
System.out.println("矩形");
}
}
class Circle1 extends Shape1{
Circle1(){
super.type = 2;
}
public void draw(){
System.out.println("圓形");
}
}
這樣的話,調用的寫法是沒有任何改變的。
但是呢,如果我們要加一個新的形狀,那么就讓他自己去實現方法就可以了,對於【使用方】GraphicEidtor 是修改關閉的。
六、迪米特法則
Demeter Principle 迪米特法則,又叫最少知道原則:
即一個類對自己依賴的類知道的越少越好,也就是說,對於被依賴的類不管多么復雜,都盡量將邏輯封裝再類的內部。對外提供public方法,不對外泄露任何信息。
迪米特法則還有個更簡單的定義:只與直接的朋友通信。
什么是直接朋友?每個對象都會和其他對象之間有耦合關系,只要兩個對象之間有耦合關系,我們就說這兩個對象之間是朋友關系,耦合的方式有很多,依賴、關聯、組合、聚合等。
其中我們稱出現在成員變量、方法參數、方法返回值中的類為直接的朋友,而出現在局部變量中 的類不是直接的朋友,也就是說,陌生的類最好不要以局部變量的形式出現在類的內部。
比如A類里面直接有用到一個B b,或者某個方法有參數fuc1(B b);或者返回值類型是B,那么這叫做B以直接朋友的形式出現在了A里面。
比如A類里面有個方法 fuc1,fuc1用到一個 B b = new B();這樣的局部變量形式讓A里面出現了B,這就是陌生的類,而不是直接朋友。
6.1 示例
比如一個應用實例:
有一個學校,下屬各個學院和總部,現在要求打印出學校總部的員工ID和學院員工的ID。
代碼略長,但是邏輯很簡單:
public class Demeter1 {
public static void main(String[] args) {
CollegeManager collegeManager = new CollegeManager();
collegeManager.printAllEmp(new SchoolManager());
}
}
//學校總員工
class Employee{
private String id;
public void setId(String id){
this.id = id;
}
public String getId(){
return id;
}
}
//學院員工
class SchoolEmployee{
private String id;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
//管理學院員工
class SchoolManager{
//添加學院員工
public List<SchoolEmployee> getAllEmployee(){
List<SchoolEmployee> list = new ArrayList<>();
for( int i=0; i<10; i++){
SchoolEmployee employee = new SchoolEmployee();
employee.setId("學院員工:"+i);
list.add(employee);
}
return list;
}
}
//學校管理類
//直接朋友類有:Employee、SchoolManager,第一個作為添加方法返回值,第二個作為輸出方法的參數
//陌生類:SchoolEmployee,違背了迪米特法則
class CollegeManager{
//添加學校員工,返回值參數Employee:直接朋友
public List<Employee> getAllEmployee(){
List<Employee> list = new ArrayList<>();
for(int i=0; i<5; i++){
Employee employee = new Employee();
employee.setId("學校總部員工:"+i);
list.add(employee);
}
return list;
}
//輸出所有員工信息
//參數SchoolManager:直接朋友
public void printAllEmp(SchoolManager sub){
//SchoolEmployee:陌生朋友,局部變量的方式
List<SchoolEmployee> list1 = sub.getAllEmployee();
System.out.println("-----學院員工-----");
for(SchoolEmployee e: list1){
System.out.println(e.getId());
}
List<Employee> list2 = this.getAllEmployee();
System.out.println("-----學校員工-----");
for(Employee e: list2){
System.out.println(e.getId());
}
}
}
這里面的問題,關於直接朋友和非直接朋友已經標注。
按照迪米特法則,上面出現了 SchoolEmployee 是陌生朋友,出現在了 SchoolManager 類里。這是非直接朋友關系的耦合,根據迪米特法則是應該避免的。
6.2 改進
其實例子寫的有點故意,顯然出問題的那一段,學校的輸出信息部分,要輸出學院的信息,沒必要,解決起來把輸出學院員工信息的那段,放到學院員工自己的類里面就可以了。也就是遵守了前面說的 " 盡量將邏輯封裝在類的內部 "。
那么就可以把 SchoolManager 部分輸出學院信息部分改成:
//學院員工
sub.printEmployee();
然后對應的輸出方法,寫去CollegeManager類里面,添加一個方法:
//輸出學院所有員工的信息
public void printEmployee(){
List<SchoolEmployee> list1 = this.getAllEmployee();//不用this也行
System.out.println("-----學院員工-----");
for(SchoolEmployee e: list1){
System.out.println(e.getId());
}
}
這樣的話,邏輯在自己類內部,提供一個public方法供外部使用。
注意:每個類之間多多少少都會有耦合,迪米特法則只是要求降低耦合關系,而不是要求完全沒有依賴關系。完全沒有,就相當於每個對象干個啥都在自己類里寫完,也用不着直接朋友了。
七、合成復用原則
合成復用原則:盡量使用組合/聚合的方式,而不要使用繼承。
例如: A 類和 B 類,B 類想要使用 A 類的兩個方法,第一個想到的是繼承,但是這種做法耦合性很高,如果說僅僅是想要使用這兩個方法,而沒有別的根本上需要用到繼承的必要性,那么可能會帶來很多麻煩,比如還有別的類也繼承了 A ,有必要修改 A 的時候還要考慮 B 會不會收影響。
所以盡量使用的做法就是聚合或者合成:
7.1 聚合
將 A 作為一個私有變量加入到 B 里面,在 B 里面寫一個 set 方法將 A 實例化,然后去調用想要的方法,這就叫做聚合。類似於我們在前面第三個原則”依賴倒轉原則“最后寫的傳遞依賴的方式的setter方法。
7.2 組合
將 A 直接實例化在 B 里面,那么 B 創建的時候,A 就已經有了一個實例化的對象,然后調用方法,和前面的里氏替換法則后面的做法是一樣的。
八、總結
其實上面的七個原則很多地方的解決方案和沖突都是有重復的部分,實際上我們總結一下核心思想就是:
- 盡量把需要變化的部分獨立出來,不要和不變的代碼寫在一起;
- 如果對別的部分影響大,盡量寫成接口;
- 為了松耦合努力。(可以說七個原則這個理論本身就是非常松耦合……)
