了解有关在Java应用程序中通过使用充血模型+构建器等设计器模式设计防弹代码的方法。
毫无疑问,优秀的编码实践带来了诸多好处,例如干净的代码,易于维护以及流畅的API。但是,最佳实践是否有助于数据完整性?
本贴主要涉及新的存储技术,例如NoSQL数据库,它们没有开发人员在使用SQL模式时通常会有的原生验证。
干净代码 是一个好主题 它是将对象行为公开和数据隐藏,这与结构化编程不同,这篇文章目的是解释使用充血模型而不是失血模型获得数据完整性和防弹bulletproof代码的好处。
需求用例
这篇文章将创建一个系统,将足球运动员分成一个团队; 该系统的规则是:
根据收集的信息,有第一个草案代码版本:
<b>import</b> java.math.BigDecimal;
<b>public</b> <b>class</b> Player {
String name;
Integer start;
Integer end;
String email;
String position;
Integer gols;
BigDecimal salary;
}
<b>public</b> <b>class</b> Team {
String name;
List<Player> players;
}
这里球员只能有一个固定的位置,需要重构,我们将使用 枚举 替代String类型的位置position。
<b>public</b> enum Position {
GOALKEEPER, DEFENDER, MIDFIELDER, FORWARD;
}
对象的封装
下一步是关于安全性和封装:目标是最小化可访问性,因此只需将所有字段定义为私有,那么下一步是啥?使用public公开化 getter 和 setter 方法?方法的访问方式默认应该是protected,这是基于封装考虑的,考虑本文:
在 Team 类中,有一个用于添加球员的方法;getter方法可以返回团队中的所有球员。添加球员必须验证,例如不能添加空球员或不能对于于20个球员。对getter返回集合的关键点是直接返回集合实例时,客户端可能会使用该方法直接将新元素写入集合,例如clean,add等,因此要解决封装问题,一个好的做法是返回一个只读实例,例如 unmodifiableList :
<b>import</b> java.util.ArrayList;
<b>import</b> java.util.Collections;
<b>import</b> java.util.List;
<b>import</b> java.util.Objects;
<b>public</b> <b>class</b> Team {
<b>static</b> <b>final</b> <b>int</b> SIZE = 20;
<b>private</b> String name;
<b>private</b> List<Player> players = <b>new</b> ArrayList<>();
@Deprecated
Team() {
}
<b>private</b> Team(String name) {
<b>this</b>.name = name;
}
<b>public</b> String getName() {
<b>return</b> name;
}
<b>public</b> <b>void</b> add(Player player) {
Objects.requireNonNull(player, <font>"player is required"</font><font>);
<b>if</b> (players.size() == SIZE) {
<b>throw</b> <b>new</b> IllegalArgumentException(</font><font>"The team is full"</font><font>);
}
<b>this</b>.players.add(player);
}
<b>public</b> List<Player> getPlayers() {
<b>return</b> Collections.unmodifiableList(players);
}
<b>public</b> <b>static</b> Team of(String name) {
<b>return</b> <b>new</b> Team(Objects.requireNonNull(name, </font><font>"name is required"</font><font>));
}
}
</font>
下一步是关于Player类设计,所有字段都有一个getter 方法,end字段除外:
<b>import</b> java.math.BigDecimal;
<b>import</b> java.util.Objects;
<b>import</b> java.util.Optional;
<b>public</b> <b>class</b> Player {
<b>private</b> String name;
<b>private</b> Integer start;
<b>private</b> Integer end;
<b>private</b> String email;
<b>private</b> Position position;
<b>private</b> BigDecimal salary;
<b>private</b> <b>int</b> goal = 0;
<b>public</b> String getName() {
<b>return</b> name;
}
<b>public</b> Integer getStart() {
<b>return</b> start;
}
<b>public</b> String getEmail() {
<b>return</b> email;
}
<b>public</b> Position getPosition() {
<b>return</b> position;
}
<b>public</b> BigDecimal getSalary() {
<b>return</b> salary;
}
<b>public</b> Optional<Integer> getEnd() {
<b>return</b> Optional.ofNullable(end);
}
<b>public</b> <b>void</b> setEnd(Integer end) {
<b>if</b> (end != <b>null</b> && end <= start) {
<b>throw</b> <b>new</b> IllegalArgumentException(<font>"the last year of a player must be equal or higher than the start."</font><font>);
}
<b>this</b>.end = end;
}
}
<b>public</b> <b>int</b> getGoal() {
<b>return</b> goal;
}
<b>public</b> <b>void</b> goal() {
goal++;
}
</font>
getEnd()使用Optional返回一个可能为空的字段,setEnd字段用于更新该球员离职情况,当然离职日期不能大于入职日期。(banq注:使用Lombok时会忽略这个问题)
实例创建
前面讨论了public和private以及protected的纠结使用,现在该讨论实例创建了,首先我们可能会创建一个接收所有参数的构造函数,这适合Team类,因为它有一个name参数,但是在球员Player中会有几个问题:
两个步骤解决:
第一步是类型定义。当一个对象具有诸如金钱,日期之类的巨大复杂性时,使用类型定义是有意义的。下面是邮件类型:
<b>import</b> java.util.Objects;
<b>import</b> java.util.function.Supplier;
<b>import</b> java.util.regex.Pattern;
<b>public</b> <b>final</b> <b>class</b> Email implements Supplier<String> {
<b>private</b> <b>static</b> <b>final</b> String EMAIL_PATTERN =
<font>"^[_A-Za-z0-9-//+]+(//.[_A-Za-z0-9-]+)*@"</font><font>
+ </font><font>"[A-Za-z0-9-]+(//.[A-Za-z0-9]+)*(//.[A-Za-z]{2,})$"</font><font>;
<b>private</b> <b>static</b> <b>final</b> Pattern PATTERN = Pattern.compile(EMAIL_PATTERN);
<b>private</b> <b>final</b> String value;
@Override
<b>public</b> String get() {
<b>return</b> value;
}
<b>private</b> Email(String value) {
<b>this</b>.value = value;
}
@Override
<b>public</b> <b>boolean</b> equals(Object o) {
<b>if</b> (<b>this</b> == o) {
<b>return</b> <b>true</b>;
}
<b>if</b> (o == <b>null</b> || getClass() != o.getClass()) {
<b>return</b> false;
}
Email email = (Email) o;
<b>return</b> Objects.equals(value, email.value);
}
@Override
<b>public</b> <b>int</b> hashCode() {
<b>return</b> Objects.hashCode(value);
}
@Override
<b>public</b> String toString() {
<b>return</b> value;
}
<b>public</b> <b>static</b> Email of(String value) {
Objects.requireNonNull(value, </font><font>"o valor é obrigatório"</font><font>);
<b>if</b> (!PATTERN.matcher(value).matches()) {
<b>throw</b> <b>new</b> IllegalArgumentException(</font><font>"Email nao válido"</font><font>);
}
<b>return</b> <b>new</b> Email(value);
}
}
</font>
创建了电子邮件类型后,我们有了Player类的新版本:
<b>import</b> javax.money.MonetaryAmount;
<b>import</b> java.time.Year;
<b>import</b> java.util.Objects;
<b>import</b> java.util.Optional;
<b>public</b> <b>class</b> Player {
<b>private</b> String id;
<b>private</b> String name;
<b>private</b> Year start;
<b>private</b> Year end;
<b>private</b> Email email;
<b>private</b> Position position;
<b>private</b> MonetaryAmount salary;
<font><i>//...</i></font><font>
}
</font>
构建器模式
Builder模式遵循负责创建球员实例的责任,它避免了更改输入参数顺序可能导致的错误。
通常我们还是需要一个默认构造函数,将Deprecated 注释放在此构造函数上以显示它不是推荐的方法,内部类适合用于制造构建器,因为它可以创建仅访问球员构建器的私有构造函数。
<b>import</b> javax.money.MonetaryAmount;
<b>import</b> java.time.Year;
<b>import</b> java.util.Objects;
<b>import</b> java.util.Optional;
<b>public</b> <b>class</b> Player {
<b>static</b> <b>final</b> Year SOCCER_BORN = Year.of(1863);
<font><i>//hide</i></font><font>
<b>private</b> Player(String name, Year start, Year end, Email email, Position position, MonetaryAmount salary) {
<b>this</b>.name = name;
<b>this</b>.start = start;
<b>this</b>.end = end;
<b>this</b>.email = email;
<b>this</b>.position = position;
<b>this</b>.salary = salary;
}
@Deprecated
Player() {
}
<b>public</b> <b>static</b> PlayerBuilder builder() {
<b>return</b> <b>new</b> PlayerBuilder();
}
<b>public</b> <b>static</b> <b>class</b> PlayerBuilder {
<b>private</b> String name;
<b>private</b> Year start;
<b>private</b> Year end;
<b>private</b> Email email;
<b>private</b> Position position;
<b>private</b> MonetaryAmount salary;
<b>private</b> PlayerBuilder() {
}
<b>public</b> PlayerBuilder withName(String name) {
<b>this</b>.name = Objects.requireNonNull(name, </font><font>"name is required"</font><font>);
<b>return</b> <b>this</b>;
}
<b>public</b> PlayerBuilder withStart(Year start) {
Objects.requireNonNull(start, </font><font>"start is required"</font><font>);
<b>if</b> (Year.now().isBefore(start)) {
<b>throw</b> <b>new</b> IllegalArgumentException(</font><font>"you cannot start in the future"</font><font>);
}
<b>if</b> (SOCCER_BORN.isAfter(start)) {
<b>throw</b> <b>new</b> IllegalArgumentException(</font><font>"Soccer was not born on this time"</font><font>);
}
<b>this</b>.start = start;
<b>return</b> <b>this</b>;
}
<b>public</b> PlayerBuilder withEnd(Year end) {
Objects.requireNonNull(end, </font><font>"end is required"</font><font>);
<b>if</b> (start != <b>null</b> && start.isAfter(end)) {
<b>throw</b> <b>new</b> IllegalArgumentException(</font><font>"the last year of a player must be equal or higher than the start."</font><font>);
}
<b>if</b> (SOCCER_BORN.isAfter(end)) {
<b>throw</b> <b>new</b> IllegalArgumentException(</font><font>"Soccer was not born on this time"</font><font>);
}
<b>this</b>.end = end;
<b>return</b> <b>this</b>;
}
<b>public</b> PlayerBuilder withEmail(Email email) {
<b>this</b>.email = Objects.requireNonNull(email, </font><font>"email is required"</font><font>);
<b>return</b> <b>this</b>;
}
<b>public</b> PlayerBuilder withPosition(Position position) {
<b>this</b>.position = Objects.requireNonNull(position, </font><font>"position is required"</font><font>);
<b>return</b> <b>this</b>;
}
<b>public</b> PlayerBuilder withSalary(MonetaryAmount salary) {
Objects.requireNonNull(salary, </font><font>"salary is required"</font><font>);
<b>if</b> (salary.isNegativeOrZero()) {
<b>throw</b> <b>new</b> IllegalArgumentException(</font><font>"A player needs to earn money to play; otherwise, it is illegal."</font><font>);
}
<b>this</b>.salary = salary;
<b>return</b> <b>this</b>;
}
<b>public</b> Player build() {
Objects.requireNonNull(name, </font><font>"name is required"</font><font>);
Objects.requireNonNull(start, </font><font>"start is required"</font><font>);
Objects.requireNonNull(email, </font><font>"email is required"</font><font>);
Objects.requireNonNull(position, </font><font>"position is required"</font><font>);
Objects.requireNonNull(salary, </font><font>"salary is required"</font><font>);
<b>return</b> <b>new</b> Player(name, start, end, email, position, salary);
}
}
}
</font>
根据此原则使用构建器模式,Java开发人员知道实例何时存在并具有有效信息:
CurrencyUnit usd = Monetary.getCurrency(Locale.US);
MonetaryAmount salary = Money.of(1 _000_000, usd);
Email email = Email.of(<font>"marta@marta.com"</font><font>);
Year start = Year.now();
Player marta = Player.builder().withName(</font><font>"Marta"</font><font>)
.withEmail(email)
.withSalary(salary)
.withStart(start)
.withPosition(Position.FORWARD)
.build();
</font>
Team类不需要了,因为它已经很平滑了:
Team bahia = Team.of(<font>"Bahia"</font><font>);
Player marta = Player.builder().withName(</font><font>"Marta"</font><font>)
.withEmail(email)
.withSalary(salary)
.withStart(start)
.withPosition(Position.FORWARD)
.build();
bahia.add(marta);
</font>
当Java开发人员谈论验证时,无法避开实现验证的Java规范: Bean Validation 。这使得Java开发人员可以更方便地 使用注释创建验证 。至关重要的是要指出BV不会使POO概念无效。换句话说,避免松散耦合, SOLID 原则仍然有效,而不是放弃那些概念。
因此,BV可以仔细检查验证或执行验证 Builder 以返回实例,只有它传递了验证。
换句话说, SOLID 原则仍然有效,因此,BV可以仔细检查验证或执行验证 Builder以返回实例,只有它传递了验证。
<b>import</b> javax.money.MonetaryAmount;
<b>import</b> javax.validation.constraints.NotBlank;
<b>import</b> javax.validation.constraints.NotNull;
<b>import</b> javax.validation.constraints.PastOrPresent;
<b>import</b> javax.validation.constraints.PositiveOrZero;
<b>import</b> java.time.Year;
<b>import</b> java.util.Objects;
<b>import</b> java.util.Optional;
<b>public</b> <b>class</b> Player {
<b>static</b> <b>final</b> Year SOCCER_BORN = Year.of(1863);
@NotBlank
<b>private</b> String name;
@NotNull
@PastOrPresent
<b>private</b> Year start;
@PastOrPresent
<b>private</b> Year end;
@NotNull
<b>private</b> Email email;
@NotNull
<b>private</b> Position position;
@NotNull
<b>private</b> MonetaryAmount salary;
@PositiveOrZero
<b>private</b> <b>int</b> goal = 0;
<font><i>//continue</i></font><font>
}
<b>import</b> javax.validation.constraints.NotBlank;
<b>import</b> javax.validation.constraints.NotNull;
<b>import</b> javax.validation.constraints.Size;
<b>import</b> java.util.ArrayList;
<b>import</b> java.util.Collections;
<b>import</b> java.util.List;
<b>import</b> java.util.Objects;
<b>public</b> <b>class</b> Team {
<b>static</b> <b>final</b> <b>int</b> SIZE = 20;
@NotBlank
<b>private</b> String name;
@NotNull
@Size(max = SIZE)
<b>private</b> List<Player> players = <b>new</b> ArrayList<>();
</font><font><i>//continue</i></font><font>
}
</font>
总而言之,本文演示了如何使用最佳设计实践使代码防弹。此外,我们同时获得对象和数据完整性。这些技术与存储技术无关 - 开发人员可以在任何企业软件中使用这些原则。重要的是说测试是必不可少的,但这超出了文章的范围。
可以在 GitHub上 找到源代码。