淺談面向物件的六大設計原則

image.png
原則一、單一職責原則(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
需要完成兩個功能:
由於類 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
的穩定,尤其要保證被其他物件呼叫的介面的穩定;否則,就會導致修改蔓延,牽一髮而動全身。 -
對具體實現的修改,因為具體實現的修改,可能會給呼叫者帶來意想不到的結果。如果確實需要修改具體的實現,就需要做好達到測試覆蓋率要求的單元測試。