In object-oriented languages, types can be used beyond just ensuring that data is say an integer or string. They can also be used to constrain encapsulated data and makes sure it conforms to our expectations. Using visibility modifiers in conjuction with specialized types can ensure that rules of our system accurately reflect our expectations.

The following is an example of how we’d use this idea to enforce constraints on a user password.

A Typical Example

Imagine you’re working on a new application. You need to model the users of your application and they need to be able to change their password. How do you do it?

A vast majority of Java programmers might write it like this:

package myapp.users;

@Data               // Maybe some lombok
@AllArgsConstructor // Maybe MORE lombok...
@Entity             // Maybe some JPA/ORM annotation
class User {

  private String email;

  private String password;

}

If they wanted to update the password of that user, they could write something like this:

// Get the 'user' object from the persistence layer:
final var user = entityManager.findByEmail<User>(userEmail);

// Encode the new password.
final var hashedPassword = passwordEncoder.encode(newPassword);

// Finally we can update the password.
user.setPassword(hashedPassword);

What’s wrong with this?

Take a second look at the User class. Ask yourself: if I gave someone this user object, would they be able to figure out the rules of a password?

The answer to this should be: heck no. A password isn’t just a string. If I was given this user object out of the blue, I would not know whether password is encoded or not and therefore, we shouldn’t accept any old string. We should be accepting an encoded password! So let’s change our model to reflect this knowledge:

// ... annotations ...
public class User {

  private String email;

  private EncodedPassword password;

  public void updatePassword(EncodedPassword encodedPassword) {
    this.password = encodedPassword;
  }
}

This code change forces all developers working on the code base to first encode the password. Taking advantage of visibility keywords, we can guarantee that EncodedPassword objects can’t be created ad-hoc like this:

final notEncoded = new EncodedPassword("password123!");

Using Visibility to Our Advantage

Now within the same package as the user(myapp.users), we’ll create a new class:

package myapp.users;

public final class EncodablePassword {

  private final String password;
  private final PasswordEncoder encoder;

  public EncodablePassword(String password, PasswordEncoder encoder) {
    this.password = password;
    this.encoder = encoder;
  }

  public EncodedPassword encode() {
    return new EncodedPassword(encoder.encode(password));
  }
}

The EncodedPassword class will have a constructor with package-private visibility. This means that the only possible way to retrieve an EncodedPassword is through an EncodablePassword which forces encoding before creating the object.

package myapp.users;

public final class EncodedPassword {

  private final String password;

  // No visibility keyword specified, thus the constructor is only visible in this package.
  EncodedPassword(String password) {
    this.password = password;
  }

  public String password() {
    return this.password;
  }

}

Closing Words

The example provided here is similar to what the functional programming crowd refers to as “smart constructors”. Smart constructors are a good way to ensure constraints on unverified data that’s entering a system and force a sort of “checkpoint” before data goes into your model. In an OOP language, they’re a bit more verbose but still worth it in my opinion.

I first read about this idea in Scott Wlaschin’s book Domain Modelling Made Functional. The book’s examples are all in F#, but there’s definitely lessons to be learned that can be adapted to languages such as Java.