DDD丰富的领域模型,以及聚合根
本文关键字:领域模型 DDD | 更新日期: 2023-09-27 18:15:00
我正在尝试开发一个允许用户为多租户应用程序创建租户的小应用程序。在生产服务器上,我们有2个站点实例,一个版本N(生产)和一个版本N + 1(测试生产)。
有些租户是test-tenant。所以我们必须可以更改站点版本的租户。
当一个租户被分配到一个站点时,我需要使用API Microsoft.Web.Administration来配置IIS,以便向站点实例添加绑定。
示例:如果我们将租户(tenant1)从生产站点传递到测试生产站点,我们必须删除"production"站点的绑定"www.tenant1.com",并将其添加到"production-test"站点
对于这个域,我设计了两个聚合根
public class Tenant : IEntity, IAggregateRoot
{
public int Id { get; set; }
public string Name { get; set; }
public string DbInstanceName { get; set; }
public Site Site { get; set; }
public virtual Icollection<Binding> Bindings { get; set; }
}
public class Binding: IEntity
{
public int Id { get; set; }
public int Port { get; set; }
public string Protocol { get; set; }
public string Adress { get; set; }
}
当我加载一个租户时,它的绑定被加载,站点正在加载,但站点的绑定没有加载。
public class Site : IEntity, IAggregateRoot
{
public int Id { get; set; }
public string Name { get; set; }
public string IISiteName { get; set; }
public virtual ICollection<Tenant> Tenants { get; set; }
public virtual ICollection<Binding> Bindings { get; set; }
}
当我加载一个站点时,它的绑定被加载,租户正在加载,但租户的绑定没有加载。
首先,在站点中有一个租户列表,在租户中有一个站点,知道租户和站点是聚合,这是可以接受的?
然后我有一个用例给我带来了问题,因为视图允许用户编辑租户,包含一个网站组合,当租户更新时,可以更改与网站的关联。
第一种方法:
public class TenantService {
public void UpdateTenant(Tenant tenant, int newSiteId) {
var currentTenant = _repoTenant.find(tenant.Id);
var newSite = _repoSite.find(newSiteId);
// mapping
// ...
if(tenant.Site != null) {
// important: i have to load site from it's aggregate, because I KNOW (but if i was another developer i wouldn't)
// that's site's bindings are note loaded from client's aggregate
var currentSite = _repoSite.find(tenant.Site);
currentSite.RemoveTenant(tenant);
// iisadministrator, supply an abstraction to configure binding on iis
iisadministrator.SetBinding(currentSite .IISSiteName, currentSite.Bindings);
}
newSite.Add(tenant);
// iisadministrator, supply an abstraction to configure binding on iis
iisadministrator.SetBinding(newSite.IISSiteName, newSite.Bindings);
// saving..
}
}
public class Site : IEntity, IAggregateRoot
{
public void AddTenant(Tenant tenant) {
this.Tenants.Add(tenant);
tenants.Bindings.ToList().Foreach( b => this.Bindings.Add(b));
}
public void RemoveTenant(Tenant tenant) {
this.Tenants.Remove(tenant);
tenants.Bindings.ToList().Foreach( b => this.Bindings.Remove(b));
}
}
这种方法有3个问题:
- 当另一个开发商向一个站点添加租户时,我不能确定他已经从以前的站点中移除
- 当开发人员将租户添加到站点时,我不能确定他是否更新了IIS上的绑定
- 可能是一个建模问题:开发人员必须知道它必须完全加载当前站点(拥有其所有绑定并可以更新iis)第二个:
public class TenantService {
public void UpdateTenant(Tenant tenant, int newSiteId) {
var currentTenant = _repoTenant.find(tenant.Id);
var newSite = _repoSite.find(newSiteId);
// mapping
// ...
// iisadministrator, supply an abstraction to configure binding on iis
newSite.Add(client, iisAdministrator);
// saving..
}
}
public class Site : IEntity, IAggregateRoot
{
public void AddTenant(Tenant tenant, IISAdministrator iisadministrator) {
if(tenant.Site == Site) return;
if(tenant.Site != null) tenant.Site.removeTenant(tenant); <-- all bindings are not loaded (1)
this.Tenants.Add(tenant);
tenants.Bindings.ToList().Foreach( b => this.Bindings.Add(b));
iisadministrator.SetBinding(this.IISSiteName, this.Bindings);
}
public void RemoveTenant(Tenant tenant, IISAdministrator iisadministrator) {
this.Tenants.Remove(tenant);
tenants.Bindings.ToList().Foreach( b => this.Bindings.Remove(b));
iisadministrator.SetBinding(this.IISSiteName, this.Bindings); <-- all bindings are not loadedn see(1)
}
}
好了,但还有其他问题:
- 所有当前站点的绑定都没有加载,所以在更新iis时存在问题
另外在2种情况下,如何在同一事务中更改实体和更改IIS ?
关于建模,如何选择:
tenant.setSite(Site site) { }
tenant.changeSite(Site oldSite, Site newSite) { }
site.AddTenant(Tenant tenant) { }
有方法吗?
谢谢。
首先,在站点中有一个租户列表,在租户中有一个站点,知道租户和站点是聚合,这是可以接受的?
不,这没有任何意义。引用列表可能是有意义的,这取决于您需要每个聚合执行什么不变量。试图在一个聚合中嵌套另一个聚合表明您的聚合边界出现了严重问题。
如何在同一事务中更改实体和更改IIS ?
好吧,您可以四处寻找一种管理两阶段提交的方法,但通常的答案是在从更新到模型的单独事务中向您的端口发送消息。您通常会放弃防止模型和远程系统脱离同步的想法,而将注意力集中在检测和缓解上。
参见Udi Dahan关于可靠消息传递的演讲。"设置器"通常是幂等的,因此"至少一次"交付可能会产生令人满意的结果。
关于建模,如何选择:
tenant.setSite(Site site) { }
tenant.changeSite(Site oldSite, Site newSite) { }
site.AddTenant(Tenant tenant) { }
有方法吗?
一般规则是"set"应该被视为代码气味;把业务逻辑放到聚合里,让它来做判断。如果不需要做出判断,那么为什么这个数据成员是聚合的一部分呢?
或者,换句话说,您需要评估您的解决方案是数据库还是服务。再次借用乌地……
Dahan认为服务必须同时具有某种功能和某些数据。如果它没有数据,那么它只是一个函数。如果它所做的只是对数据执行CRUD操作,那么它就是数据库。
如果您的解决方案不能否决更改,如果它的职责仅限于记录在其他地方做出的业务决策,那么您应该考虑数据库。