面向對象設計的六大設計原則(附 Demo & UML類圖)

學習初衷與講解方式

筆者想在 iOS 從業第三年結束以前系統學習一下關於設計模式方面的知識。而在學習設計模式以前,以爲更有必要先學習面向對象設計(OOD:Object Oriented Design)的幾大設計原則,爲後面設計模式的學習打下基礎。前端

本篇分享的就是筆者近階段學習和總結的面向對象設計的六個設計原則:git

縮寫 英文名稱 中文名稱
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 依賴倒置原則

注意,一般所說的SOLID(上方表格縮寫的首字母,從上到下)設計原則沒有包含本篇介紹的迪米特法則,而只有其餘五項。另外,本篇不包含合成/聚合複用原則(CARP),由於筆者認爲該原則沒有其餘六個原則典型,並且在實踐中也不容易違背。有興趣的同窗能夠自行查資料學習。程序員

在下一章節筆者將分別講解這些設計原則,講解的方式是將概念與代碼及其對應的UML 類圖結合起來說解的方式。github

代碼的語言使用的是筆者最熟悉的Objective-C語言。雖然是一個比較小衆的語言,可是由於有 UML 類圖的幫助,並且主流的面嚮對象語言關於類,接口(Objective-C裏面是協議)的使用在形式上相似,因此筆者相信語言的小衆不會對知識的理解產生太大的阻力。編程

另外,在每一個設計模式的講解裏,筆者會首先描述一個應用場景(需求點),接着用兩種設計的代碼來進行對比講解:先提供相對很差的設計的代碼,再提供相對好的設計的代碼。並且兩種代碼都會附上標準的 UML 類圖來進行更形象地對比,幫助你們來理解。同時也能夠幫助不瞭解 UML 類圖的讀者先簡單熟悉一下 UML 類圖的語法。後端

六大設計原則

本篇講解六大設計原則的順序大體按照難易程序排列。在這裏最早講解開閉原則,由於其在理解上比較簡單,並且也是其餘設計原則的基石。設計模式

注意:數組

  1. 六個原則的講解所用的例子之間並無關聯,因此閱讀順序能夠按照讀者的喜愛來定。
  2. Java語言裏的接口在Objective-C裏面叫作協議。雖然Demo是用Objective-C寫的,可是由於協議的叫法比較小衆,故後面一概用接口代替協議這個說法。

原則一:開閉原則(Open Close Principle)

定義

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類聲明瞭最初的在線課程所須要包含的數據:

  • 課程名稱
  • 課程介紹
  • 講師姓名
  • 文字內容

接着按照上面所說的需求變動:增長了視頻,音頻,直播課程:

//================== 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類實例化的視頻課程對象會包含並不屬於本身的數據:audioUrlliveUrl:這樣就形成了冗餘,視頻課程對象並非純粹的視頻課程對象,它包含了音頻地址,直播地址等成員。

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

  1. 隨着需求的增長,須要反覆修改以前建立的類。
  2. 給新增的類形成了沒必要要的冗餘。

之因此會形成上述兩個缺陷,是由於該設計沒有遵循對修改關閉,對擴展開放的開閉原則,而是反其道而行之:開放修改,並且不給擴展提供便利。

難麼怎麼作能夠遵循開閉原則呢?下面看一下遵循開閉原則的較好的設計:

較好的設計

首先在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
複製代碼

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

  1. 隨着課程類型的增長,不須要反覆修改最初的父類(Course),只須要新建一個繼承於它的子類並在子類中添加僅屬於該子類的數據(或行爲)便可。
  2. 由於各類課程獨有的數據(或行爲)都被分散到了不一樣的課程子類裏,因此每一個子類的數據(或行爲)沒有任何冗餘。

並且對於第二點:或許從此的視頻課程能夠有高清地址,視頻加速功能。而這些功能只須要在VideoCourse類裏添加便可,由於它們都是視頻課程所獨有的。一樣地,直播課程後面還能夠支持在線問答功能,也能夠僅加在LiveCourse裏面。

咱們能夠看到,正是因爲最初程序設計合理,因此對後面需求的增長才會處理得很好。

下面來看一下這兩個設計的UML 類圖,能夠更形象地看出兩種設計上的區別:

UML 類圖對比

未實踐開閉原則:

未實踐開閉原則

實踐了開閉原則:

實踐了開閉原則

在實踐了開閉原則的 UML 類圖中,四個課程類繼承了Course類並添加了本身獨有的屬性。(在 UML 類圖中:實線空心三角箭頭表明繼承關係:由子類指向其父類)

如何實踐

爲了更好地實踐開閉原則,在設計之初就要想清楚在該場景裏哪些數據(或行爲)是必定不變(或很難再改變)的,哪些是很容易變更的。將後者抽象成接口或抽象方法,以便於在未來經過創造具體的實現應對不一樣的需求。

原則二:單一職責原則(Single Responsibility Principle)

定義

A class should have a single responsibility, where a responsibility is nothing but a reason to change.

即:一個類只容許有一個職責,即只有一個致使該類變動的緣由。

定義的解讀

  • 類職責的變化每每就是致使類變化的緣由:也就是說若是一個類具備多種職責,就會有多種致使這個類變化的緣由,從而致使這個類的維護變得困難。

  • 每每在軟件開發中隨着需求的不斷增長,可能會給原來的類添加一些原本不屬於它的一些職責,從而違反了單一職責原則。若是咱們發現當前類的職責不只僅有一個,就應該將原本不屬於該類真正的職責分離出去。

  • 不只僅是類,函數(方法)也要遵循單一職責原則,即:一個函數(方法)只作一件事情。若是發現一個函數(方法)裏面有不一樣的任務,則須要將不一樣的任務以另外一個函數(方法)的形式分離出去。

優勢

若是類與方法的職責劃分得很清晰,不但能夠提升代碼的可讀性,更實際性地更下降了程序出錯的風險,由於清晰的代碼會讓bug無處藏身,也有利於bug的追蹤,也就是下降了程序的維護成本。

代碼講解

單一職責原則的demo比較簡單,經過對象(屬性)的設計上講解已經足夠,不須要具體的客戶端調用。咱們先看一下需求點:

需求點

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

新需求:增長兩個方法:

  • 斷定員工在今年是否升職
  • 計算員工的薪水

先來看一下很差的設計:

很差的設計

//================== 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
複製代碼

由上面的代碼能夠看出:

  • 在初始需求下,咱們建立了Employee這個員工類,並聲明瞭3個員工信息的屬性:員工姓名,地址,員工ID。
  • 在新需求下,兩個方法直接加到了員工類裏面。

新需求的作法看似沒有問題,由於都是和員工有關的,但卻違反了單一職責原則:由於這兩個方法並非員工自己的職責

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

而上面的設計將原本不屬於員工本身的職責強加進了員工類裏面,而這個類的設計初衷(原始職責)就是單純地保留員工的一些信息而已。所以這麼作就是給這個類引入了新的職責,故此設計違反了單一職責原則

咱們能夠簡單想象一下這麼作的後果是什麼:若是員工的晉升機制變了,或者稅收政策等影響員工工資的因素變了,咱們還須要修改當前這個類。

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

較好的設計

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

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

@interface Employee : NSObject

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

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

//================== 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
複製代碼

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

這樣一來,不只僅在這次新需求中知足了單一職責原則,之後若是還要增長人事部門和會計部門處理的任務,就能夠直接在這兩個類裏面添加便可。

下面來看一下這兩個設計的UML 類圖,能夠更形象地看出兩種設計上的區別:

UML 類圖對比

未實踐單一職責原則:

未實踐單一職責原則

實踐了單一職責原則:

實踐了單一職責原則

能夠看到,在實踐了單一職責原則的 UML 類圖中,不屬於Employee的兩個職責被分類了FinancialApartment類 和 HRApartment類。(在 UML 類圖中,虛線箭頭表示依賴關係,經常使用在方法參數等,由依賴方指向被依賴方)

上面說過除了類要遵循單一職責設計原則以外,在函數(方法)的設計上也要遵循單一職責的設計原則。因函數(方法)的單一職責原則理解起來比較容易,故在這裏就不提供Demo和UML 類圖了。

能夠簡單舉一個例子:

APP的默認導航欄的樣式是這樣的:

  • 白色底
  • 黑色標題
  • 底部有陰影

那麼建立默認導航欄的僞代碼多是這樣子的:

//默認樣式的導航欄
- (void)createDefaultNavigationBarWithTitle:(NSString *)title{
    
    //create white color background view
    
    //create black color title
    
    //create shadow bottom
}
複製代碼

如今咱們能夠用這個方法統一建立默認的導航欄了。 可是過不久又有新的需求來了,有的頁面的導航欄須要作成透明的,所以須要一個透明樣式的導航欄:

  • 透明底
  • 白色標題
  • 底部無陰影

針對這個需求,咱們能夠新增一個方法:

//透明樣式的導航欄
- (void)createTransParentNavigationBarWithTitle:(NSString *)title{
    
    //create transparent color background view
    
    //create white color title
}
複製代碼

看出問題來了麼?在這兩個方法裏面,創造background view和 title color title的方法的差異僅僅是顏色不一樣而已,而其餘部分的代碼是重複的。 所以咱們應該將這兩個方法抽出來:

//根據傳入的顏色參數設置導航欄的背景色
- (void)createBackgroundViewWithColor:(UIColor)color;

//根據傳入的標題字符串和顏色參數設置標題
- (void)createTitlewWithColorWithTitle:(NSString *)title color:(UIColor)color;
複製代碼

並且上面的製造陰影的部分也能夠做爲方法抽出來:

- (void)createShadowBottom;
複製代碼

這樣一來,原來的兩個方法能夠寫成:

//默認樣式的導航欄
- (void)createDefaultNavigationBarWithTitle:(NSString *)title{
    
    //設置白色背景
    [self createBackgroundViewWithColor:[UIColor whiteColor]];
    
    //設置黑色標題
    [self createTitlewWithColorWithTitle:title color:[UIColor blackColor]];
    
    //設置底部陰影
    [self createShadowBottom];
}


//透明樣式的導航欄
- (void)createTransParentNavigationBarWithTitle:(NSString *)title{
    
    //設置透明背景
    [self createBackgroundViewWithColor:[UIColor clearColor]];
    
    //設置白色標題
    [self createTitlewWithColorWithTitle:title color:[UIColor whiteColor]];
}
複製代碼

並且咱們也能夠將裏面的方法拿出來在外面調用也能夠:

設置默認樣式的導航欄:

//設置白色背景
[navigationBar createBackgroundViewWithColor:[UIColor whiteColor]];

//設置黑色標題
[navigationBar createTitlewWithColorWithTitle:title color:[UIColor blackColor]];

//設置陰影
[navigationBar createShadowBottom];
複製代碼

設置透明樣式的導航欄:

//設置透明色背景
[navigationBar createBackgroundViewWithColor:[UIColor clearColor]];

//設置白色標題
[navigationBar createTitlewWithColorWithTitle:title color:[UIColor whiteColor]];
複製代碼

這樣一來,不管寫在一個大方法裏面調用或是分別在外面調用,都能很清楚地看到導航欄的每一個元素是如何生成的,由於每一個職責都分配到了一個單獨的方法裏面。並且還有一個好處是,透明導航欄若是遇到淺色背景的話,使用白色字體不如使用黑色字體好,因此遇到這種狀況咱們能夠在createTitlewWithColorWithTitle:color:方法裏面傳入黑色色值。 並且從此可能還會有更多的導航欄樣式,那麼咱們只須要分別改變傳入的色值便可,不須要有大量的重複代碼了,修改起來也很方便。

如何實踐

對於上面的員工類的例子,或許是由於咱們先入爲主,知道一個公司的合理組織架構,以爲這麼設計理所固然。可是在實際開發中,咱們很容易會將不一樣的責任揉在一塊兒,這點仍是須要開發者注意的。

原則三:依賴倒置原則(Dependency Inversion Principle)

定義

  • 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.

即:

  • 依賴抽象,而不是依賴實現。
  • 抽象不該該依賴細節;細節應該依賴抽象。
  • 高層模塊不能依賴低層模塊,兩者都應該依賴抽象。

定義解讀

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

優勢

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

代碼講解

下面經過一個模擬項目開發的例子來說解依賴倒置原則。

需求點

實現下面這樣的需求:

用代碼模擬一個實際項目開發的場景:前端和後端開發人員開發同一個項目。

很差的設計

首先生成兩個類,分別對應前端和後端開發者:

前端開發者:

//================== FrondEndDeveloper.h ==================

@interface FrondEndDeveloper : NSObject

- (void)writeJavaScriptCode;

@end



//================== FrondEndDeveloper.m ==================

@implementation FrondEndDeveloper

- (void)writeJavaScriptCode{
    NSLog(@"Write JavaScript code");
}

@end
複製代碼

後端開發者:

//================== BackEndDeveloper.h ==================

@interface BackEndDeveloper : NSObject

- (void)writeJavaCode;

@end



//================== BackEndDeveloper.m ==================

@implementation BackEndDeveloper

- (void)writeJavaCode{
    NSLog(@"Write Java code");
}
@end
複製代碼

這兩個開發者分別對外提供了本身開發的方法:writeJavaScriptCodewriteJavaCode

接着建立一個Project類:

//================== Project.h ==================

@interface Project : NSObject

//構造方法,傳入開發者的數組
- (instancetype)initWithDevelopers:(NSArray *)developers;

//開始開發
- (void)startDeveloping;

@end



//================== Project.m ==================

#import "Project.h"
#import "FrondEndDeveloper.h"
#import "BackEndDeveloper.h"

@implementation Project
{
    NSArray *_developers;
}


- (instancetype)initWithDevelopers:(NSArray *)developers{
    
    if (self = [super init]) {
        _developers = developers;
    }
    return self;
}



- (void)startDeveloping{
    
    [_developers enumerateObjectsUsingBlock:^(id  _Nonnull developer, NSUInteger idx, BOOL * _Nonnull stop) {
        
        if ([developer isKindOfClass:[FrondEndDeveloper class]]) {
            
            [developer writeJavaScriptCode];
            
        }else if ([developer isKindOfClass:[BackEndDeveloper class]]){
            
            [developer writeJavaCode];
            
        }else{
            //no such developer
        }
    }];
}

@end
複製代碼

Project類中,咱們首先經過一個構造器方法,將開發者的數組傳入project的實例對象。而後在開始開發的方法startDeveloping裏面,遍歷數組並判斷元素類型的方式讓不一樣類型的開發者調用和本身對應的函數。

思考一下,這樣的設計有什麼問題?

問題一:

假如後臺的開發語言改爲了GO語言,那麼上述代碼須要改動兩個地方:

  • BackEndDeveloper:須要向外提供一個writeGolangCode方法。
  • Project類的startDeveloping方法裏面須要將BackEndDeveloper類的writeJavaCode改爲writeGolangCode

問題二:

假如後期老闆要求作移動端的APP(須要iOS和安卓的開發者),那麼上述代碼仍然須要改動兩個地方:

  • 還須要給Project類的構造器方法裏面傳入IOSDeveloperAndroidDeveloper兩個類。並且按照現有的設計,還要分別向外部提供writeSwiftCodewriteKotlinCode
  • Project類的startDeveloping方法裏面須要再多兩個elseif判斷,專門判斷IOSDeveloperAndroidDeveloper這兩個類。

開發安卓的代碼也能夠用Java,可是爲了和後臺的開發代碼區分一下,這裏用了一樣能夠開發安卓的Kotlin語言。

很顯然,在這兩種假設的場景下,高層模塊(Project)都依賴了低層模塊(BackEndDeveloper)的改動,所以上述設計不符合依賴倒置原則

那麼該如何設計才能夠符合依賴倒置原則呢?

答案是將開發者寫代碼的方法抽象出來,讓Project類再也不依賴全部低層的開發者類的具體實現,而是依賴抽象。並且從下至上,全部底層的開發者類也都依賴這個抽象,經過實現這個抽象來作本身的任務

這個抽象能夠用接口,也能夠用抽象類的方式來作,在這裏筆者用使用接口的方式進行講解:

較好的設計

首先,建立一個接口,接口裏面有一個寫代碼的方法writeCode

//================== DeveloperProtocol.h ==================

@protocol DeveloperProtocol <NSObject>

- (void)writeCode;

@end
複製代碼

而後,讓前端程序員和後端程序員類實現這個接口(遵循這個協議)並按照本身的方式實現:

前端程序員類:

//================== FrondEndDeveloper.h ==================

@interface FrondEndDeveloper : NSObject<DeveloperProtocol>
@end



//================== FrondEndDeveloper.m ==================

@implementation FrondEndDeveloper

- (void)writeCode{
    NSLog(@"Write JavaScript code");
}
@end
複製代碼

後端程序員類:

//================== BackEndDeveloper.h ==================

@interface BackEndDeveloper : NSObject<DeveloperProtocol>
@end



//================== BackEndDeveloper.m ==================
@implementation BackEndDeveloper

- (void)writeCode{
    NSLog(@"Write Java code");
}
@end
複製代碼

最後咱們看一下新設計後的Project類:

//================== Project.h ==================

#import "DeveloperProtocol.h"

@interface Project : NSObject

//只需傳入遵循DeveloperProtocol的對象數組便可
- (instancetype)initWithDevelopers:(NSArray <id <DeveloperProtocol>>*)developers;

//開始開發
- (void)startDeveloping;

@end


//================== Project.m ==================

#import "FrondEndDeveloper.h"
#import "BackEndDeveloper.h"

@implementation Project
{
    NSArray <id <DeveloperProtocol>>* _developers;
}


- (instancetype)initWithDevelopers:(NSArray <id <DeveloperProtocol>>*)developers{
    
    if (self = [super init]) {
        _developers = developers;
    }
    return self;
    
}


- (void)startDeveloping{
    
    //每次循環,直接向對象發送writeCode方法便可,不須要判斷
    [_developers enumerateObjectsUsingBlock:^(id<DeveloperProtocol>  _Nonnull developer, NSUInteger idx, BOOL * _Nonnull stop) {
        
        [developer writeCode];
    }];
    
}

@end
複製代碼

新的Project的構造方法只需傳入遵循DeveloperProtocol協議的對象構成的數組便可。這樣也比較符合現實中的需求:只須要會寫代碼就能夠加入到項目中。

而新的startDeveloping方法裏:每次循環,直接向當前對象發送writeCode方法便可,不須要對程序員的類型作判斷。由於這個對象必定是遵循DeveloperProtocol接口的,而遵循該接口的對象必定會實現writeCode方法(就算不實現也不會引發重大錯誤)。

如今新的設計接受完了,咱們經過上面假設的兩個狀況來和以前的設計作個對比:

假設1:後臺的開發語言改爲了GO語言

在這種狀況下,只需更改BackEndDeveloper類裏面對於DeveloperProtocol接口的writeCode方法的實現便可:

//================== BackEndDeveloper.m ==================
@implementation BackEndDeveloper

- (void)writeCode{

    //Old:
    //NSLog(@"Write Java code");
    
    //New:
    NSLog(@"Write Golang code");
}
@end
複製代碼

而在Project裏面不須要修改任何代碼,由於Project類只依賴了接口方法WriteCode,沒有依賴其具體的實現。

咱們接着看一下第二個假設:

假設2:後期老闆要求作移動端的APP(須要iOS和安卓的開發者)

在這個新場景下,咱們只須要將新建立的兩個開發者類:IOSDeveloperAndroidDeveloper分別實現DeveloperProtocol接口的writeCode方法便可。

一樣,Project的接口和實現代碼都不用修改:客戶端只須要在Project的構建方法的數組參數裏面添加這兩個新類的實例便可,不須要在startDeveloping方法裏面添加類型判斷,緣由同上。

咱們能夠看到,新設計很好地在高層類(Project)與低層類(各類developer類)中間加了一層抽象,解除了兩者在舊設計中的耦合,使得在低層類中的改動沒有影響到高層類。

一樣是抽象,新設計一樣也能夠用抽象類的方式:建立一個Developer的抽象類並提供一個writeCode方法,讓不一樣的開發者類繼承與它並按照本身的方式實現writeCode方法。這樣一來,在Project類的構造方法就是傳入已Developer類型爲元素的數組了。有興趣的小夥伴能夠本身實現一下~

下面來看一下這兩個設計的UML 類圖,能夠更形象地看出兩種設計上的區別:

UML 類圖對比

未實踐依賴倒置原則:

未實踐依賴倒置原則

實踐了依賴倒置原則:

實踐了依賴倒置原則

在實踐了依賴倒置原則的 UML 類圖中,咱們能夠看到Project僅僅依賴於新的接口;並且低層的FrondEndDevelopeBackEndDevelope類按照本身的方式實現了這個接口:經過接口解除了原有的依賴。(在 UML 類圖中,虛線三角箭頭表示接口實線,由實現方指向接口)

如何實踐

從此在處理高低層模塊(類)交互的情景時,儘可能將兩者的依賴經過抽象的方式解除掉,實現方式能夠是經過接口也能夠是抽象類的方式。

原則四:接口分離原則(Interface Segregation Principle)

定義

Many client specific interfaces are better than one general purpose interface.

即:多個特定的客戶端接口要好於一個通用性的總接口。

定義解讀

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

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

優勢

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

代碼講解

下面經過一個餐廳服務的例子講解一下接口分離原則。

需求點

如今的餐廳除了提供傳統的店內服務,多數也都支持網上下單,網上支付功能。寫一些接口方法來涵蓋餐廳的全部的下單及支付功能。

很差的設計

//================== RestaurantProtocol.h ==================

@protocol RestaurantProtocol <NSObject>

- (void)placeOnlineOrder;         //下訂單:online
- (void)placeTelephoneOrder;      //下訂單:經過電話
- (void)placeWalkInCustomerOrder; //下訂單:在店裏

- (void)payOnline;                //支付訂單:online
- (void)payInPerson;              //支付訂單:在店裏支付

@end
複製代碼

在這裏聲明瞭一個接口,它包含了下單和支付的幾種方式:

  • 下單:

    • online下單
    • 電話下單
    • 店裏下單(店內服務)
  • 支付

    • online支付(適用於online下單和電話下單的顧客)
    • 店裏支付(店內服務)

這裏先不討論電話下單的顧客是用online支付仍是店內支付。

對應的,咱們有三種下單方式的顧客:

1.online下單,online支付的顧客

//================== OnlineClient.h ==================

#import "RestaurantProtocol.h"

@interface OnlineClient : NSObject<RestaurantProtocol>
@end



//================== OnlineClient.m ==================

@implementation OnlineClient

- (void)placeOnlineOrder{
    NSLog(@"place on line order");
}

- (void)placeTelephoneOrder{
    //not necessarily
}

- (void)placeWalkInCustomerOrder{
    //not necessarily
}

- (void)payOnline{
    NSLog(@"pay on line");
}

- (void)payInPerson{
    //not necessarily
}
@end

複製代碼

2.電話下單,online支付的顧客

//================== TelephoneClient.h ==================

#import "RestaurantProtocol.h"

@interface TelephoneClient : NSObject<RestaurantProtocol>
@end



//================== TelephoneClient.m ==================

@implementation TelephoneClient

- (void)placeOnlineOrder{
    //not necessarily
}

- (void)placeTelephoneOrder{
    NSLog(@"place telephone order");
}

- (void)placeWalkInCustomerOrder{
    //not necessarily
}

- (void)payOnline{
    NSLog(@"pay on line");
}

- (void)payInPerson{
    //not necessarily
}

@end
複製代碼

3.在店裏下單並支付的顧客:

//================== WalkinClient.h ==================

#import "RestaurantProtocol.h"

@interface WalkinClient : NSObject<RestaurantProtocol>
@end



//================== WalkinClient.m ==================

@implementation WalkinClient

- (void)placeOnlineOrder{
   //not necessarily
}

- (void)placeTelephoneOrder{
    //not necessarily
}

- (void)placeWalkInCustomerOrder{
    NSLog(@"place walk in customer order");
}

- (void)payOnline{
   //not necessarily
}

- (void)payInPerson{
    NSLog(@"pay in person");
}

@end
複製代碼

咱們發現,並非全部顧客都必需要實現RestaurantProtocol裏面的全部方法。因爲接口方法的設計形成了冗餘,所以該設計不符合接口隔離原則

注意,Objective-C中的協議能夠經過@optional關鍵字設置不須要必須實現的方法,該特性不與接口分離原則衝突:只要屬於同一類責任的接口,均可以放入同一接口中。

那麼如何作才符合接口隔離原則呢?咱們來看一下較好的設計。

較好的設計

要符合接口隔離原則,只須要將不一樣類型的接口分離出來便可。咱們將原來的RestaurantProtocol接口拆分紅兩個接口:下單接口和支付接口。

下單接口:

//================== RestaurantPlaceOrderProtocol.h ==================

@protocol RestaurantPlaceOrderProtocol <NSObject>

- (void)placeOrder;

@end
複製代碼

支付接口:

//================== RestaurantPaymentProtocol.h ==================

@protocol RestaurantPaymentProtocol <NSObject>

- (void)payOrder;

@end
複製代碼

如今有了下單接口和支付接口,咱們就可讓不一樣的客戶來以本身的方式實現下單和支付操做了:

首先建立一個全部客戶的父類,來遵循這個兩個接口:

//================== Client.h ==================

#import "RestaurantPlaceOrderProtocol.h"
#import "RestaurantPaymentProtocol.h"

@interface Client : NSObject<RestaurantPlaceOrderProtocol,RestaurantPaymentProtocol>
@end
複製代碼

接着另online下單,電話下單,店內下單的顧客繼承這個父類,分別實現這兩個接口的方法:

1.online下單,online支付的顧客

//================== OnlineClient.h ==================

#import "Client.h"
@interface OnlineClient : Client
@end



//================== OnlineClient.m ==================

@implementation OnlineClient

- (void)placeOrder{
    NSLog(@"place on line order");
}

- (void)payOrder{
    NSLog(@"pay on line");
}

@end
複製代碼

2.電話下單,online支付的顧客

//================== TelephoneClient.h ==================
#import "Client.h"
@interface TelephoneClient : Client
@end



//================== TelephoneClient.m ==================
@implementation TelephoneClient

- (void)placeOrder{
    NSLog(@"place telephone order");
}

- (void)payOrder{
    NSLog(@"pay on line");
}

@end
複製代碼

3.在店裏下單並支付顧客:

//================== WalkinClient.h ==================

#import "Client.h"
@interface WalkinClient : Client
@end



//================== WalkinClient.m ==================

@implementation WalkinClient

- (void)placeOrder{
    NSLog(@"place walk in customer order");
}

- (void)payOrder{
    NSLog(@"pay in person");
}

@end
複製代碼

由於咱們把不一樣職責的接口拆開,使得接口的責任更加清晰,簡潔明瞭。不一樣的客戶端能夠根據本身的需求遵循所須要的接口來以本身的方式實現。

並且從此若是還有和下單或者支付相關的方法,也能夠分別加入到各自的接口中,避免了接口的臃腫,同時也提升了程序的內聚性。

下面來看一下這兩個設計的UML 類圖,能夠更形象地看出兩種設計上的區別:

UML 類圖對比

未實踐接口分離原則:

未實踐接口分離原則

實踐了接口分離原則:

實踐了接口分離原則

經過遵照接口分離原則,接口的設計變得更加簡潔,並且各類客戶類不須要實現本身不須要實現的接口。

如何實踐

在設計接口時,尤爲是在向現有的接口添加方法時,咱們須要仔細斟酌這些方法是不是處理同一類任務的:若是是則能夠放在一塊兒;若是不是則須要作拆分。

作iOS開發的朋友對UITableViewUITableViewDelegateUITableViewDataSource這兩個協議應該會很是熟悉。這兩個協議裏的方法都是與UITableView相關的,但iOS SDK的設計者卻把這些方法放在不一樣的兩個協議中。緣由就是這兩個協議所包含的方法所處理的任務是不一樣的兩種:

  • UITableViewDelegate:含有的方法是UITableView的實例告知其代理一些點擊事件的方法,即事件的傳遞,方向是從UITableView的實例到其代理。
  • UITableViewDataSource:含有的方法是UITableView的代理傳給UITableView一些必要數據供UITableView展現出來,即數據的傳遞,方向是從UITableView的代理到UITableView

很顯然,UITableView協議的設計者很好地實踐了接口分離的原則,值得咱們你們學習。

原則五:迪米特法則(Law of Demeter)

定義

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方法實現,將新引擎的品牌名稱返回便可。

下面來看一下這兩個設計的UML 類圖,能夠更形象地看出兩種設計上的區別:

UML 類圖對比

未實踐迪米特法則:

未實踐迪米特法則

實踐了迪米特法則:

實踐了迪米特法則

很明顯,在實踐了迪米特法則的 UML 類圖裏面,沒有了ClientGasEngine的依賴,耦合性下降。

如何實踐

從此在作對象與對象之間交互的設計時,應該極力避免引出中間對象的狀況(須要導入其餘對象的類):須要什麼對象直接返回便可,下降類之間的耦合度。

原則六:里氏替換原則(Liskov Substitution Principle)

定義

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.)

即:全部引用基類的地方必須能透明地使用其子類的對象,也就是說子類對象能夠替換其父類對象,而程序執行效果不變。

定義的解讀

在繼承體系中,子類中能夠增長本身特有的方法,也能夠實現父類的抽象方法,可是不能重寫父類的非抽象方法,不然該繼承關係就不是一個正確的繼承關係。

優勢

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

代碼講解

在這裏用一個簡單的長方形與正方形的例子講解一下里氏替換原則。

需求點

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

很差的設計

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

長方形類:

//================== 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
複製代碼

咱們能夠看到,RectangeSquare類都以本身的方式實現了父類Quadrangle的公共方法。並且因爲Square的特殊性,它也聲明瞭本身獨有的成員變量_sideLength以及其對應的公共方法。

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

下面來看一下這兩個設計的UML 類圖,能夠更形象地看出兩種設計上的區別:

UML 類圖對比

未實踐里氏替換原則:

未實踐里氏替換原則

實踐了里氏替換原則:

實踐了里氏替換原則

如何實踐

里氏替換原則是對繼承關係的一種檢驗:檢驗是否真正符合繼承關係,以免繼承的濫用。所以,在使用繼承以前,須要反覆思考和確認該繼承關係是否正確,或者當前的繼承體系是否還能夠支持後續的需求變動,若是沒法支持,則須要及時重構,採用更好的方式來設計程序。

最後的話

到這裏關於六大設計原則的講解已經結束了。本篇文章所展現的Demo和UML 類圖都在筆者維護的一個專門的GitHub庫中:object-oriented-design。想看Demo和UML圖的同窗能夠點擊連接查看。歡迎fork,更歡迎給出更多語言的不一樣例子的PR~ 並且後續還會添加關於設計模式的 代碼和 UML 類圖。

關於這幾個設計原則還有最後一點須要強調的是: 設計原則是設計模式的基石,可是很難在使實際開發中的某個設計中所有都知足這些設計原則。所以咱們須要抓住具體設計場景的特殊性,有選擇地遵循最合適的設計原則。

本篇已同步到我的博客:面向對象設計的六大設計原則(附 Demo 及 UML 類圖)


筆者在近期開通了我的公衆號,主要分享編程,讀書筆記,思考類的文章。

  • 編程類文章:包括筆者之前發佈的精選技術文章,以及後續發佈的技術文章(以原創爲主),而且逐漸脫離 iOS 的內容,將側重點會轉移到提升編程能力的方向上。
  • 讀書筆記類文章:分享編程類思考類心理類職場類書籍的讀書筆記。
  • 思考類文章:分享筆者平時在技術上生活上的思考。

由於公衆號天天發佈的消息數有限制,因此到目前爲止尚未將全部過去的精選文章都發布在公衆號上,後續會逐步發佈的。

並且由於各大博客平臺的各類限制,後面還會在公衆號上發佈一些短小精幹,以小見大的乾貨文章哦~

掃下方的公衆號二維碼並點擊關注,期待與您的共同成長~

相關文章
相關標籤/搜索