一夷陋、六大設(shè)計(jì)原則
縮寫 | 英文名稱 | 中文名稱 |
---|---|---|
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)該只負(fù)責(zé)一件事情。
1广鳍、類職責(zé)的變化往往就是導(dǎo)致類變化的原因:也就是說如果一個類具有多種職責(zé)说莫,就會有多種導(dǎo)致這個類變化的原因,從而導(dǎo)致這個類的維護(hù)變得困難膝蜈。
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)點(diǎn)
- 可以降低類的復(fù)雜度压怠,一個類只負(fù)責(zé)一項(xiàng)職責(zé)眠冈,這樣邏輯也簡單很多
- 提高類的可讀性,和系統(tǒng)的維護(hù)性菌瘫,因?yàn)椴粫衅渌婀值姆椒▉砀蓴_我們理解這個類的含義
- 當(dāng)發(fā)生變化的時候蜗顽,能將變化的影響降到最小,因?yàn)橹粫谶@個類中做出修改雨让。
案例分析
初始需求:需要創(chuàng)造一個員工類雇盖,這個類有員工的一些基本信息。
新需求:增加兩個方法:
- 判定員工在今年是否升職
- 計(jì)算員工的薪水
不好的設(shè)計(jì)
//================== Employee.h ==================
@interface Employee : NSObject
//============ 初始需求 ============
@property (nonatomic, copy) NSString *name; //員工姓名
@property (nonatomic, copy) NSString *address; //員工住址
@property (nonatomic, copy) NSString *employeeID; //員工ID
//============ 新需求 ============
//計(jì)算薪水
- (double)calculateSalary;
//今年是否晉升
- (BOOL)willGetPromotionThisYear;
@end
新需求的做法看似沒有問題栖忠,因?yàn)槎际呛蛦T工有關(guān)的崔挖,但卻違反了單一職責(zé)原則:因?yàn)檫@兩個方法并不是員工本身的職責(zé)贸街。
- calculateSalary這個方法的職責(zé)是屬于會計(jì)部門的:薪水的計(jì)算是會計(jì)部門負(fù)責(zé)朋腋。
- willPromotionThisYear這個方法的職責(zé)是屬于人事部門的:考核與晉升機(jī)制是人事部門負(fù)責(zé)偶器。
而上面的設(shè)計(jì)將本來不屬于員工自己的職責(zé)強(qiáng)加進(jìn)了員工類里面前翎,而這個類的設(shè)計(jì)初衷(原始職責(zé))就是單純地保留員工的一些信息而已于购。因此這么做就是給這個類引入了新的職責(zé)勾缭,故此設(shè)計(jì)違反了單一職責(zé)原則毡鉴。
我們可以簡單想象一下這么做的后果是什么:如果員工的晉升機(jī)制變了闺骚,或者稅收政策等影響員工工資的因素變了素标,我們還需要修改當(dāng)前這個類瘸右。
那么怎么做才能不違反單一職責(zé)原則呢冷溶?- 我們需要將這兩個方法(責(zé)任)分離出去,讓本應(yīng)該處理這類任務(wù)的類來處理尊浓。
好的設(shè)計(jì)
我們保留員工類的基本信息:
//================== Employee.h ==================
@interface Employee : NSObject
//初始需求
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *address;
@property (nonatomic, copy) NSString *employeeID;
@end
接著創(chuàng)建新的會計(jì)部門類:
//================== FinancialApartment.h ==================
#import "Employee.h"
//會計(jì)部門類
@interface FinancialApartment : NSObject
//計(jì)算薪水
- (double)calculateSalary:(Employee *)employee;
@end
和人事部門類:
//================== HRApartment.h ==================
#import "Employee.h"
//人事部門類
@interface HRApartment : NSObject
//今年是否晉升
- (BOOL)willGetPromotionThisYear:(Employee*)employee;
@end
通過創(chuàng)建了兩個分別專門處理薪水和晉升的部門逞频,會計(jì)部門和人事部門的類:FinancialApartment 和 HRApartment,把兩個任務(wù)(責(zé)任)分離了出去栋齿,讓本該處理這些職責(zé)的類來處理這些職責(zé)苗胀。
這樣一來,不僅僅在此次新需求中滿足了單一職責(zé)原則瓦堵,以后如果還要增加人事部門和會計(jì)部門處理的任務(wù)基协,就可以直接在這兩個類里面添加即可。
原則二:開閉原則
定義
Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.
開閉原則的定義是軟件中的對象(類菇用,模塊澜驮,函數(shù)等)應(yīng)該對于擴(kuò)展是開放的,但是對于修改是關(guān)閉的惋鸥。
當(dāng)需求發(fā)生改變的時候杂穷,我們需要對代碼進(jìn)行修改,這個時候我們應(yīng)該盡量去擴(kuò)展原來的代碼卦绣,而不是去修改原來的代碼耐量,因?yàn)檫@樣可能會引起更多的問題。
開閉原則我們可以用一種方式來確保他滤港,我們用抽象去構(gòu)建框架廊蜒,用實(shí)現(xiàn)擴(kuò)展細(xì)節(jié)。這樣當(dāng)發(fā)生修改的時候溅漾,我們就直接用抽象了派生一個具體類去實(shí)現(xiàn)修改山叮。
優(yōu)點(diǎn)
實(shí)踐開閉原則的優(yōu)點(diǎn)在于可以在不改動原有代碼的前提下給程序擴(kuò)展功能。增加了程序的可擴(kuò)展性添履,同時也降低了程序的維護(hù)成本屁倔。
案例分析
設(shè)計(jì)一個在線課程類:
由于教學(xué)資源有限,開始的時候只有類似于博客的缝龄,通過文字講解的課程汰现。 但是隨著教學(xué)資源的增多挂谍,后來增加了視頻課程,音頻課程以及直播課程瞎饲。
不好的設(shè)計(jì)
最開始的文字課程類:
//================== 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í)例化的視頻課程對象會包含并不屬于自己的數(shù)據(jù):audioUrl和liveUrl:這樣就造成了冗余驮捍,視頻課程對象并不是純粹的視頻課程對象疟呐,它包含了音頻地址,直播地址等成員东且。
很顯然启具,這個設(shè)計(jì)不是一個好的設(shè)計(jì),因?yàn)椋▽?yīng)上面兩段敘述):
隨著需求的增加珊泳,需要反復(fù)修改之前創(chuàng)建的類鲁冯。
給新增的類造成了不必要的冗余。
之所以會造成上述兩個缺陷色查,是因?yàn)樵撛O(shè)計(jì)沒有遵循對修改關(guān)閉薯演,對擴(kuò)展開放的開閉原則,而是反其道而行之:開放修改秧了,而且不給擴(kuò)展提供便利跨扮。
好的設(shè)計(jì)
首先在Course類中僅僅保留所有課程都含有的數(shù)據(jù):
/================== Course.h ==================
@interface Course : NSObject
@property (nonatomic, copy) NSString *courseTitle; //課程名稱
@property (nonatomic, copy) NSString *courseIntroduction; //課程介紹
@property (nonatomic, copy) NSString *teacherName; //講師姓名
接著,針對文字課程验毡,視頻課程衡创,音頻課程,直播課程這三種新型的課程采用繼承Course類的方式米罚。而且繼承后钧汹,添加自己獨(dú)有的數(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ù)(或行為)即可碗降。
因?yàn)楦鞣N課程獨(dú)有的數(shù)據(jù)(或行為)都被分散到了不同的課程子類里隘竭,所以每個子類的數(shù)據(jù)(或行為)沒有任何冗余。
而且對于第二點(diǎn):或許今后的視頻課程可以有高清地址讼渊,視頻加速功能动看。而這些功能只需要在VideoCourse類里添加即可,因?yàn)樗鼈兌际且曨l課程所獨(dú)有的爪幻。同樣地菱皆,直播課程后面還可以支持在線問答功能须误,也可以僅加在LiveCourse里面。
我們可以看到仇轻,正是由于最初程序設(shè)計(jì)合理京痢,所以對后面需求的增加才會處理得很好。
原則三:里氏替換原則
定義
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的子類型。
里氏替換原則通俗的去講就是:子類可以去擴(kuò)展父類的功能疲陕,但是不能改變父類原有的功能方淤。他包含以下幾層意思:
- 子類可以實(shí)現(xiàn)父類的抽象方法,但是不能覆蓋父類的非抽象方法蹄殃。
- 子類可以增加自己獨(dú)有的方法携茂。
- 當(dāng)子類的方法重載父類的方法時候,方法的形參要比父類的方法的輸入?yún)?shù)更加寬松诅岩。
- 當(dāng)子類的方法實(shí)現(xiàn)父類的抽象方法時邑蒋,方法的返回值要比父類更嚴(yán)格。
優(yōu)點(diǎn)
可以檢驗(yàn)繼承使用的正確性按厘,約束繼承在使用上的泛濫医吊。
因?yàn)槔^承有很多缺點(diǎn),他雖然是復(fù)用代碼的一種方法逮京,但同時繼承在一定程度上違反了封裝卿堂。父類的屬性和方法對子類都是透明的,子類可以隨意修改父類的成員懒棉。這也導(dǎo)致了草描,如果需求變更,子類對父類的方法進(jìn)行一些復(fù)寫的時候策严,其他的子類無法正常工作穗慕。所以里氏替換法則被提出來。
確保程序遵循里氏替換原則可以要求我們的程序建立抽象妻导,通過抽象去建立規(guī)范逛绵,然后用實(shí)現(xiàn)去擴(kuò)展細(xì)節(jié),這個是不是很耳熟倔韭,對术浪,里氏替換原則和開閉原則往往是相互依存的。
案例分析
創(chuàng)建兩個類:長方形和正方形寿酌,都可以設(shè)置寬高(邊長)胰苏,也可以輸出面積大小。
不好的設(shè)計(jì)
首先聲明一個長方形類醇疼,然后讓正方形類繼承于長方形硕并。
長方形類:
//================== 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
可以看到法焰,正方形類繼承了長方形類以后,為了保證邊長永遠(yuǎn)是相等的倔毙,特意在兩個set方法里面強(qiáng)制將寬和高都設(shè)置為傳入的值壶栋,也就是重寫了父類Rectangle的兩個set方法。但是里氏替換原則里規(guī)定普监,子類不能重寫父類的方法贵试,所以上面的設(shè)計(jì)是違反該原則的。
而且里氏替換原則原則里面所屬:子類對象能夠替換父類對象凯正,而程序執(zhí)行效果不變毙玻。我們通過一個例子來看一下上面的設(shè)計(jì)是否符合:
在客戶端類寫一個方法:傳入一個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è)計(jì)不符合里氏替換原則缭受,因?yàn)樽宇惖膶ο髎quare替換父類的對象rect以后胁澳,程序執(zhí)行的結(jié)果變了。
不符合里氏替換原則就說明該繼承關(guān)系不是正確的繼承關(guān)系米者,也就是說正方形類不能繼承于長方形類韭畸,程序需要重新設(shè)計(jì)。
好的設(shè)計(jì)
既然正方形不能繼承于長方形蔓搞,那么是否可以讓二者都繼承于其他的父類呢胰丁?答案是可以的。
既然要繼承于其他的父類喂分,它們這個父類肯定具備這兩種形狀共同的特點(diǎn):有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類都以自己的方式實(shí)現(xiàn)了父類Quadrangle的公共方法。而且由于Square的特殊性讳嘱,它也聲明了自己獨(dú)有的成員變量_sideLength以及其對應(yīng)的公共方法幔嗦。
注意,這里Rectange和Square并不是重寫了其父類的公共方法沥潭,而是實(shí)現(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)上看到的比較形象的說明這個法則的示例:
- 如果你想讓你的狗狗跑的話,你會對狗狗說還是對四條狗腿說颜懊?
- 如果你去店里買東西财岔,你會直接掃碼付款,還是會把錢包或者手機(jī)交給店員讓他自己拿河爹?
優(yōu)點(diǎn)
實(shí)踐迪米特原則可以良好地降低類與類之間的耦合匠璧,減少類與類之間的關(guān)聯(lián)程度,讓類與類之間的協(xié)作更加直接咸这,從而使得類具有很好的可讀性和可維護(hù)性夷恍。
在類的結(jié)構(gòu)設(shè)計(jì)上,每個類都應(yīng)該降低成員的訪問權(quán)限媳维∧鹧基本思想是強(qiáng)調(diào)了類之間的松耦合。類之間的耦合越弱侄刽,越利于復(fù)用指黎,一個處于弱耦合的類被修改,不會被有關(guān)系的類造成影響州丹。
案例分析
設(shè)計(jì)一個汽車類袋励,包含汽車的品牌名稱,引擎等成員變量当叭。提供一個方法返回引擎的品牌名稱茬故。
不好的設(shè)計(jì)
Car類:
//================== Car.h ==================
@class GasEngine;
@interface Car : NSObject
//構(gòu)造方法
- (instancetype)initWithEngine:(GasEngine *)engine;
//返回私有成員變量:引擎的實(shí)例
- (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)造方法需要傳入一個引擎的實(shí)例對象蚁鳖。而且因?yàn)橐娴膶?shí)例對象被賦到了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è)計(jì)完成了需求钾腺,但是卻違反了迪米特法則。原因是在客戶端的findCarEngineBrandName:中引入了和入?yún)ⅲ–ar)和返回值(NSString)無關(guān)的GasEngine對象讥裤。增加了客戶端與 GasEngine的耦合放棒。而這個耦合顯然是不必要更是可以避免的。
接下來我們看一下如何設(shè)計(jì)可以避免這種耦合:
好的設(shè)計(jì)
同樣是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
因?yàn)橹苯觰singEngineBrandName直接返回了引擎的品牌名稱,所以在客戶端里面就可以直接拿到這個值,而不需要間接地通過原來的GasEngine實(shí)例來獲取厢破。
我們看一下客戶端操作的變化:
//================== Client.m ==================
#import "Car.h"
- (NSString *)findCarEngineBrandName:(Car *)car{
NSString *engineBrandName = [car usingEngineBrandName]; //直接獲取到了引擎的品牌名稱
return engineBrandName;
}
與之前的設(shè)計(jì)不同荣瑟,在客戶端里面,沒有引入GasEngine類摩泪,而是直接通過Car實(shí)例獲取到了需要的數(shù)據(jù)笆焰。
這樣設(shè)計(jì)的好處是,如果這輛車的引擎換成了電動引擎(原來的GasEngine類換成了ElectricEngine類)见坑,客戶端代碼可以不做任何修改嚷掠!因?yàn)樗鼪]有引入任何引擎類,而是直接獲取了引擎的品牌名稱荞驴。
所以在這種情況下我們只需要修改Car類的usingEngineBrandName方法實(shí)現(xiàn)不皆,將新引擎的品牌名稱返回即可。
原則五:接口分離原則
定義
Many client specific interfaces are better than one general purpose interface.
多個特定的客戶端接口要好于一個通用性的總接口戴尸。
- 客戶端不應(yīng)該依賴它不需要實(shí)現(xiàn)的接口粟焊。
- 不建立龐大臃腫的接口,應(yīng)盡量細(xì)化接口孙蒙,接口中的方法應(yīng)該盡量少项棠。
需要注意的是:接口的粒度也不能太小。如果過小挎峦,則會造成接口數(shù)量過多香追,使設(shè)計(jì)復(fù)雜化。
優(yōu)點(diǎn)
避免同一個接口里面包含不同類職責(zé)的方法坦胶,接口責(zé)任劃分更加明確透典,符合高內(nèi)聚低耦合的思想。
案例分析
不好的設(shè)計(jì)
類A通過接口I依賴類B顿苇,類C通過接口I依賴類D峭咒,如果接口I對于類A和類B來說不是最小接口,則類B和類D必須去實(shí)現(xiàn)他們不需要的方法纪岁。
接口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{ }
//實(shí)現(xiàn)的多余方法
- (void)m4{ }
//實(shí)現(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{ }
//實(shí)現(xiàn)的多余方法
- (void)m2{ }
//實(shí)現(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è)計(jì)
將臃腫的接口I拆分為獨(dú)立的幾個接口凑队,類A和類C分別與他們需要的接口建立依賴關(guān)系。
@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
建立單一接口幔翰,不要建立龐大臃腫的接口漩氨,盡量細(xì)化接口,接口中的方法盡量少遗增。也就是說叫惊,我們要為各個類建立專用的接口,而不要試圖去建立一個很龐大的接口供所有依賴它的類去調(diào)用做修。在程序設(shè)計(jì)中霍狰,依賴幾個專用的接口要比依賴一個綜合的接口更靈活抡草。接口是設(shè)計(jì)時對外部設(shè)定的“契約”,通過分散定義多個接口蚓耽,可以預(yù)防外來變更的擴(kuò)散渠牲,提高系統(tǒng)的靈活性和可維護(hù)性旋炒。
原則六:依賴倒置原則
定義
- 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.
- 依賴抽象步悠,而不是依賴實(shí)現(xiàn)。
- 抽象不應(yīng)該依賴細(xì)節(jié)瘫镇;細(xì)節(jié)應(yīng)該依賴抽象鼎兽。
- 高層模塊不能依賴低層模塊,二者都應(yīng)該依賴抽象铣除。
1.針對接口編程谚咬,而不是針對實(shí)現(xiàn)編程。
2.盡量不要從具體的類派生尚粘,而是以繼承抽象類或?qū)崿F(xiàn)接口來實(shí)現(xiàn)择卦。
3.關(guān)于高層模塊與低層模塊的劃分可以按照決策能力的高低進(jìn)行劃分。業(yè)務(wù)層自然就處于上層模塊郎嫁,邏輯層和數(shù)據(jù)層自然就歸類為底層秉继。
優(yōu)點(diǎn)
通過抽象來搭建框架,建立類和類的關(guān)聯(lián)泽铛,以減少類間的耦合性尚辑。而且以抽象搭建的系統(tǒng)要比以具體實(shí)現(xiàn)搭建的系統(tǒng)更加穩(wěn)定,擴(kuò)展性更高盔腔,同時也便于維護(hù)杠茬。
舉一個生活中的例子,電腦中內(nèi)存或者顯卡插槽弛随,其實(shí)是一種接口瓢喉,而這就是抽象;只要符合這個接口的要求舀透,無論是用金士頓的內(nèi)存栓票,還是其它的內(nèi)存,無論是4G的盐杂,還是8G的逗载,都可以很方便、輕松的插到電腦上使用链烈。而這些內(nèi)存條就是具體實(shí)現(xiàn)厉斟,就是細(xì)節(jié)。
問題提出:
類A直接依賴類B强衡,假如需要將類A改為依賴類C擦秽,則必須通過修改類A的代碼來達(dá)成。這種場景下,類A一般是高層模塊感挥,負(fù)責(zé)復(fù)雜的業(yè)務(wù)邏輯缩搅;類B和類C是低層模塊,負(fù)責(zé)基本的原子操作触幼;假如修改類A硼瓣,會給程序帶來不必要的風(fēng)險。
解決方案:
將類A修改為依賴接口I置谦,類B和類C各自實(shí)現(xiàn)接口I堂鲤,類A通過接口I間接與類B或者類C發(fā)生聯(lián)系,則會大大降低修改類A的幾率媒峡。
案例分析
有一個發(fā)工資的場景:這里瘟栖,類SalaryManage(類似上面說的類A)負(fù)責(zé)工資的管理;Director(類似上面說的類B)是總監(jiān)類谅阿,現(xiàn)在我們要通過SalaryManage類來給總監(jiān)發(fā)放工資了半哟,主要代碼片段如下所示:
不好的設(shè)計(jì)
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)很好的實(shí)現(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è)計(jì)
我們引入一個委托EmployeeDelegate拇颅,它提供一個發(fā)放工資的方法定義,如下所示:
@protocol EmployeeDelegate <NSObject>
- (void)calculateSalary;
@end
然后我們讓具體的員工類Director乔询、Manager等都實(shí)現(xiàn)該委托方法樟插,如下所示:
修改后的SalaryManage計(jì)算工資方法:
- (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];
這樣修改后,無論以后怎樣擴(kuò)展其他的崗位竿刁,都不需要再修改SalaryManage類了黄锤。代表高層模塊的SalaryManage類將負(fù)責(zé)完成主要的業(yè)務(wù)邏輯(發(fā)工資),如果需要對SalaryManage類進(jìn)行修改食拜,引入錯誤的風(fēng)險極大鸵熟。所以遵循依賴倒置原則可以降低類之間的耦合性,提高系統(tǒng)的穩(wěn)定性负甸,降低修改程序造成的風(fēng)險流强。
同樣痹届,采用依賴倒置原則給多人并行開發(fā)帶來了極大的便利,比如在上面的例子中打月,剛開始SalaryManage類與Director類直接耦合時队腐,SalaryManage類必須等Director類編碼完成后才可以進(jìn)行編碼和測試,因?yàn)镾alaryManage類依賴于Director類奏篙。按照依賴倒置原則修改后柴淘,則可以同時開工,互不影響报破,因?yàn)镾alaryManage與Director類一點(diǎn)關(guān)系也沒有悠就,只依賴于協(xié)議(Java和C#中稱為接口)EmployeeDelegate千绪。參與協(xié)作開發(fā)的人越多充易、項(xiàng)目越龐大,采用依賴導(dǎo)致原則的意義就越重大荸型。
總結(jié)
對這六個原則的遵守并不是是與否的問題盹靴,而是多和少的問題,也就是說瑞妇,我們一般不會說有沒有遵守稿静,而是說遵守程度的多少。任何事都是過猶不及辕狰,設(shè)計(jì)模式的六個設(shè)計(jì)原則也是一樣改备,制定這六個原則的目的不是要我們刻板的遵守他們,而是根據(jù)實(shí)際情況靈活運(yùn)用蔓倍。對他們的遵守程度只要在一個合理的范圍內(nèi)悬钳,就算是良好的設(shè)計(jì)。我們用下圖來說明一下:
圖中的每一條維度各代表一項(xiàng)原則偶翅,我們依據(jù)對這項(xiàng)原則的遵守程度在維度上畫一個點(diǎn)默勾,則如果對這項(xiàng)原則遵守的合理的話,這個點(diǎn)應(yīng)該落在紅色的同心圓內(nèi)部聚谁;如果遵守的差母剥,點(diǎn)將會在小圓內(nèi)部;如果過度遵守形导,點(diǎn)將會落在大圓外部环疼。一個良好的設(shè)計(jì)體現(xiàn)在圖中,應(yīng)該是六個頂點(diǎn)都在同心圓中的六邊形朵耕。怎么去用它炫隶,用好它,就要依靠設(shè)計(jì)者的經(jīng)驗(yàn)憔披。否則一味者去使用設(shè)計(jì)原則可能會使代碼出現(xiàn)過度設(shè)計(jì)的情況等限。大多數(shù)的原則都是通過提取出抽象和接口來實(shí)現(xiàn)爸吮,如果發(fā)生過度的設(shè)計(jì),就會出現(xiàn)很多抽象類和接口望门,增加了系統(tǒng)的復(fù)雜度形娇。
在下圖中,設(shè)計(jì)1筹误、設(shè)計(jì)2屬于良好的設(shè)計(jì)桐早,他們對六項(xiàng)原則的遵守程度都在合理的范圍內(nèi);設(shè)計(jì)3厨剪、設(shè)計(jì)4設(shè)計(jì)雖然有些不足哄酝,但也基本可以接受;設(shè)計(jì)5則嚴(yán)重不足祷膳,對各項(xiàng)原則都沒有很好的遵守陶衅;而設(shè)計(jì)6則遵守過渡了,設(shè)計(jì)5和設(shè)計(jì)6都是迫切需要重構(gòu)的設(shè)計(jì)直晨。