淺談面向對象的六大設計原則

原則一、單一職責原則(Single Responsibility Principle,簡稱SRP )

定義:應該有且僅有一個原因引起類的變更。

一個類只負責一項職責,如果發生變更時,可以考慮將一個類拆分成兩個類,或者在一個類中添加新的方法。

在真實的開發中,不僅僅是類、函數和接口也要遵循單一職責原則。即:一個函數負責一個功能。如果一個函數裏面有不同的功能,則需要將不同的功能的函數分離出去。

優點:

  • 類的複雜性降低,實現什麼職責都有清晰明確的定義。
  • 類的可讀性提高,複雜性減低。

如果接口或者函數的單一職責做得好,一個接口或者函數的修改只對相應的類有影響,對其他接口或者函數無影響,這對系統的擴展性、維護性都有非常大的幫助。

例如,需求上指出用一個類描述食肉和食草動物:

//================== Animal.h ==================

@interface Animal : NSObject

- (void)eatWithAnimalName:(NSString *)animalName;

@end

運行結果:

2018-10-27 17:55:25.775317+0800 DesignPatterns[54087:24701786] 狼 喫肉
2018-10-27 17:55:25.775689+0800 DesignPatterns[54087:24701786] 豹 喫肉
2018-10-27 17:55:25.775721+0800 DesignPatterns[54087:24701786] 虎 喫肉

上線後,發現問題了,並不是所有的動物都是喫肉的,比如羊就是喫草的。修改時如果遵循單一職責原則,需要將 Animal 類細分爲食草動物類 Herbivore,食肉動物 Carnivore,代碼如下:

//================== Herbivore.h ==================
@interface Herbivore : Animal

@end

@implementation Herbivore

- (void)eatWithAnimalName:(NSString *)animalName {
    NSLog(@"%@ 喫草", animalName);
}

@end

//================== Carnivore.h ==================
@interface Carnivore : Animal

@end

@implementation Carnivore

- (void)eatWithAnimalName:(NSString *)animalName {
    NSLog(@"%@ 喫肉", animalName);
}

@end

//================== main 函數 ==================
Animal *carnivore = [Carnivore new];
[carnivore eatWithAnimalName:@"狼"];
[carnivore eatWithAnimalName:@"豹"];
[carnivore eatWithAnimalName:@"虎"];
NSLog(@"\n");
Animal *herbivore = [Herbivore new];
[herbivore eatWithAnimalName:@"羊"];

在子類裏面重寫父類的 eatWithAnimalName 函數,運行結果:

2018-10-27 18:04:49.189722+0800 DesignPatterns[54422:24725132] 狼 喫肉
2018-10-27 18:04:49.190450+0800 DesignPatterns[54422:24725132] 豹 喫肉
2018-10-27 18:04:49.190482+0800 DesignPatterns[54422:24725132] 虎 喫肉
2018-10-27 18:04:49.190498+0800 DesignPatterns[54422:24725132] 
2018-10-27 18:04:49.190530+0800 DesignPatterns[54422:24725132] 羊 喫草

這樣一來,不僅僅在此次新需求中滿足了單一職責原則,以後如果還要增加食肉動物和食草動物的其他功能,就可以直接在這兩個類裏面添加即可。但是,有一點,修改花銷是很大的,除了將原來的類分解之外,還需要修改 main 函數 。而直接修改類 Animal 來達成目的雖然違背了單一職責原則,但花銷卻小的多,代碼如下:

//================== Animal.h ==================

@interface Animal : NSObject

- (void)eatWithAnimalName:(NSString *)animalName;

@end

@implementation Animal

- (void)eatWithAnimalName:(NSString *)animalName {
    if ([@"羊" isEqualToString:animalName]) {
        NSLog(@"%@ 喫草", animalName);
    } else {
        NSLog(@"%@ 喫肉", animalName);
    }
}

@end

//================== main 函數 ==================

Animal *animal = [Animal new];
[animal eatWithAnimalName:@"狼"];
[animal eatWithAnimalName:@"豹"];
[animal eatWithAnimalName:@"虎"];
[animal eatWithAnimalName:@"羊"];

運行結果:

2018-10-27 18:16:10.910397+0800 DesignPatterns[54677:24751636] 狼 喫肉
2018-10-27 18:16:10.911105+0800 DesignPatterns[54677:24751636] 豹 喫肉
2018-10-27 18:16:10.911138+0800 DesignPatterns[54677:24751636] 虎 喫肉
2018-10-27 18:16:10.911160+0800 DesignPatterns[54677:24751636] 羊 喫草

可以看到,這種修改方式要簡單的多。
但是卻存在着隱患:有一天需求上增加牛和馬也需要喫草,則又需要修改 Animal 類的 eatWithAnimalName 函數,而對原有代碼的修改會對調用狼、豹和虎喫肉等功能帶來風險,也許某一天你會發現運行結果變爲虎也喫草了。這種修改方式直接在代碼級別上違背了單一職責原則,雖然修改起來最簡單,但隱患卻是最大的。還有一種修改方式:

//================== Animal.h ==================

@interface Animal : NSObject

/**
 *  喫草
 */
- (void)eatGrassWithAnimalName:(NSString *)animalName;

/**
 *  喫肉
 */
- (void)eatMeatWithAnimalName:(NSString *)animalName;

@end

@implementation Animal

- (void)eatGrassWithAnimalName:(NSString *)animalName {
    NSLog(@"%@ 喫草", animalName);
}

- (void)eatMeatWithAnimalName:(NSString *)animalName {
    NSLog(@"%@ 喫肉", animalName);
}

@end

//================== main 函數 ==================

Animal *animal = [Animal new];
[animal eatMeatWithAnimalName:@"狼"];
[animal eatMeatWithAnimalName:@"豹"];
[animal eatMeatWithAnimalName:@"虎"];
[animal eatGrassWithAnimalName:@"羊"];

運行結果:

2018-10-27 18:31:30.321473+0800 DesignPatterns[55048:24787008] 狼 喫肉
2018-10-27 18:31:30.321884+0800 DesignPatterns[55048:24787008] 豹 喫肉
2018-10-27 18:31:30.321922+0800 DesignPatterns[55048:24787008] 虎 喫肉
2018-10-27 18:31:30.321939+0800 DesignPatterns[55048:24787008] 羊 喫草

通過運行結果可以看到,這種修改方式沒有改動原來的函數,而是在類中新加了一個函數,這樣雖然也違背了類單一職責原則,但在函數級別上卻是符合單一職責原則的,因爲它並沒有動原來函數的代碼。

在實際的開發應用中,有很多複雜的場景,怎麼設計一個類或者一個函數,讓應用程序更加靈活,是更多程序員們值得思考的,需要結合特定的需求場景,有可能有些類裏面有很多的功能,但是切記不要將不屬於這個類本身的功能也強加進來,這樣不僅帶來不必要的維護成本,也違反了單一職責的設計原則

原則二、里氏替換原則(Liskov Substitution Principle,簡稱LSP)

定義:如果對一個類型爲 T1 的對象 o1,都有類型爲 T2 的對象 o2,使得以 T1 定義的所有程序 P 在所有的對象 o1 都替換成 o2 時,程序 P 的行爲沒有發生變化,那麼類型 T2 是類型 T1 的子類型。有點拗口,通俗點講,只要父類能出現的地方子類就可以出現,而且替換爲子類也不會產生任何錯誤或異常,使用者不需要知道是父類還是子類。但是,反過來就不行了,有子類出現的地方,父類未必就能適應

面向對象的語言的三大特點是繼承、封裝、多態,里氏替換原則就是依賴於繼承、多態這兩大特性。當使用繼承時,遵循里氏替換原則。但是使用繼承會給程序帶來侵入性,程序的可移植性降低,增加了對象間的耦合性,如果一個類被其他的類所繼承,則當這個類需要修改時,必須考慮到所有的子類,並且父類修改後,所有涉及到子類的功能都有可能會產生影響。子類可以擴展父類的功能,但不能改變父類原有的功能。

注意:

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

比如,需要完成一個兩數相加的功能:

//================== A.h ==================

@interface A : NSObject

/**
 加法

 @param a
 @param b
 @return 相加之後的和
 */
- (NSInteger)addition:(NSInteger)a b:(NSInteger)b;

@end

//================== main 函數 ==================

A *a = [[A alloc] init];
NSLog(@"100+50=%ld", [a addition:100 b:50]);
NSLog(@"100+80=%ld", [a addition:100 b:80]);

運行結果如下,

2018-11-01 22:53:23.549358+0800 DesignPatterns[18063:363232] 100+50=150
2018-11-01 22:53:23.549586+0800 DesignPatterns[18063:363232] 100+80=180

接着,需求上需要增加一個新的功能,完成兩數相加,然後再與 100 求差,由類 B 來負責。即類 B 需要完成兩個功能:

  • 兩數相減。
  • 兩數相加,然後再加 100

由於類 A 已經實現了加法功能,所以 B 繼承 A 之後,只需要完成減法功能就可以了,但是在類 B 中不小心重寫了父類 A 的減法功能,如下:

//================== B.h ==================

@interface B : A

/**
 加法
 
 @param a
 @param b
 @return 相加之後的和
 */
- (NSInteger)addition:(NSInteger)a b:(NSInteger)b;


/**
 減法
 
 @param a
 @param b
 @return 相加之後的和
 */
- (NSInteger)subtraction:(NSInteger)a b:(NSInteger)b;

@end

//================== main 函數 ==================

B *b = [[B alloc] init];
NSInteger sub = [b addition:100 b:50];
NSInteger difference = [b subtraction:sub b:100];
NSLog(@"100+50=%ld", sub);
NSLog(@"100+100+50=%ld", difference);

運行結果如下,

2018-11-01 23:15:06.530080+0800 DesignPatterns[18363:375940] 100+50=5000
2018-11-01 23:15:06.530758+0800 DesignPatterns[18363:375940] 100+100+50=4900

發現原本運行正常的相減功能發生了錯誤,原因就是類 B 在給方法起名時無意中重寫了父類的方法,造成所有運行相減功能的代碼全部調用了類 B 重寫後的方法,造成原本運行正常的功能出現了錯誤。如果按照“里氏替換原則”,只要父類能出現的地方子類就可以出現,而且替換爲子類也不會產生任何錯誤或異常,使用者不需要知道是父類還是子類,是不成立的。

在平時的日常開發中,通常會通過重寫父類的方法來完成新的功能,這樣寫起來雖然簡單,但是整個繼承體系的可複用性會比較差,特別是運用多態比較頻繁時,程序運行出錯的機率非常大。

原則三、依賴倒置原則(Dependence Inversion Principle,簡稱DIP)

依賴倒置原則的核心思想是面向接口編程。

定義:模塊間的依賴通過抽象發生,高層模塊和低層模塊之間不應該發生直接的依賴關係,二者都應該是通過接口或抽象類產生的;即依賴抽象,而不依賴具體的實現。

例如:類 A 直接依賴類 B,假如要將類 A 改爲依賴類 C,則必須通過修改類 A 的代碼來達成。比如在這種場景下,業務邏輯層類 A 相對於數據層類 B 是高層模塊,因爲業務邏輯層需要調用數據層去連接數據庫,如果業務邏輯層類 A 依賴數據層類 B 的話,那麼將來需求變更,需要把舊的數據層類 B 修改爲新的數據層類 C,就必須通過修改類 A,這樣就會給應用程序帶來不必要的風險。

解決方案:將類 A 修改爲依賴接口 I,類 B 和類 C 各自實現接口 I,類 A 通過接口 I 間接與類 B 或者類 C 發生聯繫,則會大大降低修改類 A 的機率。要做到可擴展高複用,儘量不要讓業務邏輯層依賴數據層,可以在數據層抽象出一個接口,讓業務邏輯層依賴於這個抽象接口。

比如:母親給孩子講故事,只要給她一本書,她就可以照着書給孩子講故事了。

//================== Book.h ==================

@interface Book : NSObject

/**
 故事內容
 */
- (void)theStoryContent;

@end

//================== Mother.h ==================

@class Book;
@interface Mother : NSObject

/**
 講故事
 */
- (void)tellStory:(Book *)book;

@end

//================== main 函數 ==================

Mother *mother = [Mother new];
Book *book = [Book new];
[mother tellStory:book];

運行結果如下,

2018-11-09 14:52:08.759154+0800 DesignPatterns[6135:458778] 媽媽開始講故事
2018-11-09 14:52:08.759365+0800 DesignPatterns[6135:458778] 很久很久以前有一個阿拉伯的故事……

將來有一天,需求變更成,增加讓母親講一下報紙上的故事的功能,如下:

//================== Newspaper.h ==================

@interface Newspaper : NSObject

/**
 報紙內容
 */
- (void)theStoryContent;

@end

如果將 Newspaper 類替換 Book 類,發現母親看不懂報紙上的故事,必須要修改 Mother 類裏面的 tellStory 方法才能看不懂報紙上的故事。假如以後需求換成雜誌呢?換成網頁呢?還要不斷地修改Mother 類,這顯然不是好的設計,高層模塊都依賴了低層模塊的改動,因此上述設計不符合依賴倒置原則。Mother 類與 Book 類之間的耦合性太高了,必須降低他們之間的耦合度纔行。

解決方案,將母親講故事的方法抽象一個接口或者 Protocol,讓Mother 類不再依賴 NewspaperBook 類具體實現,而是依賴抽象出來的接口或者 Protocol。並且 NewspaperBook 類也都依賴這個抽象出來的接口或者 Protocol,通過實現接口或者 Protocol 來做自己的事情。

//================== IReaderProtocol.h ==================

@protocol IReaderProtocol <NSObject>

/**
 故事內容
 */
- (void)theStoryContent;

@end

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

//================== Book.h ==================

@interface Book : NSObject <IReaderProtocol>

@end

//================== Newspaper.h ==================

@interface Newspaper : NSObject <IReaderProtocol>

@end

//================== IReaderProtocol.h ==================

@protocol IReaderProtocol <NSObject>

/**
 故事內容
 */
- (void)theStoryContent;

@end

//================== Mother.h ==================

@interface Mother : NSObject

/**
 講故事
 */
- (void)tellStory:(NSObject<IReaderProtocol> *)reading;

@end

@implementation Mother

- (void)tellStory:(NSObject<IReaderProtocol> *)reading {
    NSLog(@"媽媽開始講故事");
    if ([reading respondsToSelector:@selector(theStoryContent)]) {
        [reading theStoryContent];
    }
}

@end

//================== main 函數 ==================

Mother *mother = [Mother new];
Book *book = [Book new];
Newspaper *newspaper = [Newspaper new];
[mother tellStory:book];
[mother tellStory:newspaper];

運行結果如下,

2018-11-09 15:28:01.182603+0800 DesignPatterns[7055:532924] 媽媽開始講故事
2018-11-09 15:28:01.182879+0800 DesignPatterns[7055:532924] 很久很久以前有一個阿拉伯的故事……
2018-11-09 15:28:01.182916+0800 DesignPatterns[7055:532924] 媽媽開始講故事
2018-11-09 15:28:01.182955+0800 DesignPatterns[7055:532924] 雄鹿終結勇士八連勝……

這樣修改後,無論以後怎樣擴展 main 函數,都不需要再修改 Mother 類了。這裏只是舉了一個比較簡單的例子,在實際的項目開發中,儘可能的採用“低耦合,高內聚”的原則,採用依賴倒置原則給多人並行開發帶來了極大的便利,無論是面向過程編程還是面向對象編程,只有使各個模塊之間的耦合儘量的低,才能提高代碼的複用率。所以遵循依賴倒置原則可以降低類之間的耦合性,提高系統的穩定性,降低修改程序造成的風險。

原則四、接口隔離原則(Interface Segregation Principle,簡稱ISP)

定義:客戶端不應該依賴它不需要的接口;一個類對另一個類的依賴應該建立在最小的接口上。

Class 'ClassB' does not conform to protocol 'InterfaceH'
Class 'ClassD' does not conform to protocol 'InterfaceH'

注意:在 Objective-C 中的協議可以通過 @optional 關鍵字聲明不需要必須實現的方法,這個只是 Objective-C 的一個特性,可以消除在 ClassBClassD 中沒有實現 InterfaceHprotocol 協議。

比如,類 A 依賴接口 H 中的方法1、方法2、方法5,類 B 是對類 A 依賴的實現。類 C 依賴接口 H 中的方法3、方法4、方法5,類 D 是對類 C 依賴的實現。對於類 B 和類 D 來說,雖然他們都存在着用不到的方法,但由於實現了接口 H,因爲接口 H 對於類 A 和類 C 來說不是最小接口,所以也必須要實現這些用不到的方法。

//================== InterfaceH.h ==================

@protocol InterfaceH <NSObject>

- (void)method1;
- (void)method2;
- (void)method3;
- (void)method4;
- (void)method5;

@end

//================== ClassB.h ==================

@interface ClassB : NSObject <InterfaceH>

@end

@implementation ClassB

- (void)method1 {
    NSLog(@"類 B 實現接口 H 的方法1");
}

- (void)method2 {
    NSLog(@"類 B 實現接口 H 的方法2");
}

- (void)method3 {
    //not necessarily
}

- (void)method4 {
    //not necessarily
}

- (void)method5 {
    NSLog(@"類 B 實現接口 H 的方法5");
}

@end

//================== ClassA.h ==================

@interface ClassA : NSObject

- (void)depend:(NSObject<InterfaceH> *)classB;

@end

@implementation ClassA

- (void)depend:(NSObject<InterfaceH> *)classB {
    
    if ([classB respondsToSelector:@selector(method1)]) {
        [classB method1];
    }
    if ([classB respondsToSelector:@selector(method2)]) {
        [classB method2];
    }
    if ([classB respondsToSelector:@selector(method5)]) {
        [classB method5];
    }
}

@end

//================== ClassD.h ==================

@interface ClassD : NSObject <InterfaceH>

@end

@implementation ClassD

- (void)method1 { 
    //not necessarily
}

- (void)method2 { 
    //not necessarily
}

- (void)method3 { 
    NSLog(@"類 D 實現接口 H 的方法3");
}

- (void)method4 { 
    NSLog(@"類 D 實現接口 H 的方法4");
}

- (void)method5 { 
    NSLog(@"類 D 實現接口 H 的方法5");
}

@end

//================== ClassC.h ==================

@interface ClassC : NSObject

- (void)depend:(NSObject<InterfaceH> *)classD;

@end

@implementation ClassC

- (void)depend:(NSObject<InterfaceH> *)classD {
    
    if ([classD respondsToSelector:@selector(method3)]) {
        [classD method3];
    }
    if ([classD respondsToSelector:@selector(method4)]) {
        [classD method4];
    }
    if ([classD respondsToSelector:@selector(method5)]) {
        [classD method5];
    }
}

@end

可以看到,如果接口過於臃腫,只要接口中出現的方法,不管對依賴於它的類有沒有用處,實現類中都必須去實現這些方法,這顯然不是好的設計。由於接口方法的設計造成了冗餘,因此該設計不符合接口隔離原則。

解決方法:將臃腫的接口 H 拆分爲獨立的幾個接口,類 A 和類 C 分別與他們需要的接口建立依賴關係,也就是採用接口隔離原則。

//================== InterfaceH.h ==================

@protocol InterfaceH <NSObject>

- (void)method5;

@end

@protocol InterfaceH1 <InterfaceH>

- (void)method1;
- (void)method2;

@end

@protocol InterfaceH2 <InterfaceH>

- (void)method3;
- (void)method4;

@end

//================== ClassB.h ==================

@interface ClassB : NSObject <InterfaceH1>

@end

@implementation ClassB

- (void)method1 {
    NSLog(@"類 B 實現接口 H 的方法1");
}

- (void)method2 {
    NSLog(@"類 B 實現接口 H 的方法2");
}

- (void)method5 {
    NSLog(@"類 B 實現接口 H 的方法5");
}

@end

//================== ClassA.h ==================

@interface ClassA : NSObject

- (void)depend:(NSObject<InterfaceH1> *)classB;

@end

@implementation ClassA

- (void)depend:(NSObject<InterfaceH1> *)classB {
    
    if ([classB respondsToSelector:@selector(method1)]) {
        [classB method1];
    }
    if ([classB respondsToSelector:@selector(method2)]) {
        [classB method2];
    }
    if ([classB respondsToSelector:@selector(method5)]) {
        [classB method5];
    }
}

@end

//================== ClassD.h ==================

@interface ClassD : NSObject <InterfaceH2>

@end

@implementation ClassD

- (void)method3 { 
    NSLog(@"類 D 實現接口 H 的方法3");
}

- (void)method4 { 
    NSLog(@"類 D 實現接口 H 的方法4");
}

- (void)method5 { 
    NSLog(@"類 D 實現接口 H 的方法5");
}

@end

//================== ClassC.h ==================

@interface ClassC : NSObject

- (void)depend:(NSObject<InterfaceH2> *)classD;

@end

@implementation ClassC

- (void)depend:(NSObject<InterfaceH2> *)classD {
    
    if ([classD respondsToSelector:@selector(method3)]) {
        [classD method3];
    }
    if ([classD respondsToSelector:@selector(method4)]) {
        [classD method4];
    }
    if ([classD respondsToSelector:@selector(method5)]) {
        [classD method5];
    }
}

@end

接口隔離原則的含義是:建立單一接口,不要建立龐大臃腫的接口,儘量細化接口,接口中的方法儘量少。在實際項目開發中,只暴露給調用的類需要的方法,不需要的方法則隱藏起來。只有專注地爲一個模塊提供定製服務,才能建立最小的依賴關係,不要試圖去建立一個很龐大的接口供所有依賴它的類去調用。通過分散定義多個接口,可以預防外來變更的擴散,提高系統的靈活性和可維護性。

原則五、迪米特法則(Law of Demeter,簡稱LOD)

定義:一個對象應該對其他對象保持最少的瞭解。

當類與類之間的關係越密切,耦合度越大,當一個類發生改變時,對另一個類的影響也越大。通俗的來講,就是一個類對自己依賴的類知道的越少越好。也就是說,對於被依賴的類來說,無論邏輯多麼複雜,都儘量地的將邏輯封裝在類的內部,對外只暴露必要的接口。

解決方案:儘量降低類與類之間的耦合。

比如,有一個集團公司,下屬單位有分公司和直屬部門,現在要求打印出所有下屬單位的員工 ID
Model 類,

//================== EmployeeModel.h ==================

@interface EmployeeModel : NSObject

/**
 總公司員工ID
 */
@property (nonatomic, copy) NSString *employee_id;

@end

//================== SubEmployeeModel.h ==================

@interface SubEmployeeModel : NSObject

/**
 分公司員工ID
 */
@property (nonatomic, copy) NSString *subemployee_id;

@end

Company 類,

//================== Company.h ==================

@interface Company : NSObject

- (NSArray *)getAllEmployee;

- (void)printAllEmployeeWithSubCompany:(SubCompany *)subCompany;

@end

@implementation Company

- (NSArray *)getAllEmployee {
    NSMutableArray<EmployeeModel *> *employeeArray = [NSMutableArray<EmployeeModel *> array];
    for (int i = 0; i < 3; i++) {
        EmployeeModel *employeeModel = [[EmployeeModel alloc] init];
        [employeeModel setEmployee_id:[@(i) stringValue]];
        [employeeArray addObject:employeeModel];
    }
    return employeeArray.copy;
}

- (void)printAllEmployeeWithSubCompany:(SubCompany *)subCompany {
    // 分公司員工
    NSArray<SubEmployeeModel *> *subEmployeeArray = subCompany.getAllEmployee;
    for (SubEmployeeModel *employeeModel in subEmployeeArray) {
        NSLog(@"分公司員工ID:%@", employeeModel.subemployee_id);
    }
    
    // 總公司員工
    NSArray<EmployeeModel *> *employeeArray = self.getAllEmployee;
    for (EmployeeModel *employeeModel in employeeArray) {
        NSLog(@"總公司員工ID:%@", employeeModel.employee_id);
    }
}

@end

//================== SubCompany.h ==================

@interface SubCompany : NSObject

- (NSArray *)getAllEmployee;

@end

@implementation SubCompany

- (NSArray *)getAllEmployee {
    NSMutableArray<SubEmployeeModel *> *employeeArray = [NSMutableArray<SubEmployeeModel *> array];
    for (int i = 0; i < 3; i++) {
        SubEmployeeModel *employeeModel = [[SubEmployeeModel alloc] init];
        [employeeModel setSubemployee_id:[@(i) stringValue]];
        [employeeArray addObject:employeeModel];
    }
    return employeeArray.copy;
}

@end

從上面可以看出,打印 Company 所有員工的 ID,需要依賴分公司 SubCompany。但是在 printAllEmployeeWithSubCompany: 方法裏面必須要初始化分公司員工 SubEmployeeModel。而SubEmployeeModelCompany 並不是直接聯繫,換句話說,總公司 Company 只需要依賴分公司 SubCompany,與分公司的員工 SubEmployeeModel 並沒有任何聯繫,這樣設計顯然是增加了不必要的耦合。

按照迪米特法則,類與類之間的應該減少不必要的關聯程度。

//================== Company.h ==================

@interface Company : NSObject

/**
 獲取所有分公司員工
 */
- (NSArray *)getAllEmployee;

/**
 打印公司所有員工
 */
- (void)printAllEmployeeWithSubCompany:(SubCompany *)subCompany;

@end

@implementation Company

- (NSArray *)getAllEmployee {
    NSMutableArray<EmployeeModel *> *employeeArray = [NSMutableArray<EmployeeModel *> array];
    for (int i = 0; i < 3; i++) {
        EmployeeModel *employeeModel = [[EmployeeModel alloc] init];
        [employeeModel setEmployee_id:[@(i) stringValue]];
        [employeeArray addObject:employeeModel];
    }
    return employeeArray.copy;
}

- (void)printAllEmployeeWithSubCompany:(SubCompany *)subCompany {
    // 分公司員工
    [subCompany printAllEmployee];
    
    // 總公司員工
    NSArray<EmployeeModel *> *employeeArray = self.getAllEmployee;
    for (EmployeeModel *employeeModel in employeeArray) {
        NSLog(@"總公司員工ID:%@", employeeModel.employee_id);
    }
}

@end

//================== SubCompany.h ==================

@interface SubCompany : NSObject

/**
 獲取所有分公司員工
 */
- (NSArray *)getAllEmployee;

/**
 打印分公司所有員工
 */
- (void)printAllEmployee;

@end

@implementation SubCompany

- (NSArray *)getAllEmployee {
    NSMutableArray<SubEmployeeModel *> *employeeArray = [NSMutableArray<SubEmployeeModel *> array];
    for (int i = 0; i < 3; i++) {
        SubEmployeeModel *employeeModel = [[SubEmployeeModel alloc] init];
        [employeeModel setSubemployee_id:[@(i) stringValue]];
        [employeeArray addObject:employeeModel];
    }
    return employeeArray.copy;
}

- (void)printAllEmployee {
    // 分公司員工
    NSArray<SubEmployeeModel *> *subEmployeeArray = self.getAllEmployee;
    for (SubEmployeeModel *employeeModel in subEmployeeArray) {
        NSLog(@"分公司員工ID:%@", employeeModel.subemployee_id);
    }
}

@end

修改後,爲分公司增加了打印所有公鑰 ID 的方法,總公司直接調分公司的打印方法,從而避免了與分公司的員工發生耦合。

耦合的方式很多,依賴、關聯、組合、聚合等。

迪米特法則的初衷是降低類之間的耦合,由於每個類都減少了不必要的依賴,因此的確可以降低耦合關係。但是過分的使用迪米特原則,會產生大量傳遞類,導致系統複雜度變大。所以在採用迪米特法則時要反覆權衡,既做到結構清晰,又要高內聚低耦合。

原則六、開閉原則(Open Close Principle,簡稱OCP)

定義:一個軟件實體如類、模塊和函數應該對擴展開放,對修改關閉。

核心思想:儘量通過擴展應用程序中的類、模塊和函數來解決不同的需求場景,而不是通過直接修改已有的類、模塊和函數。

用抽象構建框架,用實現擴展細節,對擴展開放的關鍵是抽象,而對象的多態則保證了這種擴展的開放性。開放原則首先意味着我們可以自由地增加功能,而不會影響原有功能。這就要求我們能夠通過繼承完成功能的擴展。其次,開放原則還意味着實現是可替換的。只有利用抽象,纔可以爲定義提供不同的實現,然後根據不同的需求實例化不同的實現子類。

開放封閉原則的優點:

  • 代碼可讀性高,可維護性強。
  • 幫助縮小邏輯粒度,以提高可複用性。
  • 可以使維護人員只擴展一個類,而非修改一個類,從而提高可維護性。
  • 在設計之初考慮所有可能變化的因素,留下接口,從而符合面向對象開發的要求。

比如,書店售書的經典例子:

//================== IBookProtocol.h ==================

@protocol IBookProtocol <NSObject>

/**
 獲取書籍名稱
 */
- (NSString *)bookName;

/**
 獲取書籍售價
 */
- (CGFloat)bookPrice;

/**
 獲取書籍作者
 */
- (NSString *)bookAuthor;

@end

//================== NovelBook.h ==================

@interface NovelBook : NSObject <IBookProtocol>

- (instancetype)initWithBookName:(NSString *)name
                           price:(CGFloat)price
                          author:(NSString *)author;

@end

//================== BookStore.h ==================

@interface BookStore : NSObject

- (NSArray<IBookProtocol> *)bookArray;

@end

//================== main 函數 ==================

// 模擬書店賣書
BookStore *bookStore = [BookStore new];
for (NovelBook *novelBook in bookStore.bookArray) {
    NSLog(@"書籍名稱:%@ 書籍作者:%@ 書籍價格:%2f", [novelBook bookName], [novelBook bookAuthor], [novelBook bookPrice]);
}

運行結果如下,

2018-11-12 15:11:32.642070+0800 DesignPatterns[1863:5763476] 書籍名稱:天龍八部 書籍作者:金庸 書籍價格:50.000000
2018-11-12 15:11:32.642495+0800 DesignPatterns[1863:5763476] 書籍名稱:巴黎聖母院 書籍作者:雨果 書籍價格:70.000000
2018-11-12 15:11:32.642530+0800 DesignPatterns[1863:5763476] 書籍名稱:悲慘世界 書籍作者:雨果 書籍價格:80.000000
2018-11-12 15:11:32.642558+0800 DesignPatterns[1863:5763476] 書籍名稱:金瓶梅 書籍作者:蘭陵王 書籍價格:40.000000

將來某一天需求變更爲項目投產,書店盈利,書店決定,40 元以上打 8 折,40 元以下打 9 折。

在實際的項目開發中,如果不懂得開閉原則的話,很容易犯下面的錯誤:

  • IBookProtocol 上新增加一個方法 bookOffPrice() 方法,專門進行打折,所有實現類實現這個方法,但是如果其他不想打折的書籍也會因爲實現了書籍的接口必須打折。
  • 修改 NovelBook 實現類中的 bookPrice() 方中實現打折處理,由於該方法已經實現了打折處理價格,因此採購書籍人員看到的也是打折後的價格的情況。

很顯然按照上面兩種方案的話,隨着需求的增加,需要反覆修改之前創建的類,給新增的類造成了不必要的冗餘,業務邏輯的處理和需求不相符合等情況。

//================== OffNovelBook.h ==================

@interface OffNovelBook : NovelBook

@end

@implementation OffNovelBook

- (instancetype)initWithBookName:(NSString *)name
                           price:(CGFloat)price
                          author:(NSString *)author {
    return [super initWithBookName:name price:price author:author];
}

- (CGFloat)bookPrice {
    CGFloat originalPrice = [super bookPrice];
    CGFloat offPrice      = 0;
    if (originalPrice > 40) {
        offPrice = originalPrice * 0.8;
    } else {
        offPrice = originalPrice * 0.9;
    }
    return offPrice;
}

@end

//================== BookStore.h ==================

@interface BookStore : NSObject

- (NSArray<IBookProtocol> *)bookArray;

- (NSArray<IBookProtocol> *)offBookArray;

@end

@implementation BookStore

- (NSArray<IBookProtocol> *)bookArray {
    NSMutableArray<IBookProtocol> *tempArray = [NSMutableArray<IBookProtocol> array];
    
    NovelBook *book1 = [[NovelBook alloc] initWithBookName:@"天龍八部" price:30 author:@"金庸"];
    [tempArray addObject:book1];
    
    NovelBook *book2 = [[NovelBook alloc] initWithBookName:@"巴黎聖母院" price:70 author:@"雨果"];
    [tempArray addObject:book2];
    
    NovelBook *book3 = [[NovelBook alloc] initWithBookName:@"悲慘世界" price:80 author:@"雨果"];
    [tempArray addObject:book3];
    
    NovelBook *book4 = [[NovelBook alloc] initWithBookName:@"金瓶梅" price:40 author:@"蘭陵王"];
    [tempArray addObject:book4];
    return tempArray;
}

- (NSArray<IBookProtocol> *)offBookArray {
    NSMutableArray<IBookProtocol> *tempArray = [NSMutableArray<IBookProtocol> array];
    
    OffNovelBook *book1 = [[OffNovelBook alloc] initWithBookName:@"天龍八部" price:30 author:@"金庸"];
    [tempArray addObject:book1];
    
    OffNovelBook *book2 = [[OffNovelBook alloc] initWithBookName:@"巴黎聖母院" price:70 author:@"雨果"];
    [tempArray addObject:book2];
    
    OffNovelBook *book3 = [[OffNovelBook alloc] initWithBookName:@"悲慘世界" price:80 author:@"雨果"];
    [tempArray addObject:book3];
    
    OffNovelBook *book4 = [[OffNovelBook alloc] initWithBookName:@"金瓶梅" price:40 author:@"蘭陵王"];
    [tempArray addObject:book4];
    return tempArray;
}

@end

//================== main 函數 ==================

BookStore *bookStore = [BookStore new];

NSLog(@"------------書店賣出去的原價書籍記錄如下:------------");
for (NovelBook *novelBook in bookStore.bookArray) {
    NSLog(@"書籍名稱:%@ 書籍作者:%@ 書籍價格:%2f", [novelBook bookName], [novelBook bookAuthor], [novelBook bookPrice]);
}

NSLog(@"------------書店賣出去的打折書籍記錄如下:------------");
for (OffNovelBook *novelBook in bookStore.offBookArray) {
    NSLog(@"書籍名稱:%@ 書籍作者:%@ 書籍價格:%2f", [novelBook bookName], [novelBook bookAuthor], [novelBook bookPrice]);
}

運行結果如下,

2018-11-12 15:52:01.639550+0800 DesignPatterns[2962:6151804] ------------書店賣出去的原價書籍記錄如下:------------
2018-11-12 15:52:01.639895+0800 DesignPatterns[2962:6151804] 書籍名稱:天龍八部 書籍作者:金庸 書籍價格:30.000000
2018-11-12 15:52:01.639927+0800 DesignPatterns[2962:6151804] 書籍名稱:巴黎聖母院 書籍作者:雨果 書籍價格:70.000000
2018-11-12 15:52:01.639951+0800 DesignPatterns[2962:6151804] 書籍名稱:悲慘世界 書籍作者:雨果 書籍價格:80.000000
2018-11-12 15:52:01.639971+0800 DesignPatterns[2962:6151804] 書籍名稱:金瓶梅 書籍作者:蘭陵王 書籍價格:40.000000
2018-11-12 15:52:01.639988+0800 DesignPatterns[2962:6151804] ------------書店賣出去的打折書籍記錄如下:------------
2018-11-12 15:52:01.640029+0800 DesignPatterns[2962:6151804] 書籍名稱:天龍八部 書籍作者:金庸 書籍價格:27.000000
2018-11-12 15:52:01.640145+0800 DesignPatterns[2962:6151804] 書籍名稱:巴黎聖母院 書籍作者:雨果 書籍價格:56.000000
2018-11-12 15:52:01.640194+0800 DesignPatterns[2962:6151804] 書籍名稱:悲慘世界 書籍作者:雨果 書籍價格:64.000000
2018-11-12 15:52:01.640217+0800 DesignPatterns[2962:6151804] 書籍名稱:金瓶梅 書籍作者:蘭陵王 書籍價格:36.000000

在實際的項目開發中,

  • 對抽象定義的修改,要保證定義的接口或者 Protocol 的穩定,尤其要保證被其他對象調用的接口的穩定;否則,就會導致修改蔓延,牽一髮而動全身。

  • 對具體實現的修改,因爲具體實現的修改,可能會給調用者帶來意想不到的結果。如果確實需要修改具體的實現,就需要做好達到測試覆蓋率要求的單元測試。

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