在日常编码设计中,我们在领域模型中表达属性时既可以使用原始类型也可以使用强类型,例如表达金额的时候既可以使用long也可以使用Money,那在原始类型与强类型之间应该如何选择呢?
原始类型弊端(Primitive obession)
下面的例子是一个Customer类
public class Customer {
private String name;
private String email;
public Customer(String name, String email) {
this.name = name;
this.email = email;
}
}
在Customer中的属性都是通过原始类型表示的,现在需要对name和email加上一些校验逻辑(领域知识)
public class Customer {
private String name;
private String email;
public Customer(String name, String email) {
if ((name == null) || (name.length() > 4)) {
throw new InvalidArgumentException("name is invalid");
}
if ((email == null) ||
!email.matches("^([\\w\\.\\-]+)@([\\w\\-]+)((\\.(\\w){2,3})+)$")) {
throw new InvalidArgumentException("email is invalid");
}
this.name = name;
this.email = email;
}
}
然而,除了需要在领域模型中进行校验之外,相同的逻辑还需要在其他地方(如业务层,显示层等)实现一遍
public class CustomerService {
public Customer createCustomer(String name, String email) {
if ((name == null) || (name.length() > 4)) {
throw new InvalidArgumentException("name is invalid");
}
if ((email == null) ||
!email.matches("^([\\w\\.\\-]+)@([\\w\\-]+)((\\.(\\w){2,3})+)$")) {
throw new InvalidArgumentException("email is invalid");
}
return new Customer(name, email);
}
}
可以看到上面的代码违反了DRY原则,同一段逻辑在不同的地方出现。因为name与email的校验知识属于领域知识,所以在领域模型中必须有体现,不能因为service校验了就不管(而且Customer可能会在很多其它地方使用),而service也需要对参数进行检验,当然可以使用声明式校验框架来对参数进行统一检查,不过这属于技术实现。作为领域知识来思考的话,name与string,email与string是否等价,其实不然,email除了string表示的值之外了还包含了其他的隐藏规则,而这些规则是单独的string无法承载的
强类型
为了解决上面的问题,可以通过将name与email定义成强类型
public class CustomerName {
private String value;
private Customer Name(String value) {
this.value = value;
}
public static Result create(String value) {
if ((value == null) || (value.length() > 4)) {
return Result.Fail("name is invalid");
}
return Result.Success(newCustomerName(value));
}
//TODO get/equals/hashcode
}
email类型与name类型类似
这种方式的好处是将校验逻辑(或者其他业务逻辑)收敛到一个地方,如果需要修改只需要修改email与name内部逻辑即可,值得注意的是上面类型的构造函数式私有的,这样新建对象只能通过create方法,保证了创建的对象都是合法的
修改之后的业务层逻辑
public class CustomerService {
public Customer createCustomer(String name, String email) {
Result customerName = CustomerName.create(value);
Result email = Email.create(value);
if (customerName.isFail()) {
throw new InvalidArgumentException(customerName.getMessage());
}
if (email.isFail()) {
throw new InvalidArgumentException(email.getMessage());
}
returnnewCustomer(customerName.getValue(), email.getValue());
}
}
修改之后的Customer
public class Customer {
private CustomerName name;
private Email email;
public Customer(CustomerName name, Email email) {
if ((name == null) || (email == null)) {
throw new InvalidArgumentException("name and email can not be null");
}
this.name = name;
this.email = email;
}
}
通过强类型的好处有以下几点:
将业务逻辑收敛到了一个单独的领域类中,避免了逻辑散落到各个地方
强类型可以减少出错机会,在进行赋值操作时编译器会强制检查,减少了将email的值赋值给name之类的错误
需要指出的是,在使用强类型的时候应该将类型穿越整个系统,直到到达系统边界时(client调用或者数据持久化),而不是在系统中进行不但的转换。
//bad
public void changeEmail(String oldEmail, String newMail) {
Result oldEmail = Email.create(value);
Result newEmail = Email.create(value);
if ( oldEmail.isFail() || newEmail.isFail() ) {
return;
}
Customer customer = getCustomerByEmail(oldEmail);
customer.setEmail(newEmail.value);
}
//good
public void changeEmail(Email oldEmail, Email newMail) {
Customer customer = getCustomerByEmail(oldEmail);
customer.setEmail(newEmail);
}
由于使用强类型需要在类型与原始值之间进行转换,而且需要定义额外的类型,可能会影响代码的整洁性,因此需要根据不同的场景去决定到底是否需要使用强类型