[Unity設計模式與遊戲開發]七大設計原則

前言

我對設計模式的理解是它就好像習武之人的內功,當內功強的人學習各種高深的武功就很得心應手,設計模式不同層次不同階段的人對它的理解不同,我一直認爲設計模式和算法一直是程序員兩塊非常重要的基本功,當基本功紮實就能對各種框架各種新技術駕輕就熟,會學習的很快,雖然在剛畢業的時候看過一遍設計模式,但感覺對它的理解還不夠深,現在再重頭回顧捋一遍。

設計模式的目的

設計模式爲了保證程序具有更好的

  • 代碼重用性
    相同的代碼不需要多次編寫
  • 可讀性
    編程規範性,便於其他程序員的閱讀和理解
  • 可擴展性
    當需要增加新的功能時,非常的方便,可維護
  • 可靠性
    當我們新增新的功能時,對原來的功能沒有影響
  • 靈活性
  • 使程序呈現高內聚,低耦合的特性
    模塊與模塊之間耦合度低,但模塊之間相互配合功能高度聚合

設計原則

談到設計原則想必我們都聽過SOLID一詞,他就是幾個設計原則的首字母合併起來的簡稱,以前我對幾個設計原則我都是死記硬背,但發現很容易忘記,當知道有SOLID這個單詞的時候,腦開中每個字母聯想一個單子就很容易記住這幾個原則,S(單一職責原則)、O(開放封閉原則)、L(裏式替換原則)、I(接口隔離原則)、D(迪米特法則),還有兩個依賴倒轉和合成複用原則。爲什麼設計模式要先談設計原則呢?記得剛畢業面試的時候,面試官問我設計模式,我有時候嘴裏會說出設計原則,然後面試官提醒我這是設計原則這不是設計模式,那麼這兩者是一個什麼樣的關係呢?後來我才知道23種設計模式萬變不離其中,都是遵循這些設計原則的,下面我們弄清楚每一個設計原則。

單一職責原則(SRP)

單一職責原則的定義是對一個類來說,只負責一個職責或者功能。也就是說不要設計大而全的類,要設計力度小、功能單一的類。如果一個類包含兩個或者兩個以上的業務不相干的功能,那麼說它的職責不夠單一,應該將它拆分成多個功能單一的類、粒度更細的類。

單一職責原則注意事項和細節
  • 降低類的複雜度,一個類只負責一項職責,一個方法只負責一個職責
  • 提高類的可讀性,可維護性
  • 降低變更引起的風險
  • 慎用很多分支的if else的分支判斷,耦合度比較高
舉個反例

一個玩家信息類這樣設計

public class UserInfo
{
	private long UserId;
    private string UserName;
    private string Emial;
    private string PhoneNumber;
    private string AvatarUrl;
    private string Province; //省
    private string City; //市
    private string Region; //區
    private string DetailAddress; //詳細地址
}

咋一看感覺設計的還行,這個類包含了詳細的玩家信息,但如果公司業務拓展,某個業務只需要玩家的一些基本信息用於app登錄,並不關心玩家的詳細住址,那麼如果還用這個玩家信息的話就會有多餘字段,而且這個地址信息在玩家信息中佔的比重也是蠻大的,所以建議將地址信息抽出來單獨變成一個Address類,UserInfo保留一個Address類的對象即可,如果不需要詳細的地址信息這個對象可以保留爲空,或者說將玩家的基本信息單獨抽出來爲UserBaseInfo,然後通過組合模式,組合成一個詳細的玩家信息UserInfo也可以。

思考:類的職責是否越單一越好?

上面都在強調類的職責單一,那麼是否是越單一越好呢?凡事過猶不及,答案是否定的,舉例來說明:我們常用的序列化和反序列化,無論是哪種語言,序列化和反序列化的方法都在一個類裏面,如果我們把序列化方法放在序列化的類裏面,反序列化的方法放在反序列化類裏面,經過拆分感覺Serializer類和Deserializer類職責更加單一了,但隨之而來也帶來新的問題,如果我們修改了協議或者序列化方式從JSON變成了XML,那麼兩個類都要做響應的修改,內聚性就沒有原來的一個Serialization類高了,如果我們只修改了Serializer類而忘記了該Deserializer類的代碼,那麼就導致序列化、反序列化不匹配就會運行出錯,也就是說拆分之後代碼的可維護性變差了。

如何判斷一個類職責是否單一?

不同的場景、不同的階段的背景需求、不同的業務層面,對同一個類的職責是否單一,可能會有不同的判定結果。實際上,一些側面的判斷更具有指導意義和可執行性,比如,出現下面這些情況就有可能說明這個類不滿足單一職責原則:

  • 代碼的行數、函數或者屬性過多;
  • 類依賴的其他類過多,或者依賴類的其他類過多;
  • 私有方法過多;
  • 比較難給類起一個合適的名字;
  • 類中大量的方法都是集中操作類中某幾個屬性。

開放封閉原則

OCP開閉原則,對擴展開放,對修改關閉,這是編程中最基礎、最重要的設計原則。當軟件需求發生變化的時候,儘量通過擴展軟件實體的行爲爲未來實現變化,而不是通過修改已有的代碼來實現變化。

如何理解"對擴展開放、對修改關閉"?

添加一個新的功能,應該是通過在已有的代碼基礎上擴展代碼(新增模塊、類、方法、屬性等),而非修改已有的代碼(修改模塊、類、方法、屬性等)的方式來完成。關於定義,我們有兩點需要注意。第一點是,開閉原則並不是說完全杜絕修改,而是以最小的改動代價來完成新功能的開發。第二點是,同樣的代碼改動下,在粗代碼力度下,可能認定爲"修改",在細代碼粒度下,可能又被認爲是"擴展"。

如何做到"對擴展開放、對修改關閉"?

我們要時刻具備擴展意識、抽象意識、封裝意識。在寫代碼的時候,我們要多花點時間思考一下,這段代碼未來可能有哪些需求變更,如何設計代碼結構,事先留好擴展點,以便在未來需求變更的時候,在不改動代碼整體結構、做到最小代碼改動的情況下,將新的代碼靈活的插入到擴展點上。

很多設計原則、設計思想、設計模式,都是以提高代碼的擴展性爲最終目的的。特別是23中經典設計模式,大部分都是爲了解決代碼的擴展性問題而總結出來的,都是以開閉原則爲指導原則的。最常用來提高代碼擴展性的方法有:多態、依賴注入、基於接口而非實現編程,以及大部分的設計模式(比如,裝飾、策略、模板、責任鏈、狀態)。

裏式替換原則

子類對象能夠替換程序中父類對象出現的任何對象,並且保證原來的程序的邏輯行爲不變及正確性不被破壞。這麼一說有點跟多態類似,多態是面向對象編程的一大特性,也是面向對象編程語言的一種語法。它是一種代碼實現思路。而裏式替換是一種設計原則,是用來指導繼承關係中子類該如何設計,子類的設計要保證在替換父類的時候,不改變原有程序邏輯以及不破壞原有程序的正確性。

哪些違背了裏式替換原則的例子

子類違背父類聲明要實現的功能

父類提供的訂單排序函數,是按照金額從小到大給訂單進行排序,而子類重寫這個方法之後,是按照創建日期來給訂單排序。那子類就違背了裏式替換原則。

子類違背父類對輸入、輸出、異常的約定

在父類中,某個函數約定:運行出錯的時候返回null,獲取數據爲空 到時候返回空集合(empty collection),而子類重載函數之後,實現變了,運行出錯返回異常(exception),獲取不到數據返回null。那這個子類的設計就違背了裏式替換原則。

在父類中,某個函數約定,輸入數據可以是任意整數,但子類實現的時候,只允許輸入的數據是正整數,負數就拋出,也就是說,子類對輸入的數據的校驗比父類更加嚴格,那子類的設計就違背了裏式替換原則。

在父類中,某個函數約定,只會拋出ArgumentNullException異常,那子類的設計實現中只允許拋出ArgumentNullException異常,任何其他異常的拋出,都會導致子類違背裏式替換原則。

子類違背父類註釋中所羅列的任何特殊說明

父類中定義的withdraw()提現函數的註釋這麼寫:“用戶的提現金額不得超過賬戶餘額…”,而子類重寫這個函數之後,針對VIP賬戶實現了透支提現的功能,也就是提現金額可以大於賬戶餘額,那這個子類設計也是不符合裏式替換原則的。

以上就是違背裏式替換原則的大多數情況。除此之外,判斷子類的設計是否違背裏式替換原則,還有個小竅門就是拿父類的單元測試去驗證子類的代碼。如果某些單元測試運行失敗,就有可能說明子類的設計沒有遵循父類的約定,子類有可能違背了裏式替換原則。

實際上,裏式替換這個原則是非常寬鬆的,我們寫代碼的時候都不怎麼會違背它,但或許有人少不注意也會違背它哦。

接口隔離原則(ISP)

接口隔離原則的英文翻譯是"Interface Segregation Principle",縮寫ISP。英文意思是"Clients should not be forced to depend upon interfaces that the do not use."翻譯就是:客戶端不應該強迫依賴它不需要的接口。其中的"客戶端"可以理解爲調用者或者使用者。

如何理解"接口隔離原則"?

理解"接口隔離原則"的重點是理解其中的"接口"二字。可以有三種不同的理解:

如果把"接口"理解爲一組接口的集合,可以是某個微服務的接口,也可以是某個類庫的接口等。如果部分接口只被部分調用者使用,我們就需要將這部分接口隔離出來,單獨給給這部分調用者使用,而不強迫其他調用者也依賴這部分不會被調用到的接口。

如果把"接口"理解爲單個API接口或函數,部分調用者值需要函數中的部分功能,那我們就需要把函數拆分成粒度更細的多個函數,讓調用者只依賴它需要的那個細粒度函數。

如果把"接口"理解爲OOP中的接口,也惡意理解爲面向對象編程語言中的接口語法。那接口的設計要儘量單一,不要讓接口的實現類和調用者,依賴不需要的接口函數。

接口隔離原則與單一職責原則的區別

單一職責原則針對的是模塊、類、接口的設計。接口隔離原則相對於單一職責原則,一方面更側重接口的設計,另一方面它的思考角度也是不同的。接口隔離原則則提供了一種判斷接口的職責是否單一的標準:通過調用者如何使用接口來間接地判定。如果調用者只使用部分接口或接口的部分功能,那接口的設計就不夠單一。

依賴倒轉原則

基本介紹
  • 高層模塊不應該依賴低層模塊,二者都應該依賴其抽象對象
  • 抽象不應該依賴細節,細節應該依賴抽象
  • 依賴倒轉(倒置)的中心思想是面向接口編程
  • 依賴倒轉原則是基於這樣的設計理念:相對於細節的多變性,抽象的東西要穩定的多。以抽象爲基礎搭建的架構比以細節爲基礎的架構要穩定的多。在C#中,抽象指的是接口或抽象類,細節就是具體實現的類
  • 使用接口或抽象類的目的是定製好規範,而不涉及任何具體的操作,把展現細節的任務交給他們的實現類去完成
依賴關係傳遞的三種方式
  • 接口傳遞
  • 構造方法傳遞
  • setter方法傳遞

之前有寫過一篇依賴倒置的詳細文章,可以點擊看一下。

控制翻轉、依賴翻轉、依賴注入的關係和聯繫
  • 控制反轉
    控制反轉是一個比較籠統的設計思想,並不是一種具體的實現方法,一般用來指導框架層面的設計。這裏所說的"控制"指的是對程序執行流程的控制,而"反轉"指的是在沒有使用框架之前,程序員自己控制整個程序的執行。在使用框架之後,整個程序的執行流程通過框架來控制。流程的控制權從程序員"反轉"給了框架。

  • 依賴注入
    依賴注入與控制反轉恰恰相反,它是一種具體的編程技巧。我們不通過new的方式在類的內部創建依賴類的對象,而是將依賴的類的對象在外部創建好之後,通過構造函數、函數參數等方式傳遞(注入)給類來使用。

  • 依賴注入框架
    我們通過依賴注入框架提供的擴展點,簡單配置一下所有需要的類及其類與類之間依賴關係,就可以實現由框架來自動創建對象、管理對象的生命週期、依賴注入等原本需要程序員來做的事情。

  • 依賴反轉原則
    依賴反轉原則也叫做依賴倒置原則。這條原則跟控制反轉有點類似,主要是用來指導框架層面的設計。高層模塊不應該依賴低層模塊,它們共同依賴同一個抽象。抽象不要依賴具體實現細節,具體實現細節依賴抽象對象。

迪米特法則

迪米特法則又叫"最少知道原則",即一個類對自己依賴的類知道的越少越好。每個模塊(unit)只應該瞭解那些與它關係密切的模塊的有限知識。或者說,每個模塊之和自己的朋友"說話"(talk),不和陌生人"說話"(talk)。“不該有直接依賴關係的類之間,不要有依賴”。“有依賴關係的類之間,儘量只依賴必要的接口”

如何理解"高內聚、低耦合"

“高內聚、松耦合”是一個非常重要的設計思想,能夠有效提高代碼的可讀性和可維護性,縮小功能改動導致的代碼改動範圍。"高內聚"用來指導類本身的設計,"低耦合"用來指導類與類之間依賴關係的設計。

所謂高內聚,就是指相近的功能應該放到同一個類中,不想近的功能不要放在同一個類中。相近的功能旺旺會被同時修改,放到同一個類中,修改比較集中。所謂低耦合指的是,在代碼中,類與類之間的依賴關係簡單清晰。即使兩個類有依賴關係,一個類的代碼改動也不會或者很少導致依賴類的代碼改動。

如何理解"迪米特法則"

不該有直接依賴關係的類之間,不要有依賴;有依賴關係的類之間,儘量只依賴必要的接口。迪米特法則是希望減少類之間的耦合,讓類越獨立越好。每個類都應該少了解系統的其他部分。一旦發生變化,需要了解這一變化的類就會比較少。

迪米特法則有一個更簡單的定義:只跟直接朋友通信。

直接的朋友:每個對象都會與其他對象有耦合關係,只要兩個對象之間有耦合關係,我們就說這兩個對象之間是朋友關係。耦合的方式很多,依賴、關聯、組合、聚合等。其中,我們稱出現成員變量,方法參數,方法返回值中的類爲直接的朋友,而出現在局部變量中的類不是直接的朋友。也就是說,默認的類最好不要以局部變量的形式出現在的類的內部。

迪米特法則注意事項
  1. 迪米特法則的核心是降低類之間的耦合。
    2)但是注意:由於每個類都減少了不必要的依賴,因此迪米特法則只是要求降低類之間(對象間)耦合關係,並不是要求完全沒有依賴關係。

合成複用原則

原則是儘量使用合成/聚合的方式,而不是使用繼承。

舉例:如果A類有兩個方法,要讓B類能夠使用這兩個方法,我們可以將B繼承自A,那麼B類就可以使用A類的兩個方法,這樣就增強了他們的耦合性。但如果A類還有其他方法,但B類並不需要用,那就不太適合。還有一種方案就是讓B依賴A,通過參數將A的對象傳進來(依賴),或者B類添加一個A類的屬性(聚合),或者setter方法,又或者在B類實例化一個A類的對象(組合),這些都是設置依賴方式,如下圖
在這裏插入圖片描述

設計模式系列教程彙總

http://dingxiaowei.cn/tags/設計模式/

教程代碼下載

https://github.com/dingxiaowei/UnityDesignPatterns

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