設計模式的七大原則

一、設計模式的目的

  設計模式主要是為了解決在編寫代碼過程中,面臨的耦合性、內聚性、可維護性、可擴展行、重用性、靈活性等多方面的挑戰。

  1. 代碼重用性:相同功能的代碼不用多次編寫
  2. 可讀性:編程規範,便於他人的閱讀和理解
  3. 可擴展性:當需要添加新功能時,非常的方便
  4. 可靠性:當增加新的功能後,對原來的功能沒有影響
  5. 最終達到高內聚低耦合的特性

二、設計模式七大原則

  設計模式原則,其實就是程序員在編程時,應當遵守的原則,也是各種設計模式的基礎。

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

三、單一職責原則

1,基本介紹

  對類來說的,即一個類應該只負責一項職責。如類A負責兩個不同職責:職責1,職責2。當職責1需求變更而改變A時,可能造成職責2執行錯誤,所以需要將類A的粒度分解為A1,A2

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 + " 在公路上運行....");
    }
}

b)方案2

/**
 * 方案2的分析
 * 1. 遵守單一職責原則
 * 2. 但是這樣做的改動很大,即將類分解,同時修改客戶端
 * 3. 改進:直接修改Vehicle 類,改動的代碼會比較少=>方案3
 */
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 + " 天空運行");
    }
}

c)方案3

/**
 * 方式3的分析
 * 1. 這種修改方法沒有對原來的類做大的修改,只是增加方法
 * 2. 這裡雖然沒有在類這個級別上遵守單一職責原則,但是在方法級別上,仍然是遵守單一職責
 */
public class SingleResponsibility3 {
    public static void main(String[] args) {
        Vehicle2 vehicle2  = new Vehicle2();
        vehicle2.run("汽車");
        vehicle2.runWater("輪船");
        vehicle2.runAir("飛機");
    }
}

class Vehicle2 {
    public void run(String vehicle) {
        System.out.println(vehicle + " 在公路上運行....");

    }

    public void runAir(String vehicle) {
        System.out.println(vehicle + " 在天空上運行....");
    }

    public void runWater(String vehicle) {
        System.out.println(vehicle + " 在水中行....");
    }

}

3,注意事項

  • 降低類的複雜度,一個類只負責一項職責
  • 提交類的可讀性,可維護性
  • 降低變更引起的風險
  • 通常情況下,我們應當遵守單一職責原則。只有邏輯足夠簡單,才可以在代碼級違反單一職責原則;只有類中方法數量足夠少,才可以在方法級保持單一職責原則

四、接口隔離原則

1,基本介紹

  客戶端不應該依賴它不需要的接口,即一個類對另一個類的依賴應該建立在最小的接口上

2,應用案例

  類A通過接口Interface1依賴類B,類C通過接口Interface1依賴類D。但是類A只需要操作類B中的operation1、operation2和operation3,同時類C也只需要操作類D中的operation1、operation4和operation5,但是由於類B和類D都是接口Interface1的實現類,故而都重寫了不需要的方法。

  

 源碼:基本案例

public interface Interface1 {
    void operation1();

    void operation2();

    void operation3();

    void operation4();

    void operation5();
}

public abstract class A {
    public void dependecy1(Interface1 interface1) {
        interface1.operation1();
    }
    public void dependecy2(Interface1 interface1) {
        interface1.operation2();
    }
    public void dependecy3(Interface1 interface1) {
        interface1.operation3();
    }
}

public  class B implements Interface1{
    @Override
    public void operation1() {
        System.out.println("B實現了operation1");
    }

    @Override
    public void operation2() {
        System.out.println("B實現了operation2");
    }

    @Override
    public void operation3() {
        System.out.println("B實現了operation3");
    }

    @Override
    public void operation4() {
        System.out.println("B實現了operation4");
    }

    @Override
    public void operation5() {
        System.out.println("B實現了operation5");
    }
}

public  class C {
    public void dependecy1(Interface1 interface1) {
        interface1.operation1();
    }
    public void dependecy4(Interface1 interface1) {
        interface1.operation4();
    }
    public void dependecy5(Interface1 interface1) {
        interface1.operation5();
    }
}

public  class D implements Interface1{
    @Override
    public void operation1() {
        System.out.println("D實現了operation1");
    }

    @Override
    public void operation2() {
        System.out.println("D實現了operation2");
    }

    @Override
    public void operation3() {
        System.out.println("D實現了operation3");
    }

    @Override
    public void operation4() {
        System.out.println("D實現了operation4");
    }

    @Override
    public void operation5() {
        System.out.println("D實現了operation5");
    }
}

View Code

3,基於接口隔離原則的改進

  將接口Interface1拆分為獨立的幾個接口,類A和類C分別與他們需要的接口建立依賴關係,即採用接口隔離原則。

  

源碼:接口隔離實現

public interface Interface1 {
    void operation1();
}

public interface Interface2 {
    void operation2();

    void operation3();
}

public interface Interface3 {
    void operation4();

    void operation5();
}

public class A {
    public void dependecy1(Interface1 interface1) {
        interface1.operation1();
    }

    public void dependecy2(Interface2 interface2) {
        interface2.operation2();
    }

    public void dependecy3(Interface2 interface2) {
        interface2.operation3();
    }
}

public class B implements Interface1, Interface2 {
    @Override
    public void operation1() {
        System.out.println("B實現了operation1");
    }

    @Override
    public void operation2() {
        System.out.println("B實現了operation2");
    }

    @Override
    public void operation3() {
        System.out.println("B實現了operation3");
    }
}

public class C {
    public void dependecy1(Interface1 interface1) {
        interface1.operation1();
    }

    public void dependecy4(Interface3 interface3) {
        interface3.operation4();
    }

    public void dependecy5(Interface3 interface3) {
        interface3.operation5();
    }
}

public class D implements Interface1, Interface3 {
    @Override
    public void operation1() {
        System.out.println("D實現了operation1");
    }

    @Override
    public void operation4() {
        System.out.println("D實現了operation4");
    }

    @Override
    public void operation5() {
        System.out.println("D實現了operation5");
    }
}

View Code

五、依賴倒轉原則

1,基本介紹

  • 高層模塊不應該依賴底層模塊,二者都應該依賴其抽象
  • 抽象不應該依賴細節,細節應該依賴抽象
  • 依賴倒轉(倒置)的中心思想是面向接口編程
  • 依賴倒轉原則是基於這樣的設計理念:相對於細節的多變性,抽象的東西要穩定的多。以抽象為基礎搭建的架構比以細節為基礎的架構要穩定的多。在java中,抽象指的是接口或抽象類,細節就是具體的實現類
  • 使用接口或抽象類的目的是制定好規範,而不涉及任何具體的操作,把展示細節的任務交給他們的實現類去完成

2,應用案例

a)方案1

/**
 * 方案1:完成Person接受消息的功能
 *  簡單易懂
 *  如果我們獲取的對象是 微信、短訊等,則新增類,同時Person也要增加對應的接收方法
 */
public class Person {
    public void receive(Email email){
        System.out.println(email.getInfo());
    }
}

class Email{
    public String getInfo() {
        return "電子郵件信息:hello word";
    }
}

b)方案2改進

/**
 * 方案2:依賴倒置原則
 *  引入一個抽象的接口IReceiver接口,讓不同的接收消息方法都實現這個接口重寫getInfo方法
 *  由調用方決定傳入的具體對象
 */
public class Person {
    public void receive(IReceiver receiver) {
        System.out.println(receiver.getInfo());
    }
}

interface IReceiver{
    public String getInfo();
}

class Email implements IReceiver{
    @Override
    public String getInfo() {
        return "電子郵件信息:hello word";
    }
}

class WeiXin implements IReceiver {
    @Override
    public String getInfo() {
        return "微信信息:hello wechat";
    }
}

3,依賴關係傳遞的三種方式(同Spring DI)

a)接口傳遞

/**
 * 接口傳遞的方式,將接口ITV傳遞open方法
 */
class OPenAndClose implements IOPenAndClose {
    @Override
    public void open(ITV itv) {
        itv.play();
    }
}

interface IOPenAndClose {
    public void open(ITV itv);
}

interface ITV {
    public void play();
}

View Code

b)構造方法傳遞

//方式二:構造方法傳遞
interface IOpenAndClose {
    public void open();//抽象方法
}

interface ITV {//ITV接口

    public void play();
}

class OpenAndClose implements IOpenAndClose {
    public ITV tv;//成員

    public OpenAndClose(ITV tv) {//構造方法
        this.tv = tv;
    }

    @Override
    public void open() {
        this.tv.play();
    }
}

View Code

c)setter傳遞

//方式三:setter方法傳遞
interface IOpenAndClose {
    public void open();//抽象方法
}

interface ITV {//ITV接口

    public void play();
}

class OpenAndClose implements IOpenAndClose {
    private ITV tv;

    public void setTv(ITV tv) {
        this.tv = tv;
    }

    @Override
    public void open() {
        this.tv.play();
    }
}

View Code

4,注意事項和細節

  • 低層模塊盡量都要有抽象類和接口,或者兩者都有,提高程序的穩定性
  • 變量的聲明類型盡量時抽象類或接口,這樣我們的變量引用和實際對象間就存在一個緩衝層,利於程序擴展和優化
  • 繼承時遵循里氏替換原則

六、里氏替換原則

1,OO中繼承性的思考和說明

  • 繼承:父類中凡是已經實現好的方法,實際上是在設定規範和契約,雖然它不強制要求所有的子類必須遵循這些契約,但是如果子類對這些已經實現的方法任意修改,就會對整個繼承體系造成破壞。
  • 繼承在程序設計上帶來了便利的同時,也帶來了弊端。比如使用繼承會給程序帶來侵入性,程序的可移植性降低,增加對象間的耦合性。如果一個類被其他的類所繼承,則當這個類需要修改時,必須考慮所有的子類,並且父類修改後,所有涉及到子類的功能都有可能產生故障。

2,基本介紹

  • 如果對每一個類型為T1的對象o1,都有類型為T2的對象o2,使得以T1定義的所有程序P在所有的對象o1都代換成o2時,程序P的行為沒有發生變化,那麼類型T2是類型T1的子類型。
  • 所有引用基類的地方必須能透明地使用其子類的對象。
  • 在使用繼承時,遵循里氏替換原則,在子類中盡量不要重寫父類的方法
  • 繼承實際上讓兩個類的耦合性增強了,在適當的情況下,可以通過聚合、組合、依賴來處理

3,應用案例

a)方案1

public class A {
    public int fuc1(int a, int b) {
        return a - b;
    }
}

/**
 * 類B繼承A
 *  由於B無意識重寫了A的fuc1方法,導致最終調用時發現預期類A的fuc1不生效
 */
class B extends A {
    public int fuc1(int a, int b) {
        return a + b;
    }

    public int fuc2(int a, int b) {
        return a * b;
    }
}

b)方案2改進

//定義基類
public class Base {
}

class A extends Base {
    public int fuc1(int a, int b) {
        return a - b;
    }
}

/**
 * 類A和類B繼承Base類
 *  B使用組合的方式使用A,這樣fuc3仍然是類A的方法
 */
class B extends Base {
    A a = new A();
    //這裡,重寫了 A 類的方法,  可能是無意識
    public int fuc1(int a, int b) {
        return a + b;
    }

    public int fuc2(int a, int b) {
        return fuc1(a, b) + 9;
    }

    //我們仍然想使用 A 的方法
    public int fuc3(int a, int b) {
        return this.a.fuc1(a, b);
    }
}

七、開閉原則

1,基本介紹

  • 開閉原則是編程中最基礎、最重要的設計原則
  • 一個軟件實體如類、模塊和函數應該對擴展開發(指對提供方開放),對修改關閉(指對使用方關閉)。用抽象構建框架,用實現擴展細節
  • 當軟件需要變化時,盡量通過擴展軟件實體的行為來實現變化,而不是通過修改已有的代碼來實現變化。(盡量增加一種功能/擴展,而不是修改這部分可能正在使用的功能)
  • 編程中遵循其他原則,以及使用設計模式的目的就是遵循開閉原則

2,應用案例

a)方案1

public class OCP {

    public static void main(String[] args) {
        GraphicEditor graphicEditor = new GraphicEditor();
        graphicEditor.drawShape(new Rectangle());
        graphicEditor.drawShape(new Circle());
    }

}
//繪圖的類,使用方
class GraphicEditor{
    //根據不同的type繪製不同的圖
    public void drawShape(Shape s) {
        if (s.type == 1) {
            drawRectangle(s);
        } else if (s.type == 2) {
            drawRectangle(s);
        }
    }

    public void drawRectangle(Shape r) {
        System.out.println("繪製矩形");
    }
    public void drawCircle(Shape r) {
        System.out.println("繪製圓形");
    }
}

//基類
abstract class Shape{
    int type;
}

class Rectangle extends Shape {
    public Rectangle() {
        super.type = 1;
    }
}

class Circle extends Shape {
    public Circle() {
        super.type = 2;
    }
}

問題:該方案違反了設計模式的開閉原則,當需要增加一個圖形種類(例:三角形),不僅需要新建類,而且使用方代碼也需要添加適配

b)方案2改進

public class OCP {
    public static void main(String[] args) {
        GraphicEditor graphicEditor = new GraphicEditor();
        graphicEditor.draw(new Rectangle());
        graphicEditor.draw(new Circle());
    }
}

class GraphicEditor{
    public void draw(Shape s) {
        s.draw();
    }
}

abstract class Shape{
    public abstract void draw();
}

class Rectangle extends Shape {

    @Override
    public void draw() {
        System.out.println("繪製矩形");
    }
}

class Circle extends Shape {

    @Override
    public void draw() {
        System.out.println("繪製圓形");
    }
}

八、迪米特法則

1,基本介紹

  • 一個對象應該對其他對象保持最少的了解
  • 類與類關係越密切,耦合度越大
  • 迪米特法則又叫最少知道原則,即一個類對自己依賴的類知道的越少越好。也就是說,對於被依賴的類不管多麼複雜,都盡量將邏輯封裝在類的內部。對外除了提供public方法,不對外泄露任何信息
  • 迪米特法則還有個更簡單的定義:只與直接的朋友通信
  • 直接的朋友:每個對象都會與其他對象有耦合關係,只要兩個對象之間有耦合關係,我們就說這兩個對象之間是朋友關係。耦合的方式很多,依賴、關聯、組合、聚合等。其中,我們成出現成員變量、方法參數、方法返回值中的類為直接朋友,而出現在局部變量中的類不是直接的朋友。也就是說,陌生的類最好不要以局部變量的形式出現在類的內部。

2,應用實例

  有一個學校,下屬有各個學院和總部,現要求打印出學校總部員工 ID 和學院員工的 id

a)方案1

public class Demeter {
    public static void main(String[] args) {
        SchoolManage manage = new SchoolManage();
        manage.printAllEmp(new CollegeManage());
    }
}

//學校總部員工
class Employee {
    private String id;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }
}

//學院員工
class CollegeEmployee {
    private String id;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }
}

//學院管理
class CollegeManage {

    public List<CollegeEmployee> getAllEmployee() {
        List<CollegeEmployee> list = new ArrayList<CollegeEmployee>();
        for (int i = 0; i < 10; i++) {
            CollegeEmployee emp = new CollegeEmployee();
            emp.setId("學院員工 id= " + i);
            list.add(emp);
        }
        return list;
    }
}

//學校總部管理
class SchoolManage {
    public List<Employee> getAllEmployee() {
        List<Employee> list = new ArrayList<Employee>();
        for (int i = 0; i < 10; i++) {
            Employee emp = new Employee();
            emp.setId("學校總部員工 id= " + i);
            list.add(emp);
        }
        return list;
    }

    //這裡的  CollegeEmployee 不是    SchoolManager 的直接朋友
    //違反了迪米特法則
    public void printAllEmp(CollegeManage collegeManage) {
        List<CollegeEmployee> allEmployee = collegeManage.getAllEmployee();
        System.out.println("------------學院員工------------");
        for (CollegeEmployee e : allEmployee) {
            System.out.println(e.getId());
        }
        //獲取到學校總部員工
        List<Employee> list2 = this.getAllEmployee();
        System.out.println("------------學校總部員工------------");
        for (Employee e : list2) {
            System.out.println(e.getId());
        }

    }
}

問題:由於在SchoolManage中,CollegeEmployee類並不是SchoolManage類的直接朋友。按照迪米特法則,應該避免類中出現這樣非直接朋友關係的耦合

b)方案2

優化:完整代碼

//學院管理
class CollegeManage {

    public List<CollegeEmployee> getAllEmployee() {
        List<CollegeEmployee> list = new ArrayList<CollegeEmployee>();
        for (int i = 0; i < 10; i++) {
            CollegeEmployee emp = new CollegeEmployee();
            emp.setId("學院員工 id= " + i);
            list.add(emp);
        }
        return list;
    }

    //在學院管理中增加遍歷類,方便直接被調用
    public void printCollegeEmp() {
        List<CollegeEmployee> allEmployee = this.getAllEmployee();
        System.out.println("------------學院員工------------");
        for (CollegeEmployee e : allEmployee) {
            System.out.println(e.getId());
        }
    }
}

//學校總部管理
class SchoolManage {
    public List<Employee> getAllEmployee() {
        List<Employee> list = new ArrayList<Employee>();
        for (int i = 0; i < 10; i++) {
            Employee emp = new Employee();
            emp.setId("學校總部員工 id= " + i);
            list.add(emp);
        }
        return list;
    }

    public void printAllEmp(CollegeManage collegeManage) {
        collegeManage.printCollegeEmp();
        //獲取到學校總部員工
        List<Employee> list2 = this.getAllEmployee();
        System.out.println("------------學校總部員工------------");
        for (Employee e : list2) {
            System.out.println(e.getId());
        }

    }
}

3,注意事項

  • 迪米特法則的核心是降低類之間的耦合
  • 由於每個類都減少了不必要的依賴,因此迪米特法則只是要求降低類間(對象間)耦合關係,並不是要求完全沒有依賴關係

九、合成復用原則

1,定義

  • 合成復用原則是指盡量使用對象組合/聚合,而不是繼承關係達到軟件復用的目的。可以說使系統更加靈活,降低類與類之間的耦合度,一個類的變化對其他類造成的影響相對較少。
  • 繼承我們叫做白箱復用,相當於把所有的實現細節暴露給子類。組合/聚合也稱之為黑箱復用,對類以外的對象是無法獲取到實現細節的。要根據具體的業務場景來做代碼設計,其實也都要遵循OOP模型

十、 設計原則核心思想

  • 1)找出應用中可能需要變化之處,把它們獨立出來,不要和那些不需要變化的代碼混在一起。
  • 2)針對接口編程,而不是針對實現編程。
  • 3)為了交互對象之間的松耦合設計而努力