原則一、單一職責原則(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
類不再依賴 Newspaper
和 Book
類具體實現,而是依賴抽象出來的接口或者 Protocol
。並且 Newspaper
和 Book
類也都依賴這個抽象出來的接口或者 Protocol
,通過實現接口或者 Protocol
來做自己的事情。
//================== IReaderProtocol.h ==================
@protocol IReaderProtocol <NSObject>
/**
故事內容
*/
- (void)theStoryContent;
@end
Mother
類與接口 IReader
發生依賴關係,而 Book
和 Newspaper
都屬於讀物的範疇,他們各自都去實現 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
的一個特性,可以消除在 ClassB
和 ClassD
中沒有實現 InterfaceH
的 protocol
協議。
比如,類 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
。而SubEmployeeModel
和 Company
並不是直接聯繫,換句話說,總公司 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
的穩定,尤其要保證被其他對象調用的接口的穩定;否則,就會導致修改蔓延,牽一髮而動全身。對具體實現的修改,因爲具體實現的修改,可能會給調用者帶來意想不到的結果。如果確實需要修改具體的實現,就需要做好達到測試覆蓋率要求的單元測試。