Designing Bulletproof Code
Learn more about designing bulletproof code in your Java applications.
Join the DZone community and get the full member experience.
Join For FreeThere is no doubt about the benefits good coding practices bring, such as clean code, easy maintaining, and a fluent API. However, do best practices help with data integrity? This discussion came up, mainly, with new storage technologies, such as the NoSQL database, that do not have native validation that the developer usually faces when working to SQL schema. A good topic that covers clean code is whose objects expose behavior and hide data, which differs from structured programming. The goal of this post is to explain the benefits of using a rich model against the anemic model to get data integrity and bulletproof code.
As mentioned above, an ethical code design also has an advantage in performance, once it saves requests to a database that has schema to check the validation. Thus, the code is agnostic of a database so that it will work in any database. However, to follow these principles, it requires both good OOP concepts and the business rules to make also a ubiquitous language. To make it smoother, this post will create a system that sorts a soccer player into a team; the rules of this system are:
- Player's name is required
- All players must have a position (goalkeeper, forward, defender, and midfielder).
- Goals counter whose the player does on the team
- Contact's email
- A team has players and name both as required
- A team cannot handle more than twenty players
With the information that was collected, there is the first draft code version:
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;
}
As the first refactoring, once there are fixed positions, it does not fit in very well on String. To fix it, we will use enumeration instead of String.
public enum Position {
GOALKEEPER, DEFENDER, MIDFIELDER, FORWARD;
}
Formerly introduced to the module fields, the next step is about security and encapsulation. Using Effective Java as a reference, the goal is to minimize the accessibility, thus just define all fields as private. Next, let's put public getter
and setter
methods, right? Wrong! When a Java developer uses getters
and setters
unrestrained, it has the same effect on a public's field, causing loose coupling.
An accessor method must be used as the last resource that includes don't create it as public visibility, in other words, as either default or protected, based on the encapsulation concern, it is possible to make the following conclusions:
- In the system sample, players do not change email, name, and position. Therefore, it does not need to create a
setter
method in these fields. - The last year field indicates when the player will leave the team by contract. When it is optional, it means that there is no expectation to the player leave the club. The setter method is required, but the last year must be equals or higher than a year of start. Also, a player cannot start to play before when soccer was born in 1863.
- Just the team can handle a player; it must be a tight coupling.
In the Team
class, there is a method used to add a player and a getter
method returns all players from the team. The player must have a validation such as it cannot add a null player or cannot be higher than twenty players. A critical point on the getter
to a collection is when directly returning an instance, the client might use the method to write directly such as clean, add, and so on. To solve the encapsulation, a good practice is to return a read-only instance such as 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"));
}
}
About the Player
's class, all fields will have a getter
method. The end field is an exception; once it may not exist on some point, a developer can return this attribute using an Optional to handle it instead of a null element. Still, on the end field, thesetter
method is required once it either sets an end value or updates the end because the player renewed the contract and the validation is vital to guarantee the object integrity.
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++;
}
With the access method defined, the next step is the strategy of instance creation. Almost always, attributes are required for a new instance; the first move might create a constructor that receives all parameters. That fits on the Team
class because it has a name parameter; however, in the player, there are several issues:
- The first is on the parameters quantity; a polyadic is not a good practice for several reasons. For example, with too many arguments of the same type, it is possible to make a mistake when changing the order.
- The second one is on the complexity of these validations.
An excellent way to solve it has two steps:
The first step is the type definition. This makes sense when an object has a colossal intricacy such as money, date. There is a nice post on When to Make a Type, which explains more on how to enhance code with this resource. A programmatic developer knows that reinventing the wheel is also a code smell. Therefore, the code will use the Date and Time API from Java 8 and JSR 354: Money-API. I just left the email type, as show the code below:
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, "value is required");
if (!PATTERN.matcher(value).matches()) {
throw new IllegalArgumentException("Email is not valid");
}
return new Email(value);
}
}
With the email type created, we have a new version of the Player
's class:
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;
//...
}
The last step is a Builder design pattern, which follows the single of responsibility to hold the duty to create a player instance. Furthermore, it avoids a mistake that changes the parameter order. Usually, mapper frameworks require a default constructor because it goes through reflection. Put the Deprecated
annotation on this constructor to show that it is not a recommended method. An inner class fits in a builder because it can create a private constructor that only accesses the player builder.
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);
}
}
}
Using a builder pattern on this principle, a Java developer knows when the instance exists and has valid information.
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();
As mentioned before Team
's class does not need once it is a smooth one.
Team bahia = Team.of("Bahia");
Player marta = Player.builder().withName("Marta")
.withEmail(email)
.withSalary(salary)
.withStart(start)
.withPosition(Position.FORWARD)
.build();
bahia.add(marta);
When Java developers talk about validation, it is essential to speak of the Java specification that does it, the Bean Validation, BV, that makes more accessible to a Java developer to create validation using annotation. It is critical to point out that BV does not invalidate POO concepts. In other words, avoid loose coupling, the SOLID principle is still valid instead of putting those concepts away. Therefore, BV can either double-check validation or execute the validation on the Builder
to return an instance, only if it passes on the validation.
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
}
To conclude, this article demonstrates how to make code bulletproof using best design practices. Furthermore, we gain both object and data integrity at the same time. These techniques are agnostic from storage technology — the developer can use these principles in any enterprise software. That is important to say the tests are essential, but that is out of the article's scope. On the next part of this topic, we will cover integration between this coding design with the database.
You can find the source code on GitHub.
Opinions expressed by DZone contributors are their own.
Comments