[英]Dependency injection: templates (/generics) or virtual functions?
這是關於接受編碼實踐的好奇心問題。 我(主要)是Java開發人員,並且一直在努力對我的代碼進行單元測試。 我花了一些時間研究如何編寫可測試性最高的代碼,特別注意Google的“ 如何編寫不可測試的代碼指南”(如果您還沒有看過的話,非常值得一看)。
自然,我最近與一個面向C ++的朋友爭論每種語言的繼承模型的優勢,我想我會說出一張C牌,說出C ++程序員通過不斷地忘記代碼的難易程度來測試其代碼的想法。 virtual
關鍵字(對於C ++ ers-這是Java中的默認關鍵字;您可以使用final
擺脫它)。
我發布了一個代碼示例,我認為它可以很好地展示Java模型的優勢( 完整的內容已經在GitHub上結束了 )。 簡短版本:
class MyClassForTesting {
private final Database mDatabase;
private final Api mApi;
void myFunctionForTesting() {
for (User u : mDatabase.getUsers()) {
mRemoteApi.updateUserData(u);
}
}
MyClassForTesting ( Database usersDatabase, Api remoteApi) {
mDatabase = userDatabase;
mRemoteApi = remoteApi;
}
}
無論我在這里寫的質量如何,想法都是該類需要對數據庫進行一些(可能非常昂貴)調用,以及一些API(可能在遠程Web服務器上)。 myFunctionForTesting()
沒有返回類型,那么如何對該單元進行單元測試? 在Java中,我認為答案並不難-我們嘲笑:
/*** Tests ***/
/*
* This will record some stuff and we'll check it later to see that
* the things we expect really happened.
*/
ActionRecorder ar = new ActionRecorder();
/** Mock up some classes **/
Database mockedDatabase = new Database(ar) {
@Override
public Set<User> getUsers() {
ar.recordAction("got list of users");
/* Excuse my abuse of notation */
return new Set<User>( {new User("Jim"), new User("Kyle")} );
}
Database(ActionRecorder ar) {
this.ar = ar;
}
}
Api mockApi = new Api() {
@Override
public void updateUserData(User u) {
ar.recordAction("Updated user data for " + u.name());
}
Api(ActionRecorder ar) {
this.ar = ar;
}
}
/** Carry out the tests with the mocked up classes **/
MyClassForTesting testObj = new MyClassForTesting(mockDatabase, mockApi);
testObj.myFunctionForTesting();
// Check that it really fetches users from the database
assert ar.contains("got list of users");
// Check that it is checking the users we passed it
assert ar.contains("Updated user data for Jim");
assert ar.contains("Updated user data for Kyle");
通過模擬這些類,我們為我們的輕量級版本注入了依賴關系,我們可以對其進行斷言以進行單元測試,並避免對數據庫/ api-land進行昂貴而費時的調用。 Database
和Api
的設計人員不必太了解這就是我們要做的事情,而MyClassForTesting
的設計人員當然也不必知道! (在我看來)這是一種很好的做事方法。
但是,我的C ++朋友反駁說這是一個可怕的駭客,並且有充分的理由說C ++不允許您這樣做! 然后,他提出了一個基於Generics的解決方案,該解決方案具有相同的作用。 為了簡潔起見,我只列出他提供的解決方案的一部分,但是您仍然可以在Github上找到全部內容 。
template<typename A, typename D>
class MyClassForTesting {
private:
A mApi;
D mDatabase;
public MyClassForTesting(D database, A api) {
mApi = api;
mDatabase = database;
}
...
};
然后將像以前一樣測試它,但是要替換的重要部分如下所示:
class MockDatabase : Database {
...
}
class MockApi : Api {
...
}
MyClassForTesting<MockApi, MockDatabase>
testingObj(MockApi(ar), MockDatabase(ar));
所以我的問題是:首選的方法是什么? 我一直以為基於多態的方法會更好-我沒有理由不會在Java中使用它-但是通常認為使用泛型比在C ++中對所有內容進行虛擬化更好嗎? 你怎么在你的代碼做(假設你做單元測試)?
我可能有偏見,但我想說C ++版本更好。 除其他外,多態性帶來一些成本。 在這種情況下,即使您沒有從中直接受益,也要讓您的用戶支付該費用。
例如,如果您有一個多態對象的列表,並且想通過基類來操縱所有這些對象,那么使用多態就可以證明是合理的。 然而,在這種情況下,多態性被用於用戶甚至看不到的東西。 您已經內置了處理多態對象的功能,但從未真正使用過-測試時只有模擬對象,而在實際使用中只有真實對象。 您將永遠不會(例如)擁有一組數據庫對象,其中一些是模擬數據庫,而另一些是真實數據庫。
這還不僅僅是效率問題(或者至少是運行時效率問題)。 您的代碼中的關系應該有意義。 當有人看到(公共)繼承時,應該告訴他們有關設計的一些信息。 但是,正如您在Java中概述的那樣,涉及到的公共繼承關系基本上是一個謊言-即他應該從中知道(您正在處理多態后代)是完全虛假的。 相比之下,C ++代碼將意圖正確傳達給了讀者。
在某種程度上,我當然誇大了那里的情況。 那些通常閱讀Java的人幾乎可以肯定已經習慣了通常濫用繼承的方式,因此他們根本不認為這是謊言。 不過,這有點把嬰兒和洗澡水一起扔掉了-他們沒有完全看到“謊言”,而是學會了完全忽略繼承的真正含義(或者只是不知道,特別是如果他們上了大學) Java是教授OOP的主要工具)。 正如我所說,我可能有些偏頗,但是對我來說,這(大多數)Java代碼變得更加難以理解。 您基本上必須小心忽略OOP的基本原理,並習慣其不斷被濫用的習慣。
一些關鍵建議是“優先考慮繼承而不是繼承”,這是MyClassForTesting
針對Database
和Api
所做的。 這也是一個很好的C ++建議:IIRC是有效的C ++ 。
您的朋友宣稱使用多態性是“可怕的駭客”,但使用模板則不是,這有點豐富。 他以什么依據聲稱一個人比另一個人更少hacky? 我什么也看不到,並且在我的C ++代碼中一直都使用它們。
我會說多態方法(如您所做的)更好。 考慮Database
和Api
可能是接口。 在這種情況下,您將明確聲明MyClassForTesting
使用的API:某人可以讀取Api.java
和Database.java
文件。 而且您正在松散地耦合模塊: Api
和Database
接口自然是最窄的可接受接口,比實現它們的任何conerete類的public
接口要窄得多。
更重要的是,您無法創建模板化的虛函數。 這樣就不可能通過繼承來測試使用模板的C ++中的函數,因此在C ++中通過繼承進行測試是不可靠的,因為您不能以這種方式測試所有類,並且肯定不是每個基類的使用都可以用派生類,尤其是實例化它們的模板。 當然,模板會引入自己的問題,但是我認為這超出了問題的范圍。
您是在繼承問題,但實際上這不是正確的解決方案-您只需要在編譯時而不是運行時在模擬和實數之間進行更改。 這個基本事實使模板成為更好的選擇。
在C ++中,我們不會忘記虛擬關鍵字,我們只是不需要它,因為運行時多態只應在需要在運行時更改類型時發生 。 否則,您正在用釘子發射火箭發射器。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.