DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Related

  • Hints for Unit Testing With AssertJ
  • How to Merge HTML Documents in Java
  • The Future of Java and AI: Coding in 2025
  • Tired of Spring Overhead? Try Dropwizard for Your Next Java Microservice

Trending

  • Navigating the LLM Landscape: A Comparative Analysis of Leading Large Language Models
  • How Large Tech Companies Architect Resilient Systems for Millions of Users
  • Metrics at a Glance for Production Clusters
  • A Modern Stack for Building Scalable Systems
  1. DZone
  2. Coding
  3. Java
  4. Designing Bulletproof Code

Designing Bulletproof Code

Learn more about designing bulletproof code in your Java applications.

By 
Otavio Santana user avatar
Otavio Santana
DZone Core CORE ·
Updated Apr. 24, 19 · Tutorial
Likes (38)
Comment
Save
Tweet
Share
32.4K Views

Join the DZone community and get the full member experience.

Join For Free

There 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:

  1. 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.
  2. 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.

Java (programming language) Coding best practices

Opinions expressed by DZone contributors are their own.

Related

  • Hints for Unit Testing With AssertJ
  • How to Merge HTML Documents in Java
  • The Future of Java and AI: Coding in 2025
  • Tired of Spring Overhead? Try Dropwizard for Your Next Java Microservice

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!