簡體   English   中英

依賴注入:模板(泛型)還是虛函數?

[英]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進行昂貴而費時的調用。 DatabaseApi的設計人員不必太了解這就是我們要做的事情,而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針對DatabaseApi所做的。 這也是一個很好的C ++建議:IIRC是有效的C ++

您的朋友宣稱使用多態性是“可怕的駭客”,但使用模板則不是,這有點豐富。 他以什么依據聲稱一個人比另一個人更少hacky? 我什么也看不到,並且在我的C ++代碼中一直都使用它們。

我會說多態方法(如您所做的)更好。 考慮DatabaseApi可能是接口。 在這種情況下,您將明確聲明MyClassForTesting使用的API:某人可以讀取Api.javaDatabase.java文件。 而且您正在松散地耦合模塊: ApiDatabase接口自然是最窄的可接受接口,比實現它們的任何conerete類的public接口要窄得多。

更重要的是,您無法創建模板化的虛函數。 這樣就不可能通過繼承來測試使用模板的C ++中的函數,因此在C ++中通過繼承進行測試是不可靠的,因為您不能以這種方式測試所有類,並且肯定不是每個基類的使用都可以用派生類,尤其是實例化它們的模板。 當然,模板會引入自己的問題,但是我認為這超出了問題的范圍。

您是在繼承問題,但實際上這不是正確的解決方案-您只需要在編譯時而不是運行時在模擬和實數之間進行更改。 這個基本事實使模板成為更好的選擇。

在C ++中,我們不會忘記虛擬關鍵字,我們只是不需要它,因為運行時多態只應在需要在運行時更改類型時發生 否則,您正在用釘子發射火箭發射器。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM