Search
Write a publication
Pull to refresh
10
0
Андрей Алексеев @aa0ndrey

User

Send message

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

Наблюдателю могут быть переданы любые объекты, появляющиеся в основном методе. Поэтому проблем с тем, что какие-то данные не передать быть не должно. Это не нарушает абстракции

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

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

Не понял вас. То есть TransactionManager будет уметь работать только с одним уровнем изоляции? Или как включать тогда любой другой уровень изоляции, если в реализации TransactionManager всегда будет repeatable read ?

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

Не думаю, что паритет. Для решения через наблюдателей, наблюдатели со стороны Redis модуля будут захватывать блокировку, а наблюдатели со стороны Postgres реализовывать получение product и user при этом OrderService останется без изменений.

//Redis пакет и модуль
class CreateOrderObserverImpl {
    private RedisClient redisClient;
    public void onStart(CreateOrderContext context) {
        redisClient.lock(context.getCommand().getUserId());
        redisClient.lock(context.getCommand().getProductId());
    }
    
    public void onEnd(CreateOrderContext context) {
        redisClient.unlock(context.getCommand().getProductId());
        redisClient.unlcok(context.getCommand().getUserId());
    }
}
//Postgres пакет и модуль
class CreateOrderObserverImpl {
    private TransactionManager transactionManager;
    public void onStart(CreateOrderContext context) {
        transactionManager.begin("read-commited");
        //...
    }
    
    public void onEnd(CreateOrderContext context) {
        //...
        transactionManager.commit();
    }
}

При решении через TransacitonManager или UoM вся логика по захвату блокировок, по выбору уровня изоляции окажется в OrderService.

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

Проблема в том, что не уходим, т.к. UoW принимает команды. А наблюдатель работает с событиями. То есть с UoW вы должны говорить, что надо сделать и передовать для этого конкретные значения. То есть если, мне надо открыть транзакцию с уровнем "read commited" то я должен сделать что-то вроде UoW.begin("read commited");

Наблюдатель работает же иначе, мы не говорим, что необходимо сделать, а заявляем что произошло. observer.onStart и уже имплементация знает, что необходимо открыть транзакцию "read commited".

Как вы с помощью UoW можете скрыть факт наличия уровня изоляции? А если не можете, то тогда UoW не сильно отличается от TransactionManager.

А это не будет реализовано в OrderService. Это будет реализовано на уровне репозиториев — они будут брать ConsistenceManager из контекста и захватывать нужные блокировки — так же, как это было в случае репозиториев поверх БД. Причем для этого даже не надо вмешиваться в код исходных репозиториев, достаточно добавить декораторы.

Декораторы не подойдут. Пусть необходимо сначала захватить все необходимые блокировки, а потом уже пытаться получить user и product. То есть:
1. lock user-id (redis)
2. lock product-id (redis)
3. get user (postgres)
4. get product (postgres)

Нет, не будет. У меня всегда больше информации и возможностей, чем у наблюдателя.

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

Допустим TransactionManagerImpl в методе begin принимает уровень изоляции для постгреса. Тогда

class CreateOrderObserverImpl {
  private TransactionManagerImpl transactionManager;

  public void onStart(CreateOrderContext context) {
    transactionManager.begin("REPEATABLE READ");
  }
  
  public void onEnd(CreateOrderContext context) {
  	transactionManager.commit();
  }
}

Почему не смогу? Метод Begin всегда возвращает объект ConsistencyScope, на котором вызывается Commit (и End), а уже какие данные нужны БД (ид или что-то еще) — лежит внутри этого объекта.

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

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

  2. Во втором решении не увидел разницы между UoW и TransactionManager

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

В предложенном решении также один интерфейс (Observer) и каждая релизация своя у каждой СУБД. В этом смысле не вижу противоречий.

Три изменения вместо одного — субъективный вопрос?

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

Почему не смогу? Метод Begin всегда возвращает объект ConsistencyScope, на котором вызывается Commit (и End), а уже какие данные нужны БД (ид или что-то еще) — лежит внутри этого объекта.

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

Нет, предлагаю заменить реализацию ConsistencyManager, которая использовалась в основном коде, с postgres на redis.

У вас так не выйдет. Смотрите, когда мы получали данные из postgres, мы могли сделать что-то вроде select for update/share, для user и product. Теперь мы говорим следующее. Что у нас есть внешний арбитр для захвата блокировок через Redis, см. библиотека . То есть через postgres мы получаем данные и открываем его транзакцию, но блокировку мы захватываем в Redis. То есть там будет некоторый код, которые для user-id и product-id захватить блокировку. Прошу привести пример, как это будет реализовано в OrderService.

Мы сравниваем 2+n решений, где одно — базовое в посте (с явным вызовом транзакции), второе — ваше решение с обсервером, а n — все остальное пространство решений данной задачи (например, использование UoW).

Не смотря на то, что мы можем сравнивать какое угодно множество решений, это не конкретно. В статье было приведено 2 решения и ещё одно вами UoW.

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

Пока из этого множества для задачи консистентности решение с наблюдателем наихудшее.

По вашему субъективному мнению. Если кстати вы говорите, что мы рассматриваем 2+n решений, то я могу привести бесконечное множество решений, которые будут очевидно хуже. Этим я хочу подчеркнуть неуместность разговора о 2+n решениях без конкретики.

То есть если у нас две СУБД, то гарантия консистентности будет в двух местах?

Да, каждая БД будет ответственна за свои гарантии или вы предлагаете весь код для каждой СУБД поместить вместе с общей логикой?

С моей точки зрения — сложно, потому что три изменения (в сервисе, в контексте и в обсервере) вместо одного (в сервисе).

Да это субъективный вопрос и вопрос привычки

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

  1. Не достигается. Возьмём пример. Пусть есть один TransactionManager который требует id транзакции для коммита, а другой TransactionManager этого не требует. С прямым интерфейсом вы сможете сделать только один тип решения.

  2. Вы правильно заметили, что требования по конкурентности могут меняться. Не обязательно даже брать блокировку в самой базе, может быть придётся переделать решение на использование внешнего механизма блокировак на основе Redis? Предлагаете добавить работу с Redis в основной код, через интерфейс? В этом коде тогда будет больше инфраструктурных задач, чем основного кода.

Смотря от контекста. Если всего два события, а очень часто так и бывает, то можно увеличивать число типов Observer-ов. Но никто не отменял решения: 2. и 3. предложенные ранее.

… каком?

Postgres

Нет, у вас все еще два события: start и end. То, что у них контекст одного типа, не сильно помогает проблеме.

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

… и нет гарантий.

Да все верно, OrderService их и не пытается дать. В этом и преимущество, что мы можем отделять разные типы задач.

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

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

При подходе из (6) вам при многих изменениях бизнес-логики (например, вам нужна еще одна сущность) понадобится менять обсерверы, это излишняя неоправданная связанность.

Если понадобиться ещё одна сущность, то да вы правы, тут будет добавлено ещё одно поле в Context и вызов метода для получения этого объекта. Стоит отметить, что именно лишнего вызова репозитория не будет. Он будет перенесен из одного места в другое. Это болейрплейт, который не слишком сложно поддерживать.

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

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

Я не очень понимаю, что вы хотите сказать.

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

Нет, это очевидно разные, независимые наблюдатели. Аудит — это не уровень БД, он должен для всех СУБД работать.

Тут опять необходимо конкретика. Как аудит подключается? Прошу привести пример. Аудит может быть и отдельной библиотекой для конкретной СУБД интегрированной с общим решением. Также прошу превести пример, как бы и куда вы его поместили.

Покажите пример кода, потому что я вас не очень понимаю.

class OrderService {
  //.. другие поля
  List<CreateOrderObserver.OnEnd> onEndObservers;
  List<CreateOrderObserver.OnStart> onStartObserversl
}

… а кто и где у вас порядком наблюдателей управляет?

В примерах неуказано, но вообще с помощью IoC контейнера

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

Не обязательно. Аудит можно подключать через адаптер, который будет мостом между postgres и аудитом

Другую по сравнению с чем?

Другую по сравнению с тем, чтобы не использовать шаблон наблюдатель

Стоит задача "операция по созданию заказа должна быть консистентной". Где, как не в OrderService, это искать?

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

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

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

В нем порядок запросов все еще контролируется OrderService, поэтому я и говорю, что не выйдет. А представьте, что у вас операций записи больше одной (надо уменьшить число на складе, надо уменьшить баланс, надо создать заказ), как тогда будет выглядеть ваш пример?

В CreateOrderObserverImpl можно менять порядок вызовов получения user и product. Это никак не регламентируется OrderService

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

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

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

  1. Нет проблем, чтобы создать на отдельный тип событий свой список наблюдателей, чтобы решить подобные коллизии

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

  3. core модуль относится к postgres модулю со своей логикой, как postgres модуль к модулю аудита. То есть postgres модуль тоже может отправлять событие наблюдателям, например, на момент открытие и закрытие транзакций. Стоит отметить, что многие фреймворки пользуются таким способом как раз для возможности внедрения расширений и в частности аудита.

Потому что у нас есть требование строгой консистентности (strong consistency). Если его нет, разговор другой, но я именно из этого требования исхожу.

Так а разве сделав другую организацию кода, мы теряем возможность удовлетворить требованию? Ведь не стоит же задачи добиться консистентности конкретно в OrderService.

Я не вижу никакого "в меньшей степени", в этом и беда. Размер вмешательства тот же самый, только степень читаемости еще упала по сравнению с обычной транзакцией.

Потому что решение в отношении 1 к 1. Но а что если добавить ещё дополнительных инфраструктурных задач, которые будут решаться в наблюдателе, вместо OrderService. При масштабировании проблемы, в решении с наблюдателями меняется только кол-во мест в которых происходит отправка. А при прямом использовании интерфейсов вся инфраструктурная логика начинает уходить в основной код.

Не выйдет. У вас запросы — это бизнес-логика, если вы их вынесете из OrderService, в нем ничего не останется.

В 6 разделе был продемонстрирован пример. При вынесении всех вызовов репозиториев из core модуля в нем останется вся логика, связанная с выполнением бизнес-процесса, все проверки бизнес-требований с соответствующим ветвлением

Смотря какую. Если мы получаем, например, что id заказа занят из базы данных. То можно завести соответствующую ошибку на уровне core OrderIdAlreadyUsedException, которая будет обарачивать ошибку от postgres - IntegrityViolationException. Если мы например говорим, что потерялось соединение с базой. То такие ошибки даже не ожидается, что будут решаться на уровне OrderService.

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

Information

Rating
Does not participate
Location
Санкт-Петербург, Санкт-Петербург и область, Россия
Registered
Activity

Specialization

Backend Developer, Software Architect
Java