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