При выполнении IoC я (думаю, что я) понимаю его использование для получения желаемой функциональности уровня приложения, составляя правильные части и преимущества для проверки. Но на микроуровне я не совсем понимаю, как убедиться, что объект получает зависимости, в которые он может работать. Мой пример для этого - BackupMaker
для базы данных.
Чтобы сделать резервную копию, база данных должна быть экспортирована в определенном формате, сжата с использованием специального алгоритма сжатия, а затем упакована вместе с некоторыми метаданными для формирования окончательного двоичного файла. Выполнение всех этих задач, похоже, далеки от единой ответственности, поэтому я оказался с двумя сотрудниками: a DatabaseExporter
и a Compressor
.
BackupMaker
все равно, как экспортируется база данных (например, с помощью IPC для утилиты, поставляемой с программным обеспечением базы данных, или путем правильного вызова API), но она очень заботится о результате, т.е. он должен быть резервным копированием такого типа в первую очередь в переносимом (версия агностическом) формате, который я действительно не знаю, как заключить контракт. Также не важно, сжат ли компрессор в памяти или на диске, но он должен быть BZip2.
Если я дам BackupMaker
неправильные виды экспортера или компрессора, он все равно даст результат, но он будет поврежден - он будет выглядеть как резервная копия, но у него не будет формата, который он должен иметь. Похоже, никакая другая часть системы не может быть доверена, чтобы дать ей эти соавторы, потому что BackupMaker
не сможет гарантировать правильное выполнение самой вещи; его работа (с моей точки зрения) заключается в том, чтобы создать действующую резервную копию, и это не будет, если обстоятельства не правильные, и, что еще хуже, она не будет знать об этом. В то же время, даже когда я пишу это, мне кажется, что сейчас я говорю что-то глупое, потому что весь смысл отдельных обязанностей заключается в том, что каждая часть должна выполнять свою работу, а не беспокоиться о работе других. Если бы это было так просто, не было бы необходимости в контрактах - J.B. Рейнсбергер просто научил меня. (FYI, я отправил ему этот вопрос напрямую, но у меня пока нет ответа, и больше мнения по этому вопросу будут замечательными.)
Интуитивно, моим любимым вариантом было бы сделать невозможным совмещение классов/объектов недопустимым образом, но я не вижу, как это сделать. Должен ли я писать чудовищно специфические имена интерфейсов, например IDatabaseExportInSuchAndSuchFormatProducer
и ICompressorUsingAlgorithmXAndParametersY
, и предположить, что ни один из классов не реализует их, если они не ведут себя как таковые, а затем называть его днем, поскольку ничего не может быть сделано о прямом лежачем коде? Должен ли я дойти до мирской задачи по анализу бинарного формата моих алгоритмов экспорта и сжатия базы данных, чтобы иметь контрактные тесты для проверки не только синтаксиса, но и поведения, а затем быть уверенным (но как?) Использовать только проверенные классы? Или я могу как-то перераспределить обязанности, чтобы эта проблема исчезла? Должен ли быть еще один класс, чья ответственность состоит в том, чтобы составить правильные элементы нижнего уровня? Или я даже сильно разлагаюсь?
Перефразировать
Я замечаю, что этому особому примеру уделяется большое внимание. Однако мой вопрос более общий, чем этот. Поэтому в последний день щедрости я попытаюсь обобщить следующее:
При использовании инъекции зависимостей, по определению, объект зависит от других объектов за то, что ему нужно. Во многих книжных примерах способ указания совместимости - возможность обеспечить эту потребность - осуществляется с помощью системы типов (например, для реализации интерфейса). Помимо этого, и особенно в динамических языках, используются контрактные тесты. Компилятор (если присутствует) проверяет синтаксис, и тесты контракта (о которых программист должен запомнить) проверяют семантику. Все идет нормально. Однако иногда семантика все еще слишком проста, чтобы гарантировать, что некоторый класс/объект можно использовать в качестве зависимости от другого или слишком сложно описывать в контракте должным образом.
В моем примере мой класс с зависимостью от экспортера базы данных рассматривает все, что реализует IDatabaseExportInSuchAndSuchFormatProducer
, и возвращает байты как действительные (поскольку я не знаю, как проверить формат). Является ли очень конкретным наименованием и таким очень грубым контрактом путь, или я могу сделать лучше, чем это?. Должен ли я перевести контрактный тест на интеграционный тест? Может быть (интеграция) проверить состав всех трех? Я не очень стараюсь быть родовым, но стараюсь не разделять обязанности и поддерживать тестируемость.