Чистая Архитектура В Приложении Go. Часть 1

От переводчика: эта статья автор Мануэль Кисслинг в сентябре 2012 года по мере реализации Статьи дяди Боба о чистой архитектуре с учетом Go-специфики.



Чистая архитектура в приложении Go. Часть 1

Перед этой статьей я перевел ее прототип — смотри здесь .

Поскольку в этой статье будет активно использоваться то, что описано в статье дяди Боба, то лучше начать с нее.

если, конечно, вы ее еще не читали.

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

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

В этой части будет описана общая концепция и работа с внутренним слоем.

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

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



Слабосвязанные системы

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

Я постараюсь продемонстрировать проявление этих качеств при применении Правила зависимости.

Для этого мы реализуем простое, но в то же время полноценное Go-приложение, попутно обсуждая, когда и как следует применять концепции «Чистой архитектуры».

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

Исходный код, включая некоторое тестовое покрытие, доступен.

найти на github .

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

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

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



Архитектура приложения

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

Архитектура нашего приложения будет разделена на 4 уровня: Домен , Сценарии , Интерфейсы И Инфраструктура .

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

Затем мы рассмотрим низкоуровневую реализацию каждого уровня, переходя от внутренних к внешним слоям.

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

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



Что, куда и зачем ставить

Я решил поговорить о клиентах, а не о пользователях.

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

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

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

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

Что, если представить, что бизнес-домен — это не часть программы, а часть, скажем, настольной игры? Представьте себе, если бы мы реализовали eBay или Amazon как настольную игру.

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

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

Причина в том, что в 99% ситуаций предположение о том, что пользователи и клиенты — это одно и то же, абсолютно ничего не значит, пока в игру не вступит оставшийся 1%.

Наше приложение проиллюстрирует эту ситуацию.

Таким образом, если заказы и продукты относятся к слою «Домен», то пользователи принадлежат следующему слою — «Скрипты».

Что еще принадлежит слою «Сценарии»? Сценарии — это уровень, на котором реализуются варианты использования, возникающие из-за того, что пользователям приложения необходимо что-то «сделать» с субъектами базового домена.

Примером варианта использования может быть «клиент добавляет товар в заказ».

Для реализации этого и других вариантов использования необходимо реализовать методы, «приводящие в движение» бизнес-субъекты.

Хотя это можно сделать на уровне домена, я рекомендую делать это в скриптах.

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

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

Слои домена и сценариев вместе составляют ядро нашего приложения, отражая реалии бизнеса, с которым мы работаем.

Все остальное — детали реализации, не связанные со спецификой нашего бизнеса.

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

Мы можем переключить веб-сервис с HTTP на SPDY или базу данных с MySQL на Oracle — это не изменит того факта, что у нас есть магазин с клиентами, которые делают заказы, состоящие из товаров (Домен), и клиентами, которые могут создавать заказ, изменять количество товара и варианты оплаты (Сценарии) В то же время это лакмусовая бумажка для наших внутренних слоев — стоит ли нам менять хотя бы одну строчку кода при переходе с MySQL на Oracle? Если ответ «да», то мы нарушили правило зависимости, сделав некоторые из наших внутренних слоев зависимыми от деталей внешних слоев.

Также есть место для кода, который работает с базой данных или обрабатывает HTTP-запросы или внешние сервисы.

Это место является интерфейсным слоем.

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

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

Без них наши методы Script и сущности предметной области были бы просто мертвым грузом.

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

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

Вот тут-то применение Правила Зависимости становится особенно интересным: если код SQL живет в слое Интерфейсов, и ничто из внутреннего слоя не может вызвать внешний слой, а инициализация сохранения происходит на уровне Script, то как можно мы избегаем нарушения правил зависимостей? Мы рассмотрим это подробнее ближе к реализации кода.

Последний уровень называется Инфраструктура.

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

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

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

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

В этом смысле большая часть стандартной библиотеки Go концептуально будет находиться на уровне инфраструктуры.

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

  • Сущностный клиент
  • Эссенциальный продукт
  • Заказ сущности
Сценарии:
  • Пользователь сущности
  • Сценарий: добавление товара в заказ.

  • Сценарий: получение списка позиций заказа.

  • Сценарий: Администратор добавляет товар в заказ.

Интерфейсы:
  • Веб-сервис обработки товаров/заказов
  • Репозиторий скриптов и объектов домена
Инфраструктура:
  • База данных
  • Код, который обрабатывает соединения с базой данных
  • HTTP-сервер
  • Стандартные библиотеки Go
Как видите, этот список включает в себя некоторые элементы, о которых мы еще не говорили.

Сценарии администратора и репозитории будут подробно рассмотрены при обсуждении реализации.

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

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

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

Чем дальше мы двигаемся влево, тем более низкоуровневым становится код («передать поток байтов на порт 80»), чем больше мы двигаемся вправо, тем более высокоуровневым становится код («добавить продукт в порт 80»).

заказ").



Выполнение



Домен

Сначала мы реализуем слой домена.

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

Таким образом, код реализации Домена будет довольно коротким и мы реализуем его в одном файле:

   

// $GOPATH/src/domain/domain.go package domain import ( "errors" ) type CustomerRepository interface { Store(customer Customer) FindById(id int) Customer } type ItemRepository interface { Store(item Item) FindById(id int) Item } type OrderRepository interface { Store(order Order) FindById(id int) Order } type Customer struct { Id int Name string } type Item struct { Id int Name string Value float64 Available bool } type Order struct { Id int Customer Customer Items []Item } func (order *Order) Add(item Item) error { if !item.Available { return errors.New("Cannot add unavailable items to order") } if order.value()+item.Value > 250.00 { return errors.New(`An order may not exceed a total value of $250.00`) } order.Items = append(order.Items, item) return nil } func (order *Order) value() float64 { sum := 0.0 for i := range order.Items { sum = sum + order.Items[i].

Value } return sum }

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

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

Вместо этого мы определяем интерфейсы Go для трех так называемых репозиториев.

Репозиторий — это концепция DDD (Domain Driven Design): это способ абстрагироваться от того факта, что объекты должны храниться или извлекаться с помощью какого-то механизма постоянного хранения.

С точки зрения домена репозиторий — это просто контейнер, через который извлекаются (FindById) или сохраняются (Store) объекты домена.

CustomerRepository, ItemRepository и OrderRepository — это просто интерфейсы.

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

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

Его реализация определяется во внешнем слое.

Реализация интерфейса осуществляется на уровне, который впоследствии должен его использовать.

Мы увидим это позже, когда доберемся до слоя «Сценарии».

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

Однако фактический код выполняется на уровне интерфейсов.

Для каждой части каждого слоя интересуют три вопроса: где он используется, где его интерфейс, где его реализация? Если мы посмотрим на OrderRepository, то ответы будут следующими: он используется на уровне «Скрипты», его интерфейс принадлежит слою «Домен», а его реализация — слою «Интерфейсы».

С другой стороны, метод Add сущности Order также используется на уровне сценариев и по сути является интерфейсом к уровню домена.

Но его реализация осуществляется на уровне домена, поскольку вне его методу фактически ничего не нужно.

Мы также определяем следующие 3 структуры: Клиент, Заказ и Товар.

Это представление трех наших доменных сущностей.

Сущность Order также реализуется двумя методами Add и value, а value — это вспомогательная функция только для внутреннего использования.

Метод Add реализует специфичную для бизнеса функцию, необходимую для использования на уровне сценариев.

Также в этом коде есть дополнительные детали, существенные, если речь идет об архитектуре в целом.

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

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

Это правило не зависит от приложения, это бизнес-требование.

То же самое касается правила, согласно которому общая стоимость заказов не может превышать 250 долларов США — неважно, является ли наш магазин веб-сайтом или настольной игрой, это бизнес-правило, которое применяется всегда.

Другие правила определены в другом месте.

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

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

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

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

Я предпочитаю всегда иметь все бизнес-правила в одном месте.

Продолжение в следующая часть Теги: #дядя Боб #Дядя Боб #мануэль кисслинг #чистая архитектура #архитектура #Go #дизайн и рефакторинг #Go

Вместе с данным постом часто просматривают: