設計模式七大原則及代碼示例


七大原則:

  1. 單一職責原則;
  2. 接口隔離原則;
  3. 依賴倒轉原則;
  4. 里氏替換原則;
  5. 開閉原則ocp;
  6. 迪米特法則;
  7. 合成復用原則。

設計模式其實包含了面向對象的精髓,封裝、繼承、多態。


一、單一職責原則


對於類來說,一個類應該只負責一項職責。

假設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 總結單一職責原則

  1. 降低類的復雜度,一個類只負責一項職責(上面的例子因為過於簡單,所以看起來第三個寫法更有效)
  2. 提高類的可讀性、可維護性,降低變更帶來的風險
  3. 通常情況下,我們應該遵守這個職責,只有在邏輯足夠簡單的時候,才可以在代碼級別違反這個原則,也就是上面的,改為在方法級別保持單一職責原則。

二、接口隔離原則


接口隔離(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 ) 指的是:

  1. 高層模塊不應該依賴低層模塊,二者都應該依賴其抽象
  2. 抽象不應該依賴細節,細節應該依賴抽象
  3. 依賴倒轉的核心思想是面向接口編程

為什么要有依賴倒轉原則:主要是因為,相對於細節的多變,抽象的東西要穩定的多,以抽象為基礎搭建的架構比以細節為基礎的架構穩定的多,在java種,抽象指的就是接口或者抽象類,細節就是具體的實現類,抽象類制定好規范,展現細節的任務交給實現類去做。

依賴倒轉原則需要注意:

  1. 底層模塊盡量都要有抽象類或接口,或者兩者都有,程序的穩定性會更好;
  2. 變量的聲明類型盡量是抽象類或接口,這樣我們的變量引用和實際對象之間,就存在一個緩沖層,利於程序擴展和優化;
  3. 繼承時遵循里氏替換原則。

例如:有一個功能,一個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 擴展:依賴關系傳遞的三種方式

依賴關系的傳遞一般有三種方式:

  1. 接口傳遞;
  2. 構造方法傳遞;
  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();

四、里氏替換原則


面向對象中繼承的問題:

  1. 父類實現好的方法,實際上設定了規范,雖然不強制要求子類都要遵守,到那時如果子類對這些方法進行了修改,會對整個繼承體系造成破壞;
  2. 如果使用繼承會給程序帶來入侵性,使得移植性降低,增加對象之間的耦合性,如果一個類被其他的類所繼承,則當這個類需要修改時,會影響所有子類;
  3. 那么,如何在編程中正確使用繼承呢?=> 里氏替換原則

里氏替換

  1. 里氏替換原則:如果每個類型 T1 的對象 o1 ,都有類型 T2 的對象 o2,使得以 T1 定義的所有程序 P 在所有的對象 o1 都換成 o2 時,程序 P 的行為沒有發生變化,那么類型 T2 是類型 T1 的子類型,也就是說,引用基類(父類)的地方必須能透明的使用其子類(派生類)的對象。
  2. 使用繼承的時候,遵循里氏替換原則,也就是盡量不要重寫父類的方法
  3. 這個原則告訴我們,繼承讓兩個類的耦合性增強了,適當情況下,可以通過聚合、組合、依賴來解決問題。

如何理解這個原則呢,假如一個父類是 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)是編程中最基礎、最重要的設計原則。

  1. 一個軟件實體、如類,模塊和函數應該對擴展開放(對提供方),對修改關閉(對使用方)用抽象構建框架,用實現擴展細節;
  2. 當軟件需要變化時,盡量通過擴展軟件實體的行為來實現變化,而不是通過已有代碼實現變化;
  3. 編程中遵循其他原則的目的就是遵循開閉原則

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 就已經有了一個實例化的對象,然后調用方法,和前面的里氏替換法則后面的做法是一樣的。


八、總結


其實上面的七個原則很多地方的解決方案和沖突都是有重復的部分,實際上我們總結一下核心思想就是:

  1. 盡量把需要變化的部分獨立出來,不要和不變的代碼寫在一起
  2. 如果對別的部分影響大,盡量寫成接口
  3. 為了松耦合努力。(可以說七個原則這個理論本身就是非常松耦合……)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM