使用类型重构_java


在本文中,我们将介绍一些使用类型进行重构的技术。类型可以精确定义某个领域,还可以通过它在合并业务规则时保证代码的正确性。这样使得我们能够编写简单优雅的单元测试,验证代码是否正确。


使用类型重构


最近在检查代码时,我遇到了下面这个类:

public class OrderLine {
 private int quantity;
 private Double unitPrice;
 private Double listPrice;
 private Double tax;
 private Double charge;
 // 其余实现
}


上面是典型的“代码异味”,称为基本类型强迫症


上面代码中,所有参数都用数字表示。但是,它们只是数字吗?unitPrice是否可以与 listPrice 或者 tax互换?


在领域驱动的设计中,这些的确是不同的东西,而不仅仅表示数字。理想情况下,我们希望用特定类型来表示这些概念。


第一级重构是为这些类创建简单的封装类型:


public class ListPrice {
   private ListPrice() {
   }
   private @Getter Double listPrice;
   public ListPrice(Double listPrice) {
       setListPrice(listPrice);
   }
   private void setListPrice(Double listPrice) {
       Objects.requireNonNull(listPrice, "list price can not be null");
       if (listPrice < 0) {
           throw new IllegalArgumentException("Invalid list price: "+listPrice);
       }
       this.listPrice = listPrice;
   }
}

public class UnitPrice {
   private UnitPrice() {
   }
   private @Getter Double unitPrice;
   public unitPrice(Double unitPrice) {
       setUnitPrice(unitPrice);
   }
   private void setUnitPrice(Double unitPrice) {
       Objects.requireNonNull(unitPrice, "unit price can not be null");
       if (unitPrice < 0) {
           throw new IllegalArgumentException("Invalid unit price: "+unitPrice);
       }
       this.unitPrice = unitPrice;
   }
}


这是一个很好的开始。现在,我们为他们定义了概念。可以把需要的业务规则加到这些结构中,不必在OrderLine容器类里实现。


但是,如果发现有检查listPrice和unitPrice是否为null或者非负数负数的重复代码,那么这些检查很可能也适用于quantity、tax和charge。


因此,创建一个代表非负数的Type很有意义。


public class NonNegativeDouble {
   private @Getter Double value;
   public NonNegativeDouble(Double value){
       this.setValue(value);
   }
   private void setValue(Double value) {
       Objects.requireNonNull(value,"Value cannot be null");
       if(value < 0){
           throw new IllegalArgumentException("Value has to be positive");
       }
     this.value = value;
   }
}


现在,可以用 NonNegativeDouble 安全地重构UnitPrice和ListPrice类。


public class UnitPrice {
   private UnitPrice() {
   }
   private
   @Getter
   NonNegativeDouble unitPrice;
   public UnitPrice(NonNegativeDouble unitPrice) {
       setUnitPrice(unitPrice);
   }
   private void setUnitPrice(NonNegativeDouble unitPrice) {
       this.unitPrice = unitPrice;
   }
}


可以用一个简单的测试来验证UnitPrice为非负数,代码如下:


@Unroll
class UnitPriceSpec extends Specification {
   def "#text creation of Unit Price object with value - (#unitPriceValue)"() {
       given:
       def unitPrice
       when:
       boolean isExceptionThrown = false
       try {
           unitPrice = new UnitPrice(new NonNegativeDouble(unitPriceValue))
       } catch (Exception ex) {
           isExceptionThrown = true
       }
       then:
       assert isExceptionThrown == isExceptionExpected
       where:
       text        |   unitPriceValue      |   isExceptionExpected
       'Valid'     |   120                 |   false
       'Valid'     |   12.34               |   false
       'Valid'     |   0.8989              |   false
       'Valid'     |   12567652365.67667   |   false
       'Invalid'   |   0                   |   false
       'Invalid'   |   0.00000             |   false
       'Invalid'   |   -23.5676            |   true
       'Invalid'   |   -23478687           |   true
       'Invalid'   |   null                |   true
   }
}


尽管上面展示的重构用法很简单,但它同样适用于基本类型,例如Email、名字、货币、 范围以及日期和时间。


重构:使用类型让非法状态无处躲藏


重构带来的另一个巨大价值是让非法状态在模型中无处躲藏。作为示例,请思考下面这个 Java 类:


public class CustomerContact {
private @Getter EmailContactInfo emailContactInfo;
private @Getter PostalContactInfo postalContactInfo;
public CustomerContact(EmailContactInfo emailContactInfo,
PostalContactInfo postalContactInfo)
{
setEmailContactInfo(emailContactInfo);
setPostalContactInfo(postalContactInfo);
}
private void setEmailContactInfo(EmailContactInfo emailContactInfo){
Objects.requireNonNull(emailContactInfo,"Email Contact
               Info cannot be null"
);
this.emailContactInfo = emailContactInfo;
}
private void setPostalContactInfo(PostalContactInfo postalContactInfo){
Objects.requireNonNull(postalContactInfo,"Postal Contact
               Info cannot be null"
);
this.postalContactInfo = postalContactInfo;
}
}


在之前重构基础上,我们已经提取了EmailContactInfo和PostalContactInfo。它们与strings不同,是真正的领域结构。


假设有这么一个简单的业务规则:“客户必须有 Email 信息或邮政地址。”


这意味着至少有一个EmailContactInfo或者CustomerContactInfo。也可以两个都有。但是,当前的实现要求两个同时存在。


为了实现业务规则,第一次重构看起来像下面这样:


public class CustomerContact {
   private @Getter Optional<EmailContactInfo> emailContactInfo;
   private @Getter Optional<PostalContactInfo> postalContactInfo;
   public CustomerContact(PersonName name, Optional<EmailContactInfo> emailContactInfo,
                          Optional<PostalContactInfo> postalContactInfo)
{
       setEmailContactInfo(emailContactInfo);
       setPostalContactInfo(postalContactInfo);
   }
   private void setEmailContactInfo(Optional<EmailContactInfo> emailContactInfo){
       this.emailContactInfo = emailContactInfo;
   }
   private void setPostalContactInfo(Optional<PostalContactInfo> postalContactInfo){
       this.postalContactInfo = postalContactInfo;
   }
}


现在这个版本反而超出了规则要求。规则要求CustomerContact至少包含 Email 或者邮政地址。但是,当前的实现中,CustomerContact可能一个联系方式也没有。


这种简化的业务规则会导致下面的结果


客户联系方式 = Email or 邮政地址 or Email和邮政地址都有


在函数式语言中,可以用sum types满足这种条件。但是,像 Java 这样的语言没有把这种结构作为一等公民。尽管如此,还是有JavaSealedUnions这样的开发库为 Java 提供  Sum和Union支持。


使用 JavaSealedUnions,实现业务规则:


public abstract class CustomerContact implements Union2<EmailContact, PostalContact> {
   public abstract boolean valid();
   public static CustomerContact email(String emailAddress) {
       return new EmailContact(emailAddress);
   }
   public static CustomerContact postal(String postalAddress) {
       return new PostalContact(postalAddress);
   }
}
class EmailContact extends CustomerContact {
   private final String emailAddress;
   EmailContact(String emailAddress) {
       this.emailAddress = emailAddress;
   }
   public boolean valid() {
       return /* 一些业务逻辑 */
   }
   public void continued(Consumer<EmailContact> continuationLeft, Consumer<PostalContact> continuationRight) {
       continuationLeft.call(value);
   }
   public <T> T join(Function<EmailContact, T> mapLeft, Function<PostalContact, T> mapRight) {
       return mapLeft.call(value);
   }
}
class PostalContact extends CustomerContact {
   private final String address;
   PostalContact(String address) {
       this.address = address;
   }
   public boolean valid() {
       return /* 一些业务逻辑 */
   }
   public void continued(Consumer<EmailContact> continuationLeft, Consumer<PostalContact> continuationRight) {
       continuationRight.call(value);
   }
   public <T> T join(Function<EmailContact, T> mapLeft, Function<PostalContact, T> mapRight) {
       return mapRight.call(value);
   }
}
// 示例
CustomerContact customerContact = getCustomerContact();
if (customerContact.valid()) {
   customerContact.continued(customerContactService::byEmail(), customerContactService::byPostalAddress())
}


本文展示了如何使用类型让设计变得更简洁。使用类型还能够有助于避免业务规则本身的歧义。前文展示的方法也可以在其他情况下使用,例如得到允许使用、发生成功或失败的情况。