Ответ 1
Что касается вашего вопроса: я не думаю, что в сообществе JS есть обычная практика. Я видел оба типа в дикой природе, нуждаюсь в модификациях (например, rewire или proxyquire) и впрыска конструктора (часто используя выделенный контейнер DI). Однако лично я считаю, что использование контейнера DI лучше не подходит для JS. И это потому, что JS является динамическим языком с функционирует как первоклассный гражданин. Позвольте мне объяснить, что:
Использование контейнеров DI обеспечивает принудительное вложение конструктора для всех. Это создает огромные накладные расходы по конфигурации по двум основным причинам:
- Предоставление mocks в модульных тестах
- Создание абстрактных компонентов, которые ничего не знают об окружающей среде
Что касается первого аргумента: я бы не корректировал свой код только для моих модульных тестов. Если это делает ваш код более чистым, простым, более универсальным и менее подверженным ошибкам, тогда идите. Но если ваша единственная причина - ваш unit test, я бы не пошел на компромисс. Вы можете получить довольно далеко, требуя изменений, и патч обезьян. И если вы обнаружите, что пишете слишком много издевок, вы, вероятно, вообще не должны писать unit test, а тест интеграции. Эрик Эллиот написал отличную статью об этой проблеме.
Что касается второго аргумента: это допустимый аргумент. Если вы хотите создать компонент, который заботится только об интерфейсе, но не о фактической реализации, я бы выбрал простую конструкторскую инъекцию. Однако, поскольку JS не заставляет вас использовать классы для всего, почему бы просто не использовать функции?
В функциональном программировании разделение ИО от состояния с реальной обработкой является общей парадигмой. Например, если вы пишете код, который должен подсчитывать типы файлов в папке, можно написать это (особенно, когда он или она идет с языка, который применяет классы везде):
const fs = require("fs");
class FileTypeCounter {
countFileTypes(dirname, callback) {
fs.readdir(dirname, function (err) {
if (err) return callback(err);
// recursively walk all folders and count file types
// ...
callback(null, fileTypes);
});
}
}
Теперь, если вы хотите проверить это, вам нужно изменить свой код, чтобы ввести поддельный модуль fs
:
class FileTypeCounter {
constructor(fs) {
this.fs = fs;
}
countFileTypes(dirname, callback) {
this.fs.readdir(dirname, function (err) {
// ...
});
}
}
Теперь каждый, кто использует ваш класс, должен вставлять fs
в конструктор. Поскольку это скучно и делает ваш код более сложным, как только у вас есть длинные графики зависимостей, разработчики изобрели контейнеры DI, в которых они могут просто настроить материал, а контейнер DI отображает экземпляр.
Однако, как насчет простого написания чистых функций?
function fileTypeCounter(allFiles) {
// count file types
return fileTypes;
}
function getAllFilesInDir(dirname, callback) {
// recursively walk all folders and collect all files
// ...
callback(null, allFiles);
}
// now let compose both functions
function getAllFileTypesInDir(dirname, callback) {
getAllFilesInDir(dirname, (err, allFiles) => {
callback(err, !err && fileTypeCounter(allFiles));
});
}
Теперь у вас есть две супер-универсальные функции из коробки, одна из которых выполняет IO, а другая - обрабатывает данные. fileTypeCounter
является чистой функцией и супер-легко тестируется. getAllFilesInDir
является нечистым, но такая общая задача, вы часто найдете его уже на npm, где другие люди имеют письменные интеграционные тесты для Это. getAllFileTypesInDir
просто создает ваши функции с небольшим количеством потока управления. Это типичный случай для теста интеграции, в котором вы хотите убедиться, что все ваше приложение работает правильно.
Разделив код между IO и обработкой данных, вы не найдете необходимости вводить что-либо вообще. И если вам не нужно ничего вводить, это хороший знак. Чистые функции - это самая легкая вещь для тестирования и все еще самый простой способ совместного использования кода между проектами.