top of page
Search

Following OOP principles - hermetization (part 1)

Have you ever looked at the code that you had written couple of months ago and asked yourself: "who could leave such a mess here?" Have you ever been so lazy that you didn't think of what accessors/mutators you need, simply hitting "Generate getters and setters" for your entity in IDE? Or maybe you have used lombok's @Getter/@Setter annotations to get the same effect?


Yep, just as I thought!


Well, honestly, I did this way too many times, too.


The problem is that it is not the only crime that we can commit in order to break some of the basic OOP principles.


In this article we will focus on hermetization (information hiding) but other paradigms, like abstraction or encapsulation will be mentioned too, as they all are complementary to each other. In the following paragraphs I will give you an explanation of what is hermetization and how to provide one within Java.


We will discuss common pitfalls and some good practices. You will also find here some philosophical considerations and my own judgements and opinions. I am very open for further discussions. I will walk you through an example of a very poorly written classes, which we will improve step by step, looking at hermetization from numerous perspectives. Enjoy!



Hermetization


Hermetization is about hiding information (implementation details) that shouldn't be visible to clients of a class or a module. As long as we follow the encapsulation paradigm, we enclose information (state) and interface (behavior) in a class. Now an interface (API) is a declaration of how we can interact with a particular object. Should we care about how this interaction is implemented? Should we care about the data representation inside? Should we publish methods modifying internal state, that is supposed to be done automatically? Nope, not at all. We just want to send it a command, get the result, and forget. At the same time we want our internals to stay safe and untouched (intentionally or unintentionally) from the outside.


The less information we expose to the outer world, the less coupled modules we get. Thus, we gain a better separation of classes, which it turn means that we can easily:


  • manipulate the logic/internals inside a class not worrying that we will break our clients,

  • analyse, use, and test such classes, as we have a clear interface

  • reuse them, as independent classes might appear to be useful in some other contexts

  • keep object's state safe.



Example overview


In this article we will focus on a simple business case. We need to provide following things:


  • the ability to create both contact person and a customer,

  • each contact person can have an email address - we want to both present and modify this data,

  • a particular contact person might be assigned to more than one customer

  • each customer can have a name, and a list of contact people

  • we want to store a timestamp of the moment of customers' creation and activation

  • we also want to be able to easily activate a customer and verify if a customer is activated or not

  • we want to present customer's name, contact people, creation date and a flag of activation

  • name and contact person list can be modified in future, but creation date must be set only once during creation phase.


Here is a piece of extremely poorly written code:


public class ContactPerson {
  public long id;
  public String email;
}
public class Customer {
  public long id;
  public String name;
  public Date creationDate;
  public Date activationDate;
  public ArrayList<ContactPerson> contactPeople = new ArrayList<>();
}
public class CustomerService {

  public Customer createCustomer(String name,
                                 ArrayList<ContactPerson> contactPeople) {
    if(contactPeople == null) {
      throw new IllegalArgumentException("Contact people list cannot be null");
    }
    if (StringUtils.isEmpty(name)) {
      throw new IllegalArgumentException("Name cannot be empty");
    }
    final Customer customer = new Customer();
    customer.id = Sequence.nextValue();
    customer.creationDate = new Date();
    customer.name = name;
    customer.contactPeople = contactPeople;
    return customer;
  }

  public ContactPerson createContactPerson(String email) {
    final ContactPerson contactPerson = new ContactPerson();
    contactPerson.id = Sequence.nextValue();
    contactPerson.email = email;
    return contactPerson;
  }

  void activateCustomer(Customer customer) {
    customer.activationDate = new Date();
  }

  boolean isCustomerActive(Customer customer) {
    return customer.activationDate != null;
  }

  void addContactPerson(Customer customer, ContactPerson contactPerson) {
    customer.contactPeople.add(contactPerson);
  }

  void removeContactPerson(Customer customer, ContactPerson contactPerson) {
    customer.contactPeople.removeIf(it -> it.id == contactPerson.id);
  }
}

You can see that we have two model classes and a service fulfilling mentioned business requirements. Let's try to work on this example to make it a shiny one. You can find the final version of this example here.


Please note that we skip here all aspects of concurrency.



Access control, accessors, mutators


The first facility for information hiding that Java gives us is access control mechanism. We have a few options to choose:


  • Private

  • Package-private

  • Protected

  • Public


By default, we are granted a package-private scope which gives us a bit of hermetization out of the box. The language itself suggests that we should keep our modules (packages) independent, reusable - it should be our conscious decision to make a class, a method or a field a public one.

The code from above does what it is supposed to do, but with this approach you face following problems:


  • you cannot change the data representation without modifying client's code - the information about how you store data becomes a part of your API,

  • you cannot perform any extra actions (like validation) when field is being accessed/modified.


It means we have no hermetization at all. The easiest way of dealing with it is to restrict all instance members' visibility to private scope and define their accessors (getters) and mutators (setters) like below:


public class ContactPerson {
  private long id;
  private String email;

  public long getId() {
    return id;
  }

  public void setId(long id) {
    this.id = id;
  }

  public String getEmail() {
    return email;
  }

  public void setEmail(String email) {
    this.email = email;
  }

  @Override
  public String toString() {
    return "ContactPerson{" +
      "id=" + id +
      ", email='" + email + '\'' +
      '}';
  }
}
public class Customer {
  private long id;
  private String name;
  private Date creationDate;
  private Date activationDate;
  private ArrayList<ContactPerson> contactPeople = new ArrayList<>();

  public long getId() {
    return id;
  }

  public void setId(long id) {
    this.id = id;
  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public Date getCreationDate() {
    return creationDate;
  }

  public void setCreationDate(Date creationDate) {
    this.creationDate = creationDate;
  }

  public Date getActivationDate() {
    return activationDate;
  }

  public void setActivationDate(Date activationDate) {
    this.activationDate = activationDate;
  }

  public ArrayList<ContactPerson> getContactPeople() {
    return contactPeople;
  }

  public void setContactPeople(ArrayList<ContactPerson> contactPeople) {
    this.contactPeople = contactPeople;
  }

  @Override
  public String toString() {
    return "Customer{" +
      "id=" + id +
      ", name='" + name + '\'' +
      ", creationDate=" + creationDate +
      ", activationDate=" + activationDate +
      ", contactPeople=" + contactPeople +
      '}';
  }
}

And now our client's code need to be refactored, too:


public class CustomerService {

  public Customer createCustomer(String name,
                                 ArrayList<ContactPerson> contactPeople) {
    if(contactPeople == null) {
      throw new IllegalArgumentException("Contact people list cannot be null");
    }
    if (StringUtils.isEmpty(name)) {
      throw new IllegalArgumentException("Name cannot be empty");
    }
    final Customer customer = new Customer();
    customer.setId(Sequence.nextValue());
    customer.setCreationDate(new Date());
    customer.setName(name);
    customer.setContactPeople(contactPeople);
    return customer;
  }

  public ContactPerson createContactPerson(String email) {
    final ContactPerson contactPerson = new ContactPerson();
    contactPerson.setId(Sequence.nextValue());
    contactPerson.setEmail(email);
    return contactPerson;
  }

  public void activateCustomer(Customer customer) {
    customer.setActivationDate(new Date());
  }

  public boolean isCustomerActive(Customer customer) {
    return customer.getActivationDate() != null;
  }

  public void addContactPerson(Customer customer, ContactPerson contactPerson) {
    customer.getContactPeople().add(contactPerson);
  }

  public void removeContactPerson(Customer customer, long contactPersonId) {
    customer.getContactPeople().removeIf(it -> it.getId() == contactPersonId);
  }
}

You may think now that we are done - our fields are private, accessors and mutators cover implementation details, right? Well, it is a common mistake that we treat objects as data containers. We start with defining a set of fields, and afterwards we declare corresponding getters and setters to each and every field in a class. Then we put the whole logic into some service class making our entity a dumb data structure. As long as a class can manage the values of its fields (e.g. there is no need to call a repository or some external system) it should be done inside this class. In other words, every class should encapsulate both instance fields and business methods, which are nothing more than an abstraction of a real-life domain object, hermetizing implementation details as much as possible. You see - OOP is about composing all paradigms together. Now let's try to answer a few questions, bearing in mind previously defined example's business requirements:


ContactPerson:


  1. Do I really need getters to all fields? Well, I suppose yes - we might want to present customers' contact people information. Okay then, let's leave the getters.

  2. Is ID something that I should set? Do I even need what value should I give it? No. It is something that shouldn't bother us. It should be generated automatically, e.g. by Hibernate. It means that having a setId method we break hermetization! Let's remove this mutator and put its logic to a constructor. For the simplicity of an example - we put a static sequence generator there.


Customer:


  1. Do I really need getters to all fields? Nope. Like we said in the example description, we just want to get information whether a customer is active or not. Exposing getActivationDate method forces our clients to put some logic around activationDate value. It smells badly to me. A customer is able to decide about its activation status using its own fields' values (one field actually). It suggests that we hermetize activation details inside an entity. We simply move isCustomerActive method logic into isActive method inside Customer class, like it is depicted below.

  2. Is ID something I should set? You should know the answer now - it is the same situation like with ContactPerson.

  3. Should I set creationDate in client's code? Well, as the name implies it is a timestamp of object creation and shouldn't be modifiable at any other time. Thus, giving a setter we create a threat that someone will update this value in the future or set something really strange. It is up to entity to decide what time it should set. Let's move it to the constructor, then, and forget about this mutator.

  4. Do I need to set activationDate in client's code? Well, who told you (the client) that we store activation date? Ha! We don't care about data representation again, we just want to be able to activate a customer. What we should do is to remove setActivationDate from the service and create activateCustomer method inside an entity instead.

  5. Do I really need methods that add or remove contact people from my customer in a service class? Well again, collection belongs to a Customer entity, and letting some third parties modify that collection is a crime. Let's move these methods to the entity, too.

  6. Should I keep validations in service? In this case we can validate params without calling external services or resources, so the answer is no. Every condition that must be fulfilled while creating or setting a field should be hermetized inside the entity (in constructors and/or mutators), so that clients don't need to worry about performing such checks. Thus, we will have consistent conditions across all clients. Passing wrong parameters will simply cause an exception.


Uhh, finally... that was tough. Let's see how our code looks now:


public class ContactPerson {
  private long id;
  private String email;

  public ContactPerson() {
    this.id = Sequence.nextValue();
  }

  public long getId() {
    return id;
  }

  public String getEmail() {
    return email;
  }

  public void setEmail(String email) {
    this.email = email;
  }

  @Override
  public String toString() {
    return "ContactPerson{" +
      "id=" + id +
      ", email='" + email + '\'' +
      '}';
  }
}
public class Customer {
  private long id;
  private String name;
  private Date creationDate;
  private Date activationDate;
  private ArrayList<ContactPerson> contactPeople = new ArrayList<>();

  public Customer() {
    this.id = Sequence.nextValue();
    this.creationDate = new Date();
  }

  public long getId() {
    return id;
  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    if (StringUtils.isEmpty(name)) {
      throw new IllegalArgumentException("Name cannot be empty");
    } else {
      this.name = name;
    }
  }

  public Date getCreationDate() {
    return creationDate;
  }

  public ArrayList<ContactPerson> getContactPeople() {
    return contactPeople;
  }

  public void setContactPeople(ArrayList<ContactPerson> contactPeople) {
    if (contactPeople == null) {
      throw new IllegalArgumentException("Contact people list cannot be null");
    } else {
      this.contactPeople = contactPeople;
    }
  }

  public void activate() {
    this.activationDate = new Date();
  }

  public boolean isActive() {
    return this.activationDate != null;
  }

  public void addContactPerson(ContactPerson contactPerson) {
    this.contactPeople.add(contactPerson);
  }

  public void removeContactPerson(long contactPersonId) {
    this.contactPeople.removeIf(it -> it.getId() == contactPersonId);
  }

  @Override
  public String toString() {
    return "Customer{" +
      "id=" + id +
      ", name='" + name + '\'' +
      ", creationDate=" + creationDate +
      ", activationDate=" + activationDate +
      ", contactPeople=" + contactPeople +
      '}';
  }
}
public class CustomerService {

  public Customer createCustomer(String name, ArrayList<ContactPerson> contactPeople) {
    final Customer customer = new Customer();
    customer.setName(name);
    customer.setContactPeople(contactPeople);
    return customer;
  }

  public ContactPerson createContactPerson(String email) {
    final ContactPerson contactPerson = new ContactPerson();
    contactPerson.setEmail(email);
    return contactPerson;
  }
}

What we can see now, is that our entities are not just simple data stores. They have a real behavior. Implementation details are hidden behind constructors, and business methods. Those business methods are nothing more than an abstraction of a real-world behavior of customer and its contact people. You can see now that abstraction, encapsulation and hermetization are complementary to each other. As a result of our refactoring, the CustomerService does nothing more than just creating objects, so we could even remove this class, and implement proper constructors or factory methods. Nice, huh?



Interface vs implementation


Let's have a look at the list of contact people. It is working, right? But don't we tell our client too much with getContactPeople and setContactPeople methods' signatures? I think we are, as we are using a concrete implementation of a java.util.List interface to declare a type of contact people collection. We are sharing yet another implementation detail here. In such situations what we should do is to use an interface (if such interface exists of course) instead - java.util.List in this case. The only place where we can refer to a specific class is object's construction. This way we both hide data representation and create a possibility to change the implementation from an ArrayList to a LinkedList if needed, without modifying our clients. Isn't it cool?


Don't think that you must always follow this approach. It is completely correct to refer to objects via class when one of the following situations apply to your case. Firstly, when you hava a class that does not implement any interface - then you simply have no choice, and must use class as a type.Secondly, when a class belongs to some class hierarchy, then it is recommended to refer to an object via the base class (usually an abstract one). And finally, your class might implement an interface, but contain some additional methods, not existing in mentioned interface. If and only if your client's code need to call this extra methods - then you need to refer to object via this class instead of using its interface.


It is all about providing both hermetization and flexibility.



 
 
 

Recent Posts

See All

Comentarios


© 2025 by Bartek Słota.

bottom of page