引言 本文以簡單的示例輔助闡述面向對象設計的五個基本原則。 概念 在程式設計領域, SOLID(單一功能、開閉原則、里氏替換、介面隔離以及依賴反轉)是由羅伯特·C·馬丁在21世紀早期引入的記憶術首字母縮略字, 指代了面向對象編程和麵向對象設計的五個基本原則。當這些原則被一起應用時,它們使得一個程式員 ...
引言
本文以簡單的示例輔助闡述面向對象設計的五個基本原則。
概念
在程式設計領域, SOLID(單一功能、開閉原則、里氏替換、介面隔離以及依賴反轉)是由羅伯特·C·馬丁在21世紀早期引入的記憶術首字母縮略字,
指代了面向對象編程和麵向對象設計的五個基本原則。當這些原則被一起應用時,它們使得一個程式員開發一個容易進行軟體維護和擴展的系統變得
更加可能。SOLID所包含的原則是通過引發編程者進行軟體源代碼的代碼重構進行軟體的代碼異味清掃,從而使得軟體清晰可讀以及可擴展時可以應用
的指南。SOLID被典型的應用在測試驅動開發上,並且是敏捷開發以及自適應軟體開發的基本原則的重要組成部分。
首字母 指代 概念 S 單一功能原則 認為對象應該僅具有一種單一功能的概念 O 開閉原則 認為軟體體應該是對於擴展開放的,但是對於修改封閉的概念 L 里氏替換原則 認為“程式中的對象應該是可以在不改變程式正確性的前提下被它的子類所替換的”概念 I 介面隔離原則 認為“多個特定客戶端介面要好於一個寬泛用途的介面” D 依賴反轉原則 認為一個方法應該遵從“依賴於抽象而不是一個實例”的概念
依賴註入所以該原則的一種實現方式
以上源自Wiki百科:SOLID(面向對象設計)
在我們的實際工作中,這五個原則互相關聯和支持,可能實現一個業務功能把五個原則都用到了,也可能只用到了其中的三個
單一功能原則(S)
按照字面意思不難理解,一個對象有且僅有一種功能,同時也僅有一種功能需求的變更能夠引發該對象實現的修改。
- 試想以下場景,如果我們有一個對象,裡面大而全的實現了某業務領域內的全部功能,那麼,當該領域內的需求發生變更時,我們是不是要對該對象內的代碼進行修改?
- 再試想一下,如果我們將該業務領域內的需求進行細分,針對細分後的功能實現對應的功能(業務)對象,那麼當該領域內的需求發生變更時,我們僅僅需要修改有變更需求對應的功能(業務)對象就可以?
可能有人會說,可是兩種方式都是修改代碼了呀?!是的,兩種方式都對我們的實現進行了代碼修改,這是不可避免的。
但換個角度想,此時我們修改的代碼,對其他需求沒有變更的功能(業務)對象來說,是不是已經沒有了太大的關聯?
因為實現了“單一功能原則(S)”,順帶也滿足了“介面隔離(I)”原則,當然嚴格來說是面向介面,但面向實現也未嘗不可。而此時,我們對特定業務/功能的修改不會影響到該領域內的其他實現,是不是也捎帶手實現了“開閉原則(O)”?
其實,在開發過程中,有一個百試不爽的竅門就是“拆”,大拆小,直到不能再拆(原子級),遇到複雜的業務如是,遇到複雜的功能同樣適用。不但有利於實現,同時也有利於測試。
延伸一下思路,無論是當下流行的微服務,還是一個方法只完成一個功能的普遍認知,是不是和單一功能原則有著相似的理念?
理論講完,來點具體的代碼實現驗證一下
// C# 示例
// 做飯介面
public interface ICooking
{
//做飯介面方法
bool Cook();
}
//做中餐
public class ChineseCook: ICooking
{
public bool Cook()
{
Console.WriteLine("開始做魚香肉絲")
Console.WriteLine("魚香肉絲做好了,快來米西吧")
}
}
//做西餐
public class WesternCook: ICooking
{
public bool Cook()
{
Console.WriteLine("開始做蔬菜沙拉")
Console.WriteLine("蔬菜沙拉做好了, 快來eat吧")
}
}
例子寫的可能不太恰當,但足夠說明吃貨腦子裡不想別的。
如果哪天魚香肉絲吃膩味了換成宮保雞丁,是不是就只要修改ChineseCook的Cook實現就可以了?
又或者說如果哪天新增加了墨西哥菜,泰國菜,日本料理等,是不是只需要實現ICooking介面就可以了?
可能這個例子還不能對“拆”這個行為提供足夠的支持,因為好處不夠明顯,但別忘了五個原則是互相關聯和支持的,個原則是互相關聯和支持的,原則是互相關聯和支持的,是互相關聯和支持的,互相關聯和支持的,相關聯和支持的,關聯和支持的,聯和支持的,相和支持的,支持的,持的,的...
上面的示例中,實現了單一功能原則(S)的同時,也實現了開閉原則(O),介面隔離原則(I)。
依賴反轉原則(D)
請看如下代碼示例:
// 廚師 - 依賴反轉示例
public class Cooker
{
//這裡只需要ICooking介面對象,不關心介面實現
public void Cook(ICooking cook)
{
cook.Cook();
}
}
// 食客 - 消費者
public class Diner
{
public static void main(string[] args)
{
Cooker cooker = new Cooker();
ICooking chineseCook = new ChineseCook(); //做中餐實例
ICooking westernCook = new WesternCook(); //做西餐實例
// 或許哪天增加日本料理
// ICooking japaneseCook = new JapaneseCook(); //日本料理實例
cooker.Cook(chineseCook); // 做中餐
cooker.Cook(westernCook); // 做西餐
cooker.Cook(japaneseCook); // 日本料理
}
}
代碼說明:
以上示例,在ICooking介面的兩個實現的基礎上,實現了依賴反轉/倒置(DIP)。
我們來看一下定義:
- 高層次的模塊不應該依賴於低層次的模塊,兩者都應該依賴於抽象介面
- 抽象介面不應該依賴於具體實現。而具體實現則應該依賴於抽象介面
在此示例中:
- 高層次的模塊 Cooker 不依賴於 ChineseCook 或 WesternCook, 而是依賴於 ICooking 介面
- ICooking介面不依賴於具體的實現。而 ChineseCook 和 WesternCook 則依賴於 ICooking 介面
- 如果新增加了墨西哥菜,日本料理,只需要實現介面ICooking即可(開閉原則:面向修改關閉,面向擴展開放)
依賴倒置原則基於這樣一個事實:
相對於實現的多變性,抽象的東西(介面)要穩定的多。以抽象為基礎搭建起來的架構要比以實現為基礎搭建起來的架構要穩定的多。
在不同的語言領域中,抽象和實現可能有著不同的實現。不一而論。在C#或者Java中,抽象指的是介面或者抽象類,實現則是具體的實現類。
依賴倒置原則的核心就是要我們面向介面編程,理解了面向介面編程,也就理解了依賴倒置。
出自設計模式六大原則(3):依賴倒置原則
里氏替換原則(L)
定義:
派生類(子類)對象能夠替換其基類(超類)對象被使用。
出自里氏替換原則
這個概念比較好理解,結合上面的示例,如果我們把介面替換成抽象類,該抽象類實現了預設的Cook方法(預設做一份老北京雞肉捲):
// 父類,實現了一個預設的Cook方法
public class Cooking
{
public virtual Cook()
{
Console.WriteLine("開始做老北京雞肉捲");
Console.WriteLine("老北京雞肉捲做完了,快來啃吧!");
}
}
//做中餐(重載Cook方法)
public class ChineseCook: Cooking
{
public override bool Cook()
{
Console.WriteLine("開始做魚香肉絲")
Console.WriteLine("魚香肉絲做好了,快來米西吧")
}
}
//做西餐(重載Cook方法)
public class WesternCook: Cooking
{
public override bool Cook()
{
Console.WriteLine("開始做蔬菜沙拉")
Console.WriteLine("蔬菜沙拉做好了, 快來eat吧")
}
}
代碼說明:
上例中,父類Cooking實現了Cook的預設方法(做一份老北京雞肉捲),所以如果不繼承該父類的話,服務員端上來的就是雞肉捲,很顯然,該類可以被子類替換。
本文完。