一篇文章帶你瞭解設計模式原理——UML圖和軟體設計原則

2022-12-28 15:00:23

一篇文章帶你瞭解設計模式原理——UML圖和軟體設計原則

我們在學習過程中可能並不會關心設計模式,但一旦牽扯到專案和麵試,設計模式就成了我們的短板

這篇文章並不會講到二十三種設計模式,但是會講解設計模式的設計原則以及設計依據和最明顯的圖形表示

或許我們只有先去了解設計模式的來源才能真正理解設計模式吧

我們該篇會提及到以下內容:

  • 設計模式概述
  • UML圖
  • 軟體設計原理

設計模式概述

既然我們要講述設計模式原理,自然就需要先來了解設計模式了

軟體設計模式產生背景

其實我們軟體的很多概念往往來自於其他專業,設計模式就是其中一個:

  • "設計模式"最初並不是出現在軟體設計中,而是被用於建築領域的設計中。

我們的設計模式就是根據建築領域中設計模式的概念而產生的:

  • 1977年Christopher Alexander提出了 253 種關於對城鎮、鄰里、住宅、花園和房間等進行設計的基本模式。
  • 1990年軟體工程界開始研討設計模式的話題,最終在《設計模式:可複用物件導向軟體的基礎》一書中收錄了23 個設計模式

軟體設計模式基本概念

我們以官方角度來講述設計模式:

  • 軟體設計模式(Software Design Pattern),是一套被反覆使用、多數人知曉的、經過分類編目的、程式碼設計經驗的總結。

簡單來說,設計模式就是前輩們的經驗之談:

  • 它描述了在軟體設計過程中的一些不斷重複發生的問題,以及該問題的解決方案。
  • 也就是說,它是解決特定問題的一系列套路,是前輩們的程式碼設計經驗的總結,具有一定的普遍性,可以反覆使用。

軟體設計模式學習必要性

設計模式的本質是物件導向設計原則的實際運用,是對類的封裝性、繼承性和多型性以及類的關聯關係和組合關係的充分理解。

正確使用設計模式具有以下優點:

  • 可以提高程式設計師的思維能力、程式設計能力和設計能力
  • 使程式設計更加標準化、程式碼編制更加工程化,使軟體開發效率大大提高,從而縮短軟體的開發週期
  • 使設計的程式碼可重用性高、可讀性強、可靠性高、靈活性好、可維護性強

軟體設計模式簡單分類

我們在未正式學習設計模式之前先去簡單瞭解一下設計模式的主要三種分類:

  • 建立型模式

    用於描述「怎樣建立物件」,它的主要特點是「將物件的建立與使用分離」。

    書中提供了單例、原型、工廠方法、抽象工廠、建造者等 5 種建立型模式。

  • 結構型模式

    用於描述如何將類或物件按某種佈局組成更大的結構

    書中提供了代理、介面卡、橋接、裝飾、外觀、享元、組合等 7 種結構型模式。

  • 行為型模式

    用於描述類或物件之間怎樣相互共同作業共同完成單個物件無法單獨完成的任務,以及怎樣分配職責。

    書中提供了模板方法、策略、命令、職責鏈、狀態、觀察者、中介者、迭代器、存取者、備忘錄、直譯器等 11 種行為型模式。

UML圖

統一建模語言(UML)是用來設計軟體的視覺化建模語言。它的特點是簡單、統一、圖形化、能表達軟體設計中的動態與靜態資訊。

UML 從目標系統的不同角度出發,定義了用例圖、類圖、物件圖、狀態圖、活動圖、時序圖、共同作業圖、構件圖、部署圖等 9 種圖。

類圖概述

我們在設計模式中最常用的無非只有類圖:

  • 類圖(Class diagram)是顯示了模型的靜態結構,特別是模型中存在的類、類的內部結構以及它們與其他類的關係等。
  • 類圖不顯示暫時性的資訊。類圖是物件導向建模的主要組成部分。

類圖作用

類圖主要具有以下兩種作用:

  • 在軟體工程中,類圖是一種靜態的結構圖,描述了系統的類的集合,類的屬性和類之間的關係,可以簡化了人們對系統的理解;
  • 類圖是系統分析和設計階段的重要產物,是系統編碼和測試的重要模型。

類表示法

首先我們先來介紹UML中類的基本表示:

  • 在UML類圖中,類使用包含類名、屬性(field) 和方法(method) 且帶有分割線的矩形來表示

屬性/方法名稱前加的加號和減號表示了這個屬性/方法的可見性:

  • +:表示public
  • -:表示private
  • #:表示protected

那麼我們就可以給出對應的屬性方法的完整表達方式:

  • 屬性的完整表示方式是: 可見性 名稱 :型別 [ = 預設值]
  • 方法的完整表示方式是: 可見性 名稱(參數列) [ : 返回型別]

我們給出一個簡單的範例圖:

我們可以對上述的類圖進行一個簡單的解析:

  • 類名:Employee
  • 屬性:private String name,private int age,private String address
  • 方法:public void work()

類關係表示法

類關係表示法大致分為關聯關係,繼承關係,實現關係

關聯關係

首先我們先來介紹一下關聯關係:

  • 關聯關係是物件之間的一種參照關係,用於表示一類物件與另一類物件之間的聯絡,如老師和學生、師傅和徒弟、丈夫和妻子等。
  • 關聯關係是類與類之間最常用的一種關係,分為一般關聯關係、聚合關係,組合關係和依賴關係。
單項關聯

在UML類圖中單向關聯用一個帶箭頭的實線表示。

上圖表示每個顧客都有一個地址,這通過讓Customer類持有一個型別為Address的成員變數類實現。

雙向關聯

在UML類圖中,雙向關聯用一個不帶箭頭的直線表示。

從上圖中我們很容易看出,所謂的雙向關聯就是雙方各自持有對方型別的成員變數。上圖中在Customer類中維護一個List<Product>,表示一個顧客可以購買多個商品;在Product類中維護一個Customer型別的成員變數表示這個產品被哪個顧客所購買。

自關聯

自關聯在UML類圖中用一個帶有箭頭且指向自身的線表示。

上圖的意思就是Node類包含型別為Node的成員變數,也就是「自己包含自己」。

聚合關係

在 UML 類圖中,聚合關係可以用帶空心菱形的實線來表示,菱形指向整體。

聚合關係是關聯關係的一種,是強關聯關係,是整體和部分之間的關係。

聚合關係也是通過成員物件來實現的,其中成員物件是整體物件的一部分,但是成員物件可以脫離整體物件而獨立存在。例如,學校與老師的關係,學校包含老師,但如果學校停辦了,老師依然存在。

組合關係

在 UML 類圖中,組合關係用帶實心菱形的實線來表示,菱形指向整體。

組合表示類之間的整體與部分的關係,但它是一種更強烈的聚合關係。

在組合關係中,整體物件可以控制部分物件的生命週期,一旦整體物件不存在,部分物件也將不存在,部分物件不能脫離整體物件而存在。例如,頭和嘴的關係,沒有了頭,嘴也就不存在了。

依賴關係

在 UML 類圖中,依賴關係使用帶箭頭的虛線來表示,箭頭從使用類指向被依賴的類。

依賴關係是一種使用關係,它是物件之間耦合度最弱的一種關聯方式,是臨時性的關聯。在程式碼中,某個類的方法通過區域性變數、方法的引數或者對靜態方法的呼叫來存取另一個類(被依賴類)中的某些方法來完成一些職責。

繼承關係

繼承關係是物件之間耦合度最大的一種關係,表示一般與特殊的關係,是父類別與子類之間的關係,是一種繼承關係。

在 UML 類圖中,泛化關係用帶空心三角箭頭的實線來表示,箭頭從子類指向父類別。在程式碼實現時,使用物件導向的繼承機制來實現泛化關係。例如,Student 類和 Teacher 類都是 Person 類的子類,其類圖如下圖所示:

實現關係

實現關係是介面與實現類之間的關係。在這種關係中,類實現了介面,類中的操作實現了介面中所宣告的所有的抽象操作。

在 UML 類圖中,實現關係使用帶空心三角箭頭的虛線來表示,箭頭從實現類指向介面。例如,汽車和船實現了交通工具,其類圖如圖 9 所示。

軟體設計原則

在軟體開發中,為了提高軟體系統的可維護性和可複用性,增加軟體的可延伸性和靈活性,程式設計師要儘量根據6條原則來開發程式,從而提高軟體開發效率、節約軟體開發成本和維護成本,同時設計模式也是根據這些原則所產生的。

開閉原則

首先我們給出官方解釋:

  • 對擴充套件開放,對修改關閉
  • 在程式需要進行拓展的時候,不能去修改原有的程式碼。簡言之,是為了使程式的擴充套件性好,易於維護和升級。

那麼從我們的視角來看是怎樣的:

  • 利用介面和抽象類來實現上述原則
  • 我們使用抽象類完成眾多子類都具有的方法以減少程式碼冗餘,我們使用介面去規定子類的行為但不對其內部細節進行約束

我們來給出一個簡單的範例:

我們對上圖進行解釋並講解:

/* 案例介紹 */

【例】`搜狗輸入法` 的面板設計。

分析:
    `搜狗輸入法` 的面板是輸入法背景圖片、視窗顏色和聲音等元素的組合。
    使用者可以根據自己的喜愛更換自己的輸入法的面板,也可以從網上下載新的面板。
    這些面板有共同的特點,可以為其定義一個抽象類(AbstractSkin),而每個具體的面板是其子類。
    使用者表單可以根據需要選擇或者增加新的主題,而不需要修改原始碼,所以它是滿足開閉原則的。
    
/* 程式碼展示 */

/**
 * @version v1.0
 * @ClassName: Client
 * @Description: 測試程式碼
 */
public class Client {
    public static void main(String[] args) {
        //1,建立搜狗輸入法物件
        SougouInput input = new SougouInput();
        //2,建立面板物件
        //DefaultSkin skin = new DefaultSkin();
        HeimaSkin skin = new HeimaSkin();
        //3,將面板設定到輸入法中
        input.setSkin(skin);

        //4,顯示面板
        input.display();
    }
}
    
/**
 * @version v1.0
 * @ClassName: SougouInput
 * @Description: 搜狗輸入法
 */
public class SougouInput {

    private AbstractSkin skin;

    public void setSkin(AbstractSkin skin) {
        this.skin = skin;
    }

    public void display() {
        skin.display();
    }
}

/**
 * @version v1.0
 * @ClassName: AbstractSkin
 * @Description: 抽象面板類
 */
public abstract class AbstractSkin {

    //顯示的方法
    public abstract void display();
}

/**
 * @version v1.0
 * @ClassName: DefaultSkin
 * @Description: 預設面板類
 */
public class DefaultSkin extends AbstractSkin {

    public void display() {
        System.out.println("預設面板");
    }
}

/**
 * @version v1.0
 * @ClassName: HeimaSkin
 * @Description: 黑馬程式設計師面板
 */
public class HeimaSkin extends AbstractSkin {

    public void display() {
        System.out.println("黑馬面板");
    }
}

里氏代換原則

首先我們給出官方解釋:

  • 任何基礎類別可以出現的地方,子類一定可以出現。

從我們的視角來解釋:

  • 子類可以擴充套件父類別的功能,但不能改變父類別原有的功能
  • 子類繼承父類別時,除新增新的方法完成新增功能外,儘量不要重寫父類別的方法
  • 如果通過重寫父類別的方法來完成新的功能,整個繼承體系的可複用性會比較差

我們給出一個對比案例來展示里氏代換原則:

  1. 正方形不是長方形

我們對問題和上圖進行解釋:

/* 問題展示 */

【例】正方形不是長方形。

在數學領域裡,正方形毫無疑問是長方形,它是一個長寬相等的長方形。
所以,我們開發的一個與幾何圖形相關的軟體系統,就可以順理成章的讓正方形繼承自長方形。
    
/* 程式碼展示 */
    
// 但我們可以作用在長方形的方法,如果作用在正方形上卻不可以,這就說明正方形不能繼承長方形

/**
 * @version v1.0
 * @ClassName: RectangleDemo
 * @Description: 測試方法,這裡的resize方法只能作用在長方形,卻不能作用於正方形,但正方形繼承於長方形導致思維錯誤
 */
public class RectangleDemo {

    public static void main(String[] args) {
        //建立長方形物件
        Rectangle r = new Rectangle();
        //設定長和寬
        r.setLength(20);
        r.setWidth(10);
        //呼叫resize方法進行擴寬
        resize(r);
        printLengthAndWidth(r);

        System.out.println("==================");
        //建立正方形物件
        Square s = new Square();
        //設定長和寬
        s.setLength(10);
        //呼叫resize方法進行擴寬
        resize(s);
        printLengthAndWidth(s);
    }

    //擴寬方法
    public static void resize(Rectangle rectangle) {
        //判斷寬如果比長小,進行擴寬的操作
        while(rectangle.getWidth() <= rectangle.getLength()) {
            rectangle.setWidth(rectangle.getWidth() + 1);
        }
    }

    //列印長和寬
    public static void printLengthAndWidth(Rectangle rectangle) {
        System.out.println(rectangle.getLength());
        System.out.println(rectangle.getWidth());
    }
}

/**
 * @version v1.0
 * @ClassName: Rectangle
 * @Description: 長方形類
 */
public class Rectangle {
    private double length;
    private double width;

    public double getLength() {
        return length;
    }

    public void setLength(double length) {
        this.length = length;
    }

    public double getWidth() {
        return width;
    }

    public void setWidth(double width) {
        this.width = width;
    }
}

package com.itheima.principles.demo2.before;

/**
 * @version v1.0
 * @ClassName: Square
 * @Description: 正方形類
 */
public class Square extends Rectangle {

    @Override
    public void setLength(double length) {
        super.setLength(length);
        super.setWidth(length);
    }

    @Override
    public void setWidth(double width) {
        super.setLength(width);
        super.setWidth(width);
    }
}
  1. 正方形,長方形都是四邊形

我們對問題和上圖進行解釋:

/* 問題解釋 */

抽象出來一個四邊形介面(Quadrilateral),讓Rectangle類和Square類實現Quadrilateral介面
    
/* 程式碼展示 */

/**
 * @version v1.0
 * @ClassName: RectangleDemo
 * @Description: 測試類,這裡測試僅對長方形類測試,而不對正方形測試
 */
public class RectangleDemo {
    public static void main(String[] args) {
        //建立長方形物件
        Rectangle r = new Rectangle();
        r.setLength(20);
        r.setWidth(10);
        //呼叫方法進行擴寬操作
        resize(r);

        printLengthAndWidth(r);
    }

    //擴寬的方法
    public static void resize(Rectangle rectangle) {
        //判斷寬如果比長小,進行擴寬的操作
        while(rectangle.getWidth() <= rectangle.getLength()) {
            rectangle.setWidth(rectangle.getWidth() + 1);
        }
    }

    //列印長和寬
    public static void printLengthAndWidth(Quadrilateral quadrilateral) {
        System.out.println(quadrilateral.getLength());
        System.out.println(quadrilateral.getWidth());
    }
}

/**
 * @version v1.0
 * @ClassName: Quadrilateral
 * @Description: 四邊形介面
 */
public interface Quadrilateral {

    //獲取長
    double getLength();

    //獲取寬
    double getWidth();
}

/**
 * @version v1.0
 * @ClassName: Rectangle
 * @Description: 長方形類
 */
public class Rectangle implements Quadrilateral {

    private double length;
    private double width;

    public void setLength(double length) {
        this.length = length;
    }

    public void setWidth(double width) {
        this.width = width;
    }

    public double getLength() {
        return length;
    }

    public double getWidth() {
        return width;
    }
}

/**
 * @version v1.0
 * @ClassName: Square
 * @Description: 正方形
 */
public class Square implements Quadrilateral {

    private double side;

    public double getSide() {
        return side;
    }

    public void setSide(double side) {
        this.side = side;
    }

    public double getLength() {
        return side;
    }

    public double getWidth() {
        return side;
    }
}

依賴倒轉原則

我們首先用官方話語解釋:

  • 高層模組不應該依賴低層模組,兩者都應該依賴其抽象;抽象不應該依賴細節,細節應該依賴抽象。

從我們的視角解釋就是:

  • 對抽象進行程式設計,不要對實現進行程式設計,這樣就降低了客戶與實現模組間的耦合。

下面看一個例子來理解依賴倒轉原則:

  1. 假設我們直接依賴於實現類

我們對問題和上圖進行解釋:

/* 問題解釋 */

【例】組裝電腦

現要組裝一臺電腦,需要配件cpu,硬碟,記憶體條。只有這些設定都有了,計算機才能正常的執行。選擇cpu有很多選擇,如Intel,AMD等,硬碟可以選擇希捷,西數等,記憶體條可以選擇金士頓,海盜船等。
    
/* 程式碼展示 */
    
// 如果我們直接在Computer中設定對應的cpu,硬碟,記憶體條的實現類也就是特定的型號,那麼我們這臺電腦就只能使用這些型號

/**
 * @version v1.0
 * @ClassName: ComputerDemo
 * @Description: 測試類
 */
public class ComputerDemo {
    public static void main(String[] args) {
        //建立元件物件
        XiJieHardDisk hardDisk = new XiJieHardDisk();
        IntelCpu cpu = new IntelCpu();
        KingstonMemory memory = new KingstonMemory();

        //建立計算機物件
        Computer c = new Computer();
        //組裝計算機
        c.setCpu(cpu);
        c.setHardDisk(hardDisk);
        c.setMemory(memory);

        //執行計算機
        c.run();
    }
}

/**
 * @version v1.0
 * @ClassName: Computer
 * @Description: 電腦
 */
public class Computer {

    // 注意:這裡的屬性直接使用了實現類,也就是我們無法更換硬體型號
    private XiJieHardDisk hardDisk;
    private IntelCpu cpu;
    private KingstonMemory memory;

    public XiJieHardDisk getHardDisk() {
        return hardDisk;
    }

    public void setHardDisk(XiJieHardDisk hardDisk) {
        this.hardDisk = hardDisk;
    }

    public IntelCpu getCpu() {
        return cpu;
    }

    public void setCpu(IntelCpu cpu) {
        this.cpu = cpu;
    }

    public KingstonMemory getMemory() {
        return memory;
    }

    public void setMemory(KingstonMemory memory) {
        this.memory = memory;
    }

    public void run() {
        System.out.println("執行計算機");
        String data = hardDisk.get();
        System.out.println("從硬碟上獲取的資料是:" + data);
        cpu.run();
        memory.save();
    }
}

/**
 * @version v1.0
 * @ClassName: XiJieHardDisk
 * @Description: 希捷硬碟
 */
public class XiJieHardDisk {

    //儲存資料的方法
    public void save(String data) {
        System.out.println("使用希捷硬碟儲存資料為:" + data);
    }

    //獲取資料的方法
    public String get() {
        System.out.println("使用希捷希捷硬碟取資料");
        return "資料";
    }
}

/**
 * @version v1.0
 * @ClassName: IntelCpu
 * @Description: Intel cpu
 */
public class IntelCpu {

    public void run() {
        System.out.println("使用Intel處理器");
    }
}

/**
 * @version v1.0
 * @ClassName: KingstonMemory
 * @Description: 金士頓記憶體條類
 */
public class KingstonMemory {

    public void save() {
        System.out.println("使用金士頓記憶體條");
    }
}
  1. 假設我們依賴於介面,裝配用實現類

我們對問題和上圖進行解釋:

/* 問題解釋 */

目前我們的電腦屬性連線的是介面,而該介面可以去實現多型號的硬體,說明我們的電腦可以採用多種設定方式
    
/* 程式碼展示 */

/**
 * @version v1.0
 * @ClassName: ComputerDemo
 * @Description: 測試類,為電腦裝配不同的設定
 */
public class ComputerDemo {
    public static void main(String[] args) {

        //建立計算機的元件物件
        HardDisk hardDisk = new XiJieHardDisk();
        Cpu cpu = new IntelCpu();
        Memory memory = new KingstonMemory();

        //建立計算機物件
        Computer c = new Computer();
        //組裝計算機
        c.setCpu(cpu);
        c.setHardDisk(hardDisk);
        c.setMemory(memory);

        //執行計算機
        c.run();
    }
}

    
/**
 * @version v2.0
 * @ClassName: Computer
 * @Description: 電腦
 */
public class Computer {

    // 注意這裡採用的是介面,我們具體的介面實現可以依賴於不同的實現類
    private HardDisk hardDisk;
    private Cpu cpu;
    private Memory memory;

    public HardDisk getHardDisk() {
        return hardDisk;
    }

    public void setHardDisk(HardDisk hardDisk) {
        this.hardDisk = hardDisk;
    }

    public Cpu getCpu() {
        return cpu;
    }

    public void setCpu(Cpu cpu) {
        this.cpu = cpu;
    }

    public Memory getMemory() {
        return memory;
    }

    public void setMemory(Memory memory) {
        this.memory = memory;
    }

    public void run() {
        System.out.println("計算機工作");
    }
}

// 後續就是硬體的介面和實現類,這裡就不贅述了

介面隔離原則

首先我們給出官方解釋:

  • 使用者端不應該被迫依賴於它不使用的方法
  • 一個類對另一個類的依賴應該建立在最小的介面上。

從我們的角度來解釋:

  • 一個類所使用多少功能,在它的父類別或者介面上就應該只具有這些功能
  • 將多功能分散在多介面中,只為該類繼承它所需要使用的介面

我們同樣給出案例:

  1. 介面多功能繼承

我們針對上圖和問題進行解釋:

/* 問題解釋 */

【例】安全門案例

我們需要建立一個`黑馬`品牌的安全門,該安全門具有防火、防水、防盜的功能。可以將防火,防水,防盜功能提取成一個介面,形成一套規範。
    
現在如果我們還需要再建立一個傳智品牌的安全門,而該安全門只具有防盜、防水功能,很顯然如果實現SafetyDoor介面就違背了介面隔離原則
    
/* 程式碼展示 */

/**
 * @version v1.0
 * @ClassName: Client
 * @Description: 測試
 */
public class Client {
    public static void main(String[] args) {
        HeimaSafetyDoor door = new HeimaSafetyDoor();
        door.antiTheft();
        door.fireProof();
        door.waterProof();
    }
}

/**
 * @version v1.0
 * @ClassName: SafetyDoor
 * @Description: 多功能介面
 */
public interface SafetyDoor {

    //防盜
    void antiTheft();

    //防火
    void fireProof();

    //防水
    void waterProof();
}

/**
 * @version v1.0
 * @ClassName: HeimaSafetyDoor
 * @Description: 黑馬品牌的安全門
 */
public class HeimaSafetyDoor implements SafetyDoor {
    public void antiTheft() {
        System.out.println("防盜");
    }

    public void fireProof() {
        System.out.println("防火");
    }

    public void waterProof() {
        System.out.println("防水");
    }
}
  1. 介面單功能繼承

我們針對上圖和問題進行解釋:

/* 問題解釋 */

現在我們將介面的功能拆分為多介面,我們的類想要使用單個介面就可以直接繼承單個介面
    
/* 程式碼展示 */

/**
 * @version v1.0
 * @ClassName: Client
 * @Description: TODO(一句話描述該類的功能)
 */
public class Client {
    public static void main(String[] args) {
        //建立黑馬安全門物件
        HeimaSafetyDoor door = new HeimaSafetyDoor();
        //呼叫功能
        door.antiTheft();
        door.fireProof();
        door.waterProof();

        System.out.println("============");
        //建立傳智安全門物件
        ItcastSafetyDoor door1 = new ItcastSafetyDoor();
        //呼叫功能
        door1.antiTheft();
        door1.fireproof();
    }
}

/**
 * @version v1.0
 * @ClassName: HeiMaSafetyDoor
 * @Description: TODO(一句話描述該類的功能)
 */
public class HeiMaSafetyDoor implements AntiTheft,Fireproof,Waterproof {
    public void antiTheft() {
        System.out.println("防盜");
    }

    public void fireproof() {
        System.out.println("防火");
    }

    public void waterproof() {
        System.out.println("防水");
    }
}

/**
 * @version v1.0
 * @ClassName: ItcastSafetyDoor
 * @Description: 傳智安全門
 */
public class ItcastSafetyDoor implements AntiTheft,Fireproof {
    public void antiTheft() {
        System.out.println("防盜");
    }

    public void fireproof() {
        System.out.println("防火");
    }
}

public interface AntiTheft {
    void antiTheft();
}

public interface Fireproof {
    void fireproof();
}

public interface Waterproof {
    void waterproof();
}

迪米特法則

首先我們給出官方解釋:

  • 如果兩個軟體實體無須直接通訊,那麼就不應當發生直接的相互呼叫,可以通過第三方轉發該呼叫。
  • 其目的是降低類之間的耦合度,提高模組的相對獨立性。

從我們的視角來解釋:

  • 只和你的直接朋友交談,不跟「陌生人」說話(Talk only to your immediate friends and not to strangers)
  • 迪米特法則中的「朋友」是指:當前物件本身、當前物件的成員物件、當前物件所建立的物件、當前物件的方法引數等
  • 這些物件同當前物件存在關聯、聚合或組合關係,可以直接存取這些物件的方法。

我們給出一個簡單的案例進行解釋:

我們對上述問題和圖進行解釋:

/* 問題展示 */

【例】明星與經紀人的關係範例

明星由於全身心投入藝術,所以許多日常事務由經紀人負責處理,如和粉絲的見面會,和媒體公司的業務洽淡等。這裡的經紀人是明星的朋友,而粉絲和媒體公司是陌生人,所以適合使用迪米特法則。
    
/* 程式碼展示 */

/**
 * @version v1.0
 * @ClassName: Client
 * @Description: 測試類
 */
public class Client {
    public static void main(String[] args) {
        //建立經紀人類
        Agent agent = new Agent();
        //建立明星物件
        Star star = new Star("林青霞");
        agent.setStar(star);
        //建立粉絲物件
        Fans fans = new Fans("李四");
        agent.setFans(fans);
        //建立媒體公司物件
        Company company = new Company("黑馬媒體公司");
        agent.setCompany(company);

        agent.meeting();//和粉絲見面
        agent.business();//和媒體公司洽談業務
    }
}

/**
 * @version v1.0
 * @ClassName: Agent
 * @Description: 經紀人類
 */
public class Agent {

    private Star star;
    private Fans fans;
    private Company company;

    public void setStar(Star star) {
        this.star = star;
    }

    public void setFans(Fans fans) {
        this.fans = fans;
    }

    public void setCompany(Company company) {
        this.company = company;
    }

    //和粉絲見面的方法
    public void meeting() {
        System.out.println(star.getName() + "和粉絲" + fans.getName() + "見面");
    }

    //和媒體公司洽談的方法
    public void business() {
        System.out.println(star.getName() + "和" + company.getName() + "洽談");
    }
}

/**
 * @version v1.0
 * @ClassName: Company
 * @Description: 媒體公司類
 */
public class Company {
    private String name;

    public String getName() {
        return name;
    }

    public Company(String name) {
        this.name = name;
    }
}

/**
 * @version v1.0
 * @ClassName: Fans
 * @Description: 粉絲類
 */
public class Fans {

    private String name;

    public String getName() {
        return name;
    }

    public Fans(String name) {
        this.name = name;
    }
}

/**
 * @version v1.0
 * @ClassName: Star
 * @Description: 明星類
 */
public class Star {
    private String name;

    public Star(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

合成複用原則

我們給出官方解釋:

  • 儘量先使用組合或者聚合等關聯關係來實現,其次才考慮使用繼承關係來實現。

通常類的複用分為繼承複用和合成複用兩種。

繼承複用雖然有簡單和易實現的優點,但它也存在以下缺點:

  • 繼承複用破壞了類的封裝性。因為繼承會將父類別的實現細節暴露給子類,父類別對子類是透明的,所以這種複用又稱為「白箱」複用。

  • 子類與父類別的耦合度高。父類別的實現的任何改變都會導致子類的實現發生變化,這不利於類的擴充套件與維護。

  • 它限制了複用的靈活性。從父類別繼承而來的實現是靜態的,在編譯時已經定義,所以在執行時不可能發生變化。

採用組合或聚合複用時,可以將已有物件納入新物件中,使之成為新物件的一部分,新物件可以呼叫已有物件的功能,它有以下優點:

  • 它維持了類的封裝性。因為成分物件的內部細節是新物件看不見的,所以這種複用又稱為「黑箱」複用。

  • 物件間的耦合度低。可以在類的成員位置宣告抽象。

  • 複用的靈活性高。這種複用可以在執行時動態進行,新物件可以動態地參照與成分物件型別相同的物件。

最後我們給出兩張圖來介紹為什麼組合優於繼承:

  1. 繼承圖

  1. 組合圖

我們進行簡單的解釋:

/* 問題解釋 */

【例】汽車分類管理程式

汽車按「動力源」劃分可分為汽油汽車、電動汽車等;按「顏色」劃分可分為白色汽車、黑色汽車和紅色汽車等。如果同時考慮這兩種分類,其組合就很多。 
   
/* 圖形介紹 */
    
繼承圖:我們會發現我們每多一種屬性,就需要建立多個子類,類的建立是非常耗費資源的,上述多類的建立也是資源耗費的一種
    
組合圖:我們將部分屬性以屬性的形式介入,並採用介面存放,後續我們只需要更新其介面就可以更新多種實體類,節省資源的使用

結束語

關於設計模式原理我們就介紹到這裡,後面我會繼續更新二十三種設計模式,希望能給你帶來幫助~

附錄

該文章屬於學習內容,具體參考B站黑馬程式設計師的Java設計模式詳解

這裡附上視訊連結:黑馬程式設計師Java設計模式詳解, 23種Java設計模式(圖解+框架原始碼分析+實戰)_嗶哩嗶哩_bilibili