精通代碼復(fù)用:設(shè)計(jì)原則與最佳實(shí)踐
在你開始設(shè)計(jì)的所有層次上,從單一函數(shù)、類,到整個(gè)庫(kù)和框架,都需要從一開始就考慮到代碼復(fù)用。在接下來(lái)的文本中,所有這些不同的層次都被稱為組件。以下策略將幫助你合理地組織你的代碼。注意,所有這些策略都專注于使你的代碼具有通用性。設(shè)計(jì)可復(fù)用代碼的第二個(gè)方面,即提供易用性,更多地與你的接口設(shè)計(jì)相關(guān),將在后面進(jìn)行討論。
避免合并無(wú)關(guān)或邏輯獨(dú)立的概念
當(dāng)你設(shè)計(jì)一個(gè)組件時(shí),應(yīng)該讓它專注于一個(gè)單一任務(wù)或一組任務(wù),即,你應(yīng)該追求高內(nèi)聚。這也被稱為單一職責(zé)原則(SRP)。不要合并無(wú)關(guān)的概念,例如隨機(jī)數(shù)生成器和XML解析器。即使你沒有專門為了復(fù)用而設(shè)計(jì)代碼,也要牢記這一策略。整個(gè)程序很少會(huì)被單獨(dú)復(fù)用。相反,程序的部分或子系統(tǒng)會(huì)直接被納入其他應(yīng)用,或者被改編以用于稍微不同的用途。因此,你應(yīng)該設(shè)計(jì)你的程序,以便你將邏輯上獨(dú)立的功能劃分為可以在不同程序中復(fù)用的獨(dú)立組件。每個(gè)這樣的組件都應(yīng)具有明確定義的職責(zé)。這種程序策略模仿了現(xiàn)實(shí)世界中的離散、可互換部件的設(shè)計(jì)原則。
例如,你可以編寫一個(gè)Car
類,并將發(fā)動(dòng)機(jī)的所有屬性和行為都放入其中。然而,發(fā)動(dòng)機(jī)是可分離的組件,不與汽車的其他方面綁定在一起。發(fā)動(dòng)機(jī)可以從一輛汽車中取出,放入另一輛汽車中。一個(gè)合適的設(shè)計(jì)應(yīng)該包括一個(gè)Engine
類,其中包含所有與發(fā)動(dòng)機(jī)相關(guān)的功能。一個(gè)Car
實(shí)例然后只包含一個(gè)Engine
實(shí)例。
將程序劃分為邏輯子系統(tǒng)
你應(yīng)該將你的子系統(tǒng)設(shè)計(jì)為可以獨(dú)立復(fù)用的離散組件,即,追求低耦合。例如,如果你正在設(shè)計(jì)一個(gè)網(wǎng)絡(luò)游戲,應(yīng)該將網(wǎng)絡(luò)和圖形用戶界面方面分開。這樣,你就可以在不拖入另一個(gè)組件的情況下復(fù)用其中一個(gè)組件。例如,你可能想寫一個(gè)非網(wǎng)絡(luò)游戲,在這種情況下,你可以復(fù)用圖形界面子系統(tǒng),但不需要網(wǎng)絡(luò)方面。同樣地,你可以設(shè)計(jì)一個(gè)P2P文件共享程序,在這種情況下,你可以復(fù)用網(wǎng)絡(luò)子系統(tǒng),但不需要圖形用戶界面功能。確保遵循每個(gè)子系統(tǒng)的抽象原則。把每個(gè)子系統(tǒng)看作一個(gè)微型庫(kù),并為其提供一個(gè)連貫且易于使用的接口。即使你是唯一使用這些微型庫(kù)的程序員,你也將從設(shè)計(jì)良好的接口和實(shí)現(xiàn)中受益,這些接口和實(shí)現(xiàn)將邏輯上不同的功能進(jìn)行了分離。
使用類層次結(jié)構(gòu)以分離邏輯概念
除了將程序劃分為邏輯子系統(tǒng)外,你還應(yīng)避免在類級(jí)別合并無(wú)關(guān)的概念。例如,假設(shè)你想為自動(dòng)駕駛汽車編寫一個(gè)類。你決定從一個(gè)基礎(chǔ)的汽車類開始,并直接將所有自動(dòng)駕駛邏輯加入其中。然而,如果你的程序中只需要一個(gè)非自動(dòng)駕駛汽車呢?在這種情況下,與自動(dòng)駕駛有關(guān)的所有邏輯都是無(wú)用的,可能會(huì)要求你的程序鏈接到它本來(lái)可以避免的庫(kù),如視覺庫(kù)、LIDAR庫(kù)等。一個(gè)解決方案是創(chuàng)建一個(gè)類層次結(jié)構(gòu),在其中自動(dòng)駕駛汽車是通用汽車的派生類。這樣,你就可以在不需要自動(dòng)駕駛功能的程序中使用汽車基類,而不會(huì)招致這種算法的成本。
當(dāng)有兩個(gè)邏輯概念時(shí),如自動(dòng)駕駛和汽車,這種策略效果很好。當(dāng)有三個(gè)或更多概念時(shí),情況就變得更復(fù)雜了。例如,假設(shè)你想提供一輛卡車和一輛汽車,每輛都可能是自動(dòng)駕駛或非自動(dòng)駕駛的。從邏輯上講,卡車和汽車都是車輛的特殊情況,因此它們應(yīng)該是車輛類的派生類。同樣,自動(dòng)駕駛類可以是非自動(dòng)駕駛類的派生類。你不能用一個(gè)線性層次結(jié)構(gòu)提供這些分離。一個(gè)可能性是將自動(dòng)駕駛方面作為一個(gè)混合類。通過(guò)使用多重繼承在C++中實(shí)現(xiàn)了混合類的一種方式。例如,一個(gè)PictureButton
可以從Image
類和Clickable
混合類繼承。然而,對(duì)于自動(dòng)駕駛設(shè)計(jì),最好使用一種不同類型的混合實(shí)現(xiàn),即使用類模板?;旧?,SelfDrivable
混合類可以定義如下:
template <typename T>
class SelfDrivable : public T {
};
這個(gè)SelfDrivable
混合類提供了實(shí)現(xiàn)自動(dòng)駕駛功能所需的所有算法。一旦你有了這個(gè)SelfDrivable
混合類模板
,你就可以為汽車和卡車分別實(shí)例化一個(gè):
SelfDrivable<Car> selfDrivingCar;
SelfDrivable<Truck> selfDrivingTruck;
這兩行代碼的結(jié)果是,編譯器將使用SelfDrivable
混合類模板創(chuàng)建一個(gè)實(shí)例,其中所有的T都被替換為Car
,因此是從Car
派生的,另一個(gè)實(shí)例的T被替換為Truck
,因此是從Truck
派生的。
使用聚合以分離邏輯概念
聚合在接下來(lái)的內(nèi)容中討論,它模擬了“有一個(gè)”關(guān)系:對(duì)象包含其他對(duì)象以執(zhí)行其某些方面的功能。當(dāng)繼承不適當(dāng)時(shí),你可以使用聚合來(lái)分離無(wú)關(guān)或相關(guān)但獨(dú)立的功能。
無(wú)論你的設(shè)計(jì)在哪個(gè)層次,都應(yīng)避免合并無(wú)關(guān)的概念,即,追求高內(nèi)聚。例如,在方法級(jí)別,單一方法不應(yīng)執(zhí)行邏輯上無(wú)關(guān)的事情,混合變異(set)和檢查(get)等。
例如,假設(shè)你想寫一個(gè)Family
類來(lái)存儲(chǔ)一個(gè)家庭的成員。顯然,樹狀數(shù)據(jù)結(jié)構(gòu)將是理想的存儲(chǔ)這些信息的方式。你應(yīng)該寫一個(gè)單獨(dú)的Tree
類,而不是在你的Family
類中集成樹結(jié)構(gòu)的代碼。然后,你的Family
類可以包含和使用一個(gè)Tree
實(shí)例。用面向?qū)ο蟮男g(shù)語(yǔ)來(lái)說(shuō),Family
has-a Tree
。采用這種技術(shù),樹狀數(shù)據(jù)結(jié)構(gòu)在另一個(gè)程序中更容易被復(fù)用。
消除用戶界面依賴性
如果你的庫(kù)是一個(gè)數(shù)據(jù)操作庫(kù),你會(huì)希望將數(shù)據(jù)操作與用戶界面分開。這意味著對(duì)于這種類型的庫(kù),你絕對(duì)不應(yīng)該假設(shè)庫(kù)將在哪種類型的用戶界面中使用。庫(kù)不應(yīng)使用任何標(biāo)準(zhǔn)輸入和輸出流,如cout
、cerr
和cin
,因?yàn)槿绻麕?kù)是在圖形用戶界面的環(huán)境中使用,這些流可能沒有意義。例如,一個(gè)基于Windows GUI的應(yīng)用程序通常不會(huì)有任何形式的控制臺(tái)I/O。即使你認(rèn)為你的庫(kù)只會(huì)在基于GUI的應(yīng)用程序中使用,你也絕不應(yīng)彈出任何類型的消息框或其他類型的通知給最終用戶,因?yàn)檫@是客戶端代碼的責(zé)任。客戶端代碼決定如何向用戶顯示消息。這種類型的依賴性不僅導(dǎo)致可復(fù)用性差,而且還阻止了客戶端代碼適當(dāng)?shù)仨憫?yīng)錯(cuò)誤,例如,靜默處理它。
模型-視圖-控制器(MVC)范式是一個(gè)用于分離數(shù)據(jù)存儲(chǔ)和數(shù)據(jù)可視化的著名設(shè)計(jì)模式。使用這個(gè)范式,模型可以在庫(kù)中,而客戶端代碼可以提供視圖和控制器。
使用模板進(jìn)行通用數(shù)據(jù)結(jié)構(gòu)和算法設(shè)計(jì)
C++有一個(gè)叫做模板(Templates)的概念,它允許你創(chuàng)建對(duì)類型或類具有通用性的結(jié)構(gòu)。例如,你可能已經(jīng)為整數(shù)數(shù)組編寫了代碼。如果你隨后想要一個(gè)雙精度浮點(diǎn)數(shù)數(shù)組,你需要重寫和復(fù)制所有代碼以適應(yīng)雙精度浮點(diǎn)數(shù)。模板的概念是,類型變成了規(guī)范的一個(gè)參數(shù),你可以創(chuàng)建一個(gè)可以在任何類型上工作的單一代碼體。模板允許你編寫在任何類型上工作的數(shù)據(jù)結(jié)構(gòu)和算法。
最簡(jiǎn)單的例子是std::vector
類,它是C++標(biāo)準(zhǔn)庫(kù)的一部分。要?jiǎng)?chuàng)建一個(gè)整數(shù)向量,你寫std::vector<int>
;要?jiǎng)?chuàng)建一個(gè)雙精度浮點(diǎn)數(shù)向量,你寫std::vector<double>
。模板編程通常非常強(qiáng)大,但也可能非常復(fù)雜。幸運(yùn)的是,可以創(chuàng)建相對(duì)簡(jiǎn)單的模板用法,根據(jù)類型進(jìn)行參數(shù)化。
無(wú)論何時(shí)有可能,你都應(yīng)該使用通用設(shè)計(jì)來(lái)編寫數(shù)據(jù)結(jié)構(gòu)和算法,而不是編碼某個(gè)特定程序的細(xì)節(jié)。不要編寫只存儲(chǔ)書籍對(duì)象的平衡二叉樹結(jié)構(gòu)。使其通用,以便它可以存儲(chǔ)任何類型的對(duì)象。這樣,你可以在書店、音樂(lè)商店、操作系統(tǒng)或任何需要平衡二叉樹的地方使用它。
為什么模板比其他通用編程技術(shù)更好
模板并不是編寫通用數(shù)據(jù)結(jié)構(gòu)的唯一機(jī)制。另一種、盡管更老的方法是在C和C++中存儲(chǔ)void*
指針,而不是特定類型的指針。客戶端可以通過(guò)將其轉(zhuǎn)換為void*
來(lái)存儲(chǔ)他們想要的任何東西。然而,這種方法的主要問(wèn)題是它不是類型安全的:容器不能檢查或強(qiáng)制存儲(chǔ)元素的類型。
與直接在你的通用非模板數(shù)據(jù)結(jié)構(gòu)中使用void*
指針相比,你可以使用自C++17以來(lái)可用的std::any
類。std::any
類的底層實(shí)現(xiàn)在某些情況下確實(shí)使用了void*
指針,但它還跟蹤了存儲(chǔ)的類型,所以一切都保持了類型安全。
另一種方法是為特定類編寫數(shù)據(jù)結(jié)構(gòu)。通過(guò)多態(tài)性,該類的任何派生類都可以存儲(chǔ)在結(jié)構(gòu)中。模板,另一方面,在正確使用時(shí)是類型安全的。每個(gè)模板實(shí)例只存儲(chǔ)一種類型。如果你嘗試在同一個(gè)模板實(shí)例中存儲(chǔ)不同的類型,你的程序?qū)o(wú)法編譯。此外,模板允許編譯器為每個(gè)模板實(shí)例生成高度優(yōu)化的代碼。
模板的問(wèn)題
模板并不完美。首先,它們的語(yǔ)法可能令人困惑,尤其是對(duì)于那些以前沒有使用過(guò)它們的人。其次,模板需要同質(zhì)的數(shù)據(jù)結(jié)構(gòu),在單一結(jié)構(gòu)中只能存儲(chǔ)相同類型的對(duì)象。這就是模板的類型安全性直接導(dǎo)致的限制。
從C++17開始,有一種標(biāo)準(zhǔn)化的方法來(lái)繞過(guò)這種同質(zhì)性限制。你可以編寫你的數(shù)據(jù)結(jié)構(gòu)以存儲(chǔ)std::variant
或std::any
對(duì)象。一個(gè)std::any
對(duì)象可以存儲(chǔ)任何
類型的值,而一個(gè)std::variant
對(duì)象可以存儲(chǔ)一系列類型中的一個(gè)值。std::any
和std::variant
在后文討論。
模板的缺點(diǎn):代碼膨脹
模板的另一個(gè)可能的缺點(diǎn)是所謂的代碼膨脹:最終二進(jìn)制代碼的大小增加。每個(gè)模板實(shí)例的高度專門化代碼比稍慢的通用代碼需要更多的代碼。然而,通常來(lái)說(shuō),如今代碼膨脹并不是一個(gè)很大的問(wèn)題。
模板與繼承
程序員有時(shí)發(fā)現(xiàn)決定是否使用模板或繼承有點(diǎn)棘手。以下是一些幫助你做出決策的提示。
-
當(dāng)你想為不同類型提供相同的功能時(shí),使用模板。例如,如果你想編寫一個(gè)適用于任何類型的通用排序算法,使用函數(shù)模板。如果你想創(chuàng)建一個(gè)可以存儲(chǔ)任何類型的容器,使用類模板。
-
當(dāng)你想為相關(guān)類型提供不同的行為時(shí),使用繼承。例如,在一個(gè)繪圖應(yīng)用程序中,使用繼承來(lái)支持不同的形狀,如圓形、正方形、線條等。特定的形狀然后從一個(gè)基類(例如,
Shape
)派生。
值得注意的是,你可以組合繼承和模板。你可以編寫一個(gè)從基類模板派生的類模板。
提供適當(dāng)?shù)臋z查和保護(hù)措施
有兩種相反的編寫安全代碼的風(fēng)格。最佳的編程風(fēng)格可能是兩者之間的健康組合。
-
契約式設(shè)計(jì)(Design-by-Contract):這意味著函數(shù)或類的文檔代表了一份合同,詳細(xì)描述了客戶端代碼的責(zé)任和你的函數(shù)或類的責(zé)任。契約式設(shè)計(jì)有三個(gè)重要方面:前置條件、后置條件和不變量。
-
安全最大化設(shè)計(jì):這一準(zhǔn)則的最重要方面是在你的代碼中進(jìn)行錯(cuò)誤檢查。例如,如果你的隨機(jī)數(shù)生成器需要種子在特定范圍內(nèi),不要只是信任用戶傳遞一個(gè)有效的種子。檢查傳入的值,并在無(wú)效時(shí)拒絕調(diào)用。
為可擴(kuò)展性設(shè)計(jì)
你應(yīng)該努力以這樣一種方式設(shè)計(jì)你的類,使它們可以通過(guò)從它們派生另一個(gè)類來(lái)進(jìn)行擴(kuò)展,但它們應(yīng)該是封閉的,即行為應(yīng)該是可擴(kuò)展的,而無(wú)需你修改其實(shí)現(xiàn)。這被稱為開閉原則(OCP)。
作為一個(gè)例子,假設(shè)你開始實(shí)施一個(gè)繪圖應(yīng)用程序。第一個(gè)版本應(yīng)該只支持正方形。你的設(shè)計(jì)包含兩個(gè)類:Square
和Renderer
。
class Square { /* Details not important for this example. */ };
class Renderer {
public:
void render(const vector<Square>& squares) {
for (auto& square : squares) {
/* Render this square object... */
}
}
};
接下來(lái),你添加對(duì)圓形的支持,所以你創(chuàng)建了一個(gè)Circle
類。
class Circle { /* Details not important for this example. */ }
為了能夠渲染圓形,你必須修改Renderer
類的render()
方法。
在這個(gè)設(shè)計(jì)中,如果你想添加對(duì)新類型形狀的支持,你只需要編寫一個(gè)從Shape
派生并實(shí)現(xiàn)render()
方法的新類。你不需要在Renderer
類中修改任何內(nèi)容。因此,這個(gè)設(shè)計(jì)可以在不修改現(xiàn)有代碼的情況下進(jìn)行擴(kuò)展;也就是說(shuō),它是開放的,用于擴(kuò)展和封閉的,用于修改。
參考:Professional C++ (English Edition) 5th Edition by Marc Gregoire文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-720250.html
公眾號(hào):coding日記文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-720250.html
到了這里,關(guān)于精通代碼復(fù)用:設(shè)計(jì)原則與最佳實(shí)踐的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!