面向?qū)ο笤O(shè)計(jì)原則

1.0 Design Smells

糟糕設(shè)計(jì)的癥狀【Symptoms of Poor Design】

  1. Rigidity – The design is hard to change

    • 僵化性:難于修改
    • 一個(gè)改動(dòng)導(dǎo)致系統(tǒng)其他地方被迫做出修改
  2. Fragility – The design is easy to break

    • 脆弱性:易于遭到破壞
    • 對(duì)系統(tǒng)的改動(dòng)會(huì)導(dǎo)致系統(tǒng)中與被改動(dòng)部分無概念關(guān)聯(lián)的地方出問題
  3. Immobility – The design is hard to reuse

    • 固定性:難以重用
    • 很難將系統(tǒng)分成為若干個(gè)組件,以便在其他系統(tǒng)中重用
  4. Viscosity – It is hard to do the right thing

    • 粘滯性:難以做正確的事情
    • 容易出錯(cuò),不容易正確
  5. Needless Complexity – Overdesign

    • 不必要的復(fù)雜:過度設(shè)計(jì)
    • 設(shè)計(jì)中包含不具有直接益處的基礎(chǔ)結(jié)構(gòu)
    • 設(shè)計(jì)要基于業(yè)務(wù),不是所有的系統(tǒng)都要三層,也不是都要七層
  6. Needless Repetition – Mouse abuse

    • 不必要的重復(fù):濫用復(fù)制粘貼
    • 同樣的代碼出現(xiàn)兩次就值得注意,出現(xiàn)三次就必須考慮抽離
  7. Opacity – Disorganized expression

    • 晦澀性:混亂的表達(dá)
    • 難于閱讀和理解,沒有將本來的意圖表達(dá)清楚
    • 代碼即文檔,代碼混亂導(dǎo)致系統(tǒng)難以維護(hù),高級(jí)的語言特性可能導(dǎo)致理解困難

2.0 單一職責(zé)原則(Single Responsibility Principle)

單一制職責(zé)

A class should have only one reason to change

就一個(gè)類而言,應(yīng)該僅有一個(gè)引起它(狀態(tài))變化的原因

  • Responsibility = Reason to change
    • 什么是職責(zé)?(職責(zé) = 變化的原因)
  • Separate coupled responsibilities into separate classes
    • 一個(gè)類只負(fù)責(zé)一項(xiàng)職責(zé)
  • 面相對(duì)象的關(guān)注點(diǎn):類的職責(zé)

Every object should have a single responsibility, and that responsibility should be entirely encapsulated by the class

一個(gè)對(duì)象應(yīng)該只包含單一的職責(zé),并且該職責(zé)被完整地封裝在一個(gè)類中

2.1 分析:

  • 一個(gè)類(或者大到模塊,小到方法)承擔(dān)的職責(zé)越多,被復(fù)用的可能性越小

  • 在一個(gè)類中耦合過多的職責(zé),一個(gè)職責(zé)改變可能影響其他職責(zé)

  • 類的職責(zé):

    • 數(shù)據(jù)職責(zé):屬性
    • 行為職責(zé):方法
  • 單一職責(zé)原則是實(shí)現(xiàn)高內(nèi)聚、低耦合的指導(dǎo)方針

2.2 遵循單一職責(zé)原則的好處:

  • 可以降低類的復(fù)雜度
  • 提高類的可讀性
  • 提高類的重用性
  • 提高系統(tǒng)的可維護(hù)性
  • 變更引起的風(fēng)險(xiǎn)降低

3.0 開閉原則(Open-Closed Principle)

開閉原則

Software entities (classes, modules, functions, etc.,) should be open for extension, but closed for modification

一個(gè)軟件實(shí)體(如類、模塊和函數(shù)...)應(yīng)該對(duì)擴(kuò)展開放,對(duì)修改封閉

**
也就是說在設(shè)計(jì)一個(gè)模塊的時(shí)候,應(yīng)當(dāng)使這個(gè)模塊可以在不被修改的前提下被擴(kuò)展
**

3.1 分析

  • 軟件實(shí)體可以指一個(gè)軟件模塊、一個(gè)由多個(gè)類組成的局部結(jié)構(gòu)或一個(gè)獨(dú)立的類
  • Abstraction is the Key(采用抽象化):在不被修改的前提下被擴(kuò)展
  • 對(duì)可變性封裝原則(Principle of Encapsulation of Variation, EVP)
    • 要求找到系統(tǒng)的可變因素并將其封裝起來
  • In many ways, the OCP is at the heart of object-oriented design

    • OCP是面向?qū)ο笤O(shè)計(jì)的核心原則
  • $OCP=abstraction+polymorphism+inheritance$

    • OCP原則背后的機(jī)制是抽象(abstraction)多態(tài)(polymorphism),通過繼承(inheritance)方式實(shí)現(xiàn)
  • 遵循OCP原則可獲得面向?qū)ο蠹夹g(shù)的最大好處

4.0 Liskov替換原則(Liskov Substitution Principle)

Liskov替換原則

Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it

4.1 Barbara Liskov 's word

**
What is wanted here is something like the following substitution property: If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T
**

class T{
    public void run(){
        ...
    }
}

class S extends T{
    @Override
    public void run(){
        ...
    }
}

public class P{
    public static void main(String args[]){
        S o1=new S();
        T o2=new T();

        //原來
        // o1.run();

        //替換為o2,P::main 的功能不發(fā)生變化
        o2.run();
    }
}

**
Subtypes must be substitutable for their base types
**

可以用子類型替換基類型,并且不會(huì)改變系統(tǒng)的功能

4.2 分析

  • 可以使用子類替換基類,不改變程序的功能

  • 不一定可以使用基類替換子類

  • 里氏代換原則可以通俗表述為:在軟件中如果能夠使用基類對(duì)象,那么一定能夠使用其子類對(duì)象。把基類都替換成它的子類,程序?qū)⒉粫?huì)產(chǎn)生任何錯(cuò)誤和異常,反過來則不成立,如果一個(gè)軟件實(shí)體使用的是一個(gè)子類的話,那么它不一定能夠使用基類

  • 里氏代換原則是實(shí)現(xiàn)開閉原則的重要方式之一,由于使用基類對(duì)象的地方都可以使用子類對(duì)象,因此在程序中 盡量使用基類類型來對(duì)對(duì)象進(jìn)行定義,而在運(yùn)行時(shí)再確定其子類類型,用子類對(duì)象來替換父
    類對(duì)象

4.3 實(shí)例:


/**
 * 正方形
 * 設(shè)置的width or height 會(huì)保持一致
 */
public class Square extends Rectangle {
    public void setWidth(int width) {
        super.setWidth(width);
        super.setHeight(width);
    }
    public void setHeight(int height) {
        super.setWidth(height);
        super.setHeight(height);
    }
} // 破壞了Rectangle的width-height獨(dú)立性




/**
 * 客戶端代碼
 */
public class TestLSP {
    public static void main(String[] args) {
        
        // Rectangle rec = new Rectangle();
        //這里用子類替換基類,并不是透明的,會(huì)導(dǎo)致系統(tǒng)功能改變
        Rectangle rec = new Square();
        clientOfRectangle(rec); // rec是子類Square的對(duì)象
    }
    private static void clientOfRectangle(Rectangle rec) {
        rec.setWidth(4);
        rec.setHeight(5);
        System.out.println(rec.area());
        }
    }
}

/* 解決方案:
 *      讓Rectangle 和 Square 都實(shí)現(xiàn)Shape接口
 *      在客戶端,如果是Shape,則是通用接口
 *              如果是Rectangle,也不會(huì)導(dǎo)致出錯(cuò)
 * 
解決方案

當(dāng)使用繼承時(shí),要遵循Liskov替換原則

  • 類B繼承類A時(shí),除添加新的方法完成新增功能外,盡量不要重寫父類A的方法,也盡量不要重載父類A的方法

  • 父類中已經(jīng)實(shí)現(xiàn)好的方法,實(shí)際上是在設(shè)定一系列的規(guī)范和契約(contract),雖然它不強(qiáng)制要求所有的子類必須遵從這些契約,但是如果子類對(duì)這些非抽象方法任意修改,就會(huì)對(duì)整個(gè)繼承體系造成破壞

  • 繼承在給程序設(shè)計(jì)帶來巨大便利的同時(shí),也帶來了弊端

    • 父類和子類緊耦合
    • 有時(shí)候考慮聚合/組合會(huì)更好

4.4 違反LSP原則的一些線索

  1. 派生類中把基類的某些功能去掉了,即派生類能做的事情比基類還少,說明有違反LSP原則的可能
  2. 如果派生類的方法中增加了異常的拋出,也可能會(huì)導(dǎo)致派生類對(duì)象變得不可替換,從而違反LSP原則

5.0 接口隔離原則(Interface Segregation Principle)

Clients should not be forced to depend upon interfaces that they do not use.

客戶程序不應(yīng)該被強(qiáng)制依賴它不使用的接口

  • When we bundle functions for different clients into one interface/class, we create unnecessary coupling among the clients.

    • 將不同客戶所使用的函數(shù)捆綁在一個(gè)接口/類中,相當(dāng)于在這些客戶間增加了不必要的耦合

    • When one client causes the interface to change, all other clients are forced to recompile.

      • 當(dāng)一個(gè)客戶端需要修改接口時(shí),所有其他客戶端被迫修改接口
  • Once an interface has gotten too 'fat' it needs to be split into smaller and more specific interfaces so that any clients of the interface will only know about the methods that pertain to them.

    • 一旦一個(gè)接口太復(fù)雜,則需要將它分割成一些更細(xì)小的接口,使用該接口的客戶程序僅需知道與之相關(guān)的方法即可

5.1 分析

  • 接口隔離原則是指使用多個(gè)專門的接口,而不使用單一的總接口

    • 每一個(gè)接口承擔(dān)一種相對(duì)獨(dú)立的角色,角色隔離
    • 接口僅僅提供客戶程序需要的行為,即所需的方法
      • 客戶程序不需要的行為則隱藏起來,
      • 應(yīng)當(dāng)為客戶程序提供盡可能小的單獨(dú)的接口,而不要提供大的總接口
  • 使用接口隔離原則拆分接口時(shí),首先必須滿足單一職責(zé)原則,將一組相關(guān)的操作定義在一個(gè)接口中,且在滿足高內(nèi)聚的前提下,接口中的方法越少越好

  • 可以在進(jìn)行系統(tǒng)設(shè)計(jì)時(shí)采用定制服務(wù)的方式,即為不同的客戶程序提供寬窄不同的接口,只提供用戶需要的行為,而隱藏用戶不需要的行為

5.2 ISP的策略:

  • 建立職責(zé)單一的接口,不要建立龐大臃腫的接口,接口
    中的方法盡量少
  • 接口的大小要適度,設(shè)計(jì)得過大或過小都不好

5.3 遵循ISP的好處

  • 通過分散定義多個(gè)接口,可以預(yù)防外來變更的擴(kuò)散,提高系統(tǒng)的靈活性和可維護(hù)性

6.0 依賴倒置原則(Dependency Inversion Principle)

依賴倒置原則

三種表述方式:

High-level modules should not depend on low-level modules. Both should depend on abstractions.

高層模塊不應(yīng)該依賴于低層模塊,二者都應(yīng)該 依賴于抽象

Abstractions should not depend upon details. Details should depend upon abstractions.

抽象不應(yīng)該依賴細(xì)節(jié); 細(xì)節(jié)應(yīng)該依賴抽象

Program to an interface, not an implementation

針對(duì)接口編程,不針對(duì)實(shí)現(xiàn)編程

6.1 分層

“…all well-structured object-oriented architectures have clearly defined layers, with each layer providing some coherent[1] set of services through a well-defined and controlled interface…”

面向?qū)ο蠼Y(jié)構(gòu)定義了層的概念, 每個(gè)層通過定義良好和可控的接口對(duì)外提供服務(wù)

  • Dependency Inversion: Lower-level layers is dependent upon upper-level layers.

    • 依賴的倒置:低層依賴上層
  • Ownership Inversion: The client (upper-level layer) owns the interface, not the lower-level layers

    • 所有權(quán)的倒置:上層擁有接口,而不是低層
    • 上層擁有關(guān)于下層的接口,也就是下層的服務(wù)描述
    • ssm開發(fā)中,service 層接口其實(shí)屬于controller層,serviceImpl 的實(shí)現(xiàn)依賴于高層的實(shí)現(xiàn),也就是serviceImpl 依賴于service接口

生活中,對(duì)于一條電線(高層),我們只需要知道是三角還是兩級(jí)的,然后找到對(duì)應(yīng)的插座,插上去就好了,插座內(nèi)部的實(shí)現(xiàn)就是底層。這里三角插口就是一種規(guī)范(接口),是對(duì)插座提供電源(底層服務(wù))的描述。這時(shí)候電線和三角規(guī)范是耦合的,三角規(guī)范和三角插座是耦合的,電線和三角插孔是通過三角規(guī)范耦合在一起的,拿掉這個(gè)三角規(guī)范,電線和三角插座就毫無關(guān)系。

6.2 遵循DIP原則的啟發(fā)式(heuristic)做法:

  • Depend on abstractions
    • 依賴抽象
    • Do not depend on a concrete class – that all relationships in a program should terminate on an abstract class or an interface
      • No variable should hold a pointer or reference to a concrete class
        • 任何變量都不應(yīng)該直接引用具體的類
      • No class should derive from a concrete class.
        • 沒有任何類派生自一個(gè)具體類
      • No method should override an implemented method of any of its base classes.
        • 沒有任何方法重寫在基類中已實(shí)現(xiàn)的方法

6.3 分析

  • 依賴于抽象,而不依賴于具體的類;針對(duì)接口或抽象類編程,而不是針對(duì)具體類編程

  • 實(shí)現(xiàn)開閉原則的關(guān)鍵是抽象化,并且從抽象化導(dǎo)出具體化實(shí)現(xiàn),如果說 開閉原則是面向?qū)ο笤O(shè)計(jì)的目標(biāo) 的話,那么 依賴倒置原則就是面向?qū)ο笤O(shè)計(jì)的主要手段

  • 依賴倒置原則的常用實(shí)現(xiàn)方式之一是在代碼中使用抽象類,而將具體類的連接放在配置文件中

  • 依賴倒置原則要求客戶類依賴于抽象耦合,以抽象方式耦合是依賴倒置原則的關(guān)鍵

    • 類之間的耦合有:
      • 零耦合關(guān)系
      • 具體耦合關(guān)系
      • 抽象耦合關(guān)系

6.4 面相過程vs面相對(duì)象

Traditional structural programming creates adependency structure in which policies[2] depend on details.

  • 傳統(tǒng)的面相結(jié)構(gòu)編程,策略依賴于細(xì)節(jié)
  • Policies become vulnerable[3] to changes in the details
    • 直接修改細(xì)節(jié),策略將變得非常脆弱

Object-orientation enables to invert the dependency:

  • Policy and details depend on abstractions.
    • 策略和細(xì)節(jié)依賴于抽象
  • Service interfaces are owned by their clients.
    • 客戶端擁有服務(wù)接口
  • Inversion of dependency is the hallmark of good object-oriented design.
    • 依賴倒置是面向?qū)ο笤O(shè)計(jì)的標(biāo)志

6.5 DIP原則背后的原理:

Good software designs are structured into modules

好的軟件設(shè)計(jì)是模塊化的

  • High-level modules contain the important policy decisions and business models of an application – The identity of the application.
    • 高層模塊包含應(yīng)用的重要策略決策和業(yè)務(wù)模型 — 該應(yīng)用的定義
  • Low-level modules contain detailed implementations of individual mechanisms needed to realize the policy.
    • 低層模塊包含為實(shí)現(xiàn)這些策略所需要的個(gè)體機(jī)制的具體實(shí)現(xiàn)
  • High-level Policy
    • 高層的策略(可理解為業(yè)務(wù)邏輯,Business Logic)
    • The abstraction that underlies the application;
    • The truth that does not vary when details are changed;
    • The system inside the system;
    • The metaphor[4]

7.0 合成復(fù)用原則(Composite Reuse Principle)

Favor composition of objects over inheritance as a reuse mechanism

盡量使用對(duì)象組合,而不是繼承來達(dá)到復(fù)用的目的

  • 在一個(gè)新的對(duì)象里通過關(guān)聯(lián)關(guān)系(包括組合關(guān)系和聚合關(guān)系)來使用一些已有的對(duì)象,使之成為新對(duì)象的一部分
  • 新對(duì)象通過委派調(diào)用已有對(duì)象的方法達(dá)到復(fù)用其已有功能的目的

簡(jiǎn)言之:要盡量使用組合/聚合關(guān)系,少用繼承

也稱為:組合/聚合復(fù)用原則:

Composition / Aggregate Reuse Principle, CARP

分析

面向?qū)ο笤O(shè)計(jì)-復(fù)用

  • 繼承
    • 實(shí)現(xiàn)簡(jiǎn)單,易于擴(kuò)展。
    • 破壞系統(tǒng)的封裝性,父類對(duì)子類是可見的
    • 從基類繼承而來的實(shí)現(xiàn)是靜態(tài)的,不可能在運(yùn)行時(shí)發(fā)生改變
    • 沒有足夠的靈活性;只能在有限的環(huán)境中使用。(白箱復(fù)用[5]
  • 組合/聚合
    • 耦合度相對(duì)較低,選擇性地調(diào)用成員對(duì)象的操作;可以在運(yùn)行時(shí)動(dòng)態(tài)進(jìn)行。(黑箱復(fù)用 )
    • 組合/聚合可以使系統(tǒng)更加靈活,類之間低耦合度

選擇:

  • 首選組合/聚合,其次才考慮繼承
  • 在使用繼承時(shí),需要嚴(yán)格遵循Liskov替換原則,有效使用繼承會(huì)有助于對(duì)問題的理解,降低復(fù)雜度,
  • 濫用繼承反而會(huì)增加系統(tǒng)構(gòu)建和維護(hù)的難度以及系統(tǒng)的復(fù)雜度,因此需要慎重使用繼承復(fù)用

8.0 Demeter法則(Law of Demeter)

Don't talk to strangers

不要和“陌生人”說話

Talk only to your immediate friends

只與你的直接朋友通信

Each unit should have only limited knowledge about other units: only units "closely" related to the current unit

每一個(gè)軟件單元對(duì)其他的單元都只有最少的知識(shí),而且局限于那些與本單元密切相關(guān)的軟件單元

also called: 最少知識(shí)原則(Least Knowledge Principle)

Demeter法則來自于1987年秋美國東北大學(xué)(Northeastern University)一個(gè)名為“Demeter” 的研究項(xiàng)目

簡(jiǎn)單地說,Demeter法則就是指一個(gè)軟件實(shí)體應(yīng)當(dāng)盡可能少的與其他實(shí)體發(fā)生相互作用。
這樣,當(dāng)一個(gè)模塊修改時(shí),就會(huì)盡量少地影響其他的模塊,擴(kuò)展會(huì)相對(duì)容易,這是對(duì)軟件實(shí)體之間通信的限制,它要求 限制軟件實(shí)體之間通信的寬度和深度

分析

在Demeter法則中,朋友包括:

  • 對(duì)象本身(this)
  • 以參數(shù)形式傳入到當(dāng)前對(duì)象方法中的對(duì)象
  • 當(dāng)前對(duì)象的成員對(duì)象
  • 如果當(dāng)前對(duì)象的成員對(duì)象是一個(gè)集合,那么集合中的元素也都朋友
  • 當(dāng)前對(duì)象所創(chuàng)建的對(duì)象

Demeter法則可分為 狹義法則和廣義法則

狹義的Demeter法則:如果兩個(gè)類之間不必彼此直接通信,那么這兩個(gè)類就不應(yīng)當(dāng)發(fā)生直接的相互作用,如果其中的一個(gè)類需要調(diào)用另一個(gè)類的某一個(gè)方法的話,可以通過第三者轉(zhuǎn)發(fā)這個(gè)調(diào)用

  • 可以降低類之間的耦合
  • 使一個(gè)系統(tǒng)的局部設(shè)計(jì)簡(jiǎn)化,每一個(gè)局部都不會(huì)和遠(yuǎn)距離的對(duì)象有直接的關(guān)聯(lián)
  • 但是會(huì)在系統(tǒng)中增加大量的小方法并散落在系統(tǒng)的各個(gè)角落
  • 造成系統(tǒng)的不同模塊之間的通信效率降低,使得系統(tǒng)的不同模塊之間不容易協(xié)調(diào)

廣義的Demeter法則:指對(duì)對(duì)象之間的信息流量、流向以及信息的影響的控制,主要是對(duì)信息隱藏的控制

  • 信息的隱藏可以使各個(gè)子系統(tǒng)之間脫耦,從而允許它們獨(dú)立地被開發(fā)、優(yōu)化、使用和修改,同時(shí)可以促進(jìn)軟件的復(fù)用,
  • 由于每一個(gè)模塊都不依賴于其他模塊而存在,因此每一個(gè)模塊都可以獨(dú)立地在其他的地方使用。信息不會(huì)流出本模塊
  • 一個(gè)系統(tǒng)的規(guī)模越大,信息的隱藏就越重要,而信息隱藏的重要性也就越明顯

Demeter法則的主要用途在于控制信息的過載

  • 類的劃分:盡量創(chuàng)建松耦合的類
    • 類之間的耦合度越低,就越有利于復(fù)用,一個(gè)處在松耦合中的類一旦被修改,不會(huì)對(duì)關(guān)聯(lián)的類造成太大波及
  • 類的結(jié)構(gòu)設(shè)計(jì):盡量降低其成員變量和成員函數(shù)的訪問權(quán)限
  • 類的設(shè)計(jì):盡量設(shè)計(jì)成不變類,則信息不會(huì)導(dǎo)致狀態(tài)的改變
  • 類的引用:盡量減少對(duì)其他對(duì)象的引用

總結(jié)

對(duì)于面向?qū)ο蟮能浖到y(tǒng)設(shè)計(jì)來說,在支持可維護(hù)性的同時(shí),需要提高系統(tǒng)的可復(fù)用性

軟件的復(fù)用可提高軟件開發(fā)效率,提高軟件質(zhì)量,節(jié)約開發(fā)成本,恰當(dāng)?shù)膹?fù)用還可改善系統(tǒng)的可維護(hù)性

  • 單一職責(zé)原則:一個(gè)類只負(fù)責(zé)一個(gè)功能領(lǐng)域中的相應(yīng)職責(zé)
  • 開閉原則對(duì)擴(kuò)展開放,對(duì)修改關(guān)閉
  • Liskov替換原則如果能夠使用基類對(duì)象,那么一定能夠使用其子類對(duì)象
  • 接口隔離原則接口按照職能細(xì)分,客戶端只使用最小接口,客戶端不需要的功能對(duì)客戶端隔離
  • 依賴倒置原則依賴于抽象;針對(duì)接口編程
  • 合成復(fù)用原則復(fù)用時(shí)盡量使用組合,慎用繼承
  • Demeter法則一個(gè)軟件實(shí)體應(yīng)當(dāng)盡可能少的與其他實(shí)體發(fā)生相互作用

參考:敏捷軟件開發(fā):原則、模式與實(shí)踐


  1. coherent:adj. 一致的,條理清晰的 ?

  2. policies: n. 策略 ?

  3. vulnerable: adj. 脆弱 ?

  4. metaphor:n.隱喻 ?

  5. 白箱復(fù)用:B extends A,B可以看到A中的細(xì)節(jié),一般是public 繼承

    黑箱復(fù)用:B 通過組合或聚合復(fù)用A功能

    兩者的區(qū)別在于復(fù)用是否了解被復(fù)用者的內(nèi)部細(xì)節(jié) ?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容