fusion构建器代码语法
我发现构建器设计模式偶尔在代码中有用,但在测试中经常有用。 本文简要概述了该模式,然后介绍了在测试中使用该模式的一个有效示例。 请参阅github中的代码。
生成器模式的背景
根据GoF的书 ,构建器设计模式用于“将复杂对象的构造与其表示分离,以便同一构造过程可以创建不同的表示”。 像大多数GoF书中一样,这是一个准确而乏味的描述。
乔什·布洛赫(Josh Bloch)在他的《 有效的Java》一书中,为建筑商提出了一种更有趣的用法。 他的方法试图解决的问题是,当一个类具有“不止几个”参数时,这些参数通常是通过构造函数设置的,其中许多参数可能是可选的。 典型的解决方案是
- 伸缩构造函数模式,在该模式中,您将为构造函数仅提供必需的参数,并为其他构造函数提供可选参数的变体,最终形成具有所有可选参数的构造函数。
这行得通,但是导致一个相当混乱的解决方案,该解决方案可能包含大量构造函数来覆盖所有排列 - 一个简单的构造函数(例如,仅用于必需的参数),由setter方法支持可选参数(JavaBeans方法)。 但是,这可能会使对象在构造过程中处于不一致状态,并且由于无法将字段定为
final
,因此当然会阻止不变性。 - 使用一个生成器。 这是Bloch建议的方法。 客户端创建一个生成器(通常使用无参数的构造器),然后在最终调用一个build方法之前,调用类似setter的方法来获取感兴趣的值(其余假定为默认值)。
几年前,我参加了一次演讲 ,其中Ted Young讨论了通过将构建器模式用于测试对象的构建,使构建器模式更进一步,下面将讨论这种方法。 [更新:请在此处查看Ted对这篇文章的回复]
使用Builder模式构建测试装置
使用Builder可以更轻松,更清晰地创建测试装置。
我通常使用此Builder方法进行测试的对象类型是域模型对象,例如Account,User,Widget或其他对象。 我支持使此类对象不可变 。
例如:
public final class Account {private final Integer id;private final String name;private final AccountType type;private final BigDecimal balance;private final DateTime openDate;private final Status status;public Account(Integer id, String name, AccountType type,BigDecimal balance, DateTime openDate, Status status) {this.id = id;this.name = name;this.type = type;this.balance = balance;this.openDate = openDate;this.status = status;}public Integer getId() {return id;} //other getters, toString(), equals() and hashCode() omitted for brevity//no setters
}
使用这样的课程,您经常会遇到Bloch讨论的问题。 在此示例中,我们有一个强制您设置所有值的构造函数,但是我们也可以有很多变体,其中一些值可以省略以使用默认值。 因此,为测试创建这样一个类的实例可能会有些痛苦,如果它具有比这个简单示例更多的字段,则情况会更加痛苦。 您甚至必须为可能不需要测试的字段提供值。 这也使得很难知道哪些值实际上是测试所需要的,哪些值纯粹是为了编译。
建设者可以提供帮助。
例
public class AccountBuilder {//account fields with default valuesInteger id = 1;String name = "default account name";AccountType type = AccountType.CHECKING;BigDecimal balance = new BigDecimal(0);DateTime openDate = new DateTime(2013, 01, 01, 0, 0, 0);Status status = Status.ACTIVE;public AccountBuilder() {}public AccountBuilder withId(Integer id) {this.id = id;return this;}public AccountBuilder withName(String name) {this.name = name;return this;}public AccountBuilder withType(AccountType type) {this.type = type;return this;}public AccountBuilder withBalance(BigDecimal balance) {this.balance = balance;return this;}public AccountBuilder withOpenDate(DateTime openDate) {this.openDate = openDate;return this;}public AccountBuilder withStatus(Status status) {this.status = status;return this;}public Account build() {return new Account(id, name, type, balance, openDate, status);}
}
现在,您可以创建一个Account对象,以更轻松地进行测试。
关于使用Builder进行测试的注意事项
- 默认值
构建器中使用的缺省值是避免出现异常的便利。 如果您的测试需要特定的测试值,则最好明确设置它们,而不要依赖于任何默认值。 它使您的测试意图更加清晰,并且在您需要更改默认值(例如由于业务需求变化)而使无意中止测试的风险降到最低时。
- 非最终领域
域模型类本身是不可变的,因此具有最终字段。 根据设计,生成器中的所有字段都是非最终的。 因此,构建器不是线程安全的。 因此,请勿重复使用Builders; 而是为每个测试创建一个新实例。
- 方法顺序不大
在大多数情况下,在构建器上调用方法的顺序应该不重要,并且在调用build()之前不会构造该对象。 这使构建器更易于使用,并避免了意外的意外。
此经验法则有明显且可以接受的例外。
例如打电话
Account account = new AccountBuilder().withType(AccountType.SAVING).withType(AccountType.CHECKING).build();
很傻,但被允许。 它只会为您提供类型检查的权限。
很好,但是请尝试避免引起更细微的混乱,例如,如果您的集合中可以添加某些内容,或者替换了整个集合(因此请清除以前的添加内容)。
使用Builder进行测试的优势
- 易于阅读
以下声明不是特别清楚:
Account account = new Account(1, "test", 10, ...);
该声明更加清晰:
Account account = new AccountBuilder().withId(1).withName("test").withBalance(10).build();
正如Bloch所说,“ Builder模式模拟命名的可选参数”。
- 仅指定与您的测试实际相关的值
如果您的测试仅涉及帐户余额和状态:
Account account = new AccountBuilder().withBalance(new BigDecimal(-100)).withStatus(Status.OVERDRAWN).build();
与必须在Accounts构造函数中指定每个值相反。
- 创建无效对象的能力
域模型类的构造函数可能会(希望!)强迫您创建有效的对象。 在测试中,您可能要故意创建无效的对象以进行测试。
进一步的增强
便利方法
您可以为测试中使用的常见方案添加便捷方法。
例如
public AccountBuilder withNegativeBalance() {this.balance = new BigDecimal(-100);return this;}
灯具类
除了使用Builder类之外,我还发现拥有一个关联的Fixtures类很有用,该类提供用于测试的预构建实例。 这些可以利用Builder对象进行构造(尽管也没有什么可以阻止您使用原始构造函数)。
例如
public class AccountFixtures {//a shortcut to creating a basic Account objectpublic final Account ACCOUNT = new AccountBuilder().build();public final Account OVERDRAWN_CHECKING_ACCOUNT = new AccountBuilder().withType(AccountType.CHECKING).withNegativeBalance().build();public final Account CLOSED_SAVING_ACCOUNT = new AccountBuilder().withType(AccountType.SAVING).withZeroBalance().withStatus(Status.CLOSED).build();
}
翻译自: https://www.javacodegeeks.com/2013/06/builder-pattern-good-for-code-great-for-tests.html
fusion构建器代码语法