設計模式 | 詳解設計模式的七大原則 一、設計模式的目的 二、設計模式七大原則

一、設計模式的目的

編寫軟件的過程中,程序員面臨着來自耦合性,內聚性以及可維護性,可擴展性,重用性,靈活性等多方面的挑戰,設計模式是爲了讓程序具有更好的:

  • 代碼重用性(即相同功能的代碼,不用多次編寫)
  • 可讀性(即編程規範性,便於其他程序員的閱讀和理解)
  • 可擴展性(即當需要增加新功能時,非常方便,稱爲可維護)
  • 可靠性(即當我們增加新的功能後,對原來的功能沒有影響)
  • 使程序呈現高內聚,低耦合的特性

二、設計模式七大原則

設計模式的七大原則有:

  • 單一職責原則
  • 接口隔離原則
  • 依賴倒轉原則
  • 里氏替換原則
  • 開閉原則
  • 迪米特法則
  • 合成複用原則

1、單一職責原則

概念:對類來說,即一個類應該只負責一項職責

如類A負責兩個不同職責:職責1、職責2。當職責1需求變更而改變A類時,可能造成職責2執行錯誤,因此要將類A的粒度分解爲A1、A2。

單一職責原則的注意事項

  • 降低類的複雜性,一個類只負責一項職責
  • 提高類的可讀性,可維護性
  • 降低變更引起的風險
  • 通常情況下,我們應當遵守單一職責原則,只有邏輯足夠簡單,纔可以在代碼級違反單一職責原則(只有類中方法數量足夠少,纔可以在方法級別保持單一職責原則。即把“降低類的複雜性”同個分解爲多個方法來實現)。

2、接口隔離原則

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

通俗一點來說就是一個類通過接口去依賴另一個類時,這個接口不應該存在太多多餘的方法。可以將大的接口拆分成多個小的接口

示例

我們現在有一個接口,接口裏面有五個方法,然後有一個類B和類D分別實現了該接口。然後類A和類C分別通過這個接口去依賴類B和類D,但是他們只會用到接口的部分方法,第一種寫法如下:

package com.cxc.principle.segregation;

/**
 * 實現:類B和類D分別去實現接口1
 *      然後類A通過接口1依賴類B(使用了1,2,3方法)
 *      類C通過接口1依賴類D(使用了1,4,5方法)
 */
public class Segregation1 {

    public static void main(String[] args) {
        A a = new A();
        a.depend1(new B()); //類A通過接口去依賴類B
    }
}

/**
 * 接口1
 */
interface Interface1{
    void operation1();
    void operation2();
    void operation3();
    void operation4();
    void operation5();
}

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");
    }
}

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");
    }
}

/**
 * A類通過Interface1依賴B類,但是隻會用到1,2,3方法
 */
class A{
    public void depend1(Interface1 i){
        i.operation1();
    }
    public void depend2(Interface1 i){
        i.operation2();
    }
    public void depend3(Interface1 i){
        i.operation3();
    }
}
/**
 * C類通過Interface1依賴D類,但是隻會用到1,4,5方法
 */
class C{
    public void depend1(Interface1 i){
        i.operation1();
    }
    public void depend4(Interface1 i){
        i.operation4();
    }
    public void depend5(Interface1 i){
        i.operation5();
    }
}

上面這種代碼實現的話,就不符合我們的接口隔離原則,接口隔離原則中強調我們要將接口依賴降低到最小接口,而不論是類A還是類C,依賴時都並沒有使用到接口的全部方法。

因此我們要進行改進,將大接口分解成小接口,在此我們將接口1分爲接口1,接口2和接口3:

interface Interface1{
    void operation1();
}
interface Interface2{
    void operation2();
    void operation3();
}
interface Interface3{
    void operation4();
    void operation5();
}

改進後的實現如下:

package com.cxc.principle.segregation.improve;

public class Segregation1 {

    public static void main(String[] args) {
        A a = new A();
        a.depend1(new B()); //A類通過接口去依賴B類
        a.depend2(new B());
        a.depend3(new B());

        C c = new C();
        c.depend1(new D());
        c.depend4(new D());
        c.depend5(new D());
    }
}

interface Interface1{
    void operation1();
}

interface Interface2{
    void operation2();
    void operation3();
}

interface Interface3{
    void operation4();
    void operation5();
}

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");
    }

}

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");
    }
}

/**
 * A類通過Interface1依賴B類,但是隻會用到1,2,3方法
 */
class A{
    public void depend1(Interface1 i){
        i.operation1();
    }
    public void depend2(Interface2 i){
        i.operation2();
    }
    public void depend3(Interface2 i){
        i.operation3();
    }
}

/**
 * C類通過Interface1依賴D類,但是隻會用到1,4,5方法
 */
class C{
    public void depend1(Interface1 i){
        i.operation1();
    }
    public void depend4(Interface3 i){
        i.operation4();
    }
    public void depend5(Interface3 i){
        i.operation5();
    }
}

這樣實現就遵守了接口隔離原則,使依賴接口降低到最小接口。

3、依賴倒轉原則

依賴倒轉原則是指:

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

依賴倒轉原則的注意事項和細節

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

依賴關係傳遞的三種方式

  • 接口傳遞
  • 構造方法傳遞
  • setter方式傳遞

示例

我們要實現一個簡單的用戶接收消息的功能

package com.cxc.principle.inversion;
/**
 * 方式1問題
 *      1.簡單,比較容易想到
 *      2.如果我們獲取的對象是微信,短信等,則需要新增類,同時Person也要增加響應的接收方法
 *      3.解決思路:引入一個抽象的接口IReceiver,表示接收者,這樣Person類與接口發現依賴。
 *                 因爲Email,Weixin都屬於接收的範圍,他們各自實現IReceiver接口就可以了,就符合了依賴倒轉原則
 */
public class DependecyInversion {
    public static void main(String[] args) {
        new Person().receive(new Email());
    }
}
class Email{
    public String getInfo(){
        return "電子郵件信息:hello,world";
    }
}

//完成person接受消息的功能
class Person{
    public void receive(Email email){
        System.out.println(email.getInfo());
    }
}

上述方式中,在Person類的接收消息方法中,直接傳入了一個Email類,這樣的話如果以後有其他方式比如微信、短信等消息方式還需要多寫幾個方法。不符合依賴倒轉原則,我們可以作如下更改:將這個類替換爲一個接口,實現如下:

package com.cxc.principle.inversion.improve;
public class DependecyInversion {
    public static void main(String[] args) {
        Person person = new Person();
        person.receive(new Email());
        person.receive(new WeiXin());
    }
}
/**
 * 接口
 */
interface IReceiver{
    public String getInfo();
}

/**
 * 實現類1
 */
class Email implements IReceiver{
    public String getInfo(){
        return "電子郵件信息:hello,world";
    }
}

/**
 * 實現類2
 */
class WeiXin implements IReceiver{
    @Override
    public String getInfo() {
        return "微信消息:hello,ok";
    }
}
//完成person接受消息的功能
class Person{
    public void receive(IReceiver receiver){
        System.out.println(receiver.getInfo());
    }
}

4、里氏替換原則

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

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

里氏替換原則

  • 如果對每個類型爲T1的對象o1,都有類型爲T2的對象o2,使得以T1定義的所有程序P在所有的對象o1都代換成o2時,程序P的行爲沒有發生變化,那麼類型T2是類型T1的子類型。換句話說,所有引用基類的地方必須能透明的使用其子類的對象。
  • 在使用繼承時,遵循里氏替換原則,在子類中儘量不要重寫父類的方法。
  • 里氏替換原則告訴我們,繼承實際上讓兩個類耦合性增強了,在適當的情況下,可以通過聚合,組合,依賴來解決問題
  • 我們可以讓原來的父類和子類都繼承一個更通俗的基類,原有的繼承關係去掉,採用依賴,聚合,組合燈關係代替。

5、開閉原則

  • 開閉原則是編程中最基礎、最重要的設計原則。
  • 一個軟件實體如類,模塊和函數應該對外擴展開放(對提供方),對修改關閉(對使用方)。用抽象構建框架,用實現擴展細節。
  • 當軟件需要變化時,儘量通過擴展軟件實體的行爲來實現變化,而不是通過修改已有的代碼來實現變化。
  • 編程中遵循其它原則,以及使用涉及模式的目的就是遵循開閉原則。

示例

有一個畫圖類如下:

package com.cxc.principle.ocp;

public class Ocp {

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

}
class GraphicEditor {
    public void drawShape(Shape s) {
        if (s.m_type == 1)
            drawRectangle(s);
        else if (s.m_type == 2)
            drawCircle(s);
        else if (s.m_type == 3)
            drawTriangle(s);
    }

    //三角形
    public void drawRectangle(Shape r) {
        System.out.println("三角形 ");
    }
    //圓形
    public void drawCircle(Shape r) {
        System.out.println(" 圓形 ");
    }
    //矩形
    public void drawTriangle(Shape r) {
        System.out.println(" 矩形 ");
    }
}

class Shape {
    int m_type;
}
class Rectangle extends Shape {
    Rectangle() {
        super.m_type = 1;
    }
}
class Circle extends Shape {
    Circle() {
        super.m_type = 2;
    }
}
class Triangle extends Shape {
    Triangle() {
        super.m_type = 3;
    }
}

上述例子中,如果要新增一個其他的圖形類,那麼在GraphicEditor類中也要更改代碼來加上掉這個圖形類的處理,不滿足開閉原則。因此我們應該修改爲:把Shape做成抽象類,並提供一個抽象的draw方法,讓子類去實現:

package com.cxc.principle.ocp.improve;
public class Ocp {
    public static void main(String[] args) {

        GraphicEditor graphicEditor = new GraphicEditor();
        graphicEditor.drawShape(new Rectangle());
        graphicEditor.drawShape(new Circle());
        graphicEditor.drawShape(new Triangle());
        graphicEditor.drawShape(new OtherGraphic());
    }
}
class GraphicEditor {
    public void drawShape(Shape s) {
        s.draw();
    }
    
}

//抽象類
abstract class Shape {
    int m_type;
    
    public abstract void draw();
}

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

    @Override
    public void draw() {
        // TODO Auto-generated method stub
        System.out.println(" 三角形 ");
    }
}

class Circle extends Shape {
    Circle() {
        super.m_type = 2;
    }
    @Override
    public void draw() {
        // TODO Auto-generated method stub
        System.out.println(" 圓形 ");
    }
}


class Triangle extends Shape {
    Triangle() {
        super.m_type = 3;
    }
    @Override
    public void draw() {
        // TODO Auto-generated method stub
        System.out.println(" 矩形 ");
    }
}
class OtherGraphic extends Shape {
    OtherGraphic() {
        super.m_type = 4;
    }

    @Override
    public void draw() {
        // TODO Auto-generated method stub
        System.out.println(" 其他圖形 ");
    }
}

改進後,如果想加入其他圖形類,直接繼承抽象類然後實現抽象方法即可,不用去更改抽象類。做到了對外擴展開放(對提供方),對修改關閉(對使用方)

6、迪米特法則

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

示例

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

package com.cxc.principle.demeter;

import java.util.ArrayList;
import java.util.List;
public class Demeter1 {

    public static void main(String[] args) {
        SchoolManager schoolManager = new SchoolManager();
        schoolManager.printAllEmployee(new CollegeManager());

    }

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

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

    public String getId() {
        return id;
    }
}
//學院員工
class CollegeEmployee {
    private String id;

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

    public String getId() {
        return id;
    }
}

//管理學院員工的管理類
class CollegeManager {
    //返回學院的所有員工
    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;
    }
}

//學校管理類
//分析 SchoolManager的直接朋友: Employee、CollegeManager
//CollegeEmployee 不是直接朋友,這樣違背了迪米特法則
class SchoolManager {
    //返回學校總部的員工
    public List<Employee> getAllEmployee() {
        List<Employee> list = new ArrayList<Employee>();
        
        for (int i = 0; i < 5; i++) {
            Employee emp = new Employee();
            emp.setId("學校總部的員工id= " + i);
            list.add(emp);
        }
        return list;
    }

    //輸入學校員工和學院員工信息
    void printAllEmployee(CollegeManager sub) {
        
        //分析問題
        //1. 這裏的 CollegeEmployee 不是  SchoolManager的直接朋友
        //2. CollegeEmployee 是以局部變量方式出現在 SchoolManager
        //3. 違反了迪米特法則
        
        //獲取到學院員工
        List<CollegeEmployee> list1 = sub.getAllEmployee();
        System.out.println("------------學院員工------------");
        for (CollegeEmployee e : list1) {
            System.out.println(e.getId());
        }
        //獲取到學校總部員工
        List<Employee> list2 = this.getAllEmployee();
        System.out.println("------------學校總部員工------------");
        for (Employee e : list2) {
            System.out.println(e.getId());
        }
    }
}

上述示例違反了迪米特法則,那麼應該如何改進呢?
printAllEmployee方法裏面輸出學院員工的代碼封裝到學院員工管理類CollegeManager中:

//管理學院員工的管理類
class CollegeManager {
    //返回學院的所有員工
    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 printEmployee(){
        List<CollegeEmployee> list1 = getAllEmployee();
        System.out.println("------------學院員工------------");
        for (CollegeEmployee e : list1) {
            System.out.println(e.getId());
        }
    }
}

對應的printAllEmployee方法調用我們封裝的方法即可:

//輸入學校員工和學院員工信息
    void printAllEmployee(CollegeManager sub) {
        //輸出學院的員工
        sub.printEmployee();

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

迪米特法則注意事項和細節

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

7、合成複用原則

  • 原則是儘量使用合成/聚合的方式,而不是使用繼承



    上圖中的1就是使用繼承,這樣就會增強類的耦合性,而2,3,4使用的是組合/合成/聚合的方式,這樣就可以降低類的耦合性。

設計原則核心思想

其實歸結到底就是要注意以下幾點:

  • 找出應用中可能需要變化之處,把他們獨立出來
  • 針對接口編程,而不是針對實現編程
  • 爲了交互對象之間的松耦合設計而努力
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章