Как стать автором
Обновить

Паттерн Unit of Work в разрезе чистой архитектуры DDD на языке Golang

Уровень сложностиСредний
Время на прочтение9 мин
Количество просмотров16K
Всего голосов 6: ↑4 и ↓2+3
Комментарии7

Комментарии 7

// UnitOfWork --
func (ths *SQLStore) UnitOfWork(fn func(writer port.Writer) error) (err error) {
	trx := ths.db.Begin()
	if err != nil {
		return err
	}

А для чего здесь идёт проверка ошибки? err в данном случае объявлена как именованный результат, при входе в метод она безальтернативно будет рана nil. Код return err в этом примере недостижим.

Вы всё же реализовали просто транзакционный метод, это не Unit of Work. Тем более, что у вас Writer вдруг знает о Reader и метод этот во Writer. Что даёт забавную возможность:

ths.writer.UnitOfWork(func(rTx port.Reader, wTx port.Writer) error {
  wTx.UnitOfWork(func(rrTx port.Reader, wwTx port.Writer) error {
    wwTx.UnitOfWork(func(rrrTx port.Reader, wwwTx port.Writer) error {
      // и тд
    })
  })
})

Суть Unit of Work в том, чтобы там было для нескольких репозиториев ОДНО соединение к СУБД. К транзакциям он не имеет никакого отношения, это скорее приятное следствие. Если брать тот самый EF Core, из которого вы взяли понятие DbContext - именно он внутри отвечает там за транзакции.

По идее должно быть что-то типа такого:

type Repository1 struct {
  connection *Connection
}

func (r *Repository1) FindById(id int) Entity {}

type Repository2 struct {
  connection *Connection
}

func (r *Repository2) FindById(id int) Entity {}

type UnitOfWork struct {
  connection *Connection
  repo1 *Repository1
  repo2 *Repository2
}

func (u *UnitOfWork) GetRepository1() *Repository1 {
  if u.repo1 == nil {
    u.repo1 = &Repository1{u.connection}
  }

  return u.repo1
}

func (u *UnitOfWork) GetRepository2() *Repository2 {
  if u.repo2 == nil {
    u.repo2 = &Repository2{u.connection}
  }

  return u.repo2
}

А для транзакций делаем уже, что нам удобнее

// Транзакционный метод

func (u *UnitOfWork) Transaction(fn func() error) {}

// Или методы управления транзакцией

func (u *UnitOfWork) BeginTransaction() *Transaction {}

func (u *UnitOfWork) RollbackTransaction() {}

func (u *UnitOfWork) CommitTransaction() {}

И соответственно интерфейсы на уровне домена/приложения

type Repository1 interface {
  FindById(id int) Enity
}

type Repository2 interface {
  FindById(id int) Entity
}

type UnitOfWork interface {
  func (u *UnitOfWork) GetRepository1() *Repository1
  func (u *UnitOfWork) GetRepository2() *Repository2
  func (u *UnitOfWork) Transaction(fn func() error)
}

ну и использование

uow.Transaction(func () error {
  uow.GetRepository1().FindById(1024)
  uow.GetRepository2().FindById(1025)
})

Спасибо за статью!

SQLStore точно реализует интерфейс Writer? Мне кажется там аргумента не хватает. Могу ошибаться.

А как бы вы реализовали ограничение для unit of work, сначала должны выполниться все операции чтения и только потом операции записи?

Так DDD или чистой архитектуры?

Статья полна бесполезных ничего не значащих слов типа "чистая архитектура", "DDD" (повторяется в статье 8 раз), "Domain-Driven Design (DDD)" (вся фраза целиком повторяется в статье 3 раза)

Своей статьёй вы же и нарушаете свои "принципы" (Do not Repeat Yourself)

разработать шаблон сервиса, который можно было бы использовать как для монолитной, так и для микро-сервисной архитектуры. Шаблон должен был придерживаться принципов Domain-Driven Design (DDD)

Монолит, микросервисы, DDD, всё это пустые слова, здесь буквально написано "мне нужно было написать шаблон программы" - это невозможно.

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

В целом всю статью можно описать так:

" А давайте в интерфейс ДБ добавим методы .begin_transaction() .commit() .rollback() "

error_code my_transaction(....) {
  auto transaction_handle = db.begin_transaction();
  error_code err;
  on_scope_exit {
    if (err) transaction_handle.rollback();
    else     transaction_handle.commit();
  };
  ....
}

Вставлю свои 5 копеек, но в терминах C# на примере моей демки https://github.com/KlestovAlexej/Wattle3.DemoServer

При создании UnitOfWork необязательно начинать транзакцию БД сразу и в лоб.
Так как даже работа с доменными объектами может не потребовать БД до самого момента комита UnitOfWork - и соединение к БД не будет держаться без дела паразитно.
Всё зависит от реализации UnitOfWork и той инфраструктуры на которой он живёт.

Не очень понятно, какое отношение результат имеет к DDD. Если в Golang хотите упариваться в DDD и есть проблема транзакций, то event sourcing и SAGA (да, можно применять не только в отношениях сервис-сервис) в контексте DDD более применима.

В ином случае решение от hello_my_name_is_dany работает эффективней.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории