[Перевод] Разделите свой код с помощью внедрения зависимостей

задняя часть Программа перевода самородков

Разделите свой код с помощью внедрения зависимостей

Не требуется сторонний фреймворк

[Icons8 团队](https://unsplash.com/@icons8?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) 摄于 [Unsplash](https://unsplash.com/s/photos/ingredients?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)

Не так много компонентов могут существовать сами по себе, не завися от других компонентов. Помимо создания тесно связанных компонентов, мы также можем воспользоваться преимуществамивнедрение зависимости(DI) улучшитьразделение интересов.

Эта статья познакомит вас с основными концепциями внедрения зависимостей без использования сторонних фреймворков. Весь пример кода будет на Java, но представленные общие принципы применимы и к любому другому языку.


Пример: процессор данных

Чтобы наглядно представить, как использовать внедрение зависимостей, мы начнем с простого типа:

public class DataProcessor {

    private final DbManager manager = new SqliteDbManager("db.sqlite");
    private final Calculator calculator = new HighPrecisionCalculator(5);

    public void processData() {
        this.manager.processData();
    }

    public BigDecimal calc(BigDecimal input) {
        return this.calculator.expensiveCalculation(input);
    }
}

DataProcessorЕсть две зависимости:DbManagerиCalculator. Создание их непосредственно в наших типах имеет несколько очевидных недостатков:

  • Возможный сбой при вызове конструктора
  • Подпись конструктора может измениться
  • тесно связан с явным реализующим типом

Время улучшить его!


внедрение зависимости

«Искусство гибкой разработки»Джеймс Шорхорошо подмечено:

«Внедрение зависимостей звучит сложно, но на самом деле его концепция очень проста».

Концепция внедрения зависимостей на самом деле довольно проста: дать компоненту все, что ему нужно для выполнения своей работы.

Как правило, это означает разделение компонентов путем предоставления их зависимостей извне, а не создание зависимостей непосредственно внутри компонентов, чрезмерное связывание компонентов.

Мы можем предоставить экземпляр с необходимыми зависимостями несколькими способами:

  • Внедрение конструктора
  • инъекция свойств
  • метод инъекций

Внедрение конструктора

Внедрение конструктора или внедрение зависимостей на основе инициализатора означает предоставление всех необходимых зависимостей во время инициализации экземпляра в качестве аргументов конструктору:

public class DataProcessor {

    private final DbManager manager;
    private final Calculator calculator;

    public DataProcessor(DbManager manager, Calculator calculator) {
        this.manager = manager;
        this.calculator = calculator;
    }

    // ...
}

Благодаря этому простому изменению мы можем исправить большинство первоначальных недостатков:

  • Легко заменить:DbManagerиCalculatorМодульные тесты больше не связаны конкретными реализациями, теперь их можно имитировать.
  • инициализирован и «готов»: нам не нужно беспокоиться о каких-либо подзависимостях, требуемых зависимостью (например, имя файла базы данных,значащие цифры (примечание переводчика)д.), и не беспокойтесь о возможности их сбоя во время инициализации.
  • Обязательно: вызывающий точно знает, что создаватьDataProcessorтребуемый контент.
  • Неизменяемость: зависимости всегда одинаковы.

Хотя внедрение конструктора является предпочтительным методом для многих сред внедрения зависимостей, оно также имеет существенные недостатки. Самый большой их недостаток заключается в том, что все зависимости должны быть предоставлены во время инициализации.

Иногда мы не можем инициализировать компонент сами или в какой-то момент мы не можем предоставить все зависимости компонента. Или нам нужно использовать другой конструктор. После того, как зависимости установлены, мы больше не можем их изменить.

Но мы можем использовать другие типы инъекций, чтобы облегчить эти проблемы.

инъекция свойств

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

В этих случаях мы можем использоватьинъекция свойстввместо того, чтобы полагаться на конструктор:

public class DataProcessor {

    public DbManager manager = null;
    public Calculator calculator = null;

    // ...

    public void processData() {
        // WARNING: Possible NPE
        this.manager.processData();
    }

    public BigDecimal calc(BigDecimal input) {
        // WARNING: Possible NPE
        return this.calculator.expensiveCalculation(input);
    }
}

Нам больше не нужны конструкторы, мы всегда можем предоставить зависимости после инициализации. Но есть у этого инъекционного метода и недостатки:Волатильность.

После инициализации мы больше не гарантируемDataProcessor"всегда доступен". Возможность изменять зависимости по желанию может дать нам больше гибкости, но это также связано с недостатком слишком большого количества проверок во время выполнения.

Теперь нам нужно обработать появление при доступе к зависимостямNullPointerExceptionвозможность.

метод инъекций

Даже если мы отделим зависимости от внедрения конструктора и/или внедрения свойства, у нас все равно будет только один вариант. Если в некоторых случаях нам нужен другойCalculatorЧто мы можем с этим поделать?

мы не хотим быть вторымиCalculatorКласс добавляет дополнительные свойства или параметры конструктора, потому что в будущем может появиться третий такой класс. и каждый раз, когда ты звонишьcalc(...)Предварительное изменение свойств также невозможно и может привести к ошибкам из-за использования неправильных свойств.

Лучшим подходом является параметризация самого вызывающего метода и его зависимостей:

public class DataProcessor {

    // ...

    public BigDecimal calc(Calculator calculator, BigDecimal input) {
        return calculator.expensiveCalculation(input);
    }
}

в настоящее время,calc(...)Вызывающий абонент несет ответственность за предоставление надлежащегоCalculatorэкземпляр, иDataProcessorКлассы полностью отделены от него.

Обеспечивает значение по умолчанию, смешивая различные типы инъекций.Calculator, что обеспечивает большую гибкость:

public class DataProcessor {

    // ...

    private final Calculator defaultCalculator;
    
    public DataProcessor(Calculator calculator) {
        this.defaultCalculator = calculator;
    }

    // ...

    public BigDecimal calc(Calculator calculator, BigDecimal input) {
        return Optional.ofNullable(calculator)
                       .orElse(this.calculator)
                       .expensiveCalculation(input);
    }
}

абонентМожетпредоставить другой видCalculator, но это недолжениз. У нас все еще есть несвязанный, готовый к использованиюDataProcessor, он может адаптироваться к конкретным сценариям.

Какой метод инъекции выбрать?

У каждого типа внедрения зависимостей есть свои преимущества, и не существует одного «правильного пути». Конкретный выбор полностью зависит от ваших реальных потребностей и ситуации.

Внедрение конструктора

Внедрение конструктора — мой фаворит, и его также часто предпочитают фреймворки внедрения зависимостей.

Он ясно говорит нам обо всех зависимостях, необходимых для создания конкретного компонента, и эти зависимости не являются необязательными, эти зависимости должны быть обязательными для всего компонента.

инъекция свойств

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

Другие языки программирования, такие как Swift, интенсивно используют свойства со свойствами.Режим делегата. Поэтому использование внедрения свойств сделает наш код более знакомым разработчикам других языков.

метод инъекций

Если зависимости могут быть разными при каждом вызове, то лучше всего использовать внедрение метода. Внедрение метода дополнительно разделяет компоненты, позволяя самому методу удерживать зависимости, а не весь компонент.

Помните, это тоже не так. При необходимости мы можем свободно комбинировать различные типы инъекций.

Инверсия контейнера управления

Эти простые реализации внедрения зависимостей могут охватывать множество вариантов использования. Внедрение зависимостей — отличный инструмент для разделения, но на самом деле нам все равно нужно создавать зависимости в какой-то момент.

Но по мере роста приложений и кодовых баз нам также может понадобиться более полное решение для упрощения создания и сборки внедрения зависимостей.

Инверсия контроля(ИоК) дапоток управленияабстрактный принцип. Внедрение зависимостей — одна из конкретных реализаций инверсии управления.

Инверсия контейнера управления— это особый тип объекта, который знает, как создавать и настраивать другие объекты, а также знает, как помочь вам выполнить внедрение зависимостей.

Некоторые контейнеры могут обнаруживать связи посредством отражения, а другие необходимо настраивать вручную. Некоторые контейнеры основаны на среде выполнения, в то время как другие генерируют весь код, необходимый во время компиляции.

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

Пример: Кинжал 2

DaggerЭто легкая среда внедрения зависимостей во время компиляции. нам нужно создатьModule, он умеет строить наши зависимости, позже мы просто добавим@InjectАннотация может ввести этоModule.

@Module
public class InjectionModule {

    @Provides
    @Singleton
    static DbManager provideManager() {
        return manager;
    }

    @Provides
    @Singleton
    static Calculator provideCalculator() {
        return new HighPrecisionCalculator(5);
    }
}

@SingletonУбедитесь, что можно создать только один экземпляр зависимости.

Чтобы внедрить зависимости, нам просто нужно добавить@Injectконструктору, полю или методу.

public class DataProcessor {

    @Inject
    DbManager manager;
    
    @Inject
    Calculator calculator;

    // ...
}

Это лишь некоторые из основ, которые на первый взгляд вряд ли впечатлят. Но контейнеры и фреймворки Inversion of Control не только разделяют компоненты, но и обеспечивают максимальную гибкость при создании зависимостей.

Благодаря расширенным функциям процесс создания становится более настраиваемым и поддерживает новые способы работы с зависимостями.

Расширенные возможности

Эти функции сильно различаются между различными типами контейнеров Inversion of Control и базовыми языками, такими как:

  • прокси-режими ленивая загрузка.
  • Жизненный цикл (например, одноэлементный шаблон с одним экземпляром на поток).
  • Автоматическая привязка.
  • Несколько реализаций одного типа.
  • Круговые зависимости.

Эти свойства представляют собой реальную силу контейнеров Inversion of Control. Вы можете подумать, что такие функции, как «круговые зависимости», плохие идеи, и это так.

Однако, если эта странная конструкция кода требуется из-за устаревшего кода или неизменного плохого дизайна в прошлом, теперь у нас есть возможность сделать это.

Суммировать

Мы должны проектировать наш код с точки зрения абстракций (таких как интерфейсы), а не конкретных реализаций, которые могут помочь нам уменьшить связывание кода.

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

«Программы должны опираться на абстракции, а не на конкретные реализации»— Роберт С. Мартин (2000), Принципы проектирования и шаблоны проектирования.

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

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

Контейнеры Inversion of Control иногда обеспечивают еще один удобный макет, упрощая процесс создания компонентов почти волшебным образом.

Должны ли мы использовать его везде? конечно, нет.

Как и другие шаблоны и концепции, мы должны применять их, когда это уместно, а не тогда, когда мы можем.

Никогда не ограничивайте себя одним способом ведения дел. Может бытьзаводской узордаже противноодноэлементный шаблонявляется лучшим решением для ваших нужд.


материал


Инверсия контейнера управления

Java

Kotlin

Swift

C#

Если вы обнаружите ошибки в переводе или в других областях, требующих доработки, добро пожаловать наПрограмма перевода самородковВы также можете получить соответствующие бонусные баллы за доработку перевода и PR. начало статьиПостоянная ссылка на эту статьюЭто ссылка MarkDown этой статьи на GitHub.


Программа перевода самородковэто сообщество, которое переводит высококачественные технические статьи из Интернета сНаггетсДелитесь статьями на английском языке на . Охват контентаAndroid,iOS,внешний интерфейс,задняя часть,блокчейн,продукт,дизайн,искусственный интеллектЕсли вы хотите видеть более качественные переводы, пожалуйста, продолжайте обращать вниманиеПрограмма перевода самородков,официальный Вейбо,Знай колонку.

Категории