如何使用充血模型实现防弹代码 - DZone Java

19-04-27 banq
                   

了解有关在Java应用程序中通过使用充血模型+构建器等设计器模式设计防弹代码的方法。

毫无疑问,优秀的编码实践带来了诸多好处,例如干净的代码,易于维护以及流畅的API。但是,最佳实践是否有助于数据完整性?

本贴主要涉及新的存储技术,例如NoSQL数据库,它们没有开发人员在使用SQL模式时通常会有的原生验证。

干净代码是一个好主题  它是将对象行为公开和数据隐藏,这与结构化编程不同,这篇文章目的是解释使用充血模型而不是失血模型获得数据完整性和防弹bulletproof代码的好处。

需求用例

这篇文章将创建一个系统,将足球运动员分成一个团队; 该系统的规则是:

  • 玩家的名字是必需的
  • 所有球员必须有一个位置(守门员,前锋,后卫和中场)。
  • 球员在球队中进行的目标计数器
  • 联系电子邮件
  • 一个团队有球员,并根据需要命名
  • 一支球队无法处理超过二十名球员

根据收集的信息,有第一个草案代码版本:

import java.math.BigDecimal;
public class Player {
    String name;
    Integer start;
    Integer end;
    String email;
    String position;
    Integer gols;
    BigDecimal salary;
}
public class Team {
    String name;
    List<Player> players;
}

这里球员只能有一个固定的位置,需要重构,我们将使用 枚举替代String类型的位置position。

public enum Position {
    GOALKEEPER, DEFENDER, MIDFIELDER, FORWARD;
}

对象的封装

下一步是关于安全性和封装:目标是最小化可访问性,因此只需将所有字段定义为私有,那么下一步是啥?使用public公开化  getter 和  setter 方法?方法的访问方式默认应该是protected,这是基于封装考虑的,考虑本文:

  • 在系统示例中,球员不会更改电子邮件,姓名和职位。因此,它不需要setter方法。
  • 最后一年last year的字段表示玩家何时按合同离开球队。当它是可选时,意味着没有期望球员离开俱乐部。setter方法是必需的,但last year离职期必须等于或大于入职两份。此外,在1863年足球出生之前的球员是无法玩足球比赛。
  • 只有团队可以处理它的球员; 它必须是紧耦合(高聚合)

在  Team 类中,有一个用于添加球员的方法;getter方法可以返回团队中的所有球员。添加球员必须验证,例如不能添加空球员或不能对于于20个球员。对getter返回集合的关键点是直接返回集合实例时,客户端可能会使用该方法直接将新元素写入集合,例如clean,add等,因此要解决封装问题,一个好的做法是返回一个只读实例,例如unmodifiableList

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
public class Team {
    static final int SIZE = 20;
    private String name;
    private List<Player> players = new ArrayList<>();
    @Deprecated
    Team() {
    }
    private Team(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
    public void add(Player player) {
        Objects.requireNonNull(player, "player is required");
        if (players.size() == SIZE) {
            throw new IllegalArgumentException("The team is full");
        }
        this.players.add(player);
    }
    public List<Player> getPlayers() {
        return Collections.unmodifiableList(players);
    }
    public static Team of(String name) {
        return new Team(Objects.requireNonNull(name, "name is required"));
    }
}

下一步是关于Player类设计,所有字段都有一个getter 方法,end字段除外:

import java.math.BigDecimal;
import java.util.Objects;
import java.util.Optional;
public class Player {
    private String name;
    private Integer start;
    private Integer end;
    private String email;
    private Position position;
    private BigDecimal salary;
    private int goal = 0;
    public String getName() {
        return name;
    }
    public Integer getStart() {
        return start;
    }
    public String getEmail() {
        return email;
    }
    public Position getPosition() {
        return position;
    }
    public BigDecimal getSalary() {
        return salary;
    }
    public Optional<Integer> getEnd() {
        return Optional.ofNullable(end);
    }
    public void setEnd(Integer end) {
        if (end != null && end <= start) {
            throw new IllegalArgumentException("the last year of a player must be equal or higher than the start.");
        }
        this.end = end;
    }
}
    public int getGoal() {
        return goal;
    }
   public void goal() {
       goal++;
}

getEnd()使用Optional返回一个可能为空的字段,setEnd字段用于更新该球员离职情况,当然离职日期不能大于入职日期。(banq注:使用Lombok时会忽略这个问题)

实例创建

前面讨论了public和private以及protected的纠结使用,现在该讨论实例创建了,首先我们可能会创建一个接收所有参数的构造函数,这适合Team类,因为它有一个name参数,但是在球员Player中会有几个问题:

  1. 首先是参数数量; 由于几个原因,多个构造函数并不是一个好习惯。例如,如果相同类型的参数太多,则在更改顺序时可能会出错。
  2. 第二个是关于这些验证的复杂性。

两个步骤解决:

第一步是类型定义。当一个对象具有诸如金钱,日期之类的巨大复杂性时,使用类型定义是有意义的。下面是邮件类型:

import java.util.Objects;
import java.util.function.Supplier;
import java.util.regex.Pattern;
public final class Email implements Supplier<String> {
    private static final String EMAIL_PATTERN =
            "^[_A-Za-z0-9-\\+]+(\\.[_A-Za-z0-9-]+)*@"
                    + "[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$";
    private static final Pattern PATTERN = Pattern.compile(EMAIL_PATTERN);
    private final String value;
    @Override
    public String get() {
        return value;
    }
    private Email(String value) {
        this.value = value;
    }
    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        Email email = (Email) o;
        return Objects.equals(value, email.value);
    }
    @Override
    public int hashCode() {
        return Objects.hashCode(value);
    }
    @Override
    public String toString() {
        return value;
    }
    public static Email of(String value) {
        Objects.requireNonNull(value, "o valor é obrigatório");
        if (!PATTERN.matcher(value).matches()) {
            throw new IllegalArgumentException("Email nao válido");
        }
        return new Email(value);
    }
}

创建了电子邮件类型后,我们有了Player类的新版本:

import javax.money.MonetaryAmount;
import java.time.Year;
import java.util.Objects;
import java.util.Optional;

public class Player {
    private String id;
    private String name;
    private Year start;
    private Year end;
    private Email email;
    private Position position;
    private MonetaryAmount salary;
//...
}

构建器模式

Builder模式遵循负责创建球员实例的责任,它避免了更改输入参数顺序可能导致的错误。

通常我们还是需要一个默认构造函数,将Deprecated 注释放在此构造函数上以显示它不是推荐的方法,内部类适合用于制造构建器,因为它可以创建仅访问球员构建器的私有构造函数。

import javax.money.MonetaryAmount;
import java.time.Year;
import java.util.Objects;
import java.util.Optional;
public class Player {
    static final Year SOCCER_BORN = Year.of(1863);
    //hide
    private Player(String name, Year start, Year end, Email email, Position position, MonetaryAmount salary) {
        this.name = name;
        this.start = start;
        this.end = end;
        this.email = email;
        this.position = position;
        this.salary = salary;
    }
    @Deprecated
    Player() {
    }
    public static PlayerBuilder builder() {
        return new PlayerBuilder();
    }
    public static class PlayerBuilder {
        private String name;
        private Year start;
        private Year end;
        private Email email;
        private Position position;
        private MonetaryAmount salary;
        private PlayerBuilder() {
        }
        public PlayerBuilder withName(String name) {
            this.name = Objects.requireNonNull(name, "name is required");
            return this;
        }
        public PlayerBuilder withStart(Year start) {
            Objects.requireNonNull(start, "start is required");
            if (Year.now().isBefore(start)) {
                throw new IllegalArgumentException("you cannot start in the future");
            }
            if (SOCCER_BORN.isAfter(start)) {
                throw new IllegalArgumentException("Soccer was not born on this time");
            }
            this.start = start;
            return this;
        }
        public PlayerBuilder withEnd(Year end) {
            Objects.requireNonNull(end, "end is required");
            if (start != null && start.isAfter(end)) {
                throw new IllegalArgumentException("the last year of a player must be equal or higher than the start.");
            }
            if (SOCCER_BORN.isAfter(end)) {
                throw new IllegalArgumentException("Soccer was not born on this time");
            }
            this.end = end;
            return this;
        }
        public PlayerBuilder withEmail(Email email) {
            this.email = Objects.requireNonNull(email, "email is required");
            return this;
        }
        public PlayerBuilder withPosition(Position position) {
            this.position = Objects.requireNonNull(position, "position is required");
            return this;
        }
        public PlayerBuilder withSalary(MonetaryAmount salary) {
            Objects.requireNonNull(salary, "salary is required");
            if (salary.isNegativeOrZero()) {
                throw new IllegalArgumentException("A player needs to earn money to play; otherwise, it is illegal.");
            }
            this.salary = salary;
            return this;
        }
        public Player build() {
            Objects.requireNonNull(name, "name is required");
            Objects.requireNonNull(start, "start is required");
            Objects.requireNonNull(email, "email is required");
            Objects.requireNonNull(position, "position is required");
            Objects.requireNonNull(salary, "salary is required");
            return new Player(name, start, end, email, position, salary);
        }
    }
}

根据此原则使用构建器模式,Java开发人员知道实例何时存在并具有有效信息:

 CurrencyUnit usd = Monetary.getCurrency(Locale.US);
     MonetaryAmount salary = Money.of(1 _000_000, usd);
     Email email = Email.of("marta@marta.com");
     Year start = Year.now();
     Player marta = Player.builder().withName("Marta")
         .withEmail(email)
         .withSalary(salary)
         .withStart(start)
         .withPosition(Position.FORWARD)
         .build();

Team类不需要了,因为它已经很平滑了:

Team bahia = Team.of("Bahia");
  Player marta = Player.builder().withName("Marta")
      .withEmail(email)
      .withSalary(salary)
      .withStart(start)
      .withPosition(Position.FORWARD)
      .build();
  bahia.add(marta);

当Java开发人员谈论验证时,无法避开实现验证的Java规范:Bean Validation。这使得Java开发人员可以更方便地使用注释创建验证。至关重要的是要指出BV不会使POO概念无效。换句话说,避免松散耦合,SOLID原则仍然有效,而不是放弃那些概念。

因此,BV可以仔细检查验证或执行验证  Builder 以返回实例,只有它传递了验证。

换句话说,SOLID原则仍然有效,因此,BV可以仔细检查验证或执行验证  Builder以返回实例,只有它传递了验证。

import javax.money.MonetaryAmount;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.PastOrPresent;
import javax.validation.constraints.PositiveOrZero;
import java.time.Year;
import java.util.Objects;
import java.util.Optional;
public class Player {
    static final Year SOCCER_BORN = Year.of(1863);
    @NotBlank
    private String name;
    @NotNull
    @PastOrPresent
    private Year start;
    @PastOrPresent
    private Year end;
    @NotNull
    private Email email;
    @NotNull
    private Position position;
    @NotNull
    private MonetaryAmount salary;
    @PositiveOrZero
    private int goal = 0;
    //continue
}
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
public class Team {
    static final int SIZE = 20;
    @NotBlank
    private String name;
    @NotNull
    @Size(max = SIZE)
    private List<Player> players = new ArrayList<>();
    //continue
}

总而言之,本文演示了如何使用最佳设计实践使代码防弹。此外,我们同时获得对象和数据完整性。这些技术与存储技术无关 - 开发人员可以在任何企业软件中使用这些原则。重要的是说测试是必不可少的,但这超出了文章的范围。

可以在GitHub上找到源代码。

                   

3