Совокупное применение корневых инвариантов с квотами приложений - PullRequest
1 голос
/ 20 июня 2019

Приложение, над которым я работаю, должно обеспечить соблюдение следующих правил (среди прочих):

  1. Мы не можем зарегистрировать нового пользователя в системе, если квота активного пользователя для арендатора превышена.
  2. Мы не можем создать новый проект, если квота проекта для арендатора превышена.
  3. Мы не можем добавить больше мультимедийных ресурсов в любой проект, принадлежащий арендатору, если превышена максимальная квота хранилища, определенная в арендаторе

Основные объекты, вовлеченные в этот домен:

  • Арендатор
  • Проект
  • Пользователь
  • Ресурс

Как вы можете себе представить, это отношения между сущностями:

  • Арендатор -> Проекты
  • Арендатор -> Пользователи

  • Проект -> Ресурсы

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

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 в качестве совокупного корня.

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

Ответы [ 2 ]

1 голос
/ 21 июня 2019

Совокупность должна быть просто элементом, который проверяет правила.Это может быть от статической функции без сохранения состояния до сложного объекта с полным состоянием;и не обязательно должны соответствовать вашей схеме персистентности, вашим понятиям «реальной жизни», ни тому, как вы моделировали свои сущности, ни тому, как вы структурируете свои данные или свои представления.Вы моделируете агрегат только с данными, необходимыми для проверки правил в форме, которая подходит вам больше всего.

Не бойтесь предварительно вычислять значения и сохраняйте их (total_active_users в этом случае).

Моя рекомендация - сделать вещи как можно более простыми, а рефакторинг (это может означать разделение, перемещение и / или слияние) позже;как только вы смоделируете все поведение, вам будет легче переосмыслить и проанализировать рефакторинг.

Это будет мой первый подход без поиска событий:

TenantData { //just the data the aggregate needs from persistence
  int Id;
  int total_active_users;
  int quota;
}

UserEntity{ //the User Entity
  int id;
  string name;
  date birthDate;
  //other data and/or behaviour
}

public class RegistrarionAggregate{

    private TenantData fromTenant;//data from persistence

    public RegistrationAggregate(TenantData fromTenant){ //ctor
      this.fromTenant = fromTenant;
    }

    public UserRegistered registerUser(UserEntity user){
        if (fromTenant.total_active_users >= fromTenant.quota) throw new QuotaExceededException

        fromTeant.total_active_users++; //increase active users

        return new UserRegisteredEvent(fromTenant, user); //return system changes expressed as a event
    }
}

RegisterUserCommand{ //command structure
    int tenantId;
    UserData userData;// id, name, surname, birthDate, etc
}

class ApplicationService{
    public void registerUser(RegisterUserCommand registerUserCommand){

      var user = new UserEntity(registerUserCommand.userData); //avoid wrong entity state; ctor. fails if some data is incorrect

      RegistrationAggregate agg = aggregatesRepository.Handle(registerUserCommand); //handle is overloaded for every command we need. Use registerUserCommand.tenantId to bring total_active_users and quota from persistence, create RegistrarionAggregate fed with TenantData

      var userRegisteredEvent = agg.registerUser(user);

      persistence.Handle(userRegisteredEvent); //handle is overloaded for every event we need; open transaction, persist  userRegisteredEvent.fromTenant.total_active_users where tenantId, optimistic concurrency could fail if total_active_users has changed since we read it (rollback transaction), persist userRegisteredEvent.user in relationship with tenantId, commit transaction

    eventBus.publish(userRegisteredEvent); //notify external sources for eventual consistency

  }
}

Чтение это и это для расширенного объяснения.

1 голос
/ 20 июня 2019

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

Общий поисковый термин для решения такой проблемы: Проверка правильности набора .

Если существует некоторый инвариант, который должен всегда выполняться для всего набора, тогда весь этот набор должен быть частью "одного и того же" агрегата.

Часто сам инвариант - это бит, который вы хотите продвинуть;нужен ли бизнесу строгое соблюдение этого ограничения, или более уместно принудительно применять ограничение и взимать плату, когда клиент превышает свои ограничения по контракту?

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

Это может помочь в рассмотрении выступления Мауро Сервиенти Все наши агрегаты неверны .

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