使用类型重构

原文地址:https://dzone.com/articles/refactoring-with-types

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

  

使用类型重构

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

1
2
3
4
5
6
7
8
public class OrderLine {
private int quantity;
private Double unitPrice;
private Double listPrice;
private Double tax;
private Double charge;
// 其余实现
}

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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很有意义。

1
2
3
4
5
6
7
8
9
10
11
12
13
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类:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class UnitPrice {
private UnitPrice() {
}
private
@Getter
NonNegativeDouble unitPrice;
public UnitPrice(NonNegativeDouble unitPrice) {
setUnitPrice(unitPrice);
}
private void setUnitPrice(NonNegativeDouble unitPrice) {
this.unitPrice = unitPrice;
}
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@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 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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。也可以两个都有。但是,当前的实现要求两个同时存在。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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,实现业务规则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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())
}

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

---------------- The End ----------------
0%