Приложение, над которым я работаю, должно обеспечить соблюдение следующих правил (среди прочих):
- Мы не можем зарегистрировать нового пользователя в системе, если квота активного пользователя для арендатора превышена.
- Мы не можем создать новый проект, если квота проекта для арендатора превышена.
- Мы не можем добавить больше мультимедийных ресурсов в любой проект, принадлежащий арендатору, если превышена максимальная квота хранилища, определенная в арендаторе
Основные объекты, вовлеченные в этот домен:
- Арендатор
- Проект
- Пользователь
- Ресурс
Как вы можете себе представить, это отношения между сущностями:
На первый взгляд кажется, что агрегатный корень , который будет применять эти правила, является арендатором:
class Tenant
attr_accessor :users
attr_accessor :projects
def register_user(name, email, ...)
raise QuotaExceededError if active_users.count >= @users_quota
User.new(name, email, ...).tap do |user|
active_users << user
end
end
def activate_user(user_id)
raise QuotaExceededError if active_users.count >= @users_quota
user = users.find {|u| u.id == user_id}
user.activate
end
def make_project(name, ...)
raise QuotaExceededError if projects.count >= @projects_quota
Project.new(name, ...).tap do |project|
projects << project
end
end
...
private
def active_users
users.select(&:active?)
end
end
Итак, в сервисе приложений мы будем использовать это как:
class ApplicationService
def register_user(tenant_id, *user_attrs)
transaction do
tenant = tenants_repository.find(tenant_id, lock: true)
tenant.register_user(*user_attrs)
tenants_repository.save(tenant)!
end
end
...
end
Проблема с этим подходом заключается в том, что совокупный root довольно большой, потому что он должен загружать всех пользователей, проекты и ресурсы, а это не практично. А также, что касается параллелизма, у нас будет много штрафов за это.
Альтернативой будет (я сосредоточусь на регистрации пользователей):
class Tenant
attr_accessor :total_active_users
def register_user(name, email, ...)
raise QuotaExceededError if total_active_users >= @users_quota
# total_active_users += 1 maybe makes sense although this field wont be persisted
User.new(name, email, ...)
end
end
class ApplicationService
def register_user(tenant_id, *user_attrs)
transaction do
tenant = tenants_repository.find(tenant_id, lock: true)
user = tenant.register_user(*user_attrs)
users_repository.save!(user)
end
end
...
end
В вышеприведенном случае используется фабричный метод в Арендатор , который обеспечивает соблюдение бизнес-правил и возвращает совокупность Пользователь . Основное преимущество по сравнению с предыдущей реализацией состоит в том, что нам не нужно загружать всех пользователей (проекты и ресурсы) в сводный корень, только их количество. Тем не менее, для любого нового ресурса, пользователя или проекта, который мы хотим добавить / зарегистрировать / сделать, у нас потенциально могут быть штрафы за параллелизм из-за полученной блокировки. Например, если я регистрирую нового пользователя, мы не можем одновременно создать новый проект.
Обратите внимание, что мы получаем блокировку для Tenant и, тем не менее, мы не меняем в нем никакого состояния, поэтому мы не вызываем tenants_repository.save . Эта блокировка используется как мьютекс, и мы не можем воспользоваться оптимистичным параллелизмом, если не решим сохранить арендатора (обнаружив изменение в счетчике total_active_users ), чтобы мы могли обновить версию арендатора и вызвать ошибку для другие параллельные изменения, если версия изменилась как обычно.
В идеале, я бы хотел избавиться от этих методов в Tenant классе (потому что это также не позволяет нам разбивать некоторые части приложения в их собственных ограниченных контекстах) и применять инвариантные правила в любом другой способ, который не оказывает большого влияния на параллелизм в других объектах (проектах и ресурсах), но я не знаю, как предотвратить одновременную регистрацию двух пользователей без использования этого Tenant в качестве совокупного корня.
Я почти уверен, что это распространенный сценарий, который должен быть реализован лучше, чем мои предыдущие примеры.