От переводчика: данная статья написана 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()? Потому что это противоречит правилу — не передавать структуры во внешние слои. Сущности слоя могут содержать в себе не только данные, но и поведение. Таким образом поведение сущностей сценария может быть применено только на этом слое. Не передавая сущности во внешние слои мы гарантируем сохранение поведения в пределах слоя. Внешним слоям нужны только чистые данные и наша задача предоставить их именно в этом виде.

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

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

Комментарии (17)


  1. gandjustas
    06.11.2015 23:33
    +6

    Копипаста, строки-литералы в коде, отсутствие проверок на получение по объектов Id (привет NRE), странное разделение на AdminOrderInteractor и OrderInteractor, хотя семантических отличий нет и разница только в одной проверке, странное копирование массива и это все называется «чистой архитектурой»?


    1. trong
      07.11.2015 08:25
      -6

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


    1. taliban
      09.11.2015 20:42
      +1

      Чистая архитектура и чистый код, это не синонимы. Вы с автором обсуждаете совершенно разные вещи. Архитектура не подразумевает наличие или отсутствие литералов в коде, она описывает взаимодействие кода.


      1. gandjustas
        10.11.2015 15:54
        -1

        А зачем нужна «чистая архитектура»? Я всегда думал что все архитектурные изыскания должны приводить к упрощению поддержки кода, разработки новых модулей и других улучшений, непосредственно влияющих на код. То есть хорошая архитектура — способ поиметь хороший код. Но здесь демонстрируется плохой код и это называется «чистой архитектурой»? В чем её чистота? Может есть тайный смысл, который никто не понял?


        1. taliban
          10.11.2015 20:51

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


          1. gandjustas
            10.11.2015 22:09
            -1

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


            1. taliban
              11.11.2015 19:21

              https://ru.wikipedia.org/wiki/Архитектура_программного_обеспечения Почитайте, пожалуйста, хотя-бы википедию. Я не могу общаться когда тупо гнут свою линию лишь бы не пасть лицом в говно. Цель архитекруты — правильное взаимодействие компонентов приложения, не зависимо от того какой код эти компоненты содержат. Имея хорошую архитектуру можно говнокодить безболезненно. Если у вас в команде есть опытны архитектор, он может спроектировать так приложение, что ваш говнокод в итоге можно будет безболезненно заменить на более красивый код и приложение будет все так же работать.
              Возможно я ответил на вопрос, и все еще считаю что говорить об архитектуре без кода не глупо.


              1. gandjustas
                12.11.2015 03:04
                -2

                Что значит «правильное взаимодействие компонентов»? Зачем оно вам нужно?


                1. taliban
                  12.11.2015 15:23
                  +1

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


                  1. gandjustas
                    12.11.2015 15:26
                    -1

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


                    1. taliban
                      12.11.2015 15:40

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


                      1. gandjustas
                        12.11.2015 15:48
                        -1

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

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

                        В чем «чистота» такой архитектуры? Вы считаете такой подход «связывания компонентов» правильным? Может всетаки есть тайный смысл так делать? Потому что по всем признакам в статье описана плохая архитектура, она увеличивает число изменений.


                        1. taliban
                          12.11.2015 16:07

                          Это был отличный пример того что два метода, которые мало меня интересуют можно проигнорировать, а затем, если они работают по разному, отрефакторить, хотя я явно указал на интерфейс. А в статье, как и в моем примере, OrderInteractor и AdminOrderInteractor в дальнейшем могут измениться (приставка Admin, как бы намекает), как ни странно, и если при замене содержимого, весь остальной код продолжит работать как и раньше, значит статья написана грамотно относительно архитектуры.


                          1. gandjustas
                            12.11.2015 16:23

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

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

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


                            1. taliban
                              12.11.2015 16:40

                              О, вы начали говорить не про код а про архитектуру :) Теперь можно сказать главное — человек игнорирует что что считает менее приоритетным, из-за этого и копипаст появился итд. Вы ведь в курсе что всегда можно найти недочет? Любой код можно покритиковать. Человек говорит о чем-то, а то что считает не важным делает для виду. Вы считаете что это важно и критикуете места которые автор проигнорировал. Все просто.


  1. sergeylanz
    07.11.2015 18:24

    Дохожу сам то такой архитектуры только тяжелым путем :(
    Надо больше читать…


  1. Zhandos
    12.11.2015 16:07

    Надеюсь будет продолжение несмотря на минусы…