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

    От переводчика: данная статья написана Manuel Kiessling в сентябре 2012 года, как реализация статьи Дядюшки Боба о чистой архитектуре с учетом Go-специфики.



    Это вторая статья цикла об особенности реализации Чистой Архитектуры в Go. [Часть 1]



    Сценарии


    Сразу начнем с кода слоя Сценария:

    // $GOPATH/src/usecases/usecases.go
    
    package usecases
    
    import (
        "domain"
        "fmt"
    )
    
    type UserRepository interface {
        Store(user User)
        FindById(id int) User
    }
    
    type User struct {
        Id       int
        IsAdmin  bool
        Customer domain.Customer
    }
    
    type Item struct {
        Id    int
        Name  string
        Value float64
    }
    
    type Logger interface {
        Log(message string) error
    }
    
    type OrderInteractor struct {
        UserRepository  UserRepository
        OrderRepository domain.OrderRepository
        ItemRepository  domain.ItemRepository
        Logger          Logger
    }
    
    func (interactor *OrderInteractor) Items(userId, orderId int) ([]Item, error) {
        var items []Item
        user := interactor.UserRepository.FindById(userId)
        order := interactor.OrderRepository.FindById(orderId)
        if user.Customer.Id != order.Customer.Id {
            message := "User #%i (customer #%i) "
            message += "is not allowed to see items "
            message += "in order #%i (of customer #%i)"
            err := fmt.Errorf(message,
                user.Id,
                user.Customer.Id,
                order.Id,
                order.Customer.Id)
            interactor.Logger.Log(err.Error())
            items = make([]Item, 0)
            return items, err
        }
        items = make([]Item, len(order.Items))
        for i, item := range order.Items {
            items[i] = Item{item.Id, item.Name, item.Value}
        }
        return items, nil
    }
    
    func (interactor *OrderInteractor) Add(userId, orderId, itemId int) error {
        var message string
        user := interactor.UserRepository.FindById(userId)
        order := interactor.OrderRepository.FindById(orderId)
        if user.Customer.Id != order.Customer.Id {
            message = "User #%i (customer #%i) "
            message += "is not allowed to add items "
            message += "to order #%i (of customer #%i)"
            err := fmt.Errorf(message,
                user.Id,
                user.Customer.Id,
                order.Id,
                order.Customer.Id)
            interactor.Logger.Log(err.Error())
            return err
        }
        item := interactor.ItemRepository.FindById(itemId)
        if domainErr := order.Add(item); domainErr != nil {
            message = "Could not add item #%i "
            message += "to order #%i (of customer #%i) "
            message += "as user #%i because a business "
            message += "rule was violated: '%s'"
            err := fmt.Errorf(message,
                item.Id,
                order.Id,
                order.Customer.Id,
                user.Id,
                domainErr.Error())
            interactor.Logger.Log(err.Error())
            return err
        }
        interactor.OrderRepository.Store(order)
        interactor.Logger.Log(fmt.Sprintf(
            "User added item '%s' (#%i) to order #%i",
            item.Name, item.Id, order.Id))
        return nil
    }
    
    type AdminOrderInteractor struct {
        OrderInteractor
    }
    
    func (interactor *AdminOrderInteractor) Add(userId, orderId, itemId int) error {
        var message string
        user := interactor.UserRepository.FindById(userId)
        order := interactor.OrderRepository.FindById(orderId)
        if !user.IsAdmin {
            message = "User #%i (customer #%i) "
            message += "is not allowed to add items "
            message += "to order #%i (of customer #%i), "
            message += "because he is not an administrator"
            err := fmt.Errorf(message,
                user.Id,
                user.Customer.Id,
                order.Id,
                order.Customer.Id)
            interactor.Logger.Log(err.Error())
            return err
        }
        item := interactor.ItemRepository.FindById(itemId)
        if domainErr := order.Add(item); domainErr != nil {
            message = "Could not add item #%i "
            message += "to order #%i (of customer #%i) "
            message += "as user #%i because a business "
            message += "rule was violated: '%s'"
            err := fmt.Errorf(message,
                item.Id,
                order.Id,
                order.Customer.Id,
                user.Id,
                domainErr.Error())
            interactor.Logger.Log(err.Error())
            return err
        }
        interactor.OrderRepository.Store(order)
        interactor.Logger.Log(fmt.Sprintf(
            "Admin added item '%s' (#%i) to order #%i",
            item.Name, item.Id, order.Id))
        return nil
    }
    


    Код слоя Сценариев состоит главным образом из сущности User (пользователь) и двух сценариев. Сущность имеет репозиторий точно так же как это было в слое Домена, поскольку Пользователям требуется механизм персистентного сохранения и получения данных.

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

    Код выше является ярким примером пищи для размышления на тему «что куда поставить». Прежде всего все взаимодействия внешних слоев должны осуществляться через методы OrderInteractor и AdminOrderInteractor, структуры которые оперируют в пределах слоя Сценариев и глубже. Опять же — это все следование Правилу Зависимостей. Такой вариант работы позволяет не иметь внешних зависимостей, что, в свою очередь, позволяет нам, к примеру, протестировать этот код используя моки репозиториев или, при необходимости, можно заменить внутреннюю реализацию Logger (см в код) на другую без каких либо сложностей, поскольку эти изменения не затронут остальные слои.

    Дядюшка Боб говорит про Сценарии: «В этом слое реализуется специфика бизнес-правил. Он инкапсулирует и реализует все случаи использования системы. Эти сценарии реализуют поток данных в и из слоя Cущностей для реализации бизнес-правил.»

    Если вы посмотрите, скажем, на метод Add в OrderInteractor, вы увидите это в действии. Метод управляет получением необходимых объектов и сохранением их в пригодном для дальнейшего использования виде. В этом методе делается обработка ошибок, которые могут быть специфичны для этого Сценария, с учетом определенных ограничений именно этого слоя. Например, лимит на покупку в 250 долларов накладывается на уровне Домена, поскольку это бизнес-правило и оно приоритетнее правил Сценариев. С другой стороны, проверки, касаемые добавления товаров к заказу — это специфика Сценариев, к тому же именно этот слой содержит сущность User, что влияет в свою очередь на обработку товара в зависимости от того обычный пользователь это делает или администратор.

    Давайте так же обсудим логгирование на этом слое. В приложении все виды логгирования затрагивают несколько слоев. Даже с учетом понимания, что все лог-записи будут в конечном итоге строками в файле на диске важно отделить концептуальные детали от технических. Уровень сценариев не знает ничего о текстовых файлах и жестких дисках. Концептуально, этот уровень просто говорит: «На уровне Сценария произошло что-то интересное и я бы хотел это сообщить», где «сообщить» не означает «записать куда-либо», это означает просто «сообщить» — без какого-либо знания, что дальше с этим все произойдет.

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

    Еще более интересной ситуация видится в свете того, что мы создали два разных OrderInteractor. Если бы мы хотели логгировать действия администратора в один файл, а действия обычного пользователя в другой файл, то это так же было очень просто. В этом случае мы бы просто создали две реализации Logger и обе версии бы удовлетворяли интерфейсу usecases.Logger и использовали бы их в соответствующих OrderInteractor — OrderInteractor и AdminOrderInteractor.

    Другая важная деталь в коде Сценария — структура Item. На уровне домена у нас уже есть аналогичная структура, не так ли? Почему бы просто не вернуть ее в методе Items()? Потому что это противоречит правилу — не передавать структуры во внешние слои. Сущности слоя могут содержать в себе не только данные, но и поведение. Таким образом поведение сущностей сценария может быть применено только на этом слое. Не передавая сущности во внешние слои мы гарантируем сохранение поведения в пределах слоя. Внешним слоям нужны только чистые данные и наша задача предоставить их именно в этом виде.

    Как и в слое Домена этот код показывает как Чистая архитектура помогает понять как приложение на самом деле работает: если для понимания того какие бизнес-правила у нас есть нам достаточно посмотреть в слой домена, то для того, чтобы понять как пользователь взаимодействует с бизнесом нам достаточно посмотреть в код слоя Сценариев. Мы видим, что приложение позволяет пользователю самостоятельно добавить товары в заказ и что администратор может добавить товары в заказ пользователя.

    Продолжение следует… В третьей части обсудим слой Интерфейсов.
    Поделиться публикацией

    Похожие публикации

    Комментарии 22
      +6
      Копипаста, строки-литералы в коде, отсутствие проверок на получение по объектов Id (привет NRE), странное разделение на AdminOrderInteractor и OrderInteractor, хотя семантических отличий нет и разница только в одной проверке, странное копирование массива и это все называется «чистой архитектурой»?
        –6
        Нет, это называется объяснение на максимально упрощенном примере, чтобы объяснять не код из 1000 строк а архитектурный подход.
          +1
          Чистая архитектура и чистый код, это не синонимы. Вы с автором обсуждаете совершенно разные вещи. Архитектура не подразумевает наличие или отсутствие литералов в коде, она описывает взаимодействие кода.
            –1
            А зачем нужна «чистая архитектура»? Я всегда думал что все архитектурные изыскания должны приводить к упрощению поддержки кода, разработки новых модулей и других улучшений, непосредственно влияющих на код. То есть хорошая архитектура — способ поиметь хороший код. Но здесь демонстрируется плохой код и это называется «чистой архитектурой»? В чем её чистота? Может есть тайный смысл, который никто не понял?
              0
              Да, это тайный смысл — писать об архитектуре, игнорируя стиль кода. Когда архитектор создает архитектуру проекта, он может не написать ни единой строчки кода. Возможно это прояснит ход моих мыслей. Диаграммы вместо кода вам были бы более понятны? Некоторым нет, а вот код все понимают.
                –1
                Вы не ответили на вопрос. Какова цель архитектуры? Архитектура сама по себе не нужна. Как ответите на вопрос, то поймете что глупо говорить об архитектуре без кода. Кстати то что вы называете архитектурой является дизайном.
                  0
                  https://ru.wikipedia.org/wiki/Архитектура_программного_обеспечения Почитайте, пожалуйста, хотя-бы википедию. Я не могу общаться когда тупо гнут свою линию лишь бы не пасть лицом в говно. Цель архитекруты — правильное взаимодействие компонентов приложения, не зависимо от того какой код эти компоненты содержат. Имея хорошую архитектуру можно говнокодить безболезненно. Если у вас в команде есть опытны архитектор, он может спроектировать так приложение, что ваш говнокод в итоге можно будет безболезненно заменить на более красивый код и приложение будет все так же работать.
                  Возможно я ответил на вопрос, и все еще считаю что говорить об архитектуре без кода не глупо.
                    –2
                    Что значит «правильное взаимодействие компонентов»? Зачем оно вам нужно?
                      +1
                      Значит, например, что класс А будет взаимодействовать с классом В через посредника с интерфейсом С, и в итоге когда я хочу изменить логику взаимодействия между А и В, я просто заменяю класс-посредник, учитывая наличие интерфейса, код будет работать без изменений, а логика взаимодействия изменится. Это был базовый пример «что такое архитектура». Дальше Вы можете найти все в интернете. Спасибо за внимание.
                        –2
                        Уже лучше. Вы попытались такой «архитектурой» уменьшить количество необходимых изменений. То есть одна из целей архитектуры — уменьшать количество изменений, так?
                        Тогда почему в примерах «чистой архитектуры» тонна копипасты, которая всегда количество изменений увеличивает?
                          0
                          Есть класс «Кот», есть класс «Утка». Это не связанные классы, но у обоих есть метод «Дышать» в интерфейсах. Как думаете, если это вообще не основной метод из моего материала, буду ли я на нем акцентировать внимание? Нет, я скопипащу содержимое из одного класса в другой, и затем, когда нужно будет, я отрефакторю оба класса так, чтоб они работали без копипаста, и при этом, так как интерфейс обоих содержит метод «Дышать», который будет работать как и раньше, без изменения архитектуры приложения. Заметьте, я изменю в дальнейшем код, но без изменения архитектуры. Тоесть моя базовая архитектура взаимосвязи компонентов позволит избавиться от говнокода. Вот именно об этом статья, как связывать компоненты.
                            –2
                            Это плохой пример. То что есть два одинаково названых метода не означает, что они делают тоже самое. Поэтому имеет смысл копировать куски кода и рефакторить потом или нет. Одного имени метода недостаточно.

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

                            В чем «чистота» такой архитектуры? Вы считаете такой подход «связывания компонентов» правильным? Может всетаки есть тайный смысл так делать? Потому что по всем признакам в статье описана плохая архитектура, она увеличивает число изменений.
                              0
                              Это был отличный пример того что два метода, которые мало меня интересуют можно проигнорировать, а затем, если они работают по разному, отрефакторить, хотя я явно указал на интерфейс. А в статье, как и в моем примере, OrderInteractor и AdminOrderInteractor в дальнейшем могут измениться (приставка Admin, как бы намекает), как ни странно, и если при замене содержимого, весь остальной код продолжит работать как и раньше, значит статья написана грамотно относительно архитектуры.
                                –1
                                Могут измениться, а могут и не измениться. Сейчас плодить копипасту, потому что в будущем может что-то измениться — это признак «чистой» архитектуры? Я как раз чистоту понимал в прямо противоположном смысле.

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

                                Я продолжаю не понимать что означает «чистая архитектура». По всем признакам, подтверждённым вашими словами, архитектура плохая получилась. Объясните пожалуйста в чем «чистота»?
                                  0
                                  О, вы начали говорить не про код а про архитектуру :) Теперь можно сказать главное — человек игнорирует что что считает менее приоритетным, из-за этого и копипаст появился итд. Вы ведь в курсе что всегда можно найти недочет? Любой код можно покритиковать. Человек говорит о чем-то, а то что считает не важным делает для виду. Вы считаете что это важно и критикуете места которые автор проигнорировал. Все просто.
                                    –2
                                    Еще раз задаю тот же вопрос — в чем «чистота» описанной архитектуры?

                                    Я вас об этом спрашиваю уже четвертый или пятый раз, но ответа до сих пор не увидел, вы все время ссылаетесь на «неважные детали», но я вас спрашиваю как раз про важные детали, а вы постоянно уходите от ответа. Уже и наводящие вопросы задавал, и напрямую спрашиваю, а все равно не вижу ответа чем же такая архитектура (на самом деле не архитектура, а дизайн) хороша.
                                      0
                                      Человек разделил архитектуру на 3 слоя: Домен, Сценарии и Интерфейсы. В первой части описал домены как просто структуры данных, в этой части описал сценарии и показал как он связывает прошлый слой с текущим. как взаимодействуют слои Домена со слоем Сценария. Это главное. Взаимодействие разных слоев абстракций между собой. Что конкретно они делают уже не так важно, важно какая структура какую содержит. Методы Items и Add можете смело очистить, это просто примеры использования.

                                      Лично в моей терминологии слой Домен — модели, а Сценарии — сервисы. А у кого-то это одно и то же. Кто-то выбирает данные из базы и гоняет их по всему проекту описывая бизнес-логику прям в контроллерах. Кто-то вообще код пишет в одном файле весь. Об этом статья. Она явно не для опытных разработчиков, просто базовые понятия логического разделения структур в коде.
                                        –2
                                        Вы снова ушли от ответа на вопрос в чем чистота архитектуры.

                                        Если только в этом:
                                        Кто-то выбирает данные из базы и гоняет их по всему проекту описывая бизнес-логику прям в контроллерах.

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

                                        Кто-то вообще код пишет в одном файле весь.
                                        Об этом в статье ничего нет.

                                        Она явно не для опытных разработчиков, просто базовые понятия логического разделения структур в коде.
                                        Неопытыне разработчики имеют тенденцию копировать куски кода и паттерны из статей, которые читают. Поэтому показывая в статье копипасту, неудачные решения или просто плохой стиль наносится больший вред, чем польза от перечислений «базовых понятий».
          0
          Дохожу сам то такой архитектуры только тяжелым путем :(
          Надо больше читать…
            0
            Надеюсь будет продолжение несмотря на минусы…
              0
              Полагаю, что скоро выйдет следующая часть :)
                +1
                Как и обещал — третья часть

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

              Самое читаемое