面向對象之六大設計原則

一、六大設計原則

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

原則一:單一職責原則

定義

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

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

優點

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

案例分析

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

新需求:增加兩個方法:

  • 判定員工在今年是否升職
  • 計算員工的薪水
不好的設計
//================== 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

新需求的做法看似沒有問題,因爲都是和員工有關的,但卻違反了單一職責原則:因爲這兩個方法並不是員工本身的職責。

  • calculateSalary這個方法的職責是屬於會計部門的:薪水的計算是會計部門負責。
  • willPromotionThisYear這個方法的職責是屬於人事部門的:考覈與晉升機制是人事部門負責。

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

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

好的設計

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

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

@interface Employee : NSObject

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

@end

接着創建新的會計部門類:

//================== 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

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

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

原則二:開閉原則

定義

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

當需求發生改變的時候,我們需要對代碼進行修改,這個時候我們應該儘量去擴展原來的代碼,而不是去修改原來的代碼,因爲這樣可能會引起更多的問題。
開閉原則我們可以用一種方式來確保他,我們用抽象去構建框架,用實現擴展細節。這樣當發生修改的時候,我們就直接用抽象了派生一個具體類去實現修改。

優點

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

案例分析

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

不好的設計

最開始的文字課程類:

//================== 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;             //課程內容

@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;             //文字內容

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

@end

三種新增的課程都在原Course類中添加了對應的url。也就是每次添加一個新的類型的課程,都在原有Course類裏面修改:新增這種課程需要的數據。這就導致:我們從Course類實例化的視頻課程對象會包含並不屬於自己的數據:audioUrl和liveUrl:這樣就造成了冗餘,視頻課程對象並不是純粹的視頻課程對象,它包含了音頻地址,直播地址等成員。

很顯然,這個設計不是一個好的設計,因爲(對應上面兩段敘述):

隨着需求的增加,需要反覆修改之前創建的類。
給新增的類造成了不必要的冗餘。
之所以會造成上述兩個缺陷,是因爲該設計沒有遵循對修改關閉,對擴展開放的開閉原則,而是反其道而行之:開放修改,而且不給擴展提供便利。

好的設計

首先在Course類中僅僅保留所有課程都含有的數據:

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

@interface Course : NSObject

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

接着,針對文字課程,視頻課程,音頻課程,直播課程這三種新型的課程採用繼承Course類的方式。而且繼承後,添加自己獨有的數據:

文字課程類:

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

@interface TextCourse : Course

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

@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

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

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

我們可以看到,正是由於最初程序設計合理,所以對後面需求的增加纔會處理得很好。

原則三:里氏替換原則

定義

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的行爲都沒有發生變化,那麼類型T2是類型T1的子類型。

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

  • 子類可以實現父類的抽象方法,但是不能覆蓋父類的非抽象方法。
  • 子類可以增加自己獨有的方法。
  • 當子類的方法重載父類的方法時候,方法的形參要比父類的方法的輸入參數更加寬鬆。
  • 當子類的方法實現父類的抽象方法時,方法的返回值要比父類更嚴格。

優點

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

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

案例分析

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

不好的設計

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

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

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

//設置寬高
- (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方法裏面強制將寬和高都設置爲傳入的值,也就是重寫了父類Rectangle的兩個set方法。但是里氏替換原則裏規定,子類不能重寫父類的方法,所以上面的設計是違反該原則的。

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

在客戶端類寫一個方法:傳入一個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

長寬分別設置爲10,20以後,結果輸出200,沒有問題。

現在我們使用Rectange的子類Square的對象替換原來的Rectange對象,看一下結果如何:

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

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

結果輸出爲400,結果不一致,再次說明了上述設計不符合里氏替換原則,因爲子類的對象square替換父類的對象rect以後,程序執行的結果變了。

不符合里氏替換原則就說明該繼承關係不是正確的繼承關係,也就是說正方形類不能繼承於長方形類,程序需要重新設計。

好的設計

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

既然要繼承於其他的父類,它們這個父類肯定具備這兩種形狀共同的特點:有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類都以自己的方式實現了父類Quadrangle的公共方法。而且由於Square的特殊性,它也聲明瞭自己獨有的成員變量_sideLength以及其對應的公共方法。

注意,這裏Rectange和Square並不是重寫了其父類的公共方法,而是實現了其抽象方法。

原則四:迪米特原則

定義

You only ask for objects which you directly need.
一個對象應該對其他對象保持最小的瞭解。

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

  • 如果你想讓你的狗狗跑的話,你會對狗狗說還是對四條狗腿說?
  • 如果你去店裏買東西,你會把錢交給店員,還是會把錢包交給店員讓他自己拿?

優點

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

在類的結構設計上,每個類都應該降低成員的訪問權限。基本思想是強調了類之間的鬆耦合。類之間的耦合越弱,越利於複用,一個處於弱耦合的類被修改,不會被有關係的類造成影響。

案例分析

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

不好的設計

Car類:

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

@class GasEngine;

@interface Car : NSObject
//構造方法
- (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的構造方法需要傳入一個引擎的實例對象。而且因爲引擎的實例對象被賦到了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;
}

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

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

好的設計

同樣是Car這個類,我們去掉原有的返回引擎對象的方法,而是增加一個直接返回引擎品牌名稱的方法:

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

@class GasEngine;

@interface Car : NSObject

//構造方法
- (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;
}

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

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

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

原則五:接口分離原則

定義

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

  • 客戶端不應該依賴它不需要實現的接口。
  • 不建立龐大臃腫的接口,應儘量細化接口,接口中的方法應該儘量少。

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

優點

避免同一個接口裏麪包含不同類職責的方法,接口責任劃分更加明確,符合高內聚低耦合的思想。

案例分析

不好的設計

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

接口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{ }

//實現的多餘方法
- (void)m4{ }

//實現的多餘方法
- (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{ }

//實現的多餘方法
- (void)m2{ }

//實現的多餘方法
- (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
好的設計

將臃腫的接口I拆分爲獨立的幾個接口,類A和類C分別與他們需要的接口建立依賴關係。


@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

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

原則六:依賴倒置原則

定義

  • 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.
  • 依賴抽象,而不是依賴實現。
  • 抽象不應該依賴細節;細節應該依賴抽象。
  • 高層模塊不能依賴低層模塊,二者都應該依賴抽象。

1.針對接口編程,而不是針對實現編程。
2.儘量不要從具體的類派生,而是以繼承抽象類或實現接口來實現。
3.關於高層模塊與低層模塊的劃分可以按照決策能力的高低進行劃分。業務層自然就處於上層模塊,邏輯層和數據層自然就歸類爲底層。

優點

通過抽象來搭建框架,建立類和類的關聯,以減少類間的耦合性。而且以抽象搭建的系統要比以具體實現搭建的系統更加穩定,擴展性更高,同時也便於維護。

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

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

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

案例分析

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

Director.m:

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

SalaryManage.m:

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

調用代碼:

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

這樣給總監發放工資的功能已經很好的實現了,現在假設需要給經理髮工資,我們發現工資管理類SalaryManage沒法直接完成這個功能,需要我們添加新的方法,才能完成。再假設我們還需要給普通員工、財務總監、研發總監等更多的崗位發送工資,那麼我們就只能不斷的去修改SalaryManage類來滿足業務的需求。產生這種現象的原因就是SalaryManage與Director之間的耦合性太高了,必須降低它們之間的耦合度才行。因此我們引入一個委託EmployeeDelegate,它提供一個發放工資的方法定義,如下所示:

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

然後我們讓具體的員工類Director、Manager等都實現該委託方法,如下所示:
修改後的SalaryManage計算工資方法:

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

調用代碼:

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

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

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

總結

我們看出來,這些原則其實都是應對不斷改變的需求。每當需求變化的時候,我們利用這些原則來使我們的代碼改動量最小,而且所造成的影響也是最小的。但是我們在看這些原則的時候,我們會發現很多原則並沒有提供一種公式化的結論,而即使提供了公式化的結論的原則也只是建議去這樣做。這是因爲,這些設計原則本來就是從很多實際的代碼中提取出來的,他是一個經驗化的結論。怎麼去用它,用好他,就要依靠設計者的經驗。否則一味者去使用設計原則可能會使代碼出現過度設計的情況。大多數的原則都是通過提取出抽象和接口來實現,如果發生過度的設計,就會出現很多抽象類和接口,增加了系統的複雜度。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章