SOLID,GRASP和麵向物件設計的其他基本原理
目錄
學習SOLID,GRASP和其他核心的面向物件的設計OOD原則,使用獨立於語言的方式,以簡單的方式給其他開發人員留下深刻的印象
介紹
我將以陳詞濫調開始。
軟體程式碼應描述以下品質:
- 可維護性
- 可擴充套件性
- 模組化
- 等等
當您詢問有關任何特定程式碼是否描述以上質量特徵的問題時,您可能會發現自己陷入困境。
一種有用的技術是檢視任何軟體的開發時間表。如果軟體程式碼在其生命週期內更易於維護,擴充套件和模組化,那麼這意味著程式碼具有以上質量特性。
我寫的難以閱讀,難以擴充套件和破壞軟體程式碼。在發生變化的六個月後,我才知道這一點。因此,開發時間表對於理解質量因素非常重要。
但是這種技術(檢視開發時間表)只能通過回顧過去來應用,但我們希望將來在某個地方使用高質量的軟體。
瞭解質量的高階開發人員沒有這個問題。當他們看到他們的程式碼具有初級開發人員夢寐以求的品質因素時,他們會感到自豪。
因此,高階開發人員和專家已經提出了一系列原則,初級開發人員可以應用這些原則來編寫高質量的程式碼並在其他開發人員面前展示
在這篇文章中,我將介紹SOLID原則。這些原則是鮑勃叔叔(Robert C. Martin)給出的。我還將介紹Craig Larman出版的GRASP(一般責任分配軟體原則)和其他基本的面向物件設計原則。我從個人經歷中加入了一些例子,因此你找不到任何“動物”或“鴨子”的例子。
顯示的程式碼示例更接近於java和C#,但它們對任何瞭解面向物件程式設計基礎知識的開發人員都很有幫助。
以下是本文所涵蓋的完整原則列表:
- 單一責任原則(SOLID)
- 高內聚(GRASP)
- 低耦合(GRASP)
- 開放封閉原則(
- 利斯科夫替代原則(SOLID)
- 介面隔離原理(SOLID)
- 依賴倒置原則(SOLID)
- 程式設計到介面,而不是實現
- 好萊塢原則
- 多型性(GRASP)
- 資訊專家(GRASP)
- 創作者(GRASP)
- 純粹製造(GRASP)
- 控制器(GRASP)
- 優先使用組合而不是繼承
- 間接(GRASP)
- 不要重複自己
單一責任原則
SRP說:
引用:
一個類應該只有一個責任。
類使用其函式或契約(以及資料成員幫助函式)履行其職責。
參加以下示例類:
Class Simulation{
Public LoadSimulationFile()
Public Simulate()
Public ConvertParams()
}
這個類處理兩個職責。首先,這個類正在載入模擬資料,其次,它正在執行模擬演算法(使用Simulate和ConvertParams函式)。
類使用一個或多個功能履行責任。在上面的例子中,載入模擬資料是一個責任,執行模擬是另一個責任。載入模擬資料需要一個功能(即LoadSimulationFile)。執行模擬需要剩餘兩個函式。
我們怎麼知道類上有多少責任?考慮一下與責任類似的短語“改變的原因”。因此,尋找一個類必須改變的所有原因。如果改變一個類的原因不止一個,則意味著該類不遵循單一責任原則。
在我們上面的示例類中,此類不應包含LoadSimulationFile函式(或載入模擬資料責任)。如果我們建立一個單獨的類來載入模擬資料,那麼這個類不會違反SRP。
一個類只能承擔一項責任。您如何設計具有如此硬性規則的軟體?
讓我們考慮另一個與SRP密切相關的原則,它被稱為高內聚。高內聚為您提供主觀尺度,而不是像SRP那樣客觀的尺度。
非常低的內聚意味著一個類正在履行許多責任。例如,一個類負責的職責超過10個。
低內聚意味著一個類正在履行約5個職責,而中等內聚意味著一個類履行3個職責。高內聚意味著一個類正在履行一項責任。
因此,經驗法則是在設計時爭取高內聚。
這裡應該討論的另一個原則是低耦合。這個原則規定,應該分配一個責任,以便類之間的依賴性保持較低。
再考慮上面的示例類。應用SRP和高內聚原理後,我們決定建立一個單獨的類來處理模擬檔案。通過這種方式,我們建立了兩個相互依賴的類。
看起來應用高內聚導致我們違反另一個低耦合原則。允許這種耦合水平,因為目標是最小化耦合而不使耦合歸零。某種程度的耦合對於建立面向物件的設計是正常的,其中任務通過物件的協作來完成。
另一方面,考慮一個GUI類,它連線到資料庫,通過HTTP處理遠端客戶端並處理屏幕布局。這個GUI類依賴於太多的類。這個GUI類明顯違反了低耦合原理。如果不涉及所有相關類,則不能重用此類。對資料庫元件的任何更改都會導致更改GUI類。
開放原則
開放原則說:
引用:
軟體模組(可以是類或方法)應該是開放的以進行擴充套件,但是關閉以進行修改。
簡單來說,您無法更新已為專案編寫的程式碼,但可以向專案新增新程式碼。
有兩種方法可以應用開閉原理。您可以通過繼承或通過組合來應用此原則。
以下是使用繼承應用開放原則的示例:
Class DataStream{
Public byte[] Read()
}
Class NetworkDataStream:DataStream{
Public byte[] Read(){
//Read from the network
}
}
Class Client {
Public void ReadData(DataStream ds){
ds.Read();
}
}
在此示例中,客戶端從網路流中讀取資料(ds.Read())。如果我想擴充套件客戶端類的功能以從另一個流中讀取資料,例如PCI資料流,那麼我將新增另一個DataStream類的子類,如下面的清單所示:
Class PCIDataStream:DataStream{
Public byte[] Read(){
//Read data from PCI
}
}
在這種情況下,客戶端程式碼將執行,沒有任何錯誤。Client類知道基類,我可以傳遞DataStream的兩個子類中的任何一個的物件。通過這種方式,客戶端可以在不知道底層子類的情況下讀取資料。無需修改任何現有程式碼即可實現此目的。
我們可以使用組合來應用這個原則,並且還有其他方法和設計模式來應用這個原則。其中一些方法將在本文中討論。
我們是否必須將此原則應用於您編寫的每一段程式碼?答案是不。這是因為大多數程式碼都不會改變。在您懷疑將來會改變一段程式碼的情況下,您必須戰略性地應用此原則。
在前面的示例中,我從我的領域經驗,我知道將有多個流。因此,我採用開放式原則,以便它可以處理未來的變化而無需修改。
利斯科夫替代原則
LSP說:
引用:
派生類必須可替代其基類。
檢視此定義的另一種方法是抽象(介面或抽象類),對於客戶端是足夠的。
為了詳細說明,讓我們考慮一個例子,這裡有一個介面,其清單如下:
Public Interface IDevice{
Void Open();
Void Read();
Void Close();
}
此程式碼表示資料採集裝置抽象。資料採集裝置基於其介面型別進行區分。資料採集裝置可以使用USB介面,網路介面(TCP或UDP),PCI Express介面或任何其他計算機介面。
IDevice的客戶端不需要知道他們正在使用哪種裝置。這為程式設計師提供了極大的靈活性,可以適應新裝置,而無需更改依賴於IDevice介面的程式碼。
讓我們回顧一下實現IDevice介面的兩個具體類的歷史,如下所示:
public class PCIDevice:IDevice {
public void Open(){
// Device specific opening logic
}
public void Read(){
// Reading logic specific to this device
}
public void Close(){
// Device specific closing logic.
}
}
public class NetWorkDevice:IDevice{
public void Open(){
// Device specific opening logic
}
public void Read(){
// Reading logic specific to this device
}
public void Close(){
// Device specific closing logic.
}
}
這三種方法(open
, read
和close)足以處理來自這些裝置的資料。後來,需要新增另一個基於USB介面的資料採集裝置。
USB 裝置的問題在於,當您開啟連線時,來自先前連線的資料仍保留在緩衝區中。因此,在第一次read
USB裝置時,會從前一個會話中返回資料。該行為破壞了該特定採集會話的資料。
幸運的是,基於USB的裝置驅動程式提供了重新整理功能,可以清除基於USB的採集裝置中的緩衝區。如何在程式碼中實現此功能,以便程式碼更改保持最小化?
一個簡單的解決方案是通過識別您是否正在呼叫USB物件來更新程式碼:
public class USBDevice:IDevice{
public void Open(){
// Device specific opening logic
}
public void Read(){
// Reading logic specific to this device<br>
}
public void Close(){
// Device specific closing logic.
}
public void Refresh(){
// specific only to USB interface Device
}
}
//Client code...
Public void Acquire(IDevice aDevice){
aDevice.Open();
// Identify if the object passed here is USBDevice class Object.
if(aDevice.GetType() == typeof(USBDevice)){
USBDevice aUsbDevice = (USBDevice) aDevice;
aUsbDevice.Refresh();
}
// remaining code….
}
在此解決方案中,客戶端程式碼直接使用具體類以及介面(或抽象)。這意味著抽象不足以讓客戶履行其職責。
另一種陳述相同的方法,基類不能滿足所需的行為(重新整理行為),但派生類有這種行為。因此派生類與基類不相容,因此無法替換派生類。因此,這種解決方案違反了利斯科夫替代原則。
在上面的示例中,客戶端依賴於更多實體(IDevice
和USBDevice),並且一個實體中的任何更改都將導致其他實體發生更改。因此違反LSP會導致類之間的依賴性。
LSP之後的這個問題的解決方案?我用這種方式更新了介面:
Public Interface IDevice{
Void Open();
Void Refresh();
Void Read();
Void Close();
}
現在IDevice
的客戶端是:
Public void Acquire(IDevice aDevice)
{
aDevice.open();
aDevice.refresh();
aDevice.acquire()
//Remaining code...
}
現在客戶端不依賴於IDevice
的具體實現。因此,在此解決方案中,我們的介面(IDevice
)足以滿足客戶端的需求。
在面向物件分析的上下文中,還有另一個角度來看待LSP原理。總之,在OOA期間,我們考慮可能成為我們軟體一部分的類及其層次結構。
當我們考慮類和層次結構時,我們可以提出違反LSP的類。
讓我們考慮矩形和正方形的經典例子,它多次被錯誤引用。從一開始看,看起來該正方形是矩形的專用版本,一個快樂的設計師將繪製以下繼承層次結構。
Public class Rectangle{
Public void SetWidth(int width){}
Public void SetHeight(int height){}
}
Public Class Square:Rectangle{
//
}
接下來發生的是你不能用square
物件代替rectangle
物件。因為Square
繼承自Rectangle
,所以它繼承了它的方法setWidth()和setHeight()。Square
物件的客戶端可以將其width
和height
更改為不同的維度。但是square
的width
和height
總是相同的,因此無法正常執行軟體。
這隻能通過根據不同的使用場景和條件檢視類來避免。因此,當您單獨設計類時,您的假設可能會失敗。與Square
和Rectangle
的情況一樣,is-a關係在初始分析期間看起來很好,但是當我們檢視不同的條件時,這是一個失敗的is-a關係,軟體的正確行為。
介面隔離原理
介面隔離原則(ISP)說:
引用:
客戶端不應該被迫依賴於他們不使用的介面。
再考慮前面的例子:
Public Interface IDevice{
Void Open();
Void Read();
Void Close();
}
實現此介面有三個類。 USBDevice
, NetworkDevice
和PCIDevice。該介面足以與網路和PCI裝置配合使用。但USB裝置需要另一個功能(Refresh())才能正常工作。
與USB裝置類似,將來還有另外一種裝置可能需要重新整理功能才能正常工作。為了實現zhedian ,IDevice
更新如下所示:
Public Interface IDevice{
Void Open();
Void Refresh();
Void Read();
Void Close();
}
問題是現在每個實現IDevice
的類都必須提供refresh函式的定義。
例如,我必須將以下程式碼行新增到NetworkDevice
類和PCIDevice
類以使用此設計:
public void Refresh()
{
// Yes nothing here… just a useless blank function
}
因此,IDevice
代表一個胖介面(功能太多)。此設計違反了介面隔離原則,因為胖介面導致不必要的客戶端依賴它。
有很多方法可以解決這個問題,但我會使用我的領域特定知識來解決這個問題。
我知道在open
函式之後直接呼叫refresh。因此,我將重新整理的邏輯從IDevice
的客戶端移動到特定的具體類。在我們的例子中,我將呼叫重新整理邏輯移動到USBDevice
類,如下所示:
Public Interface IDevice{
Void Open();
Void Read();
Void Close();
}
Public class USBDevice:IDevice{
Public void Open{
// open the device here…
// refresh the device
this.Refresh();
}
Private void Refresh(){
// make the USb Device Refresh
}
}
通過這種方式,我減少了IDevice
類中的函式數量,減少了它的負擔。
依賴倒置原則
這個原則是其他原則的概括。上面討論的原則,LSP和OCP,取代了依賴性反轉原理。
在跳到DIP的教科書定義之前,讓我介紹一個有助於理解DIP的密切相關的原則。
原則是:
引用:
“程式設計到介面,而不是實現”
這很簡單。請考慮以下示例:
Class PCIDevice{
Void open(){}
Void close(){}
}
Static void Main(){
PCIDevice aDevice = new PCIDevice();
aDevice.open();
//do some work
aDevice.close();
}
上面的例子違反了“程式到介面原理”,因為我們正在使用具體類PCIDevice的參考。下面列出了這個原則:
Interface IDevice{
Void open();
Void close();
}
Class PCIDevice implements IDevice{
Void open(){ // PCI device opening code }
Void close(){ // PCI Device closing code }
}
Static void Main(){
IDevice aDevice = new PCIDevice();
aDevice.open();
//do some work
aDevice.close();
}
因此,遵循這一原則很容易。依賴倒置原則與此原則類似,但DIP要求我們再做一步。
DIP說:
引用:
高階模組不應該依賴於低階模組。兩者都應該依賴於抽象。
您可以輕鬆地理解“兩者都應該依賴於抽象”這一行,因為它說每個模組應該程式設計到一個介面。但是什麼是高階模組和低階模組?
要理解第一部分,我們必須學習什麼是高階模組和低階模組?
請參閱以下程式碼:
Class TransferManager{
public void TransferData(USBExternalDevice usbExternalDeviceObj,SSDDrive ssdDriveObj){
Byte[] dataBytes = usbExternalDeviceObj.readData();
// work on dataBytes e.g compress, encrypt etc..
ssdDriveObj.WrtieData(dataBytes);
}
}
Class USBExternalDevice{
Public byte[] readData(){
}
}
Class SSDDrive{
Public void WriteData(byte[] data){
}
}
在此程式碼中,有三個類。TransferManager
類代表一個高階模組。這是因為它在一個函式中使用了兩個類。因此,其他兩個類是低階模組。
高階模組功能(TransferData)定義資料如何從一個裝置傳輸到另一個裝置的邏輯。任何控制邏輯並使用低階模組執行此操作的模組稱為高階模組。
在上面的程式碼中,高階模組直接(沒有任何抽象)使用較低級別的模組,因此違反了依賴倒置原則。
違反此原則會導致軟體難以更改。例如,如果要新增其他外部裝置,則必須更改更高級別的模組。因此,您的更高級別模組將依賴於較低級別的模組,並且該依賴性將使程式碼難以更改。
如果您瞭解上述“程式到介面”的原則,那麼解決方案很簡單。以下是程式清單:
Class USBExternalDevice implements IExternalDevice{
Public byte[] readData(){
}
}
Class SSDDrive implements IInternalDevice{
Public void WriteData(byte[] data){
}
}
Class TransferManager implements ITransferManager{
public void Transfer(IExternalDevice externalDeviceObj, IInternalDevice internalDeviceObj){
Byte[] dataBytes = externalDeviceObj.readData();
// work on dataBytes e.g compress, encrypt etc..
internalDeviceObj.WrtieData(dataBytes);
}
}
Interface IExternalDevice{
Public byte[] readData();
}
Interface IInternalDevice{
Public void WriteData(byte[] data);
}
Interface ITransferManager {
public void Transfer(IExternalDevice usbExternalDeviceObj,SSDDrive IInternalDevice);
}
在上面的程式碼中,高階模組和低階模組都依賴於抽象。該程式碼遵循依賴性倒置原則。
好萊塢原則
該原理類似於依賴性倒置原則。這個原則說
引用:
不要打電話給我們,我們會打電話給你
這意味著高階元件可以以不相互依賴的方式指示低階元件(或呼叫它們)。
這個原則防止依賴腐敗。當每個元件依賴於每個其他元件時,依賴性腐爛就會發生。換句話說,依賴性腐爛是指在每個方向(向上,側向,向下)發生依賴時。好萊塢原則限制我們只在一個方向上依賴。
與依賴性倒置原則的不同之處在於DIP給出了一個通用的指導方針“高階和低階元件都應該依賴於抽象而不是具體的類”。另一方面,好萊塢原則指定更高級別的元件和更低級別的元件如何互動而不建立依賴關係。
多型性
什麼——多型性是一個設計原則?但我們已經知道多型性是面向物件程式設計的基本特徵。
是的,這是任何oop語言提供多型性功能的基本要求,其中派生類可以通過父類引用。
這也是GRASP的設計原則。該原則提供了有關如何在面向物件設計中使用此oop語言功能的指南。
該原則限制了執行時型別識別(RTTI)的使用。我們通過以下方式在C#中實現RTTI:
if(aDevice.GetType() == typeof(USBDevice)){
//This type is of USBDEvice
}
在java中,RTTI是使用函式getClass()或instanceOf()完成的 。
if(aDevice.getClass() == USBDevice.class){
// Implement USBDevice
Byte[] data = USBDeviceObj.ReadUART32();
}
如果您已在專案中編寫此型別程式碼,那麼現在是時候重構該程式碼並使用多型原則對其進行改進。
請看下圖:
這裡我概括了介面中的read
方法,並將裝置特定的實現委託給它們的類(例如USBDevice中的ReadUART32())。
現在我只使用read
方法。
//RefactoreCode
IDevice aDevice = dm.getDeviceObject();
aDevice.Read();
getDeviceObject()的實現將從何而來?我們將在建立者原則和資訊專家原則中討論,您將學習如何將職責分配給類。
資訊專家
這是一個簡單的GRASP原則,並給出了關於賦予類職責的指導。
它表示將責任分配給具有履行該職責所必需資訊的類。
考慮以下類:
在我們的場景中,模擬以全速(每秒600個迴圈)執行,而使用者顯示以降低的速度更新。在這裡,我必須分配是否顯示下一幀的責任。
哪個類應該承擔這個責任?我有兩個選項,simulation
類或SpeedControl類。
現在,SpeedControl類具有關於當前序列中顯示哪些幀的資訊,因此根據資訊專家原則SpeedControl應該承擔此責任。
創造者
創造者是GRASP原則,有助於確定哪個類應該負責建立類的新例項。
物件建立是一個重要的過程,在決定誰應該建立類的例項時有一個原則是有用的。
根據Larman的說法,如果滿足以下任何條件為true,則“B”類應負責建立另一個類“A”。
a)B含有A.
b)B聚合A
c)B具有A的初始化資料
d)B記錄A.
e)B密切使用A.
在我們的多型性示例中,我使用了資訊專家和創作者原則來賦予DeviceManager類建立Device
物件(dm.getDeviceObject())的職責。這是因為DeviceManager具有建立Device
物件的資訊。
純粹製造
為了理解純粹製造,先決條件是您瞭解面向物件分析(OOA)。
總之,面向物件分析是一個過程,通過它您可以識別問題域中的類。例如,銀行系統的域模型包含諸如賬戶、分支、現金、支票、交易等類。
在銀行示例中,領域類需要儲存有關客戶的資訊。為此,一個選項是將資料儲存責任委託給領域類。此選項將降低領域類的內聚性(多個職責)。最終,此選項違反了SRP原則。
另一個選擇是引入另一個不代表任何領域概念的類。在銀行示例中,我們可以引入一個類“PersistenceProvider”。此類不代表任何領域實體。此類的目的是處理資料儲存功能。因此“PersistenceProvider”是純粹的製作。
控制器
當我開始開發時,我使用Java的swing元件編寫了大部分程式,並且我將大部分邏輯寫在了監聽器之後。
然後我學習了領域模型。因此,我將邏輯從偵聽器移到了領域模型。但我直接從偵聽器呼叫領域物件。這在GUI元件(偵聽器) 領域模型之間建立了依賴關係。控制器設計原則有助於最小化GUI元件和域模型類之間的依賴關係。
控制器有兩個目的。控制器的第一個目的是封裝系統操作。系統操作是您的使用者想要實現的,例如購買產品或將物品輸入購物車。然後通過呼叫軟體物件之間的一個或多個方法呼叫來完成該系統操作。控制器的第二個目的是在UI和域模型之間提供一個層。
UI使使用者能夠執行系統操作。控制器是UI層之後的第一個物件,它處理系統操作請求,然後將責任委託給底層領域物件。
例如,這裡是MAP
類,它代表我們的一個軟體程式碼中的控制器。
從UI中,我們將“移動游標”的責任委託給該控制器,然後呼叫底層領域物件來移動游標。
通過使用控制器原則,您可以靈活地插入另一個使用者介面,如命令列介面或Web介面。
優先使用組合而不是繼承
主要是面向物件程式設計中有兩個工具來擴充套件現有程式碼的功能。第一個是繼承。
第二種方法是組合。在程式語言中,通過引用另一個物件,您可以擴充套件該物件的功能。如果使用組合,新增一個新類建立其物件,然後使用其物件來擴充套件程式碼。
該組合的一個非常有用的功能是可以在執行時設定行為。另一方面,使用繼承只能在編譯時設定行為。這將在下面的示例中顯示。
當我是一個新手並使用繼承來擴充套件行為時,這些是我設計的類:
最初,我只知道處理傳入的資料流,並且有兩種(流A和流B)資料。幾周後,我才知道應該處理資料的位元組順序。因此,我想出瞭如下所示的類設計:
後來,另一個變數被新增到需求中。這次我必須處理資料的極性。想象一下我要新增多少個類?streamA
, streamB
的兩種極性,具有位元組序等的Stream
。類會爆炸的!現在我將不得不維護大量的類。
現在,如果我使用以下組合處理同樣的問題,以下是類設計:
我新增新類,然後使用他們的引用在我的程式碼中使用它們,請參閱下面的列表:
clientData.setPolarity(new PolarityOfTypeA); // or clientData.setPolarity(new PolarityOfTypeB)
clientData.FormatPolarity;
clientData.setEndianness(new LittleEndiannes());// setting the behavior at run-time
clientData.FormatStream();
因此,我可以根據我想要的行為提供類的例項。此功能減少了類的總數和最終的可維護性問題。因此,優先使用組合而不是繼承將減少可維護性問題和在執行時設定行為的靈活性。
間接
這個原則回答了一個問題:
如何讓物件以他們之間的聯絡仍然薄弱的方式進行互動?
解決方案是:
將互動的責任交給中間物件,以便不同元件之間的耦合保持較低。
例如:
軟體應用程式使用不同的配置和選項。要將領域程式碼與配置分離,請新增以下清單中顯示的特定類:
Public Configuration{
public int GetFrameLength(){
// implementation
}
public string GetNextFileName(){
}
// Remaining configuration methods
}
這樣,如果任何領域物件想要讀取某個配置設定,它將詢問Configuration類物件。因此,主程式碼與配置程式碼分離。
如果您已閱讀純粹製造原責,則此Configuration
類是純粹製造的示例。但間接的目的是建立解耦。另一方面,純粹製造的目的是保持領域模型的清潔,並僅代表領域的概念和責任。
許多軟體設計模式如Adapter,Facade和observer都是間接原則的專門化。
不要重複自己(DRY)
不要重複自己意味著不要一次又一次地嘗試編寫相同的程式碼。我們的想法是,如果您一次又一次地編寫幾行程式碼,那麼您應該將它們組合在一個函式中,然後呼叫該函式。
最大的好處是,現在如果您想要更新這些特定的程式碼行,您可以在一個地方更新它們。否則,您將必須搜尋寫入程式碼的所有位置。
我一直猶豫是否應用這個原則。這是因為在一本舊的程式設計書中我已經讀過,編寫一個單獨的函式會使你的處理器工作得更多。例如,當您呼叫函式時,總是會在組合語言中進行額外呼叫,這稱為“JUMP”呼叫。
此jump
呼叫會產生額外的執行成本。現在,如果函式處於執行100萬次的迴圈中,則意味著處理器需要執行100萬條額外指令。
嗯。挺昂貴的!
這阻礙了我很長一段時間。也有解決方案。現在編譯器已經過優化,不會跳轉到函式。相反,當你呼叫一個函式時,這些編譯器只是用實際的程式碼行替換函式呼叫。因此,當處理器執行時,沒有“JUMP”的額外成本。
其他的一切都由編譯器負責。所以儘可能多地使用DRY原則,但要確保你的編譯器足夠聰明:)。
原文地址:https://www.codeproject.com/Articles/1166136/S-O-L-I-D-GRASP-And-Other-Basic-Principles-of-Obje