1. 單一職責原則(Single Responsibility Principle)
2. 里氏替換原則(Liskov Substitution Principle)
3. 依賴倒置原則(Dependence Inversion Principle)
4. 接口隔離原則(Interface Segregation Principle)
5. 迪米特法則(Law Of Demeter)
6. 開閉原則(Open Close Principle)
7. 組合/聚合復用原則(Composite/Aggregate Reuse Principle CARP)
縮寫 | 原則名稱 | 概述 |
---|---|---|
SPR | 單一職責原則 | 每一個類應該專注于做一件事情。 |
LSP | 里氏替換原則 | 基類存在的地方,子類是可以替換的 |
DIP | 依賴倒置原則 | 實現盡量依賴抽象,不依賴具體實現。高層模塊不應該直接依賴于低層模塊,高層模塊和低層模塊應該同時依賴一個抽象層。 |
ISP | 接口隔離原則 | 應當為客戶端提供盡可能小的單獨的接口,而不是提供大的總的接口。 |
LOD | 迪米特法則 | 又叫最少知識原則,一個軟件實體應當盡可能少的與其他實體發生相互作用。 |
OCP | 開閉原則 | 面向擴展開放,面向修改關閉。 |
CARP | 組合/聚合復用原則 | 盡量使用合成/聚合達到復用,少用繼承。原則: 一個類中有另一個類的對象。 |
解析
1. 單一職責原則(Single Responsibility Principle):
見名知意,這個條職責的潛臺詞的就是,專注做一個事。單一職責原則可以降低類的復雜度,一個類只負責一項職責,其邏輯肯定要比負責多項職責簡單的多;提高類的可讀性,提高系統的可維護性;變更引起的風險降低,變更是必然的,如果遵守的好,當修改一個功能時,可以顯著降低對其他功能的影響。需要說明的一點是單一職責原則不只是面向對象編程思想所特有的,只要是模塊化的程序設計,都適用此原則。
2. 里氏替換原則(Liskov Substitution Principle):
將一個基類對象替換成它的子類對象,程序將不會產生任何錯誤和異常。反之則不成立,因為如果使用的是一個子類對象的話,那么它不一定能夠使用基類對象(子類擁有父類未擁有的函數)。
此原則是實現開閉原則的重要方式之一,由于使用基類對象的地方都可以使用子類對象,因此在程序中盡量使用基類類型來定義對象,而在運行時再確定其子類類型,用子類對象來替換父類對象。
使用里氏替換原則時需要注意,子類的所有方法必須在父類中聲明,或子類必須實現父類中聲明的所有方法。盡量把父類設計為抽象類或者接口,讓子類繼承父類或實現父接口,并實現在父類中聲明的方法,運行時,子類實例替換父類實例,我們可以很方便地擴展系統的功能,同時無須修改原有子類的代碼,增加新的功能可以通過增加新的子類來實現。從大局看多態就屬于這個原則。
示例代碼
public abstract class Phone
{
public abstract void Call();
}
interface Android{ }
interface IOS{ }
public class OnePlus : Phone, Android
{
public override void Call()
{
Debug.Log($"{nameof(OnePlus)}進行通話。。。。。");
}
}
public class Pixel : Phone, Android
{
public override void Call()
{
Debug.Log($"{nameof(Pixel)}進行通話。。。。。");
}
}
public class XiaoMi : Phone, Android
{
public override void Call()
{
Debug.Log($"{nameof(XiaoMi)}進行通話。。。。。");
}
}
public class Apple : Phone, IOS
{
public override void Call()
{
Debug.Log($"{nameof(Apple)}進行通話。。。。。");
}
}
不使用里氏替換,調用call函數需要為每個類型的手機寫一個函數
public void WantToCall_0(OnePlus phone)
{
phone.Call();
}
public void WantToCall_1(Pixel phone)
{
phone.Call();
}
public void WantToCall_2(XiaoMi phone)
{
phone.Call();
}
public void WantToCall_3(Apple phone)
{
phone.Call();
}
使用里氏替換,只需要一個函數全部搞定,在后面的【橋接模式】會廣泛用到的
public void WantToCall_4(Phone phone)
{
phone.Call();
}
3. 依賴倒置原則(Dependence Inversion Principle):
高層模塊不應該依賴低層模塊,二者都應該依賴其抽象;抽象不應該依賴細節;細節應該依賴抽象。 依賴倒置原則的核心思想是面向接口編程。
采用依賴倒置原則可以減少類間的耦合性,提高系統的穩定性,減少并行開發引起的風險,提高代碼的可讀性和可維護性。
什么是高層模塊,什么是低層模塊?
在項目中我們經常會有一些數學函數庫,或者工具類(Log日志工具),這些封裝好的工具會被我們業務邏輯模塊經常調用,那么這些工具函數庫就是高層模塊,業務邏輯模塊就是低層模塊。
什么是細節,什么是抽象?
細節的意思就是具體的實現,例如上面里氏替換中打電話的例子,在不使用里氏替換的時候需要定義4種打電話函數,這就是依賴細節,其中的細節就是“OnePlus ”、“Pixel ”、“XiaoMi”、“Apple ”,反之抽象就是Phone 。
簡例:華碩和微型都可使用不同型號的顯卡,反之不同型號的顯卡也可以使用在不同品牌的主板上。利用依賴倒置原則,這種規則的實現變得很簡單也很靈活~
示例代碼
//顯卡
public interface IGraphicsCard
{
void BeginWork(IMainboard mainboard);
}
//主板
public interface IMainboard
{
void GetElectricity();
void DrawPicture(IGraphicsCard graphicsCard);
}
public class NVIDIA_2018Ti : IGraphicsCard
{
public NVIDIA_2018Ti(IMainboard mainboard)
{
BeginWork(mainboard);
}
public void BeginWork(IMainboard mainboard)
{
mainboard.GetElectricity();
Debug.Log($"NVIDIA_2018Ti獲取{mainboard.GetType()}電量后開始工作");
}
}
public class NVIDIA_2018 : IGraphicsCard
{
public NVIDIA_2018(IMainboard mainboard)
{
BeginWork(mainboard);
}
public void BeginWork(IMainboard mainboard)
{
mainboard.GetElectricity();
Debug.Log($"NVIDIA_2018獲取{mainboard.GetType()}電量后開始工作");
}
}
//華碩主板
public class Asus : IMainboard
{
public void DrawPicture(IGraphicsCard graphicsCard) { }
public void GetElectricity() { }
}
//微型主板
public class MSI : IMainboard
{
public void DrawPicture(IGraphicsCard graphicsCard) { }
public void GetElectricity() { }
}
實現代碼,這種2*2種模式的實現非常簡單~
public void DrawPicture()
{
IMainboard aSus = new Asus();
aSus.DrawPicture(new NVIDIA_2018Ti(aSus));
aSus.DrawPicture(new NVIDIA_2018(aSus));
IMainboard mSI = new MSI();
mSI.DrawPicture(new NVIDIA_2018Ti(mSI));
mSI.DrawPicture(new NVIDIA_2018(mSI));
}
4. 接口隔離原則(Interface Segregation Principle):
提供盡可能小的單獨接口,而不要提供大的總接口。具體行為讓實現類了解越少越好。
盡量細化接口,接口中的方法盡量少。也就是要為各個類建立專用的接口,而不要試圖去建立一個很龐大的接口供所有依賴它的類去調用。依賴幾個專用的接口要比依賴一個綜合的接口更靈活。接口是設計時對外部設定的約定,通過分散定義多個接口,可以預防外來變更的擴散,提高系統的靈活性和可維護性。
通俗的講就是定義的接口盡量按照功能細分,比如打電話功能一個接口,上網一個接口,發短信一個接口。接口粒度小不僅職能明確,也不會因為使用某種職能而必須實現一些不必要的功能。
示例代碼
5. 迪米特法則(Law Of Demeter)又稱【最少知識原則】:
類與類之間的關系越密切,耦合度越大,只有降低類與類之間的耦合才符合設計模式;對于被依賴的類來說,無論邏輯多復雜都要盡量封裝在類的內部。
每個對象都會與其他對象有耦合關系,我們稱出現成員變量、方法參數、方法返回值中的類為直接的耦合依賴,而出現在局部變量中的類則不是直接耦合依賴,也就是說,不是直接耦合依賴的類最好不要作為局部變量的形式出現在類的內部。
一個對象對另一個對象知道的越少越好,即一個實體應當盡可能少的與其他實體發生相互作用,在一個類里降低引用其他類,尤其是局部變量的依賴類,能省則省。同時兩個類不必彼此直接通信,那么這兩個類就不必發生直接的相互作用。如果其中一個類需要調用另一個類的某一方法的話,可以通過第三者轉發這個調用。
表達的意思是能用 private、protected的就不要用public,不要過多的暴露自己的內容,而且對應類與類之間的關系,盡量越少越好。后面講到的門面模式和中介者模式想表達的也是這個意思。
迪米特法則其根本思想,是強調了類之間的松耦合。類之間的耦合越弱,一個處于弱耦合的類被修改,對有關類造成波及的影響越小。
示例代碼
注:這種情況就違背了迪米特法則。因為其他的類不需要這個Log擴展,這也就破壞了原來的結構,侵入性太強了。如果所有的擴展都是以Object為基準,那么調用函數的時候就會造成下拉函數條目過多。
public static class Exted
{
public static void CustomerLog_Obj0(this GameObject Obj) { }
public static void CustomerLog_Obj1(this object Obj) { }
public static void CustomerLog_Obj2(this object Obj) { }
public static void CustomerLog_Obj3(this object Obj) { }
public static void CustomerLog_Obj4(this object Obj) { }
}
6. 開閉原則(Open Close Principle):
主要體現對擴展開放、對修改封閉,意味著有新的需求或變化時,可以對現有代碼進行擴展,以適應新的情況。軟件需求總是變化的,世界上沒有一個軟件是不變的,因此對軟件設計人員來說,在不需要對原有系統進行修改的情況下,實現靈活的系統擴展。
例如通過模板方法模式和策略模式進行重構,實現對修改封閉,對擴展開放的設計思路。
封裝變化,是實現開閉原則的重要手段,對于經常發生變化的狀態,將其封裝為一個抽象,但拒絕濫用抽象,只將經常變化的部分進行抽象。
通俗的講在功能變動的時候,盡量以增量補丁的形式更改,也就是原來代碼保持不變的同時進行更改。
7. 組合/聚合復用原則(Composite/Aggregate Reuse Principle CARP):
整個設計模式就是在講如何合理安排類與類之間的組合/聚合。在一個新的對象里面通過關聯關系,使一些已有的對象成為新對象的一部分,新對象通過委派調用已有對象的方法,達到復用其已有功能的目的。也就是,要盡量使用類的合成復用,不要使用繼承。
繼承復用破壞數據封裝性,將基類的實現細節全部暴露給了派生類,基類的內部細節常常對派生類是透明的。白箱復用,雖然簡單,但不安全,不能在程序的運行過程中隨便改變。基類的實現發生了改變,派生類的實現也不得不改變;從基類繼承而來的派生類是固定的,不能在運行時發生改變,因此沒有足夠的靈活性。
組合/聚合復用原則可以使系統更加靈活,類與類之間的耦合度降低,一個類的變化對其他類造成的影響相對較少,因此一般首選使用組合/聚合來實現復用;其次才考慮繼承,在使用繼承時,需要嚴格遵循里氏代換原則,有效使用繼承會有助于對問題的理解,降低復雜度,而濫用繼承反而會增加系統構建和維護的難度以及系統的復雜度,因此需要慎重使用繼承復用。
核心思想:組合優于繼承