Внедрение зависимостей Javascript и DIP в узле: требуется внедрение против конструктора

Я новичок в разработке NodeJ, пришедшей из мира.NET. Я ищу в Интернете лучшие практики по реорганизации DI / DIP в Javascript.

В.NET я бы объявлял свои зависимости в конструкторе, тогда как в javascript я вижу общий шаблон - объявлять зависимости на уровне модуля с помощью оператора require.

для меня это выглядит так, что когда я использую require, я связан с конкретным файлом, а использование конструктора для получения моей зависимости более гибко.

Что бы вы порекомендовали сделать в качестве лучшей практики в javascript? (Я ищу архитектурный образец, а не техническое решение МОК)

В поисках в Интернете я наткнулся на это сообщение в блоге (в комментариях которого есть очень интересное обсуждение): https://blog.risingstack.com/dependency-injection-in-node-js/

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

// team.js
var User = require('./user');

function getTeam(teamId) {  
  return User.find({teamId: teamId});
}

module.exports.getTeam = getTeam; 

Простой тест будет выглядеть примерно так:

 // team.spec.js
    var Team = require('./team');  
    var User = require('./user');

    describe('Team', function() {  
      it('#getTeam', function* () {
        var users = [{id: 1, id: 2}];

        this.sandbox.stub(User, 'find', function() {
          return Promise.resolve(users);
        });

        var team = yield team.getTeam();

        expect(team).to.eql(users);
      });
    });

VS DI:

// team.js
function Team(options) {  
  this.options = options;
}

Team.prototype.getTeam = function(teamId) {  
  return this.options.User.find({teamId: teamId})
}

function create(options) {  
  return new Team(options);
}

тестовое задание:

// team.spec.js
var Team = require('./team');

describe('Team', function() {  
  it('#getTeam', function* () {
    var users = [{id: 1, id: 2}];

    var fakeUser = {
      find: function() {
        return Promise.resolve(users);
      }
    };

    var team = Team.create({
      User: fakeUser
    });

    var team = yield team.getTeam();

    expect(team).to.eql(users);
  });
});

2 ответа

Решение

Относительно вашего вопроса: я не думаю, что в сообществе JS есть распространенная практика. Я видел оба типа в дикой природе, требующих модификаций (например, rewire или proxyquire) и инъекции в конструктор (часто с использованием специального контейнера DI). Тем не менее, лично я считаю, что не использовать DI-контейнер лучше подходит для JS. И это потому, что JS - динамический язык с функциями первоклассных граждан. Позвольте мне объяснить, что:

Использование DI-контейнеров обеспечивает внедрение конструктора для всего. Это создает огромные накладные расходы на конфигурацию по двум основным причинам:

  1. Предоставление макетов в юнит-тестах
  2. Создание абстрактных компонентов, которые ничего не знают о своей среде

Что касается первого аргумента: я не буду корректировать свой код только для своих модульных тестов. Если это делает ваш код чище, проще, более универсальным и менее подверженным ошибкам, то сделайте это. Но если твоя единственная причина - твой модульный тест, я бы не пошел на компромисс. Вы можете продвинуться довольно далеко с необходимыми изменениями и исправлениями обезьян. И если вы пишете слишком много макетов, вам, вероятно, стоит вообще не писать модульный тест, а интеграционный тест. Эрик Эллиотт написал отличную статью об этой проблеме.

Относительно второго аргумента: это действительный аргумент. Если вы хотите создать компонент, который заботится только об интерфейсе, но не о фактической реализации, я бы выбрал простой инжектор конструктора. Однако, поскольку 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's compose both functions
function getAllFileTypesInDir(dirname, callback) {
    getAllFilesInDir(dirname, (err, allFiles) => {
        callback(err, !err && fileTypeCounter(allFiles));
    });
}

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

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

В прошлом DI-контейнеры, как мы их знаем из Java и.NET, не существовали. С Node 6 пришли ES6 Proxies, которые открыли возможности таких контейнеров - например, Awilix.

Итак, давайте перепишем ваш код на современный ES6.

class Team {
  constructor ({ User }) {
    this.User = user
  }

  getTeam (teamId) {
    return this.User.find({ teamId: teamId })
  }
}

И тест:

import Team from './Team'

describe('Team', function() {
  it('#getTeam', async function () {
    const users = [{id: 1, id: 2}]

    const fakeUser = {
      find: function() {
        return Promise.resolve(users)
      }
    }

    const team = new Team({
      User: fakeUser
    })

    const team = await team.getTeam()

    expect(team).to.eql(users)
  })
})

Теперь, используя Awilix, давайте напишем наш корень композиции:

import { createContainer, asClass } from 'awilix'
import Team from './Team'
import User from './User'

const container = createContainer()
  .register({
    Team: asClass(Team),
    User: asClass(User)
  })

// Grab an instance of Team
const team = container.resolve('Team')
// Alternatively...
const team = container.cradle.Team

// Use it
team.getTeam(123) // calls User.find()

Это так просто, как только можно; Awilix также может обрабатывать время жизни объектов, как это делали контейнеры.NET / Java. Это позволяет вам делать интересные вещи, такие как внедрение текущего пользователя в ваши сервисы, создание ваших сервисов один раз за HTTP-запрос и т. Д.

Другие вопросы по тегам