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

了解有关在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中会有几个问题:

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

两个步骤解决:

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

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
}

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

读者福利


分享免费学习资料

针对于Java程序员,我这边准备免费的Java架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)

为什么某些人会一直比你优秀,是因为他本身就很优秀还一直在持续努力变得更优秀,而你是不是还在满足于现状内心在窃喜!希望读到这的您能点个小赞和关注下我,以后还会更新技术干货,谢谢您的支持!

资料领取方式:加入Java技术交流群963944895点击加入群聊,私信管理员即可免费领取

怎么提高代码质量?——来自阿里P8架构师的研发经验总结

阿里P8分享Java架构师的学习路线,第六点尤为重要

每个Java开发者应该知道的八个工具

想面试Java架构师?这些最基本的东西你都会了吗?

画个图来找你的核心竞争力,变中年危机为加油站

哪有什么中年危机,不过是把定目标当成了有计划

被裁员不是寒冬重点,重点是怎么破解职业瓶颈

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值