用应用程序配额汇总根不变性实施

时间:2019-06-20 09:16:47

标签: concurrency repository domain-driven-design aggregate entities

我正在处理的应用程序需要执行以下规则(以及其他规则):

  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

此方法的问题在于聚合根非常大,因为它需要加载所有用户,项目和资源,这是不切实际的。而且,在并发方面,我们将因此受到很多处罚。

一种替代方法是(我将重点关注用户注册):

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 中使用工厂方法,该方法强制执行业务规则并返回 User 汇总。与以前的实现相比,主要优点是我们不需要将所有用户(项目和资源)加载到聚合根中,只需加载它们的计数即可。但是,对于我们要添加/注册/制作的任何新资源,用户或项目,由于获得了锁定,我们可能会有并发惩罚。例如,如果我正在注册一个新用户,则我们不能同时创建一个新项目。

还请注意,我们正在获得 Tenant 的锁定,但是我们没有更改其中的任何状态,因此我们不调用 tenants_repository.save 。此锁用作互斥锁,除非我们决定保存租户(检测到 total_active_users 计数发生变化),否则我们无法利用开放式并发的优势,以便我们可以更新租户版本并引发错误。如果版本照常更改,则可以进行其他并发更改。

理想情况下,我想摆脱 Tenant 类中的那些方法(因为它还可以防止我们在自己的受限上下文中拆分应用程序的某些部分)并在其中强制执行不变规则任何其他方式都不会对其他实体(项目和资源)的并发产生重大影响,但是我真的不知道如何在不使用该Tenant作为聚合根的情况下阻止两个用户同时注册。

我很确定这是一个常见的场景,必须比以前的示例有更好的实现方式。

2 个答案:

答案 0 :(得分:1)

  

我很确定这是一个常见的场景,必须比以前的示例有更好的实现方式。

针对此类问题的常见搜索词:Set Validation

如果整个集合必须始终满足某些不变性,那么整个集合将必须是“相同”集合的一部分。

通常,不变量本身就是要插入的位;企业是否需要严格执行此约束,还是更松散地执行该约束并在客户超出合同规定的限制时收取费用?

具有多个集合-每个集合都必须是一个集合的一部分,但是它们不一定必须是 same 集合的一部分。如果没有跨多个集合的不变式,则可以为每个集合有一个单独的集合。可能会关联两个这样的聚合,共享相同的租户ID。

回顾毛罗·瑟维恩蒂(Mauro Servienti)的讲话All our aggregates are wrong

答案 1 :(得分:1)

聚合应该只是检查规则的元素。它可以从无状态静态函数到完整状态的复杂对象。不需要匹配您的持久性架构,“现实生活”概念,实体建模方式以及数据或视图的结构。您只需使用最适合您的形式检查规则的数据就可以对聚合建模。

不要对预先计算的值感到陌生并坚持使用它们(在这种情况下为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

  }
}

阅读thisthis以获得扩展的解释。