面向?qū)ο笾笤O(shè)計原則

一、六大設(shè)計原則

縮寫 英文名稱 中文名稱
SRP Single Responsibility Principle 單一職責(zé)原則
OCP Open Close Principle 開閉原則
LSP Liskov Substitution Principle 里氏替換原則
LoD Law of Demeter ( Least Knowledge Principle) 迪米特原則
ISP Interface Segregation Principle 接口分離原則
DIP Dependency Inversion Principle 依賴倒置原則

原則一:單一職責(zé)原則

定義

Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.
單一職責(zé)原則的定義是就一個類而言,應(yīng)該僅有一個引起他變化的原因。也就是說一個類應(yīng)該只負責(zé)一件事情。

1、類職責(zé)的變化往往就是導(dǎo)致類變化的原因:也就是說如果一個類具有多種職責(zé),就會有多種導(dǎo)致這個類變化的原因,從而導(dǎo)致這個類的維護變得困難。
2、往往在軟件開發(fā)中隨著需求的不斷增加,可能會給原來的類添加一些本來不屬于它的一些職責(zé),從而違反了單一職責(zé)原則。如果我們發(fā)現(xiàn)當(dāng)前類的職責(zé)不僅僅有一個,就應(yīng)該將本來不屬于該類真正的職責(zé)分離出去。
3、不僅僅是類,函數(shù)(方法)也要遵循單一職責(zé)原則,即:一個函數(shù)(方法)只做一件事情。如果發(fā)現(xiàn)一個函數(shù)(方法)里面有不同的任務(wù),則需要將不同的任務(wù)以另一個函數(shù)(方法)的形式分離出去。

優(yōu)點

  • 可以降低類的復(fù)雜度,一個類只負責(zé)一項職責(zé),這樣邏輯也簡單很多
  • 提高類的可讀性,和系統(tǒng)的維護性,因為不會有其他奇怪的方法來干擾我們理解這個類的含義
  • 當(dāng)發(fā)生變化的時候,能將變化的影響降到最小,因為只會在這個類中做出修改。

案例分析

初始需求:需要創(chuàng)造一個員工類,這個類有員工的一些基本信息。

新需求:增加兩個方法:

  • 判定員工在今年是否升職
  • 計算員工的薪水
不好的設(shè)計
//================== Employee.h ==================

@interface Employee : NSObject

//============ 初始需求 ============
@property (nonatomic, copy) NSString *name;       //員工姓名
@property (nonatomic, copy) NSString *address;    //員工住址
@property (nonatomic, copy) NSString *employeeID; //員工ID

//============ 新需求 ============
//計算薪水
- (double)calculateSalary;
//今年是否晉升
- (BOOL)willGetPromotionThisYear;

@end

新需求的做法看似沒有問題,因為都是和員工有關(guān)的,但卻違反了單一職責(zé)原則:因為這兩個方法并不是員工本身的職責(zé)。

  • calculateSalary這個方法的職責(zé)是屬于會計部門的:薪水的計算是會計部門負責(zé)。
  • willPromotionThisYear這個方法的職責(zé)是屬于人事部門的:考核與晉升機制是人事部門負責(zé)。

而上面的設(shè)計將本來不屬于員工自己的職責(zé)強加進了員工類里面,而這個類的設(shè)計初衷(原始職責(zé))就是單純地保留員工的一些信息而已。因此這么做就是給這個類引入了新的職責(zé),故此設(shè)計違反了單一職責(zé)原則。
我們可以簡單想象一下這么做的后果是什么:如果員工的晉升機制變了,或者稅收政策等影響員工工資的因素變了,我們還需要修改當(dāng)前這個類。

那么怎么做才能不違反單一職責(zé)原則呢?- 我們需要將這兩個方法(責(zé)任)分離出去,讓本應(yīng)該處理這類任務(wù)的類來處理。

好的設(shè)計

我們保留員工類的基本信息:

//================== Employee.h ==================

@interface Employee : NSObject

//初始需求
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *address;
@property (nonatomic, copy) NSString *employeeID;

@end

接著創(chuàng)建新的會計部門類:

//================== FinancialApartment.h ==================

#import "Employee.h"

//會計部門類
@interface FinancialApartment : NSObject

//計算薪水
- (double)calculateSalary:(Employee *)employee;

@end

和人事部門類:

//================== HRApartment.h ==================

#import "Employee.h"

//人事部門類
@interface HRApartment : NSObject

//今年是否晉升
- (BOOL)willGetPromotionThisYear:(Employee*)employee;

@end

通過創(chuàng)建了兩個分別專門處理薪水和晉升的部門,會計部門和人事部門的類:FinancialApartment 和 HRApartment,把兩個任務(wù)(責(zé)任)分離了出去,讓本該處理這些職責(zé)的類來處理這些職責(zé)。

這樣一來,不僅僅在此次新需求中滿足了單一職責(zé)原則,以后如果還要增加人事部門和會計部門處理的任務(wù),就可以直接在這兩個類里面添加即可。

原則二:開閉原則

定義

Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.
開閉原則的定義是軟件中的對象(類,模塊,函數(shù)等)應(yīng)該對于擴展是開放的,但是對于修改是關(guān)閉的。

當(dāng)需求發(fā)生改變的時候,我們需要對代碼進行修改,這個時候我們應(yīng)該盡量去擴展原來的代碼,而不是去修改原來的代碼,因為這樣可能會引起更多的問題。
開閉原則我們可以用一種方式來確保他,我們用抽象去構(gòu)建框架,用實現(xiàn)擴展細節(jié)。這樣當(dāng)發(fā)生修改的時候,我們就直接用抽象了派生一個具體類去實現(xiàn)修改。

優(yōu)點

實踐開閉原則的優(yōu)點在于可以在不改動原有代碼的前提下給程序擴展功能。增加了程序的可擴展性,同時也降低了程序的維護成本。

案例分析

設(shè)計一個在線課程類:
由于教學(xué)資源有限,開始的時候只有類似于博客的,通過文字講解的課程。 但是隨著教學(xué)資源的增多,后來增加了視頻課程,音頻課程以及直播課程。

不好的設(shè)計

最開始的文字課程類:

//================== Course.h ==================

@interface Course : NSObject

@property (nonatomic, copy) NSString *courseTitle;         //課程名稱
@property (nonatomic, copy) NSString *courseIntroduction;  //課程介紹
@property (nonatomic, copy) NSString *teacherName;         //講師姓名
@property (nonatomic, copy) NSString *content;             //課程內(nèi)容

@end

接著按照上面所說的需求變更:增加了視頻,音頻,直播課程:

//================== Course.h ==================

@interface Course : NSObject

@property (nonatomic, copy) NSString *courseTitle;         //課程名稱
@property (nonatomic, copy) NSString *courseIntroduction;  //課程介紹
@property (nonatomic, copy) NSString *teacherName;         //講師姓名
@property (nonatomic, copy) NSString *content;             //文字內(nèi)容

//新需求:視頻課程
@property (nonatomic, copy) NSString *videoUrl;
//新需求:音頻課程
@property (nonatomic, copy) NSString *audioUrl;
//新需求:直播課程
@property (nonatomic, copy) NSString *liveUrl;

@end

三種新增的課程都在原Course類中添加了對應(yīng)的url。也就是每次添加一個新的類型的課程,都在原有Course類里面修改:新增這種課程需要的數(shù)據(jù)。這就導(dǎo)致:我們從Course類實例化的視頻課程對象會包含并不屬于自己的數(shù)據(jù):audioUrl和liveUrl:這樣就造成了冗余,視頻課程對象并不是純粹的視頻課程對象,它包含了音頻地址,直播地址等成員。

很顯然,這個設(shè)計不是一個好的設(shè)計,因為(對應(yīng)上面兩段敘述):

隨著需求的增加,需要反復(fù)修改之前創(chuàng)建的類。
給新增的類造成了不必要的冗余。
之所以會造成上述兩個缺陷,是因為該設(shè)計沒有遵循對修改關(guān)閉,對擴展開放的開閉原則,而是反其道而行之:開放修改,而且不給擴展提供便利。

好的設(shè)計

首先在Course類中僅僅保留所有課程都含有的數(shù)據(jù):

/================== Course.h ==================

@interface Course : NSObject

@property (nonatomic, copy) NSString *courseTitle;         //課程名稱
@property (nonatomic, copy) NSString *courseIntroduction;  //課程介紹
@property (nonatomic, copy) NSString *teacherName;         //講師姓名

接著,針對文字課程,視頻課程,音頻課程,直播課程這三種新型的課程采用繼承Course類的方式。而且繼承后,添加自己獨有的數(shù)據(jù):

文字課程類:

//================== TextCourse.h ==================

@interface TextCourse : Course

@property (nonatomic, copy) NSString *content;             //文字內(nèi)容

@end

視頻課程類:

//================== VideoCourse.h ==================

@interface VideoCourse : Course

@property (nonatomic, copy) NSString *videoUrl;            //視頻地址

@end

音頻課程類:

//================== AudioCourse.h ==================

@interface AudioCourse : Course

@property (nonatomic, copy) NSString *audioUrl;            //音頻地址

@end

直播課程類:

//================== LiveCourse.h ==================

@interface LiveCourse : Course

@property (nonatomic, copy) NSString *liveUrl;             //直播地址

@end

這樣一來,上面的兩個問題都得到了解決:

隨著課程類型的增加,不需要反復(fù)修改最初的父類(Course),只需要新建一個繼承于它的子類并在子類中添加僅屬于該子類的數(shù)據(jù)(或行為)即可。
因為各種課程獨有的數(shù)據(jù)(或行為)都被分散到了不同的課程子類里,所以每個子類的數(shù)據(jù)(或行為)沒有任何冗余。
而且對于第二點:或許今后的視頻課程可以有高清地址,視頻加速功能。而這些功能只需要在VideoCourse類里添加即可,因為它們都是視頻課程所獨有的。同樣地,直播課程后面還可以支持在線問答功能,也可以僅加在LiveCourse里面。

我們可以看到,正是由于最初程序設(shè)計合理,所以對后面需求的增加才會處理得很好。

原則三:里氏替換原則

定義

In a computer program, if S is a subtype of T, then objects of type T may be replaced with objects of type S (i.e. an object of type T may be substituted with any object of a subtype S) without altering any of the desirable properties of the program (correctness, task performed, etc.)
如果對每一個類型為T1的對象o1,都有類型為T2的對象o2,使得以T1定義的所有程序P在所有對象o1都替換成o2的時候,程序P的行為都沒有發(fā)生變化,那么類型T2是類型T1的子類型。

里氏替換原則通俗的去講就是:子類可以去擴展父類的功能,但是不能改變父類原有的功能。他包含以下幾層意思:

  • 子類可以實現(xiàn)父類的抽象方法,但是不能覆蓋父類的非抽象方法。
  • 子類可以增加自己獨有的方法。
  • 當(dāng)子類的方法重載父類的方法時候,方法的形參要比父類的方法的輸入?yún)?shù)更加寬松。
  • 當(dāng)子類的方法實現(xiàn)父類的抽象方法時,方法的返回值要比父類更嚴格。

優(yōu)點

可以檢驗繼承使用的正確性,約束繼承在使用上的泛濫。

因為繼承有很多缺點,他雖然是復(fù)用代碼的一種方法,但同時繼承在一定程度上違反了封裝。父類的屬性和方法對子類都是透明的,子類可以隨意修改父類的成員。這也導(dǎo)致了,如果需求變更,子類對父類的方法進行一些復(fù)寫的時候,其他的子類無法正常工作。所以里氏替換法則被提出來。
確保程序遵循里氏替換原則可以要求我們的程序建立抽象,通過抽象去建立規(guī)范,然后用實現(xiàn)去擴展細節(jié),這個是不是很耳熟,對,里氏替換原則和開閉原則往往是相互依存的。

案例分析

創(chuàng)建兩個類:長方形和正方形,都可以設(shè)置寬高(邊長),也可以輸出面積大小。

不好的設(shè)計

首先聲明一個長方形類,然后讓正方形類繼承于長方形。
長方形類:

//================== Rectangle.h ==================

@interface Rectangle : NSObject
{
    @protected double _width;
    @protected double _height;
}

//設(shè)置寬高
- (void)setWidth:(double)width;
- (void)setHeight:(double)height;

//獲取寬高
- (double)width;
- (double)height;

//獲取面積
- (double)getArea;

@end

//================== Rectangle.m ==================

@implementation Rectangle

- (void)setWidth:(double)width{
    _width = width;
}

- (void)setHeight:(double)height{
    _height = height;
}

- (double)width{
    return _width;
}

- (double)height{
    return _height;
}

- (double)getArea{
    return _width * _height;
}

@end

正方形類:

//================== Square.h ==================

@interface Square : Rectangle
@end

//================== Square.m ==================

@implementation Square

- (void)setWidth:(double)width{

    _width = width;
    _height = width;
}

- (void)setHeight:(double)height{

    _width = height;
    _height = height;
}

@end

可以看到,正方形類繼承了長方形類以后,為了保證邊長永遠是相等的,特意在兩個set方法里面強制將寬和高都設(shè)置為傳入的值,也就是重寫了父類Rectangle的兩個set方法。但是里氏替換原則里規(guī)定,子類不能重寫父類的方法,所以上面的設(shè)計是違反該原則的。

而且里氏替換原則原則里面所屬:子類對象能夠替換父類對象,而程序執(zhí)行效果不變。我們通過一個例子來看一下上面的設(shè)計是否符合:

在客戶端類寫一個方法:傳入一個Rectangle類型并返回它的面積:

- (double)calculateAreaOfRect:(Rectangle *)rect{
    return rect.getArea;
}

我們先用Rectangle對象試一下:

Rectangle *rect = [[Rectangle alloc] init];
rect.width = 10;
rect.height = 20;

double rectArea = [self calculateAreaOfRect:rect];//output:200

長寬分別設(shè)置為10,20以后,結(jié)果輸出200,沒有問題。

現(xiàn)在我們使用Rectange的子類Square的對象替換原來的Rectange對象,看一下結(jié)果如何:

Square *square = [[Square alloc] init];
square.width = 10;
square.height = 20;

double squareArea = [self calculateAreaOfRect:square];//output:400

結(jié)果輸出為400,結(jié)果不一致,再次說明了上述設(shè)計不符合里氏替換原則,因為子類的對象square替換父類的對象rect以后,程序執(zhí)行的結(jié)果變了。

不符合里氏替換原則就說明該繼承關(guān)系不是正確的繼承關(guān)系,也就是說正方形類不能繼承于長方形類,程序需要重新設(shè)計。

好的設(shè)計

既然正方形不能繼承于長方形,那么是否可以讓二者都繼承于其他的父類呢?答案是可以的。

既然要繼承于其他的父類,它們這個父類肯定具備這兩種形狀共同的特點:有4個邊。那么我們就定義一個四邊形的類:Quadrangle。

//================== Quadrangle.h ==================

@interface Quadrangle : NSObject
{
    @protected double _width;
    @protected double _height;
}

- (void)setWidth:(double)width;
- (void)setHeight:(double)height;

- (double)width;
- (double)height;

- (double)getArea;
@end

接著,讓Rectangle類和Square類繼承于它:

Rectangle類:

//================== Rectangle.h ==================

#import "Quadrangle.h"

@interface Rectangle : Quadrangle

@end

//================== Rectangle.m ==================

@implementation Rectangle

- (void)setWidth:(double)width{
    _width = width;
}

- (void)setHeight:(double)height{
    _height = height;
}

- (double)width{
    return _width;
}

- (double)height{
    return _height;
}

- (double)getArea{
    return _width * _height;
}

@end

Square類:

//================== Square.h ==================

@interface Square : Quadrangle
{
    @protected double _sideLength;
}

-(void)setSideLength:(double)sideLength;

-(double)sideLength;

@end

//================== Square.m ==================

@implementation Square

-(void)setSideLength:(double)sideLength{    
    _sideLength = sideLength;
}

-(double)sideLength{
    return _sideLength;
}

- (void)setWidth:(double)width{
    _sideLength = width;
}

- (void)setHeight:(double)height{
    _sideLength = height;
}

- (double)width{
    return _sideLength;
}

- (double)height{
    return _sideLength;
}

- (double)getArea{
    return _sideLength * _sideLength;
}

@end

我們可以看到,Rectange和Square類都以自己的方式實現(xiàn)了父類Quadrangle的公共方法。而且由于Square的特殊性,它也聲明了自己獨有的成員變量_sideLength以及其對應(yīng)的公共方法。

注意,這里Rectange和Square并不是重寫了其父類的公共方法,而是實現(xiàn)了其抽象方法。

原則四:迪米特原則

定義

You only ask for objects which you directly need.
一個對象應(yīng)該對其他對象保持最小的了解。

迪米特原則也叫做最少知道原則(Least Know Principle)。如果兩個二類不必彼此直接通信,那么這兩個類就不應(yīng)當(dāng)發(fā)生直接的相互作用。如果一個雷需要調(diào)用另外一個類的某一個方法的話,可以通過第三者轉(zhuǎn)發(fā)這個調(diào)用。在網(wǎng)上看到的比較形象的說明這個法則的示例:

  • 如果你想讓你的狗狗跑的話,你會對狗狗說還是對四條狗腿說?
  • 如果你去店里買東西,你會直接掃碼付款,還是會把錢包或者手機交給店員讓他自己拿?

優(yōu)點

實踐迪米特原則可以良好地降低類與類之間的耦合,減少類與類之間的關(guān)聯(lián)程度,讓類與類之間的協(xié)作更加直接,從而使得類具有很好的可讀性和可維護性。

在類的結(jié)構(gòu)設(shè)計上,每個類都應(yīng)該降低成員的訪問權(quán)限?;舅枷胧菑娬{(diào)了類之間的松耦合。類之間的耦合越弱,越利于復(fù)用,一個處于弱耦合的類被修改,不會被有關(guān)系的類造成影響。

案例分析

設(shè)計一個汽車類,包含汽車的品牌名稱,引擎等成員變量。提供一個方法返回引擎的品牌名稱。

不好的設(shè)計

Car類:

//================== Car.h ==================

@class GasEngine;

@interface Car : NSObject
//構(gòu)造方法
- (instancetype)initWithEngine:(GasEngine *)engine;
//返回私有成員變量:引擎的實例
- (GasEngine *)usingEngine;

@end

//================== Car.m ==================

#import "Car.h"
#import "GasEngine.h"

@implementation Car{
    GasEngine *_engine;
}

- (instancetype)initWithEngine:(GasEngine *)engine{
    self = [super init];
    if (self) {
        _engine = engine;
    }
    return self;
}

- (GasEngine *)usingEngine{
    return _engine;
}

@end

從上面可以看出,Car的構(gòu)造方法需要傳入一個引擎的實例對象。而且因為引擎的實例對象被賦到了Car對象的私有成員變量里面。所以Car類給外部提供了一個返回引擎對象的方法:usingEngine。

而這個引擎類GasEngine有一個品牌名稱的成員變量brandName:

//================== GasEngine.h ==================
@interface GasEngine : NSObject

@property (nonatomic, copy) NSString *brandName;

@end

這樣一來,客戶端就可以拿到引擎的品牌名稱了:

//================== Client.m ==================

#import "GasEngine.h"
#import "Car.h"

- (NSString *)findCarEngineBrandName:(Car *)car{

    GasEngine *engine = [car usingEngine];
    NSString *engineBrandName = engine.brandName;//獲取到了引擎的品牌名稱
    return engineBrandName;
}

上面的設(shè)計完成了需求,但是卻違反了迪米特法則。原因是在客戶端的findCarEngineBrandName:中引入了和入?yún)ⅲ–ar)和返回值(NSString)無關(guān)的GasEngine對象。增加了客戶端與 GasEngine的耦合。而這個耦合顯然是不必要更是可以避免的。

接下來我們看一下如何設(shè)計可以避免這種耦合:

好的設(shè)計

同樣是Car這個類,我們?nèi)サ粼械姆祷匾鎸ο蟮姆椒?,而是增加一個直接返回引擎品牌名稱的方法:

//================== Car.h ==================

@class GasEngine;

@interface Car : NSObject

//構(gòu)造方法
- (instancetype)initWithEngine:(GasEngine *)engine;
//直接返回引擎品牌名稱
- (NSString *)usingEngineBrandName;

@end

//================== Car.m ==================

#import "Car.h"
#import "GasEngine.h"

@implementation Car
{
    GasEngine *_engine;
}

- (instancetype)initWithEngine:(GasEngine *)engine{
    self = [super init];
    if (self) {
        _engine = engine;
    }
    return self;
}

- (NSString *)usingEngineBrandName{
    return _engine.brand;
}

@end

因為直接usingEngineBrandName直接返回了引擎的品牌名稱,所以在客戶端里面就可以直接拿到這個值,而不需要間接地通過原來的GasEngine實例來獲取。

我們看一下客戶端操作的變化:

//================== Client.m ==================

#import "Car.h"

- (NSString *)findCarEngineBrandName:(Car *)car{

    NSString *engineBrandName = [car usingEngineBrandName]; //直接獲取到了引擎的品牌名稱
    return engineBrandName;
}

與之前的設(shè)計不同,在客戶端里面,沒有引入GasEngine類,而是直接通過Car實例獲取到了需要的數(shù)據(jù)。

這樣設(shè)計的好處是,如果這輛車的引擎換成了電動引擎(原來的GasEngine類換成了ElectricEngine類),客戶端代碼可以不做任何修改!因為它沒有引入任何引擎類,而是直接獲取了引擎的品牌名稱。

所以在這種情況下我們只需要修改Car類的usingEngineBrandName方法實現(xiàn),將新引擎的品牌名稱返回即可。

原則五:接口分離原則

定義

Many client specific interfaces are better than one general purpose interface.
多個特定的客戶端接口要好于一個通用性的總接口。

  • 客戶端不應(yīng)該依賴它不需要實現(xiàn)的接口。
  • 不建立龐大臃腫的接口,應(yīng)盡量細化接口,接口中的方法應(yīng)該盡量少。

需要注意的是:接口的粒度也不能太小。如果過小,則會造成接口數(shù)量過多,使設(shè)計復(fù)雜化。

優(yōu)點

避免同一個接口里面包含不同類職責(zé)的方法,接口責(zé)任劃分更加明確,符合高內(nèi)聚低耦合的思想。

案例分析

不好的設(shè)計

類A通過接口I依賴類B,類C通過接口I依賴類D,如果接口I對于類A和類B來說不是最小接口,則類B和類D必須去實現(xiàn)他們不需要的方法。

拆分之前.png

接口I:

@protocol I <NSObject>

- (void)m1;

- (void)m2;

- (void)m3;

- (void)m4;

- (void)m5;

@end

類B

@interface B : NSObject<I>
@end

@implementation B

- (void)m1{ }

- (void)m2{ }

- (void)m3{ }

//實現(xiàn)的多余方法
- (void)m4{ }

//實現(xiàn)的多余方法
- (void)m5{ }

@end

類A

@interface A : NSObject
@end

@implementation A

- (void)m1:(id<I>)I{
    [i m1];
}

- (void)m2:(id<I>)I{
    [i m2];
}

- (void)m3:(id<I>)I{
    [i m3];
}

@end

類D

@interface D : NSObject<I>
@end

@implementation D

- (void)m1{ }

//實現(xiàn)的多余方法
- (void)m2{ }

//實現(xiàn)的多余方法
- (void)m3{ }

- (void)m4{ }

- (void)m5{ }

@end

類C

@interface C : NSObject
@end

@implementation C

- (void)m1:(id<I>)I{
    [i m1];
}

- (void)m4:(id<I>)I{
    [i m4];
}

- (void)m5:(id<I>)I{
    [i m5];
}

@end
好的設(shè)計

將臃腫的接口I拆分為獨立的幾個接口,類A和類C分別與他們需要的接口建立依賴關(guān)系。


拆分之后.png
@protocol I <NSObject>

- (void)m1;

@end
@protocol I2 <NSObject>

- (void)m2;

- (void)m3;

@end
@protocol I3 <NSObject>

- (void)m4;

- (void)m5;

@end
@interface B : NSObject<I,I2>
@end

@implementation B

- (void)m1{ }

- (void)m2{ }

- (void)m3{ }

@end
@interface A : NSObject
@end

@implementation A

- (void)m1:(id<I>)I{
    [i m1];
}

- (void)m2:(id<I2>)I{
    [i m2];
}

- (void)m3:(id<I2>)I{
    [i m3];
}

@end
@interface D : NSObject<I,I3>
@end

@implementation D

- (void)m1{ }

- (void)m4{ }

- (void)m5{ }

@end
@interface C : NSObject
@end

@implementation C

- (void)m1:(id<I>)I{
    [i m1];
}

- (void)m4:(id<I3>)I{
    [i m4];
}

- (void)m5:(id<I3>)I{
    [i m5];
}

@end

建立單一接口,不要建立龐大臃腫的接口,盡量細化接口,接口中的方法盡量少。也就是說,我們要為各個類建立專用的接口,而不要試圖去建立一個很龐大的接口供所有依賴它的類去調(diào)用。在程序設(shè)計中,依賴幾個專用的接口要比依賴一個綜合的接口更靈活。接口是設(shè)計時對外部設(shè)定的“契約”,通過分散定義多個接口,可以預(yù)防外來變更的擴散,提高系統(tǒng)的靈活性和可維護性。

原則六:依賴倒置原則

定義

  • Depend upon Abstractions. Do not depend upon concretions.
  • Abstractions should not depend upon details. Details should depend upon abstractions
  • High-level modules should not depend on low-level modules. Both should depend on abstractions.
  • 依賴抽象,而不是依賴實現(xiàn)。
  • 抽象不應(yīng)該依賴細節(jié);細節(jié)應(yīng)該依賴抽象。
  • 高層模塊不能依賴低層模塊,二者都應(yīng)該依賴抽象。

1.針對接口編程,而不是針對實現(xiàn)編程。
2.盡量不要從具體的類派生,而是以繼承抽象類或?qū)崿F(xiàn)接口來實現(xiàn)。
3.關(guān)于高層模塊與低層模塊的劃分可以按照決策能力的高低進行劃分。業(yè)務(wù)層自然就處于上層模塊,邏輯層和數(shù)據(jù)層自然就歸類為底層。

優(yōu)點

通過抽象來搭建框架,建立類和類的關(guān)聯(lián),以減少類間的耦合性。而且以抽象搭建的系統(tǒng)要比以具體實現(xiàn)搭建的系統(tǒng)更加穩(wěn)定,擴展性更高,同時也便于維護。

舉一個生活中的例子,電腦中內(nèi)存或者顯卡插槽,其實是一種接口,而這就是抽象;只要符合這個接口的要求,無論是用金士頓的內(nèi)存,還是其它的內(nèi)存,無論是4G的,還是8G的,都可以很方便、輕松的插到電腦上使用。而這些內(nèi)存條就是具體實現(xiàn),就是細節(jié)。

問題提出:
類A直接依賴類B,假如需要將類A改為依賴類C,則必須通過修改類A的代碼來達成。這種場景下,類A一般是高層模塊,負責(zé)復(fù)雜的業(yè)務(wù)邏輯;類B和類C是低層模塊,負責(zé)基本的原子操作;假如修改類A,會給程序帶來不必要的風(fēng)險。

解決方案:
將類A修改為依賴接口I,類B和類C各自實現(xiàn)接口I,類A通過接口I間接與類B或者類C發(fā)生聯(lián)系,則會大大降低修改類A的幾率。

案例分析

有一個發(fā)工資的場景:這里,類SalaryManage(類似上面說的類A)負責(zé)工資的管理;Director(類似上面說的類B)是總監(jiān)類,現(xiàn)在我們要通過SalaryManage類來給總監(jiān)發(fā)放工資了,主要代碼片段如下所示:

不好的設(shè)計

Director.m:

- (void)calculateSalary {
    NSLog(@"%@總監(jiān)的工資是20000",_strName);
}

SalaryManage.m:

- (void)calculateSalary:(Director *)director{
    [director calculateSalary];
}

調(diào)用代碼:

Director *director = [[Directoralloc] init];
director.strName  = @"張三";
SalaryManage *salaryManage = [[SalaryManagealloc] init];
[salaryManage calculateSalary:director];

這樣給總監(jiān)發(fā)放工資的功能已經(jīng)很好的實現(xiàn)了,現(xiàn)在假設(shè)需要給經(jīng)理發(fā)工資,我們發(fā)現(xiàn)工資管理類SalaryManage沒法直接完成這個功能,需要我們添加新的方法,才能完成。再假設(shè)我們還需要給普通員工、財務(wù)總監(jiān)、研發(fā)總監(jiān)等更多的崗位發(fā)送工資,那么我們就只能不斷的去修改SalaryManage類來滿足業(yè)務(wù)的需求。產(chǎn)生這種現(xiàn)象的原因就是SalaryManage與Director之間的耦合性太高了,必須降低它們之間的耦合度才行。

好的設(shè)計

我們引入一個委托EmployeeDelegate,它提供一個發(fā)放工資的方法定義,如下所示:

@protocol EmployeeDelegate <NSObject>
- (void)calculateSalary; 
@end

然后我們讓具體的員工類Director、Manager等都實現(xiàn)該委托方法,如下所示:
修改后的SalaryManage計算工資方法:

- (void)calculateSalary:(id<EmployeeDelegate>)employee{
    [employee calculateSalary];
}

調(diào)用代碼:

Director *director = [[Directoralloc] init];
director.strName  = @"張三";
Manager *manager = [[Manageralloc] init];
manager.strName  = @"李四";
SalaryManage *salaryManage = [[SalaryManagealloc] init];
[salaryManage calculateSalary:director];
[salaryManage calculateSalary:manager];

這樣修改后,無論以后怎樣擴展其他的崗位,都不需要再修改SalaryManage類了。代表高層模塊的SalaryManage類將負責(zé)完成主要的業(yè)務(wù)邏輯(發(fā)工資),如果需要對SalaryManage類進行修改,引入錯誤的風(fēng)險極大。所以遵循依賴倒置原則可以降低類之間的耦合性,提高系統(tǒng)的穩(wěn)定性,降低修改程序造成的風(fēng)險。

同樣,采用依賴倒置原則給多人并行開發(fā)帶來了極大的便利,比如在上面的例子中,剛開始SalaryManage類與Director類直接耦合時,SalaryManage類必須等Director類編碼完成后才可以進行編碼和測試,因為SalaryManage類依賴于Director類。按照依賴倒置原則修改后,則可以同時開工,互不影響,因為SalaryManage與Director類一點關(guān)系也沒有,只依賴于協(xié)議(Java和C#中稱為接口)EmployeeDelegate。參與協(xié)作開發(fā)的人越多、項目越龐大,采用依賴導(dǎo)致原則的意義就越重大。

總結(jié)

對這六個原則的遵守并不是是與否的問題,而是多和少的問題,也就是說,我們一般不會說有沒有遵守,而是說遵守程度的多少。任何事都是過猶不及,設(shè)計模式的六個設(shè)計原則也是一樣,制定這六個原則的目的不是要我們刻板的遵守他們,而是根據(jù)實際情況靈活運用。對他們的遵守程度只要在一個合理的范圍內(nèi),就算是良好的設(shè)計。我們用下圖來說明一下:

圖中的每一條維度各代表一項原則,我們依據(jù)對這項原則的遵守程度在維度上畫一個點,則如果對這項原則遵守的合理的話,這個點應(yīng)該落在紅色的同心圓內(nèi)部;如果遵守的差,點將會在小圓內(nèi)部;如果過度遵守,點將會落在大圓外部。一個良好的設(shè)計體現(xiàn)在圖中,應(yīng)該是六個頂點都在同心圓中的六邊形。怎么去用它,用好它,就要依靠設(shè)計者的經(jīng)驗。否則一味者去使用設(shè)計原則可能會使代碼出現(xiàn)過度設(shè)計的情況。大多數(shù)的原則都是通過提取出抽象和接口來實現(xiàn),如果發(fā)生過度的設(shè)計,就會出現(xiàn)很多抽象類和接口,增加了系統(tǒng)的復(fù)雜度。

在下圖中,設(shè)計1、設(shè)計2屬于良好的設(shè)計,他們對六項原則的遵守程度都在合理的范圍內(nèi);設(shè)計3、設(shè)計4設(shè)計雖然有些不足,但也基本可以接受;設(shè)計5則嚴重不足,對各項原則都沒有很好的遵守;而設(shè)計6則遵守過渡了,設(shè)計5和設(shè)計6都是迫切需要重構(gòu)的設(shè)計。

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

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

  • 單一職責(zé)原則 (SRP) 全稱 SRP , Single Responsibility Principle 單一職...
    米莉_L閱讀 1,857評論 2 5
  • 設(shè)計模式概述 在學(xué)習(xí)面向?qū)ο笃叽笤O(shè)計原則時需要注意以下幾點:a) 高內(nèi)聚、低耦合和單一職能的“沖突”實際上,這兩者...
    彥幀閱讀 3,885評論 0 14
  • 目錄: 設(shè)計模式六大原則(1):單一職責(zé)原則 設(shè)計模式六大原則(2):里氏替換原則 設(shè)計模式六大原則(3):依賴倒...
    加油小杜閱讀 794評論 0 1
  • 你曾經(jīng)流過的汗,終有一天會成為別人羨慕的資本。
    行者超閱讀 277評論 14 5
  • 用了兩個多小時,便宜的馬利水彩和一般的水彩紙。還不會畫水彩,先慢慢練手。
    語兒渣畫閱讀 292評論 0 0

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