Удар TypeScript на чистую архитектуру

1656572189 udar typescript na chistuyu arhitekturu

Содержание статьи

автор Уоррен Белл

Более одного способа очистить лук от кожуры.

1*zizBT5zxwmm5mibeF55bow
Кредит изображения

Чистая архитектура

Есть много видео и статей, объясняющих чистую архитектуру. Большинство из них рассматривают концепции с учетом 20 000 футов. Не знаю, как вы, но я не очень хорошо учусь на такой высоте. Там не так много кислорода. Я учусь, прыгая с головой и кодируя. Эта статья и сопроводительный код – это то, что я получил после такого скачка.

Боб твой дядя

Термин «Чистая архитектура» стал популярен благодаря Роберту Мартину (дядя Боб) и его книге «Чистая архитектура: Пособие для мастеров по структуре и дизайну программного обеспечения». Сейчас я не провозглашаю себя специалистом в этой области и не читал его книги, хотя намерен. Но я могу полностью отнестись к проблемам, которые он пытается решить.

Как написать программную систему, не зависящую ни от чего, кроме основного языка? Нам это обещали в прошлом с интерфейсами и другими принципами ОО, но я никогда раньше не видел «чистого», каламбурного объяснения, как это сделать по всей системе. И да, я немного опоздал на эту вечеринку, поскольку дядя Боб начал говорить об этих концепциях в 2012 году, то есть столетие назад в годы программного обеспечения.

Диаграмма, которая меня смутила

Вот оригинальная диаграмма Дядя Боб и другие, использовавшие в своих презентациях, объясняя чистую архитектуру. Эта простая диаграмма стала моей одержимостью. Я давно очистил свою память от всего, что связано с UML, и боролся со связями has-a и use-a, указываемыми наконечниками стрелок «открыть» и «закрыть». Единственный способ, которым я собирался это выяснить, – написать какой-то код.

1*Amv74nfUdirQYlRmSyEMDA
Автор изображения: дядя Боб

Знай свой лук

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

1*nEATDe5dRLIWN3MSxSjG0A
Кредит изображения

В один из этих дней я собираюсь организоваться

В нашем примере есть 4 модуля, которые соответствуют каждому слою этого лука. Впоследствии это могут быть отдельные модули npm. Для наглядности я попытался назвать вещи в соответствии с оригинальной диаграммой Чистой архитектуры дяди Боба в верхней части этой статьи. В реальном мире вы, вероятно, предсказали все имена с любым вариантом использования, который они представляли.

1*xfnLsbxyTy4s-AEQY7vi2A
Автор изображения: Я

Инфраструктурный (синий) уровень – это место, где живут все наши внешние подключаемые системы. Эти внешние системы, такие как устройства, веб и интерфейсы, показанные на диаграмме лука, будут использовать наши интерфейсы IRequest и IViewModel для связи с нашим контроллером и презентатором, тогда как db и внешние интерфейсы, показанные на диаграмме лука, будут использовать интерфейс IEntityGateway для общение с нашим Interactor.

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

Больше наглядных пособий

Вот структура каталога. Всё объединяется в файле index.ts каждого модуля. Точка входа показывается в тесте, расположенном в инфраструктуре/test/TestEntryPoint.spec.ts.

1*LRbWuQzcmLh9LOJL5P2HYA
Авторство изображения: Команда Shift 4

Каким путём он пошел?

Одним из моих оригинальных зависаний было то, как внешний слой взаимодействует с внутренними. Я думал, что все, что вам нужно сделать это вызвать какую-то функцию createWidget(), например, на контроллере, и вы получите новый хороший блестящий виджет. Неверно.

Что вы хотите сделать, так это отправить виджет, который будет создан, в вариант использования (Interactor) по определенному пути, а вариант использования (Interactor) отправить новый виджет обратно к вам другим путем. Это похоже на функцию обратного вызова или обещание. Я нашел хорошую диаграмму, иллюстрирующую это в статье под названием «Внедрение чистой архитектуры – контролеров и презентаторов» (ссылка ниже). В нашем примере я не реализовал RequestModel или ResponseModel.

1*KMgKFitjUr7K0MaNxuLZGA
Кредит изображения

Войдите, переступите и выйдите, путь OO

Итак, давайте сначала создадим виджет с классами и интерфейсами.

OO Шаг 1

Это точка входа. Этот код будет расположен на уровне инфраструктуры (синий). В этом слое находится ваше мобильное приложение, веб-приложение, API, CLI и т.д. Также здесь живут все ваши внешние системы, такие как внешние API, фреймворки, библиотеки и базы данных. На этом уровне все подключается и связывается с нашей системой через предоставленные нами интерфейсы.

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

Затем вы создаете контроллер, реализующий интерфейс IRequest, передавая конструктору EntityGateway и ViewModel, которые вы создали выше. Наконец, ваш интерфейс вызывает createWidget(widget) на контроллере, где ваш новый виджет начинает свое путешествие в Interactor.

Что такое EntityGateway?

EntityGateway реализует интерфейс IEntityGateway и является местом, где вы реализуете определенный код, хранящий ваш виджет. Он живет в инфраструктурном (голубом) слое. Это может быть любой тип существующего или будущего внешнего API или системы хранения, например базы данных.

Чтобы перейти на другую систему, нужно просто подключить новую реализацию EntityGateway с интерфейсом IEntityGateway. В этом примере я использую Promise, чтобы имитировать какую-либо операцию сохранения.

Подключить что?

Файл инфраструктуры/src/index.ts в модуле инфраструктуры является местом, где можно подключить различные реализации вашего интерфейса IEntityGateway. Путь от оператора импорта указывает на правильную реализацию. В этом случае это система сохранения под названием AnyDB.

Дядя Боб также говорит об использовании класса Main, где вы можете выполнить этот тип подключения или выполнить другой код инициализации. Основной класс будет также жить в модуле инфраструктуры и будет подключаться. Он также будет общаться так же, как другие системы в модуле инфраструктуры. Например, вы должны инициировать этот класс в коде инициализации вашего пользовательского интерфейса и передавать его в ваши внутренние слои для использования через какой-то интерфейс конфигурации.

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

OO Шаг 2

Контроллер – очень занятое место. Во-первых, EntityGateway без изменений переходит к нашему конструктору Interactor. Затем наша ViewModel передается конструктору нашего Presenter, который, в свою очередь, также передается нашему конструктору Interactor. Все это происходит в функции createWidget(widget) контроллера, которая была вызвана нашим пользовательским интерфейсом на шаге 1 через интерфейс IRequest. Мы поговорим о нашем Presenter на шаге 4, когда только что созданный виджет вернется к пользовательскому интерфейсу.

OO Шаг 3

Наконец-то мы находимся на самом внутреннем слое нашего путешествия, слое вариантов использования, где живет наш Interactor. Или более известен как наш дом для всей логики использования нашего приложения. Есть еще один внутренний слой, домен. Здесь живут все наши предприятия и конкретная бизнес-логика. В этом примере нам действительно не нужно идти туда, кроме как заимствовать интерфейсы WidgetType и IEntityGateway.

Movin On Up

Здесь, в нашем Interactor мы берем EntityGateway, который был передан с контроллера, и вызываем его функцию saveWidget(widget) через интерфейс IEntityGateway. Эта функция возвращает Promise от EntityGateway, решаемого в .then() с только что созданным виджетом. Затем мы вызываем функцию presentWidget(widget) презентатора через интерфейс IOutputBoundary, который запускает вновь созданный виджет в пользовательский интерфейс. Все это происходит в функции createWidget(widget) интерактора, вызванной нашим контроллером через интерфейс IInputBoundary на шаге 2.

OO Шаг 4

Здесь в нашем Presenter мы просто передаем виджет в функцию presentWidget(widget) нашей ViewModel, которую мы создали в нашем пользовательском интерфейсе. Все это происходит в функции presentWidget(widget) Presenter через интерфейс IOutputBoundary, вызванный в функции createWidget(widget) Interactor на шаге 3. Здесь может произойти больше, но не в нашем примере.

OO Шаг 5

Наконец-то наш только что созданный виджет вернулся домой, готовый к отображению в нашем интерфейсе. Это именно то место (код), с которого мы начали на шаге 1. Обновление пользовательского интерфейса происходит в функции presentWidget(widget) ViewModel через интерфейс IViewModel, который был вызван в функции presentWidget(widget) Presenter на шаге 4.

OO Вспомогательный актерский состав

Все остальные интерфейсы и определения типов, собранные в один файл.

2 человека входят, 1 человек выходит

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

И вот разница между классом Controller и функцией Controller.

Войдите, перейдите и выйдите, функциональный путь

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

Общие отличия

WidgetType идентичен версии OO выше, а IEntityGateway, IRequest, IViewModel, IInputBoundary и IOutputBoundary теперь являются определениями типов вместо интерфейсов.

Функция Шаг 1

Все то же, что и на этапе 1 выше, за исключением того, что мы импортируем функцию с названием «controllerConstructor» вместо класса с названием «Controller». И импортировать функцию под названием “entityGateway” вместо класса с названием EntityGateway. И последнее, но не менее важное, созданное нами ViewModel теперь является объектом с функцией presentWidget() вместо класса с функцией presentWidget().

Снова EntityGateway?

EntityGateway выполняет ту же задачу, что и версия OO выше. Теперь это функция вместо класса. Он возвращает функцию saveWidget(), завернутую в объект.

Больше проводки

То же, что и версия OO выше, за исключением того, что мы сейчас экспортируем функцию вместо класса.

Функция Шаг 2

Наш контроллер все еще занят и выполняет те же задачи, что и версия OO. Теперь мы импортируем функцию с названием interactorConstructor вместо класса с названием Interactor. Мы экспортируем функцию под названием «controllerConstructor» вместо класса под названием «Controller». Он возвращает функцию под названием «createWidget, завернутая в объект.

Функция Шаг 3

Вернувшись в Iteractor в нашем модуле вариантов использования, мы выполняем те же задачи, что и версия OO выше. Сейчас мы экспортируем функцию с названием interactorConstructor вместо класса с названием Interactor. Он возвращает функцию под названием «createWidget, завернутая в объект.

Функция Шаг 4

Теперь мы передаем только что созданный виджет обратно в наш Presenter, где мы выполняем те же задачи, что и версия OO выше. Мы экспортируем функцию под названием «presenterConstructor» вместо класса «Presenter». Он возвращает функцию под названием «presentWidget, завернутой в объект.

Функция Шаг 5

Снова мы прошли полный круг, и мы вернулись в то же место (код), где мы начали на шаге 1. Наш пользовательский интерфейс обновляется с помощью нашего только что созданного виджета в функции presentWidget() ViewModel.

Функция поддержки актеров

Вот все остальные определения типов, собранные в один файл. Это наши интерфейсы.

Все это для проклятого виджета?

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

Нам не нужны Stinkin Profilers

Мое первоначальное мнение заключалось в том, что версия класса и интерфейса будет медленнее, чем версия функции. Поэтому я запускал оба проекта с помощью своих инструментов предварительного профилирования, вводя npm test и нажимая enter, пока мой палец не сжался.

Мое первое наблюдение заключалось в том, что версия функции была примерно вдвое быстрее, WOW. Затем я решил переработать версию функции, чтобы вернуть все важные функции, завернутые в объекты, чтобы я мог использовать имена функций. Затем я запустил обе версии через свои предыдущие профайлеры, и они имели примерно одинаковую скорость.

Я понятия не имею, почему обертывание функции в объект так замедляет ее. Возможно, я действительно не полностью удалил Adobe Flash со своего ноутбука, и он решил вмешаться. В любом случае, было бы интересно получить более точные измерения скорости, используя правильные инструменты против скомпилированного JavaScript.

The Take Away

Версия OO содержит больше кода, но может быть проще читать и следовать. Версия функции имеет меньше кода, но может быть труднее читать и следовать.

Лично мне нравится версия функции, поскольку я многое программировал на Java и устал писать такое количество классов. Одной из вещей, которые мне больше всего нравится в TypeScript/JavaScript, есть возможность использовать объектные литералы. А с определениями типов TypeScript теперь можно применить определенную безопасность к использованию объектных литералов.

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

Попробовать

Вот некоторые вещи, которые я намерен применить в своем следующем проекте.

  1. Зависимости всегда должны уходить в одну сторону.
  2. Зависимости всегда должны указывать от ваших внешних систем (UI, db и т.д.) к вашим бизнес-субъектам и бизнес-логике.
  3. Ваши внутренние слои (доставка, вариант использования и бизнес объекты) должны предоставлять интерфейсы для использования более внешних слоев.
  4. Вы всегда должны начинать развиваться с самого внутреннего слоя. Начните с предприятий и логики, а затем проверьте. Создайте интерфейсы, которые будут использоваться, а затем проверьте эти интерфейсы. Я виноват, что работал наоборот. Я думаю, что всем нам нравится начинать с пользовательского интерфейса, потому что он сразу дает возможность визуально увидеть, как наша система будет выглядеть для пользователя. Кроме того, в пользовательском интерфейсе живет много «крутых» технологий.
  5. Используйте TDD (Test Driven Development). Чистая архитектура позволяет сделать это гораздо проще. Все более разделено и легче издеваться. Реализация IEntityGateway, приведенная выше, в основном является макетом базы данных.
  6. Не в последнюю очередь будьте гибкими. Не сбивайте себя с строя, стараясь соблюдать чистую архитектуру, когда библиотека или фреймворк, которые вы хотите использовать, просто не будут работать с ними. Но учтите, что это, вероятно, хороший признак того, что в конечном итоге у вас возникнут какие-то проблемы с этой библиотекой или фреймворком, особенно если она хочет, чтобы вы расширили их классы. Разъединение должно быть вашей целью.

Но, Да, как насчет…

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

Ресурсы:

Код для версии OO находится по адресу:

https://github.com/warrenbell/cleanarch-tsoo

Код версии функции находится по адресу:

https://github.com/warrenbell/cleanarch-tsfun

ts-узел

Удобный маленький инструмент TypeScript.

https://github.com/TypeStrong/ts-node

Чистая архитектура дяди Боба

https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html

Книга

Одно из многих видео

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

Внедрение чистой архитектуры – контролеров и презентаторов

https://plainionist.github.io/Implementing-Clean-Architecture-Controller-Presenter/

Чистая архитектура: Стоя на плечах гигантов

Clean Architecture: Standing on the shoulders of giants

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

https://softwareengineering.stackexchange.com/questions/357052/clean-architecture-use-case-containing-the-presenter-or-returning-data

Чистая архитектура. Какие работы ведущего?

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *