我正在处理的应用程序需要执行以下规则(以及其他规则):
此域中涉及的主要实体是:
您可以想象,这些是实体之间的关系:
租户->用户
项目->资源
乍一看,执行这些规则的聚合根是租户:
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作为聚合根的情况下阻止两个用户同时注册。
我很确定这是一个常见的场景,必须比以前的示例有更好的实现方式。
答案 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
}
}