维基百科说贫血的领域模型"是指使用一个软件领域模型,其中的领域对象几乎不包含任何业务逻辑(验证、计算、业务规则等)"。
如果你不熟悉领域建模,很多项目都是这样开始的。
贫血的领域模型主要是缺乏封装和隔离的原因。
看看当我们有一个CreateUserUseCase
和一个EditUserUseCase
,验证逻辑会发生什么。
class CreateUserUseCase extends BaseUseCase<ICreateUserRequestDTO, ICreateUserResponseDTO> {
constructor () {
super();
}
exec (request: ICreateUserRequestDTO): ICreateUserResponseDTO {
const { name, email } = request.user;
const isValidName = UserValidator.validateName(name);
const isValidEmail = UserValidator.validateEmail(email);
if (isValidName && isValidEmail) {
const user: User = User.create(name, email);
// continue
} else {
// error
}
}
}
class EditUserUseCase extends BaseUseCase<IEditUserRequestDTO, IEditUserResponseDTO> {
constructor () {
super();
}
exec (request: IEditUserRequestDTO): IEditUserResponseDTO {
const { name, email } = request.user;
const isValidName = UserValidator.validateName(name);
const isValidEmail = UserValidator.validateEmail(email);
if (isValidName && isValidEmail) {
const user: User = User.create(name, email);
// continue
} else {
// error
}
}
}
你觉得这看起来很DRY吗?
我们必须对来自API的用户输入编写两次用户验证逻辑,在两个用例中都要重复一遍。
虽然这看起来不是什么大问题,但当我们添加n
更多在User
模型上操作的用例时,这就会失去控制。
不仅如此,如果我们向User
模型添加更多的属性,我们也必须为这些属性编写验证器。这就意味着我们必须回溯我们曾经写过的每一个用例,确保我们已经添加了新的验证规则。
如果我的计算是正确的,这意味着我们必须在头脑中跟踪n x m
不同的地方来更新验证规则,如果我们这样假设。
要维护的规则 = # 模型上的属性 x # 使用模型的服务
一个更好的方法是重构Validator,使其消耗整个原始User
对象,并在那里维护所有的验证规则。
class UserValidator extends BaseValidator<IUser> {
constructor () {
super();
}
private validateName (name: string) : boolean {
// should be longer than 2 chars, less than 100
}
private validateEmail (email: string) : boolean {
// regex to check string
}
public validate (user: IUser) : boolean {
const isValidName = this.validateName(name);
const isValidEmail = this.validateEmail(email);
if (isValidName && isValidEmail) {
const user: User = User.create(name, email);
// continue
} else {
// error
}
}
}
在领域驱动设计中,我们的目标是将不变量/领域逻辑封装在靠近实际模型本身的地方,所以在这个例子中,在User.create(name: string, email: string)
工厂函数中。
像这样的验证器类在理论上是可以存在的,但我们希望在创建对象时在靠近模型的地方定义和运行这个逻辑,而不是在控制器、交互器或用例等更高级别的技术构件中。
在JavaScript或TypeScript中,我喜欢使用Joi进行验证。你可以创建一个漂亮的BaseValidator
抽象类,然后用像这样的可读验证代码实现validate()
方法。
Joi.string().min(3).max(100).required()
通过封装进行不变的验证只是丰富领域模型的好处之一。
丰富领域模型的好处
更好的可发现性
如果我们确切地知道代码的归属,这就减少了我们寻找新代码应该被添加到哪里的时间。
我们的自然本能是看我们的模型,看它能做什么。当我们的模型准确地描述了它所能做的事情时,我们就会少花很多时间去寻找它在其他地方能做什么。
服务中的任何业务逻辑,如果能被确定为某个实体的唯一责任,就应该被转移到该实体。
任何不完全属于一个实体的逻辑都应该留在领域服务中。
任何在外部资源上执行操作的逻辑(比如使用Google Places API来获取地址的地理位置坐标)都应该属于应用服务。
两个相关的软件设计原则协助提出了这个指标。
缺少重复性
建立在上一点的基础上。
如果我们知道代码应该放在哪里,就会减少重复的可能性。
重复的代码是精心设计的软件的敌人。如果我们必须保持冗余,写代码来描述一块业务逻辑的两个或更多的地方,我们就需要依靠我们的记忆来记住所有要更新的地方,如果它被改变的话。
人类通常不是最善于记住东西的人。
封闭性
封装是保护数据完整性的行为。
这就是我们通过用例方案解决的问题。
通过数据的完整性,我们的意思是 "这个数据被允许采取什么形式?","什么方法可以被调用,在什么时候?","为了创建这个对象,需要什么参数和前提条件?"?
贫血的领域模型的好处
因为使用DDD对一个领域进行建模需要时间,所以有时使用贫血领域模型确实更好。
根据各种不同的原因,它可能在 "短期内 "对你更有利。
例如,如果你是一个尚未验证产品的初创公司,或者你正在开发一个概念验证或一次性应用,我建议走贫血路线,因为你可以快速迭代。
我怎么知道我什么时候应该使用富域模型?
有一个甜蜜点,你会知道。好的是,你总是可以朝着这个方向重构;如果你已经写了SOLID和设计良好的代码,那就更容易了。
旁白:事实证明,贫血的领域模型对于函数式编程真的很有用,原因有几个。
在函数式编程中,对象是不可变的。这意味着没有办法让不变性变得不满足。在这种情况下,将模型和服务明确分开是非常有意义的。
CanShootLazers
如果你想在游戏引擎中的各种模型上添加CanRun
和CanFly
,这是有意义的,而且可以节省很多时间。