Внедрение зависимостей: шаблоны (/ generics) или виртуальные функции? - PullRequest
2 голосов
/ 02 февраля 2011

Это вопрос любопытства по поводу принятых методов кодирования.Я (в первую очередь) разработчик Java и все больше и больше прилагаю усилия для модульного тестирования моего кода.Я потратил некоторое время на то, как написать наиболее тестируемый код, уделяя особое внимание руководству Google Как написать непроверяемый код (что стоит посмотреть, если вы его еще не видели).

Естественно, я недавно спорил с другом, более ориентированным на C ++, о преимуществах модели наследования каждого языка, и я подумал, что вытащу козырь, сказав, насколько сложнее программисты на C ++ проверяют свой код.постоянно забывая ключевое слово virtual (для C ++ - это значение по умолчанию в 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 (возможно,на удаленном веб-сервере).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 - но обычно считается, что лучше использовать Generics, чем виртуализировать все в C ++?Что вы делаете в вашем коде (при условии, что вы делаете модульный тест)?

Ответы [ 3 ]

3 голосов
/ 02 февраля 2011

Я, вероятно, пристрастен, но я бы сказал, что версия C ++ лучше. Среди прочего, полиморфизм несет определенную стоимость. В этом случае вы заставляете своих пользователей оплачивать эту стоимость, даже если они не получают от нее прямой выгоды.

Если, например, у вас есть список полиморфных объектов, и вы хотите манипулировать всеми ими через базовый класс, это оправдывает использование полиморфизма. В этом случае, однако, полиморфизм используется для чего-то, что пользователь даже не видит. Вы встроили способность манипулировать полиморфными объектами, но никогда по-настоящему не использовали ее - для тестирования у вас будет только фиктивных объектов, а для реального использования у вас будут только реальные объекты. Никогда не будет времени, когда у вас будет (например) массив объектов базы данных, некоторые из которых являются фиктивными базами данных, а другие - настоящими базами данных.

Это также гораздо больше, чем просто проблема эффективности (или, по крайней мере, проблема эффективности во время выполнения). Отношения в вашем коде должны быть осмысленными. Когда кто-то видит (публичное) наследование, он должен рассказать им что-то о дизайне. Однако, как вы обрисовали в Java, вовлеченные общественные отношения наследования - это в основном ложь, т. Е. То, что он должен знать из этого (что вы имеете дело с полиморфными потомками), является явной ложью. Код C ++, напротив, правильно передает намерение читателю.

В некоторой степени, я преувеличиваю случай там, конечно. Люди, которые обычно читают Java, почти наверняка хорошо привыкли к тому, как наследуется насилие, поэтому они вообще не считают это ложью. Это все равно, что выливать ребенка из воды в ванне - вместо того, чтобы видеть «ложь» в том, что это такое, они научились полностью игнорировать, что на самом деле означает наследство (или просто никогда не знали, особенно если они учились в колледже). где Java была основным средством обучения ООП). Как я уже сказал, я, вероятно, несколько предвзят, но для меня это делает (большую часть) Java-код гораздо более сложным для понимания. В основном вы должны быть осторожны, чтобы игнорировать основные принципы ООП и привыкнуть к его постоянному злоупотреблению.

1 голос
/ 02 февраля 2011

Что еще более важно, вы не можете создавать шаблонные виртуальные функции. Это делает невозможным тестирование функций в C ++, которые используют шаблоны, с использованием наследования, и, следовательно, тестирование с помощью наследования в C ++ ненадежно, поскольку вы не можете тестировать все классы таким образом, и определенно не каждое использование базового класса может быть заменено использованием производный класс, особенно его экземпляры. Конечно, шаблоны создают свои проблемы, но я думаю, что это выходит за рамки вопроса.

Вы бросаете наследство на проблему, но на самом деле это не правильное решение - вам нужно переключаться между имитатором и реальным только в время компиляции , а не в время запуска, Этот фундаментальный факт делает шаблоны лучшим вариантом.

В C ++ мы не забываем ключевое слово virtual, оно нам просто не нужно, потому что полиморфизм во время выполнения должен происходить только тогда, когда вам нужно изменить тип в run-time . Иначе, ты стреляешь из ракетницы в гвоздь.

1 голос
/ 02 февраля 2011

Некоторый ключевой совет - «предпочесть композицию наследованию», что и сделал ваш MyClassForTesting в отношении Database и Api.Это также хороший совет C ++: IIRC находится в Эффективном C ++ .

Ваш друг немного богат, чтобы утверждать, что использование полиморфизма - «ужасный хак», но использование шаблонов -,На каком основании он (-ы) заявляет, что один менее хакерский, чем другой?Я не вижу ни одного, и я все время использую оба в своем коде C ++.

Я бы сказал, что подход полиморфизма (как вы сделали) лучше.Учтите, что Database и Api могут быть интерфейсами.В этом случае вы явно декларируете API, используемый MyClassForTesting: кто-то может прочитать файлы Api.java и Database.java.И вы слабо соединяете модули: интерфейсы Api и Database, естественно, будут самыми узкими приемлемыми интерфейсами, гораздо более узкими, чем интерфейс public любого конкретного класса, который их реализует.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...