Javascript-зависимая инъекция и DIP в node: требуют встраивания конструктора

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

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

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

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

поиск в Интернете я пришел в этом сообщении в блоге (у которого есть очень интересная дискуссия в комментариях): 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);
  });
});

Ответ 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 и обработкой данных, вы не найдете необходимости вводить что-либо вообще. И если вам не нужно ничего вводить, это хороший знак. Чистые функции - это самая легкая вещь для тестирования и все еще самый простой способ совместного использования кода между проектами.

Ответ 2

В прошлом контейнеры 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 } from 'awilix'
import Team from './Team'
import User from './User'

const container = createContainer()
  .registerClass({
    Team,
    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-запрос и т.д.