面向對象編程的solid原則

  1. S - Single-responsibility Principle(單一職責原則)
  2. O - Open-closed Principle(開閉原則)
  3. L - Liskov substitution principle(裏式替換原則)
  4. I - Interface segregation principle(接口隔離原則)
  5. DDependency Inversion principle Conclusion(依賴反轉原則)

使用solid原則,可使代碼易於維護、擴展、測試和重構。html

總的來講,剛開始看可能很差掌握,可是隨着持續的使用和思考,這些原則將會成爲你的一部分。java

並且瞭解這5個原則後,再去看開源庫,會發現頗有優秀開源庫都默默遵照這些規則。編程

1、單一職責原則(Single responsibility principle)

一個類只負責一項職責。

舉例:假若有一個圖形數組,計算數組中圖形的總面積。json

1. 圓形類設計模式

#import "BXCircle.h"

@implementation BXCircle

- (instancetype)initWithRadius:(CGFloat)radius
{
    self = [super init];
    if (self) {
        self.radius = radius;
    }
    return self;
}

@end複製代碼

2. 正方形類數組

#import "BXSquare.h"

- (instancetype)initWithLength:(CGFloat)length
{
    self = [super init];
    if (self) {
        self.length = length;
    }
    return self;
}

@end複製代碼

3. 計算面積總和的類bash

#import "BXAreaCalculator.h"

- (instancetype)initWithShapes:(NSArray *)shapes
{
    self = [super init];
    if (self) {
        self.shapes = shapes;
    }
    return self;
}

- (CGFloat)sumOfShapes
{
    CGFloat sum = 0.0;
    for (id shape in _shapes) {
        if ([shape isKindOfClass:[BXCircle class]]) {
            CGFloat radius = (BXCircle *)shape.radius;
            sum = sum + radius * radius * M_PI;
        } else if ([shape isKindOfClass:[BXSquare class]]) {
            CGFloat length = (BXSquare *)shape.lenght;
            sum = sum + length * length;
        }
    }
    return sum;
}

- (NSString *)output
{
    CGFloat sumOfShapes = [self sumOfShapes];
    return [NSString stringWithFormat:@"面積總和爲%f", sumOfShapes];
}

@end複製代碼

實例化BXAreaCalculator類,傳進去一個圖形的數組,調用outPut方法就能輸出結果。架構

面積總和爲8複製代碼

而後後面來了個需求:須要用json,html的形式輸出結果框架

此時,BXAreaCalculator類就負責了兩個職責:1. 計算面積總和;2. 輸出各類形式。  測試

這時就與單一職責原則相違背了。BXAreaCalculator類應該只關心面積總和的計算,而不該該關心輸出json仍是html。 

有兩種修改方法:

  1. 直接在BXAreaCalculator類中添加各類輸出方法。(違背SRP原則)
  2. 新建一個專門負責輸出的類,在其中添加各類輸出方法。(遵循SRP原則)

 因此,我再建立一個BXSumCalculatorOutputter類,專門負責輸出各類形式。

#import "BXSumCalculatorOutputter.h"

@implementation BXSumCalculatorOutputter 

- (instancetype)initWithAreas:(CGFloat)areas
{
    self = [super init];
    if (self) {
        _areas = areas;
    }
    return self;
}

#pragma mark - public methods

- (id)JSON
{
    // 輸出json
}

- (id)HTML
{
    // 輸出HTML
}

- (id)JADE
{
    // 輸出JADE
}

- (id)XML
{
    // 輸出XML
}

@end複製代碼


以上所舉的這個例子太簡單了,它只有幾個方法,因此,不管是在代碼級別上違反單一職責原則,仍是在方法級別上違反,都不會形成太大的影響。實際應用中的類都要複雜的多,一旦發生職責擴散而須要修改類時,除非這個類自己很是簡單,不然仍是遵循單一職責原則的好。 

遵循單一職責原的優勢有: 

  1. 能夠下降類的複雜度,一個類只負責一項職責,其邏輯確定要比負責多項職責簡單的多
  2. 提升類的可讀性,提升系統的可維護性
  3. 變動引發的風險下降,變動是必然的,若是單一職責原則遵照的好,當修改一個功能時,能夠顯著下降對其餘功能的影響。 

2、開閉原則(Open-closed Principle)

This simply means that a class should be easily extendable without modifying the class itself. Let's take a look at the AreaCalculator class, especially it's sum method.
類應該能在不修改類自己的狀況下輕鬆擴展

如今看一下BXAreaCalculator類的計算面積總和的方法,sumOfShapes方法:

- (CGFloat)sumOfShapes
{
    CGFloat sum = 0.0;
    for (id shape in _shapes) {
        if ([shape isKindOfClass:[BXCircle class]]) {
            CGFloat radius = (BXCircle *)shape.radius;
            sum = sum + radius * radius * M_PI;
        } else if ([shape isKindOfClass:[BXSquare class]]) {
            CGFloat length = (BXSquare *)shape.lenght;
            sum = sum + length * length;
        }
    }
    return sum;
}複製代碼

若是想要添加其餘圖形,例如長方形,梯形等等,就要擴展sumOfShapes,在其中添加大量的if else邏輯。

應該將計算每一個圖形面積的邏輯從方法中移除,放到他自身的圖形類中。

#import "BXCircle.h"

@implementation BXCircle

- (instancetype)initWithRadius:(CGFloat)radius
{
    self = [super init];
    if (self) {
        self.radius = radius;
    }
    return self;
}

- (CGFloat)area
{
    return self.radius * self.radius * M_PI;
}

@end複製代碼

#import "BXSquare.h"

- (instancetype)initWithLength:(CGFloat)length
{
    self = [super init];
    if (self) {
        self.length = length;
    }
    return self;
}

- (CGFloat)area
{    
    return self.radius * self.radius * M_PI;
}
@end複製代碼

這樣一來,BXAreaCalculator中計算總和的方法就會很簡單

- (CGFloat)sumOfShapes
{
    // 這裏簡單這樣寫
    CGFloat sum = 0.0;
    for (id shape in _shapes) {
        SEL areaFunction = NSSelectorFromString(@"area");
        CGFloat area = ((CGFloat(*)(id,SEL))objc_msgSend)(shape, areaFunction);
        sum = sum + area;
    }
    return sum;
}複製代碼

若是再建立長方形、梯形,就不須要修改BXAreaCalculator中的代碼了。

可是又出現一個新的問題:咱們怎麼知道傳給BXAreaCalculator的數組中對象是圖形類、有area方法呢?

編寫接口是solid一個重要部分

咱們能夠建立一個協議,讓全部的圖形類都去實現它。

#import <Foundation/Foundation.h>

@protocol BXShapeInterface <NSObject>
@required

- (CGFloat)area;

@end複製代碼

BXCircle和BXSquare都實現這個協議

@interface BXCircle : NSObject <BXShapeInterface>

- (instancetype)init NS_UNAVAILABLE;

- (instancetype)initWithRadius:(CGFloat)radius NS_DESIGNATED_INITIALIZER;

@end複製代碼

@interface BXSquare : NSObject <BXShapeInterface>

- (instancetype)init NS_UNAVAILABLE;

- (instancetype)initWithLength:(CGFloat)length NS_DESIGNATED_INITIALIZER;

@end複製代碼

而後規定BXAreaCalculator中傳進來的shapes數組中的元素都遵照這個協議

- (instancetype)initWithShapes:(NSArray<id<BXShapeInterface>> *)shapes
{
    self = [super init];
    if (self) {
        _shapes = shapes;
    }
    return self;
}複製代碼



開閉原則無非就是想表達這樣一層意思:用抽象構建框架,用實現擴展細節。由於抽象靈活性好,適應性廣,只要抽象的合理,能夠基本保持軟件架構的穩定。而軟件中易變的細節,咱們用從抽象派生的實現類來進行擴展,當軟件須要發生變化時,咱們只須要根據需求從新派生一個實現類來擴展就能夠了。固然前提是咱們的抽象要合理,要對需求的變動有前瞻性和預見性才行。

3、里氏替換原則(Open-closed Principle)

Let q(x) be a property provable about objects of x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T.
對於 class T 的實例對象 x,可使q(x)成立;那麼,對於class T 的子類 classS 的實例對象 y,也要是q(y)成立。(意思是要兼容子類???)

意思是說全部的子類/派生類都應該能夠替代他的父類/基類。

確定有很多人跟我剛看到這項原則的時候同樣,對這個原則的名字充滿疑惑。其實緣由就是這項原則最先是在1988年,由麻省理工學院的一位姓裏的女士(Barbara Liskov)提出來的。

全部引用基類的地方必須能透明地使用其子類的對象。

當使用繼承時,遵循里氏替換原則。類B繼承類A時,除添加新的方法完成新增功能P2外,儘可能不要重寫父類A的方法,也儘可能不要重載父類A的方法。

繼承包含這樣一層含義:父類中凡是已經實現好的方法(相對於抽象方法而言),其實是在設定一系列的規範和契約,雖然它不強制要求全部的子類必須聽從這些契約,可是若是子類對這些非抽象方法任意修改,就會對整個繼承體系形成破壞。而里氏替換原則就是表達了這一層含義。

繼承做爲面向對象三大特性之一,在給程序設計帶來巨大便利的同時,也帶來了弊端。好比使用繼承會給程序帶來侵入性,程序的可移植性下降,增長了對象間的耦合性,若是一個類被其餘的類所繼承,則當這個類須要修改時,必須考慮到全部的子類,而且父類修改後,全部涉及到子類的功能都有可能會產生故障。

若是咱們想實現 求體積和 的 BXVolumeCalculator類,繼承自 BXAreaCalculator類。

- (NSArray *)sumOfShapes
{
    return @[@3, @5, @2];
}複製代碼

返回的是一個數組,而不是一個CGFloat類型。因此以後output的時候就會出現bug。

- (CGFloat)sumOfShapes
{
    return @6;
}複製代碼

這個例子舉得不太好,簡而言之就是在繼承父類後,不要影響子類具有的父類原有的功能。


在實際編程中,咱們經常會經過重寫父類的方法來完成新的功能,這樣寫起來雖然簡單,可是整個繼承體系的可複用性會比較差,特別是運用多態比較頻繁時,程序運行出錯的概率很是大。若是非要重寫父類的方法,比較通用的作法是:原來的父類和子類都繼承一個更通俗的基類,原有的繼承關係去掉,採用依賴、聚合,組合等關係代替。

里氏替換原則通俗的來說就是:子類能夠擴展父類的功能,但不能改變父類原有的功能。它包含如下4層含義:

  • 子類能夠實現父類的抽象方法,但不能覆蓋父類的非抽象方法。
  • 子類中能夠增長本身特有的方法。
  • 當子類的方法重載父類的方法時,方法的前置條件(即方法的形參)要比父類方法的輸入參數更寬鬆。
  • 當子類的方法實現父類的抽象方法時,方法的後置條件(即方法的返回值)要比父類更嚴格。

看上去很難以想象,由於咱們會發如今本身編程中經常會違反里氏替換原則,程序照樣跑的好好的。因此你們都會產生這樣的疑問,假如我非要不遵循里氏替換原則會有什麼後果?

後果就是:你寫的代碼出問題的概率將會大大增長。

4、接口隔離原則

A client should never be forced to implement an interface that it doesn't use or clients shouldn't be forced to depend on methods they do not use.
客戶端不該該依賴它不須要的接口;一個類對另外一個類的依賴應該創建在最小的接口上。

將臃腫的接口I拆分爲獨立的幾個接口,類A和類C分別與他們須要的接口創建依賴關係。也就是採用接口隔離原則。

未遵循接口隔離原則:


這個圖的意思是:類A依賴接口I中的方法一、方法二、方法3,類B是對類A依賴的實現。類C依賴接口I中的方法一、方法四、方法5,類D是對類C依賴的實現。對於類B和類D來講,雖然他們都存在着用不到的方法(也就是圖中紅色字體標記的方法),但因爲實現了接口I,因此也必需要實現這些用不到的方法。

接口拆分:


接口隔離原則的含義是:創建單一接口,不要創建龐大臃腫的接口,儘可能細化接口,接口中的方法儘可能少。也就是說,咱們要爲各個類創建專用的接口,而不要試圖去創建一個很龐大的接口供全部依賴它的類去調用。

說到這裏,不少人會覺的接口隔離原則跟以前的單一職責原則很類似,其實否則。其一,單一職責原則原注重的是職責;而接口隔離原則注重對接口依賴的隔離。其二,單一職責原則主要是約束類,其次纔是接口和方法,它針對的是程序中的實現和細節;而接口隔離原則主要約束接口接口,主要針對抽象,針對程序總體框架的構建。

採用接口隔離原則對接口進行約束時,要注意如下幾點:

  • 接口儘可能小,可是要有限度。對接口進行細化能夠提升程序設計靈活性是不掙的事實,可是若是太小,則會形成接口數量過多,使設計複雜化。因此必定要適度。
  • 爲依賴接口的類定製服務,只暴露給調用的類它須要的方法,它不須要的方法則隱藏起來。只有專一地爲一個模塊提供定製服務,才能創建最小的依賴關係。
  • 提升內聚,減小對外交互。使接口用最少的方法去完成最多的事情。

運用接口隔離原則,必定要適度,接口設計的過大或太小都很差。設計接口的時候,只有多花些時間去思考和籌劃,才能準確地實踐這一原則。

5、依賴倒置原則

Entities must depend on abstractions not on concretions. It states that the high level module must not depend on the low level module, but they should depend on abstractions.
高層模塊不該該直接依賴具體的底層模塊。二者都應該依賴其抽象,而不是去依賴具體細節。細節應該依賴抽象。

定義:高層模塊不該該依賴低層模塊,兩者都應該依賴其抽象;抽象不該該依賴細節;細節應該依賴抽象。

問題由來:類A直接依賴類B,假如要將類A改成依賴類C,則必須經過修改類A的代碼來達成。這種場景下,類A通常是高層模塊,負責複雜的業務邏輯;類B和類C是低層模塊,負責基本的原子操做;假如修改類A,會給程序帶來沒必要要的風險。

解決方案:將類A修改成依賴接口I,類B和類C各自實現接口I,類A經過接口I間接與類B或者類C發生聯繫,則會大大下降修改類A的概率。

依賴倒置原則基於這樣一個事實:相對於細節的多變性,抽象的東西要穩定的多。以抽象爲基礎搭建起來的架構比以細節爲基礎搭建起來的架構要穩定的多。在java中,抽象指的是接口或者抽象類,細節就是具體的實現類,使用接口或者抽象類的目的是制定好規範和契約,而不去涉及任何具體的操做,把展示細節的任務交給他們的實現類去完成。

依賴倒置原則的核心思想是面向接口編程,咱們依舊用一個例子來講明面向接口編程比相對於面向實現編程好在什麼地方。場景是這樣的,母親給孩子講故事,只要給她一本書,她就能夠照着書給孩子講故事了。代碼以下:

class Book{
	public String getContent(){
		return "好久好久之前有一個阿拉伯的故事……";
	}
}

class Mother{
	public void narrate(Book book){
		System.out.println("媽媽開始講故事");
		System.out.println(book.getContent());
	}
}

public class Client{
	public static void main(String[] args){
		Mother mother = new Mother();
		mother.narrate(new Book());
	}
} 複製代碼

運行結果:

媽媽開始講故事
好久好久之前有一個阿拉伯的故事……複製代碼

運行良好,假若有一天,需求變成這樣:不是給書而是給一份報紙,讓這位母親講一下報紙上的故事,報紙的代碼以下:

class Newspaper{
	public String getContent(){
		return "林書豪38+7領導尼克斯擊敗湖人……";
	}
} 複製代碼

這位母親卻辦不到,由於她竟然不會讀報紙上的故事,這太荒唐了,只是將書換成報紙,竟然必需要修改Mother才能讀。假如之後需求換成雜誌呢?換成網頁呢?還要不斷地修改Mother,這顯然不是好的設計。緣由就是Mother與Book之間的耦合性過高了,必須下降他們之間的耦合度才行。

咱們引入一個抽象的接口IReader。讀物,只要是帶字的都屬於讀物:

interface IReader{
	public String getContent();
} 複製代碼

Mother類與接口IReader發生依賴關係,而Book和Newspaper都屬於讀物的範疇,他們各自都去實現IReader接口,這樣就符合依賴倒置原則了,代碼修改成:

class Newspaper implements IReader {
	public String getContent(){
		return "林書豪17+9助尼克斯擊敗老鷹……";
	}
}
class Book implements IReader{
	public String getContent(){
		return "好久好久之前有一個阿拉伯的故事……";
	}
}

class Mother{
	public void narrate(IReader reader){
		System.out.println("媽媽開始講故事");
		System.out.println(reader.getContent());
	}
}

public class Client{
	public static void main(String[] args){
		Mother mother = new Mother();
		mother.narrate(new Book());
		mother.narrate(new Newspaper());
	}
}複製代碼

運行結果:

運行結果:
媽媽開始講故事
好久好久之前有一個阿拉伯的故事……
媽媽開始講故事
林書豪17+9助尼克斯擊敗老鷹……複製代碼

這樣修改後,不管之後怎樣擴展Client類,都不須要再修改Mother類了。這只是一個簡單的例子,實際狀況中,表明高層模塊的Mother類將負責完成主要的業務邏輯,一旦須要對它進行修改,引入錯誤的風險極大。因此遵循依賴倒置原則能夠下降類之間的耦合性,提升系統的穩定性,下降修改程序形成的風險。

採用依賴倒置原則給多人並行開發帶來了極大的便利,好比上例中,本來Mother類與Book類直接耦合時,Mother類必須等Book類編碼完成後才能夠進行編碼,由於Mother類依賴於Book類。修改後的程序則能夠同時開工,互不影響,由於Mother與Book類一點關係也沒有。參與協做開發的人越多、項目越龐大,採用依賴致使原則的意義就越重大。如今很流行的TDD開發模式就是依賴倒置原則最成功的應用。

傳遞依賴關係有三種方式,以上的例子中使用的方法是接口傳遞,另外還有兩種傳遞方式:構造方法傳遞和setter方法傳遞,相信用過Spring框架的,對依賴的傳遞方式必定不會陌生。

在實際編程中,咱們通常須要作到以下3點:

  • 低層模塊儘可能都要有抽象類或接口,或者二者都有。
  • 變量的聲明類型儘可能是抽象類或接口。
  • 使用繼承時遵循里氏替換原則。

依賴倒置原則的核心就是要咱們面向接口編程,理解了面向接口編程,也就理解了依賴倒置。

6、總結

說到這裏,再回想一下前面說的5項原則,偏偏是告訴咱們用抽象構建框架,用實現擴展細節的注意事項而已:單一職責原則告訴咱們實現類要職責單一;里氏替換原則告訴咱們不要破壞繼承體系;依賴倒置原則告訴咱們要面向接口編程;接口隔離原則告訴咱們在設計接口的時候要精簡單一;迪米特法則告訴咱們要下降耦合。而開閉原則是總綱,他告訴咱們要對擴展開放,對修改關閉。

最後說明一下如何去遵照這六個原則。對這六個原則的遵照並非是和否的問題,而是多和少的問題,也就是說,咱們通常不會說有沒有遵照,而是說遵照程度的多少。任何事都是過猶不及,設計模式的六個設計原則也是同樣,制定這六個原則的目的並非要咱們刻板的遵照他們,而須要根據實際狀況靈活運用。對他們的遵照程度只要在一個合理的範圍內,就算是良好的設計。咱們用一幅圖來講明一下。


圖中的每一條維度各表明一項原則,咱們依據對這項原則的遵照程度在維度上畫一個點,則若是對這項原則遵照的合理的話,這個點應該落在紅色的同心圓內部;若是遵照的差,點將會在小圓內部;若是過分遵照,點將會落在大圓外部。一個良好的設計體如今圖中,應該是六個頂點都在同心圓中的六邊形。


在上圖中,設計一、設計2屬於良好的設計,他們對六項原則的遵照程度都在合理的範圍內;設計三、設計4設計雖然有些不足,但也基本能夠接受;設計5則嚴重不足,對各項原則都沒有很好的遵照;而設計6則遵照過渡了,設計5和設計6都是迫切須要重構的設計。

參考

scotch.io/bar-talk/s-…

www.uml.org.cn/sjms/201211…

相關文章
相關標籤/搜索