Русский
Русский
English
Статистика
Реклама

Inversion

У Вас проблемы с legacy значит, Вам повезло! Распил монолита на PHP

06.01.2021 06:14:44 | Автор: admin

Вступление

Меня часто просят рассказать о работе с legacy-монолитами. Про микросервисную архитектуру и переход на нее говорят много, но редко упоминают о том, что проекты приходят ней после многих лет роста с монолитным приложением. Учебники по решению проблем не пишут. Чтобы поменять архитектуру живого решения, надо пройти через несколько этапов. Автор работал с разными проектами - и с полноценным multitenancy service-oriented REST architecture в Oracle, и с огромным монолитом, в репозитории которого были коммиты за десять лет. Эта статья - о темной стороне, о legacy-коде, и практических решениях проблем с монолитными приложениями на PHP.

Причины появления legacy

Есть две основные причины появления legacy-кода.

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

Вторая - технический долг, который создается специально. Руководство сокращает срок разработки ПО за счет отказа от проектирования, автоматического тестирования или code review, одобряет сторонние библиотеки, которые не поддерживаются, а разработчики не документируют сложную логику. Это встречается повсеместно и не зависит от количества денег на счету компании. Не стоит ругать плохих начальников. У них есть весомые причины поступать именно так.

У продуктов есть жизненный цикл, период большого спроса на популярные товары длится три-четыре месяца. Все лучшее конкуренты скопируют и сделают еще лучше, поэтому компании вынуждены регулярно выпускать новинки. Чтобы поддерживать объем выручки, новые продукты и новые версии выпускают каждые несколько месяцев, так продажи нового цикла компенсируют снижение продаж по товарам в конце цикла. По три-четыре крупных релиза в год делают и Apple, и Marvel, и в Oracle на рынке enterprise SAAS тоже квартальный релизный цикл. При этом, рецепта успеха не существует. 97% стартапов выкидывают наработки по своему продукту, и пробуют делать что-то новое, прежде чем найдут такой продукт, который у них покупают. Поэтому затраты на разработку MVP в стартапах максимально сокращают.

У вас проблемы с легаси - значит, вам повезло!

Проблемы появляются, когда жизненный цикл продукта не согласован с жизненным циклом ПО, которое связано с продуктом. В стабильных организациях обычно есть сроки вывода старого ПО из эксплуатации или его обновления. Однако, в стартапах поддерживать ПО скорее всего будет не нужно, пишется недокументированный хрупкий код, который не масштабируется. Когда стартап находит продукт, который продается лучше, чем инвесторы ожидали, такой продукт идет на следующий цикл, выпускается новая версия. Процент стартапов, которые стали успешными, настолько мал, что окупаемость длительного цикла жизни ПО с выводом из эксплуатации старого кода станет понятна только после долгого и большого роста, к этому времени у проекта уже накопится большой объем legacy-кода. Им повезло - проблем с legacy нет у тех стартапов, чей жизненный цикл предсказуем, и которые закрылись.

Проблемы?

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

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

Что делать тем, кому повезло?

Начинать надо с тестирования

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

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

Обновление версии языка

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

Для разработки новых продуктов нужны сторонние библиотеки, которые требуют современную версию платформы. Еще в старых версиях не исправляются проблемы. В проект на устаревшей версии языка сложнее найти разработчиков. Как следствие, растет и цена решения задач на основе существующего ПО, и усилия на поддержку работоспособности.

Составить список проблемы совместимости с новой версией PHP помогут утилиты статического анализа.

Rector поможет решить простые случаи несовместимости с новой версией, автоматически обновив часть кода.

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

Phan показывает использование в коде лексических конструкций, которые убраны из новых версий PHP.

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

Обновление версии платформы или языка в таком случае выполняется достаточно быстро. Автор был инициатором обновления PHP с 5-ой версии на 7-ую для приложения с очень большим объемом кода, и эта задача была успешно выполнена командой за три недели.

Переход от монолита к сервисной архитектуре

Иногда проекты вырастают. Продукты стали успешными на рынке, и регулярно выпускаются. По законам Лемана сложность ПО растёт, функциональное содержание расширяется, вместе с ними штат разработчиков и объем кода постоянно увеличиваются. Замена устаревшего ПО в бюджет разработки не закладывается, чтобы улучшить финансовые результаты, поэтому качество программ ухудшается. Размер git-репозитория может исчисляться гигабайтами. Постепенно скорость разработки уменьшается, и когда разработчики перестают успевать выпускать ПО для новых продуктов, монолит решают разделять.

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

К счастью, слона можно съесть по кусочкам - отделять от монолита модули, не переписывая код заново, зафиксировать API, а затем превращать их в сервисы. Сначала части кода приложения надо выделить в отдельные пакеты, а затем из пакетов можно будет создавать сервисы.

Перенос кода в пакеты открывает ряд возможностей:

  • можно сократить размер репозитория приложения,

  • разработчикам из разных команд можно предоставить только публичный API пакетов, и ограничить вызовы внутренних классов,

  • можно описать зависимости между своими модулями и использовать composer для управления зависимостями и версиями своих пакетов,

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

  • можно выпускать разные версии пакетов, и согласовать изменения API.

Главное - это относительно небольшая по объему работы задача. Вынести часть кода в пакет без переписывания можно за несколько дней. У автора был опыт переноса в пакеты по тысяче строк кода в день с инверсией внешних зависимостей. А после фиксации API модулей будет проще заниматься масштабным рефакторингом.

Разделение приложения на пакеты

Допустим, есть приложение на PHP, которое предоставляет клиентский API. Начинать любые изменения надо с процедур тестирования и релиза, которые включают план отката. Эти процедуры называют release, control, validation и DevOps. В активно развивающихся проектах тестирование и выкладка отработаны. В этом случае надо начинать разделять приложение с определения таких ограниченных контекстов, которые логично выделить в отдельные модули и сервисы.

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

Создание отдельного модуля - это цикл из пяти подзадач:

1. Выбрать небольшой функционал для переноса в модуль - например, изменение размера изображений;

2. Определить API модуля - написать интерфейс, доступный приложению;

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

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

5. Заменить в коде приложения прямые обращения к старому коду на вызовы сервиса из нового модуля; Для решения этой задачи используется две технологии: IoC-контейнер и менеджер зависимостей.

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

Начать создавать пакеты можно в локальном каталоге, а для полноценной сборки и развертывания стоит создать собственный репозиторий пакетов, такой, как Packeton, и перенести код модулей в собственные git-репозитории. Так же, можно использовать платный репозиторий Private Packagist.

Как создать composer-пакет в приложении и зарегистрировать его как сервис в IoC-контейнере, можно посмотреть здесь: до изменений, после изменений, diff.

В примерах используется composer для управления зависимостями пакетов и Symfony Dependency Injection как IoC-контейнер для сервисов. У Вас может быть другой контейнер. Если в приложении нет IoC-контейнера, придется делать рефакторинг и реализовать внедрение зависимостей. Простейший пример добавления IoC-контейнера в приложение.

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

Есть два типа связанности:

а) код будущего модуля содержит вызовы структур, которые описаны в других частях приложения

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

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

1. Расширение классов, реализация интерфейсов, использование трейтов, когда декларация структур используется через границу будущего модуля.

Приведу пример устранения связанности при наследовании, когда родительский и дочерний классы вызывают методы друг друга: результат, diff.

Основные алгоритмы расцепления связанности:

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

  • Для интерфейсов, которые используются и в пакете, и в приложении, надо создать пакет контрактов, и указывать его в зависимостях.

  • Наследование от внешних классов с зависимостями надо превратить в композицию с помощью адаптеров, которые внедряются как сервисы.

  • Для защищенных свойств, которые используются в дочернем классе, надо сделать getter-методы, а для защищенных методов надо создать прокси-методы.

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

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

2. Статические вызовы.

Синтаксис PHP допускает вызов статических методов у объектов как методов класса (пример). Если Вы выносите в пакет или обычную функцию или класс, у которого есть статический метод, эти функции/методы нужно добавить в публичное API пакета (пример, diff).

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

Ссылки: пример прямого статического вызова, пример инверсии зависимости статического вызова через внедрение сервиса, diff коммита.

Если несколько методов из разных классов используются вместе, для них можно создать сервис-фасад.

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

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

5. Использование глобальных констант и констант классов.

Возьмем пример: в приложении есть класс, который нарушает Single Responsibility Principle, и содержит обращения к константе другого класса. Наша задача - вынести первый класс в пакет без рефакторинга второго класса, потому что рефакторинг потребует изменения всего остального кода, в котором используется константа. Надо избавиться от прямого обращения к константе.

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

Ссылки: до изменений, после изменений, diff, декларация инъекции константы в контейнере.

6. Динамическое разрешение имен через строковые операции.

Пример: $model = new ($modelName . Class);

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

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

Оптимизация

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

Есть несколько способов решения этой задачи:

  1. Сервисы, которые передаются в пакет, можно объявить как lazy.

  2. Объект API пакета можно объявить как Service Subscriber.

  3. Разделить API пакета на несколько сервисов.

Самый гибкий способ - это реализация Service Subscriber. Когда сервис объявляется подписчиком, можно реализовать в пакете вызов внешних сервисов по мере обращения к ним в пакете. Примеры: код до изменений, где используется один из нескольких классов, и код после переноса в пакет c инверсией зависимостей, где нужный сервис создается по требованию. Diff.

Service-Oriented Architecture

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

У каждого пакета зафиксирован публичный API. На основе этого API можно создать сервис с restful-протоколом. Код нового сервиса - это код пакета, вокруг которого написан достаточно стандартный роутинг, запись логов, и прочий инфраструктурный код. А в старом коде вместо кода пакета появляется адаптер для http-вызовов через curl.

При создании отдельных внутренних приложений-сервисов надо решить две задачи:

  1. Детальное протоколирование вызовов всех сервисов. Каждому клиентскому запросу надо присваивать уникальный ID вызова, который передается во все сервисы при вызовах внутренних API, и каждый вызов сервиса надо протоколировать. Надо иметь возможность отследить вызовы сервисов по цепочке.

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

Заключение

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

Подробнее..

Pure DI для .NET

16.04.2021 22:16:20 | Автор: admin

Для того чтобы следовать принципам ООП и SOLID часто используют библиотеки внедрения зависимостей. Этих библиотек много, и всех их объединяет набор общих функции:

  • API для определения графа зависимостей

  • композиция объектов

  • управление жизненным циклом объектов

Мне было интересно разобраться как это работает, а лучший способ сделать это - написать свою библиотеку внедрения зависимостей IoC.Container. Она позволяет делать сложные вещи простыми способами: неплохо работает с общими типами - другие так не могут, позволяет создавать код, без зависимостей на инфраструктуру и обеспечивает хорошую производительность, по сравнению с другими похожими решениям, но НЕ по сравнению с чистым DI подходом.

Используя классические библиотеки внедрения зависимостей, мы получаем простоту определения графа зависимостей и теряем в производительности. Это заставляет нас искать компромиссы. Так, в случае, когда нужно работать с большим количеством объектов, использование библиотеки внедрения зависимостей может замедлить выполнение приложения. Одним из компромиссов здесь будет отказ от использования библиотек в этой части кода, и создание объектов по старинке. На этом закончится заранее определенный и предсказуемый граф, а каждый такой особый случай увеличит общую сложность кода. Помимо влияния на производительность, классические библиотеки могут быть источником проблем времени выполнения из-за их некорректной настройки.

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

Что если оставить только лучшее от этих подходов:

  • определять граф зависимостей используя простой API

  • эффективная композиция объектов как при чистом DI

  • решение проблем и "нестыковок" на этапе компиляции

На мой взгляд, эти задачи мог бы на себя взять язык программирования. Но пока большинство языков программирования заняты копированием синтаксического сахара друг у друга, предлагается следующее не идеальное решение - использование генератора кода. Входными данными для него будут .NET типы, ориентированные на внедрение зависимостей и метаданные/API описания графа зависимостей. А результатом работы будет автоматически созданный код для композиции объектов, который проверяется и оптимизируется компилятором и JIT.

Итак, представляю вам бета-версию библиотеки Pure.DI! Цель этой статьи - собрать фидбек, идеи как сделать ее лучше. На данный момент библиотека состоит из двух NuGet пакетов beta версии, с которыми уже можно поиграться:

  • первый пакет Pure.DI.Contracts это API чтобы дать инструкции генератору кода как строить граф зависимостей

  • и генератор кода Pure.DI

Пакет Pure.DI.Contracts не содержит выполняемого кода, его можно использовать в проектах .NET Framework начиная с версии 3.5, для всех версий .NET Standard и .NET Core и, кончено, же для проектов .NET 5 и 6, в будущем можно добавить поддержку и .NET Framework 2, если это будет актуально. Все типы и методы этого пакета это API, и нужны исключительно для того, чтобы описать граф зависимостей, используя обычный синтаксис языка C#. Основная часть этого API был позаимствована у IoC.Container.

.NET 5 source code generator и Roslyn стали основой для генератора кода из пакета Pure.DI. Анализ метаданных и генерация происходит на лету каждый раз когда вы редактируете свой код в IDE или автоматически, когда запускаете компиляцию своих проектов или решений. Генерируемый код не отсвечивается рядом с обычным кодом и не попадает в системы контроля версий. Пример ниже показывает, как это работает.

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

interface IBox<out T> { T Content { get; } }interface ICat { State State { get; } }enum State { Alive, Dead }

Реализация этой абстракции для кота Шрёдингера может быть такой:

class CardboardBox<T> : IBox<T>{    public CardboardBox(T content) => Content = content;    public T Content { get; }}class ShroedingersCat : ICat{  // Суперпозиция состояний  private readonly Lazy<State> _superposition;  public ShroedingersCat(Lazy<State> superposition) =>    _superposition = superposition;  // Наблюдатель проверяет кота   // и в этот момент у кота появляется состояние  public State State => _superposition.Value;  public override string ToString() => $"{State} cat";}

Это код предметной области, который не зависит от инфраструктурного кода. Он написан по технологии DI, а в идеале SOLID.

Композиция объектов создается в модуле с инфраструктурным кодом, который отвечает за хостинг приложения, поближе к точке входа. В этот модуль и необходимо добавить ссылки на пакеты Pure.DI.Contracts и Pure.DI. И в нём же следует оставить генератору кода подсказки как строить граф зависимостей:

static partial class Glue{  // Моделирует случайное субатомное событие,  // которое может произойти, а может и не произойти  private static readonly Random Indeterminacy = new();  static Glue()  {    DI.Setup()      // Квантовая суперпозиция двух состояний      .Bind<State>().To(_ => (State)Indeterminacy.Next(2))      // Абстрактный кот будет котом Шрёдингера      .Bind<ICat>().To<ShroedingersCat>()      // Коробкой будет обычная картонная коробка      .Bind<IBox<TT>>().To<CardboardBox<TT>>()      // А корнем композиции тип в котором находится точка входа      // в консольное приложение      .Bind<Program>().As(Singleton).To<Program>();  }}

Первый вызов статического метода Setup() класса DI начинает цепочку подсказок. Если он находится в static partial классе, то весь сгенерированный код станет частью этого класса, иначе будет создан новый статический класс с постфиксом DI. Метод Setup() имеет необязательный параметр типа string чтобы переопределить имя класса для генерации на свое. В примере в настройке использована переменная Indeterminacy, поэтому класс Glue является static partial, чтобы сгенерированный код мог ссылаться на эту переменную из сгенерированной части класса.

Следующие за Setup() пары методов Bind<>() и To<>() определяют привязки типов зависимостей к их реализациям, например в:

.Bind().To()

ICat - это тип зависимости, им может быть интерфейс, абстрактный класс и любой другой не статический .NET тип. ShroedingersCat - это реализация, ей может быть любой не абстрактный не статический .NET тип. По моему мнению, типами зависимостей лучше делать интерфейсы, так как они имеют ряд преимуществ по сравнению с абстрактными классами. Одна из них - это возможность реализовать одним классом сразу несколько интерфейсов, а потом использовать его для нескольких типов зависимостей. И так, в Bind<>() мы определяем тип зависимости, а в To<>() его реализацию. Между этими методами могут быть и другие методы для управления привязкой:

  • дополнительные методы Bind<>(), если нужно привязать более одного типа зависимости к одной и той же реализации

  • метод As(Lifetime) для определения времени жизни объекта, их может быть много в цепочке, но учитывается последний

  • и метод Tag(object), который принимает тег привязки, их может быть несколько на одну привязку, и учитываются все

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

  • Transient - значение по умолчанию, когда объект типа будет создаваться каждый раз

  • Singleton - будет создан единственный объект типа, в отличие от классических контейнеров здесь это будет статическое поле статического класса

  • PerThread - будет создано по одному объекту типа на поток

  • PerResolve - в процессе одной композиции объект типа будет переиспользован

  • Binding - позволяет использовать свою реализацию интерфейса ILifetime в качестве времени жизни

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

.Bind().Tag(Fat).Tag(Fluffy).To()

Обратите внимание, что методы Bind<>() и To<>() - это универсальные методы. Каждый содержит один параметр типа для определения типа зависимости, и ее реализации соответственно. Вместо неудобных открытых типов, как например, typeof(IBox<>) в API применяются маркеры универсальных типов, как TT. В нашем случае абстрактная коробка - это IBox, а ее картонная реализация это CardboardBox. Почему открытые типы менее удобны? Потому что по открытым типа невозможно однозначно определить требуемую для внедрения реализацию, в случае когда параметры универсальных типов это другие универсальные типы. Помимо обычных маркеров TT, TT1, TT2 и т.д. в API можно использовать маркеры типов с ограничениями. Их достаточно много в наборе готовых маркеров. Если нужен свой маркер c ограничениями, создайте свой тип и добавьте атрибут [GenericTypeArgument] и он станет маркером универсального типа, например:

[GenericTypeArgument]public class TTMy: IMyInterface { }

Метод To<>() завершает определение привязки. В общем случае реализация будет создаваться автоматически. Будет найден конструктор, пригодный для внедрения через конструктор с максимальным количеством параметров. При его выборе предпочтения будут отданы конструкторам без атрибута [Obsolete]. Иногда нужно переопределить то, как будет создаваться объект или, предположим, вызвать дополнительно еще какой-то метод инициализации. Для этого можно использовать другую перегрузку метода To<>(factory). Например, чтобы создать картонную коробку самостоятельно, привязка

.Bind<IBox>().To<CardboardBox>()

может быть заменена на

.Bind<IBox>().To(ctx => new CardboardBox(ctx.Resolve()))

To<>(factory) принимает аргументом lambda функцию, которая создает объект. А единственный аргумент lambda функции, здесь это - ctx, помогает внедрить зависимости самостоятельно. Генератор в последствии заменит вызов ctx.Resolve() на создание объекта типа TT на месте для лучшей производительности. Помимо метода Resolve() возможно использовать другой метод с таким же названием, но с одним дополнительным параметром - тегом типа object.

Время открывать коробки!

class Program{  // Создаем граф объектов в точке входа  public static void Main() => Glue.Resolve<Program>().Run();  private readonly IBox<ICat> _box;  internal Program(IBox<ICat> box) => _box = box;  private void Run() => Console.WriteLine(_box);}

В точке входа void Main() вызывается метод Glue.Resolve<Program>() для создания всей композиции объектов. Этот пример соответствует шаблону Composition Root, когда есть единственное такое место, очень близкое к точке входа в приложение, где выполняется композиция объектов, граф объектов четко определен, а типы предметной области не связаны с инфраструктурой. В идеальном случае метод Resolve<>() должен использоваться один раз в приложении для создания сразу всей композиции объектов:

static class ProgramSingleton{  static readonly Program Shared =     new Program(      new CardboardBox<ICat>(        new ShroedingersCat(          new Lazy<State>(            new Func<State>(              (State)Indeterminacy.Next(2))))));}

Вследствии того, что привязка для Program определена со временем жизни Singleton то метод Resolve<>() для типа Program каждый раз будет возвращать единственный объект этого типа. О многопоточности и ленивой инициализации не стоит беспокоится, так как этот объект будет создан гарантированно один раз и только при первой загрузке типа в момент первого обращения к статическому полю Shared класса статического приватного класса ProgramSingleton, вложенного в класс Glue.

Есть еще несколько интересных вопросов, которые хотелось бы обсудить. Обратите внимание, что конструктор кота Шрёдингера

ShroedingersCat(Lazy<State> superposition)

требует внедрения зависимости типа Lazy<> из стандартной библиотеки классов .NET. Как же это работает, когда в примере не определена привязка для Lazy<>? Дело в том, что пакет Pure.DI из коробки содержит набор привязок BCL типов. Конечно же, любую привязку можно переопределить при необходимости. Метод DependsOn(), с именем набора в качестве аргумента, позволяет использовать свои наборы привязок.

Другой важный вопрос, какую зависимость нужно внедрить чтобы иметь возможность создавать множество объектов определенного типа и делать это в некоторый момент времени? Все просто - Func<>, как и другие BCL типы поддерживается из коробки. Например, если вместо ICat, тип-потребитель запросит зависимость Func<ICat>, то станет возможным получить столько котов сколько и когда нужно.

Еще одна задача. Есть несколько реализаций с разными тегами, требуется получить все их. Чтобы получить всех котов, требуемую для внедрения зависимость можно определить как IEnumerable<ICat>, ICat[] или другой интерфейс коллекций из библиотеки классов .NET, например IReadOnlyCollection<T>. Конечно же, для IEnumerable<ICat> коты будут создаваться лениво.

Для простого примера, как кот Шрёдингера, такого API будет вполне достаточно. Для других случаев помогут привязки To<>(factory) c lambda функцией в аргументе, где объект можно создать вручную, хотя они не всегда удобны.

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

  • привязка: .Bind<ICat>().Tag(Fat).Tag(Fluffy).To<FatCat>()

  • и конструктор потребителя: BigBox([Tag(Fat)] T content) { }

Помимо TagAttribute есть другие предопределенные атрибуты:

  • TypeAttribute - когда нужно обозначить тип внедряемой зависимости вручную, не полагаясь, на тип параметра конструктора, метода, свойства или поля

  • OrderAttribute - для методов, свойств и полей указывается порядок вызова/инициализации

  • OrderAttribute - для конструкторов указывается на сколько один предпочтительнее других

Но, если использовать любой из этих предопределенных атрибутов в типах предметной области, то модуль предметной области будет зависеть от модуля Pure.DI.Contracts. Это может быть не очень большой проблемой, но все же рекомендуется использовать свои аналоги атрибутов, определенных в модуле предметной области, чтобы не добавлять лишних зависимостей и держать код в чистоте. Для этой цели есть три метода, оставляющие подсказки генератору кода о том какие атрибуты использовать в дополнение к предопределенным:

  • TypeAttribute<>()

  • TagAttribute<>()

  • OrderAttribute<>()

В качестве параметра типа они принимают тип атрибута, а в качестве единственного необязательного параметра - позицию аргумента в конструкторе атрибута откуда брать значение: типа, тега или порядка, соответственно. По умолчанию позиция аргумента равна 0, следовательно, значения будут взяты из первого аргумента конструктора. Позицию следует переопределять, например, когда вы добавили свой атрибут, такой как InjectAttribute, и его конструктор содержит и тег, и тип одновременно.

Теперь немного о генераторе и генерируемом коде. Генератор кода, используя синтаксические деревья и семантические модели Roslyn API, отслеживает изменения исходного кода в IDE на лету, строит граф зависимостей и генерирует эффективный код для композиции объектов. При анализе выявляются ошибки или недочеты и компилятор получает соответствующие уведомления. Например, при обнаружении циклической зависимости появится ошибка в IDE или в терминале командной строки при сборке проекта, которая не позволит компилировать код, пока она не будет устранена. Будет указано предположительное место проблемы. В другой ситуации, например, когда генератор не сможет найти требуемую зависимость, он выдаст ошибку компиляции с описанием проблемы. В этом случае необходимо либо добавить привязку для требуемой зависимости, либо переопределить fallback стратегию: привязать интерфейс IFallback к своей реализации. Её метод Resolve<>() вызывается каждый раз когда не удается найти зависимость и: возвращает созданный объект для внедрения, бросает исключение или возвращает значение null для того чтобы оставить поведение по умолчанию. Когда fallback стратегия будет привязана генератор изменит ошибку на предупреждение, полагая что ситуация под вашим контролем, и код станет компилируемым.

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

Подробнее..
Категории: C , Net , Csharp , Di , Inversion

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru