Как-то раз я сидел в баре с давним приятелем, с которым раньше мне довелось поработать на поза-поза-позапрошлой работе. Он из сомневающихся по поводу перехода на Go, ярый приверженец своего нынешнего языка. Хочу сказать, что он делает действительно классные вещи, пишет безупречный код, у него есть, чему поучиться. Но к Go у него отношение не слишком позитивное. Как он сказал: “Go — это *****код (плохой код)”. И в качестве одного из аргументов привел то, насколько, по его мнению, криво в Go реализована обработка ошибок. В чем-то он прав — в моем текущем не самом большом проекте на Go конструкция “if err != nil” встречается 1132 раза.


Этот мой приятель, не побоюсь этого слова — адепт DDD (domain driven design). Все, что не относится к DDD, — это, по его мнению, антипаттерн, ад и хаос. Когда я ему рассказал, что у меня есть довольно успешный опыт проектирования по DDD в Go-проектах, он округлил глаза. Да, ответил я, с определенной серией оговорок и компромиссов это работает, и неплохо.



Привет, меня зовут Толя и я ведущий разработчик платежного сервиса в Константе. Мой опыт разработки 15 лет, я разрабатывал на PHP, на чистом C, временами на C++, а последние 2 года разрабатываю на Go.


Мне кажется, что почти все гоферы когда-то мигрировали в мир Go из других миров. В основном это миры хардкорного ООП. Тех, кто начал свой путь в IT именно с языка Go — крайне мало, я таких даже и не встречал. Оно и понятно: язык появился сравнительно недавно, а хайпанул вообще как будто вчера.


Тем не менее, есть определенная категория разработчиков, которые подсознательно хотели бы окунуться в Go, но в силу своих собственных убеждений этого пока не делают. Или делают что-то на Go частично, какие-то небольшие кусочки логики, которую их уютные PHP/Python/etc по каким-то причинам тянут не так хорошо. В общем, “Go — не для всего”, говорят они.


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


С DDD у вас все элементы и кусочки логики строго на своих местах, при этом вы мыслите больше в терминах бизнеса, а не только технических особенностей. Вы получаете хорошо поддерживаемый, расширяемый и красиво написанный проект. Но из-за особенностей языка Go требуется повышенная дисциплина разработки. Такая дисциплина, в общем-то, и в других языках требуется. Пока еще не придумали язык DDD++, на 100% заточенный под всё, что есть в DDD, и делающий невозможным отступление от определенных правил.


Я уже молчу про то, что в Go для реализации своих domain driven амбиций местами может потребоваться отступить от официальных стайлгайдов создателей Go. Помните: творец здесь вы, а язык программирования — это всего лишь инструмент.


Пример: разделение на слои и их изоляция друг от друга. Мой опыт


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


Предположим, мы делаем приложение — платежный сервис. Пусть приложение будет иметь следующие слои:


  • прикладной (application);
  • предметной области (domain);
  • инфраструктурный (infrastructure).

Раскладываем все наши объекты по одноименным Go-пакетам. А дальше дилемма: как бы нам так все организовать, чтобы детали реализации каждого слоя не торчали наружу? Чаще всего рекомендуют непубличные объекты называть с маленькой буквы. Но мне понравилась другая идея: все структуры, скрывающие детали реализации, складывать в подпакет internal. Это такой пакет, содержимое которого доступно соседним пакетам и вышестоящему пакету, но не всем остальным. В самом же пакете слоя пусть лежат интерфейсы, а также те типы объектов, которые скрывать мы не будем (например, сущности, объекты-значения и т.д.). Отсюда следует идея, что фабричные функции (которые NewFooBar()) спрятанных в internal сервисов можно вынести в свой подпакет factory.


Вот как это может выглядеть на примере слоя domain:


- domain/
  - factory/
      user_repository_factory.go
      transaction_repository_factory.go
  - entity/
      user.go
      transaction.go
  - value/
      money.go
  - internal/
      user_repository.go
      transaction_repository.go
  user_repository_interface.go
  transaction_repository_interface.go

Итак, в нашем домене 2 сущности: User и Transaction. За вытаскивание из БД и сохранение в БД (или в какой-то другой тип хранилища) отвечают соответственно UserRepository и TransactionRepository. Опишем интерфейсы этих репозиториев и положим в соответствующие файлы в корне слоя-пакета domain:


type UserRepositoryInterface interface {
    Get(id int64) (entity.User, error)
    FindByEmail(email string) (*entity.User, error)
    Save(user entity.User) error
}

type TransactionRepositoryInterface interface {
    Get(id int64) (entity.Transaction, error)
    FindByUserId(userId int64) (*entity.Transaction, error)
    Save(transaction entity.Transaction) error
}

Соответственно, в factory будут такие фабрики (для простоты иллюстрации опустим проброс зависимостей и прочие вещи):


func NewUserRepository() domain.UserRepositoryInterface {
    return &internal.UserRepository{}
}

func NewTransactionRepository() domain.TransactionRepositoryInterface {
    return &internal.TransactionRepository{}
}

В internal будут лежать реализации UserRepository и TransactionRepository.
В случае, если у нас не доменный слой, а, например, сервисный, то в его internal ушли бы реализации сервисов.


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


- infrastructure/
  - factory/
      user_repository_factory.go
      transaction_repository_factory.go
  - internal/
      user_repository.go
      transaction_repository.go
- domain/
  - entity/
      user.go
      transaction.go
  - value/
      money.go
  user_repository_interface.go
  transaction_repository_interface.go

Пример: поведение сущностей


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


Например, мы в нашем платежном сервисе имеем сущность под названием Transaction со следуюшими полями (оставлю только те, которые нужны в этом примере):


type Transaction struct {
    ...
    Status string
    ProcessedAt *time.Time
}

Status — текущий статус транзакции (может принимать значения Created, Processing, Success, Failed). А ProcessedAt — время проведения транзакции на стороне внешней платежной системы, может быть nil, если транзакция ещё не проведена (имеет статус, отличный от Success). Если транзакция в статусе Success, то поле ProcessedAt обязательно должно иметь какое-то значение (т.е. не nil).


Получается, если мы позволим в поля транзакции записывать значения, как хотим, то инвариант с ProcessedAt и Status может быть не соблюден в какой-то момент времени — то есть в какой-то момент времени сущность Transaction может оказаться в невалидном состоянии.


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


func (t *Transaction) SetSuccess(processedAt time.Time) error {
    if t.Status != "Processing" {
        return fmt.Errorf("cannot set success status after %v", t.Status)
    }

    t.Status = "Success"
    t.ProcessedAt = &processedAt

    return nil
}

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


Окей, наделали красивых методов. Но поля-то всё ещё публичные… И тут я вам предлагаю на выбор 3 варианта, что можно сделать:


  1. Сделать поля "приватными". Но в этом случае они внутри пакета всё равно будут видны из других объектов, да и придётся наплодить кучу геттеров, что, оказывается, не go way.
  2. Каждую отдельную сущность положить в свой отдельный пакет внутри entity. Получить в итоге "пакет с пакетами" и чрезмерно усложнённое дерево папок в проекте.
  3. Забить и договориться всей командой, что поля напрямую не сеттим, состояние меняем строго через методы, а нарушителей такого правопорядка на код-ревью бьём по рукам.

Декларативный стиль описания бизнес-логики


Декларативное описание бизнес-правил в Go у меня в целом получилось приемлемым, aka паттерн "спецификации", хотя в моём случае и не на 100% его книжный вариант. Здесь покажу один из возможных примеров реализации, и я уверен, что у вас получится лучше, красивее и каноничнее.


Давайте представим: в нашем платежном сервисе возникла необходимость завести небольшой и поначалу не очень сложный компонент под названием "антифрод". Этот компонент должен разрешать или запрещать разным пользователям операции пополнения баланса или вывода средств по определенным правилам. Для каждой поддерживаемой нами платежной системы этот набор правил свой; также набор правил меняется в зависимости от того, в какой юрисдикции действует наш сервис. Поначалу таких правил немного, но у бизнеса аппетит приходит во время еды, и появляются все новые и новые требования, возникает необходимость комбинировать правила между собой. И очень важно: наш код при этом не должен превращаться в лапшу.


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


type AntifraudRule interface {
    IsDepositAllowed(user User, wallet Wallet, amount Money) (bool, error)
    IsPayoutAllowed(user User, wallet Wallet, amount Money) (bool, error)
    And(other AntifraudRule) AntifraudRule
    Or(other AntifraudRule) AntifraudRule
    AndNot(other AntifraudRule) AntifraudRule
}

Первые два метода должны выполнять проверку, стоит ли разрешить пользователю user пополнить/вывести amount денег на/с кошелька wallet во внешней платежной системе. А методы And(), Or(), AndNot() — это методы-операторы, благодаря которым мы можем выстраивать наши правила в уникальные комбинации.


Вот пример реализации одного из правил. Допустим, мы хотим разрешать всем делать пополнения только в растущую луну, а выплаты в полнолуние. Тогда напишем следующий код:


type MoonRule struct {}

func NewMoonRule() AntifraudRule {
    return &MoonRule{}
}

func (r MoonRule) IsDepositAllowed(user User, wallet Wallet, amount Money) (bool, error) {
    if IsRisingMoon() {
        return true, nil
    }
    return false, nil
}

func (r MoonRule) IsPayoutAllowed(user User, wallet Wallet, amount Money) (bool, error) {
    if IsFullMoon() {
        return true, nil
    }
    return false, nil
}

func (r MoonRule) And(other AntifraudRule) AntifraudRule {
    return NewAndRule(r, other)
}

func (r MoonRule) Or(other AntifraudRule) AntifraudRule {
    return NewOrRule(r, other)
}

func (r MoonRule) AndNot(other AntifraudRule) AntifraudRule {
    return NewAndNotRule(r, other)
}

В последних трёх методах видим создание экземпляров AndRule, OrRule и AndNotRule. Вот как выглядит, например, реализация AndRule:


type AndRule struct {
    left AntifraudRule
    right AntifraudRule
}

func NewAndRule(left AntifraudRule, right AntifraudRule) AntifraudRule {
    return &AndRule{
        left: left,
        right: right,
    }
}

func (r AndRule) IsDepositAllowed(user User, wallet Wallet, amount Money) (bool, error) {
    leftResult, err := r.left.IsDepositAllowed(user, wallet, money)
    if err != nil {
        return false, err
    }
    rightResult, err := r.right.IsDepositAllowed(user, wallet, money)
    if err != nil {
        return false, err
    }

    return leftResult && rightResult, nil
}

func (r AndRule) IsPayoutAllowed(user User, wallet Wallet, amount Money) (bool, error) {
    leftResult, err := r.left.IsPayoutAllowed(user, wallet, money)
    if err != nil {
        return false, err
    }
    rightResult, err := r.right.IsPayoutAllowed(user, wallet, money)
    if err != nil {
        return false, err
    }

    return leftResult && rightResult, nil
}

func (r AndRule) And(other AntifraudRule) AntifraudRule {
    return NewAndRule(r, other)
}

func (r AndRule) Or(other AntifraudRule) AntifraudRule {
    return NewOrRule(r, other)
}

func (r AndRule) AndNot(other AntifraudRule) AntifraudRule {
    return NewAndNotRule(r, other)
}

Аналогичным образом реализуются OrRule и AndNotRule.


И наконец о том, как всем этим пользоваться. Допустим, у нас 3 правила: MoonRule, SunRule и RetrogradeMercuryRule. Определимся, что в текущей версии нашего сервиса мы хотим разрешать платежи людям тогда, когда нам благоприятствуют: Луна И Солнце ИЛИ Ретроградный Меркурий. Давайте напишем сборку нашего антифрода с этими условиями:


func NewAntifraud() AntifraudRule {
    moon := NewMoonRule()
    sun := NewSunRule()
    retrogradeMercury := NewRetrogradeMercuryRule()

    return moon.And(sun).Or(retrogradeMercury)
}

Как видим, вроде получилось. И вроде даже Go нам палки в колеса особо не ставил, и даже назойливые if err != nil почти не путались под ногами. А если всё это дело додумать, причесать, ух… Как говорится, нет предела совершенству.


Вместо заключения


Несмотря на то, что DDD в Go внедряется с определённым количеством компромиссов, всё же я увидел от такого внедрения больше плюсов, чем минусов. Да, где-то придётся на что-то закрыть глаза, где-то извернуться. Но даже в таком виде оно того стоит. И уж точно "натягиванием совы на глобус" я бы это не назвал.

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


  1. aegoroff
    06.07.2022 16:19
    +1

    Вопрос - а в func (r AndRule) IsDepositAllowed

    Вам действительно нужна копия AndRule? Почему не ссылка?

    То же самое для всех остальных методов


    1. tolyanski Автор
      06.07.2022 19:59
      +2

      Это всего лишь доступный в Go способ явно обозначить, что данный метод не меняет состояние объекта. Разумеется, в тех случаях, когда это может создать проблемы с производительностью, так делать не стоит.


    1. Vadim_Aleks
      07.07.2022 21:38

      Так оно по ссылке будет медленней же в этом кейсе. Потому что при вызове метода вам нужно будет брать ссылку на структуру, вместо того, чтобы скопировать сторуктру размером 0


      1. aegoroff
        07.07.2022 21:42

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

        Через 5 лет структура получит пару полей, а про метод забудут - получим проблему на ровном месте

        да и вообще вы не правы AndRule имеет размер не ноль

        type AndRule struct {

        left AntifraudRule
        right AntifraudRule
        

        }


        1. Vadim_Aleks
          07.07.2022 22:13

          А, верно. Перепутал с пустой структурой, которая реализует этот интерфейс

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


  1. aceofspades88
    06.07.2022 17:09
    +4

    Мне кажется, что почти все гоферы когда-то мигрировали в мир Go из других миров. В основном это миры хардкорного ООП.

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

    Этот мой приятель, не побоюсь этого слова — адепт DDD (domain driven design).

    Возможно гоферы как раз таки и мигрировали подальше от приятелей которые адепты с безупречным ДДД кодом?


    1. AikoKirino
      06.07.2022 17:39
      +6

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


    1. auddu_k
      06.07.2022 18:17
      +5

      Чисто для дискуссии.
      А чем ДДД так не угодил? В чистом виде, как и любой ‘чистый’ фреймворк, конечно избыточен, но мысли-то все верные, нет?


      1. aceofspades88
        07.07.2022 19:21

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


      1. ainu
        09.07.2022 11:31

        Своей вот избыточностью и не угодил. Адепты при этом игнорируют любые аргументы универсальной фразой "ну да, не везде подходит, не всегда, нужно нормально делать и нормально будет". И продолжают использовать (пытаться) всегда и везде.


  1. xcono
    06.07.2022 19:08
    +4

    Если отложить в сторону фантазии Эванса, а опираться только на периодические публикации, то может показаться, что DDD - это Directory Driven Design, вследствие того, что описание подхода зачастую начинается и заканчивается структурой директорий в проекте.

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

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

    "Они запускали в космос маленькие серебряные шарики с усиками. Я называю их - спутник."


    1. tolyanski Автор
      06.07.2022 20:00

      Плюсую!


  1. zogxray
    06.07.2022 22:13
    +3

    Ваш приятель не противопостояляет хорошее DDD плохому Go. Ваш приятель противоставляет образ себя хорошего враждебному и сложному миру с целью произвести впечатление и сойти за умного.

    Например возьмем его слова: Go - это *****код. В Go криво реализована обработка ошибок. Все, что не относится к DDD - антипаттерн, ад и хаос.

    И выделим паттерн: X - это говно, потому что гладиолус. Все, что не Y - по умолчанию плохо(а значит Y хорошо и это не нуждается в доказательствах).

    X = все, что угодно чужое.

    Y = любая херня относящаяся к автору фразы.

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

    Теперь пробуем на практике:

    • iPhone - это говно. В iPhone нельзя свернуть вызов. Все, что не Android - антипаттерн, ад и хаос.

    • Android - это говно. В Android нельзя дефрагментировать. Все, что не Android - антипаттерн, ад и хаос.

    • Статья автора - это говно. В статья автора нет ссылки на репозиторий и презентации. Все, что без презентации - антипаттерн, ад и хаос.

    • DDD - это говно. В DDD нельзя описывать бизнес логику в сервисах. Все, что не Model Driven Architecture - антипаттерн, ад и хаос.


    1. tolyanski Автор
      06.07.2022 23:05
      +1

      Рассуждение о преимуществах и недостатках DDD как такового выходит за рамки данной публикации, но возможно я напишу свои мысли на этот счёт в будущем. Спасибо за идею :)


      1. muturgan
        06.07.2022 23:11

        давай!


      1. zogxray
        07.07.2022 02:09

        В этом богоугодном деле главное не стать похожим на очередного джуна, который прячет свой страх «неправильно» разложить файлы в проекте, за разговорами о паттернах :)

        Не стоит благодарить за идею, которую вам не предлагали.


    1. funca
      07.07.2022 14:28
      +1

      Прямо как у классика :)

      Четыре иллюстрации того, как новая идея огорашивает человека, к ней не подготовленного

      1. Писатель: Я писатель.
      Читатель: А, по-моему, ты говно!
      (Писатель стоит несколько минут, потрясенный этой новой идеей, и падает замертво. Его выносят.)

      2. Художник: Я художник!
      Рабочий : А, по-моему, ты говно!
      (Художник тут же побледнел как полотно, и как тростинка закачался и неожиданно скончался. Его выносят.)

      3. Композитор: Я композитор!
      Ваня Рублев: А, по-моему, ты говно!
      (Композитор, тяжело дыша, так и осел. Его неожиданно выно-
      сят.)

      4. Химик: Я химик!
      Физик: А, по-моему, ты говно!
      (Химик не сказал больше ни слова и тяжело рухнул на пол.)

      (с) Д.Хармс


  1. JekaMas
    06.07.2022 22:17

    Возвращать интерфейс - не самая лучшая идея в go. Немного похоже на интерфейсы ради интерфейсов.

    Заодно начисто убивает идею и преимущества duck typing.


    1. tolyanski Автор
      06.07.2022 23:06

      Как я и указал в статье, местами приходится отступать от различных go way гайдов. Язык - это всего лишь инструмент. :)


      1. JekaMas
        06.07.2022 23:18
        +2

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

        Ну иметь сложности с циклическими зависимостями, плодит пакеты, не иметь duck typing, иметь нужду и импортировать пакеты только ради интерфейсов, и прихватить overhead заодно - это понятно. Но зачем? Чем не устраивает обычное разделение на публичные и приватные методы и поля? В чем жизненая необходимость вводить интерфейсы? Какую они задачу решают?


        1. tolyanski Автор
          07.07.2022 01:02

          Этот вопрос видимо можно перефразировать в “Какую задачу решает DDD?” Это довольно обширная тема, пожалуй потянет для отдельной статьи


          1. JekaMas
            07.07.2022 01:38

            Отнюдь. Убираем ненужные интерфейсы и DDD никуда не делось.

            Не стоит переделывать мой вопрос.


            1. tolyanski Автор
              07.07.2022 09:37

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

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

              Если отойти от DDD, то такой вам вопрос: юнит-тесты в Go пишете?


              1. JekaMas
                07.07.2022 09:41

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

                Про изоляцию. У вас конструкторы возвращают интерфейсы вместо самих объектов. То есть вы не убрали, а добавили пакет в зависимости.


                1. tolyanski Автор
                  07.07.2022 09:45

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

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


                  1. JekaMas
                    07.07.2022 15:06

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

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


                    1. tolyanski Автор
                      07.07.2022 15:55

                      Вы отрицаете необходимость интерфейсов в принципе?

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


                      1. JekaMas
                        07.07.2022 16:09

                        Это не тот вопрос, который я задал ;)

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

                        Лично я бы и пустые структуры убрал в пользу более функционального подхода. Но вот это уже точно вкусовщина.


                      1. tolyanski Автор
                        07.07.2022 16:20

                        Отойдем от DDD. Юнит-тесты пишете в Go? Как мокаете зависимости?)


                      1. JekaMas
                        07.07.2022 17:17

                        «Я написал, что если без введения интерфейса можно решить задачу и иметь все нужные свойства, то интерфейс добавлять в golang не надо.» Вроде должно отвечать на повторяемый вами вопрос. И да, не моками едиными ) Можно и в функциональный подход переписать и передавать свои функции-заглушки. Просто как вариант, чтобы показать, что указанный в статье подход не является единственным решением для тестирования, если кто-то пытается это использовать как аргумент. Хотя по сути аргумент один «так захотелось».

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

                        MoonRule - не имеет зависимостей, равно не имеет полей и ничего изолировать вообще не надо.

                        По вашему коду имплементация и интерфейс лежат на одном уровне: MoonRule-AntifraudRule; AndRule-AntifraudRule; и там же интерфейсы User, Wallet, Money. При таком подходе смысл в интерфейсах не такой большой, поскольку всё лежит в одном большом пакете. Если же разбивать на пакеты то как? Выносить интерфейсы на уровень выше? Но тогда имплементация будет зависеть от уровня выше. Оставлять на том же уровне? Тогда не разрывается зависимость от пакета с имплементацией. Добавлять новый пакет в пакет с имплементацией? Только так останется поступить. Но опять не решенным остается вопрос конструкторов. У вас интерфейсы не уменьшают связность кода, что как-то странно. Должно быть наоборот.



                      1. tolyanski Автор
                        07.07.2022 17:36

                        Ну вот вы тестируете что-то, что зависит от репозитория. Вам удобно будет замокать репозиторий. Мне не до конца понятно, зачем отказываться от чего-то, что удобно. :)

                        Про antifraud: в этом примере я показал сам паттерн, а не разбивку на пакеты. Вы можете разбивать на пакеты так, как считаете нужным.

                        Кажется, мы с вами все-таки говорим о разных вещах. По-моему, вы просто триггернулись но одно несоответствие стайлгайдам. :)


                      1. JekaMas
                        07.07.2022 18:18

                        Как-то вы плохо приписываете мне то, чего я не писал. Попробуйте строго пройти по тексту, который выше. Там нет ничего о том, что интерфейсы не нужны. Там о том, что в го они не нужны так, как вы их используете. И обоснования почему: консирукторы с дополнительной зависимостью на интерфейс, который конструируемый тип имплементирует; большая связность кода; убийство фичи duck typing; отсутствие возможности в вашем же коде прокинуть зависимости; и то, что юниты можно не только на моках писать, часто можно обойтить передачей функции. Вы ведь понимаете, что можно DDD и в функциональном подходе иметь? И тестировать не только через тестовые имплементации интерфейса?

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

                        Я пытаюсь показать, что у вас реализация не DDD, а стайлгайда на DDD, притащенного из других языков. И ваш стайлгайд DDD в го работает плохо.


                      1. tolyanski Автор
                        07.07.2022 18:48

                        > Вы ведь понимаете, что можно DDD и в функциональном подходе иметь?

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


                      1. JekaMas
                        07.07.2022 19:33

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

                        Но мои вопросы вы "не заметили". Так что с теми проблемами, которые добавляют неуместные интерфейсы в вашем коде?


                      1. tolyanski Автор
                        07.07.2022 20:32

                        Итак,
                        - User, Wallet и Amount в моем примере интерфейсами не являются.
                        - Интерфейсы для репозиториев - ок, можем без них обойтись. Однако, на мой вкус, лучше бы их оставить объектами, а не переписывать в ФП (но тут кому как) и использовать для них моки. Кроме того, источники данных, к которым фактически будут обращаться репозитории, могут меняться, и тот, кто от них зависит, лучше чтобы не знал об этом факте. Но ок, предположим, такого никогда не будет, и мы точно об этом знаем. Ноу проблем, интерфейсы убираем.
                        - Интерфейсы в паттерне "спецификации". Вот тут как раз-таки вся соль в том, что конкретная комбинация правил сильно зависит от конкретного случая применения приложения, очень часто вносятся изменения в эту комбинацию. Весь остальной код, обращающийся к антифроду, не должен иметь ни малейшего понятия, с какой комбинацией правил он имеет дело в данный конкретный момент. Он должен знать только, что у него есть сейчас в распоряжении правильно настроенный/собранный объект, и, когда ему нужно, он ему всегда может задать вопрос и получить ответ.

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


                      1. JekaMas
                        07.07.2022 20:46

                        И что поменяется такого страшного, если у вас конструкторы начнут возвращать объекты? Хотите - возвращайте приватные объекты. Да и обычные публичные ни инкапсуляцию, ни разделения слоёв не допустят. Или гайдлайн говорит, что так нельзя?)

                        Кстати, там выше у вас есть странные штуки, когда есть неиспользуемый ресивер по значению, чтобы "обозначить только для чтения). Лучше не так делать:

                        ```

                        func (MoonRule) IsDepositAllowed(user User, wallet Wallet, amount Money) (bool, error) {

                        if IsRisingMoon() {

                        return true, nil

                        }

                        return false, nil

                        ```

                        Про классику в виде "источники данных могут и меняться". У вас часто на проде базы меняются?) Обычно это очень больно и происходит примерно никогда.


                      1. tolyanski Автор
                        07.07.2022 20:49

                        А если например конкретная комбинация зависит от конфига? Что возвращать-то?


  1. kolkov
    07.07.2022 18:13

    Почему есть метод save, но нет метода load? Get тут немного не стыкуется.

    Как делаете создание агрегатов и их сохранение. Так то с простыми примерами все понятно.)


    1. tolyanski Автор
      07.07.2022 18:19

      Про агрегаты - интересный вопрос. Врать не буду: в Go мы пока старались без них обходиться. :) Но есть пара идей, и думаю скоро провести серию экспериментов. По результатам напишу)