Following OOP Principles: Hermetization in Java
This deep dive into hermetization in Java will cover everything you need to know about hiding information in your projects.
Join the DZone community and get the full member experience.
Join For FreeHave you ever looked at the code that you had written a 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 your 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 and 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 hermetization is and how to provide it within Java. We will also discuss common pitfalls and some good practices. You will also find some philosophical considerations and my own judgements and opinions here.
I am very open for further discussion. I will walk you through an example of 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, which 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, in turn, means that we can easily:
- Manipulate the logic/internals inside a class not worrying that we will break our clients.
- Analyze, 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 an object's state safe.
Example Overview
In this article, we will focus on a simple business case. We need to provide the following things:
- The ability to create both a 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 our customers' creation and activation.
- We also want to be able to easily activate a customer and verify whether a customer is activated or not.
- We want to present a customer's name, contact people, creation date, and a flag of activation.
- The name and contact person list can be modified in the future, but the creation date must be set only once, during the 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.setActivationDate(new Date());
}
boolean isCustomerActive(Customer customer) {
return customer.activationDate != null;
}
void addContactPerson(Customer customer, ContactPerson contactPerson) {
customer.contactPeople.add(contactPerson);
}
void removeContactPerson(ContactPerson contactPerson) {
this.contactPeople.removeIf(it - > it.id == contactPerson.id);
}
}
You can see that we have two model classes and a service fulfilling the 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, and Mutators
The first facility for information hiding that Java gives us is an 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 and 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 the following problems:
- You cannot change the data representation without modifying the 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 a 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 a 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 needs 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 that we are done — our fields are private and accessors and mutators cover the implementation details, right? Well, it is a common mistake to treat objects as data containers. We start by 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 the example's previously defined business requirements:
ContactPerson
:
- 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.
- Is
ID
something that I should set? Do I even need what value I should give it? No. It is something that shouldn't bother us. It should be generated automatically, e.g. by Hibernate. It means that by having asetId
, method we break hermetization! Let's remove this mutator and put its logic into a constructor. For the simplicity of our example, we put a static sequence generator there.
Customer
:
- Do I really need getters to all fields? Nope. Like we said in the example description, we just want to get information about whether a customer is active or not. Exposing the
getActivationDate
method forces our clients to put some logic around theactivationDate
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 ourisCustomerActive
method logic into theisActive
method inside theCustomer
class, like it is depicted below. - Is
ID
something I should set? You should know the answer now — it is the same situation asContactPerson
. - Should I set
creationDate
in the 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 the entity to decide what time it should be set. Let's move it to the constructor and forget about this mutator. - Do I need to set
activationDate
in the client's code? Well, who told you (the client) that we store the activation date? Ha! We don't care about data representation, we just want to be able to activate a customer. What we should do is removesetActivationDate
from the service and create anactivateCustomer
method inside an entity instead. - Do I really need methods that add or remove contact people from my customer in a service class? Well, again, the 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. - 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.
Ugh, 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 a 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 the 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 an interface exists, of course) instead — java.util.List
in this case.
The only place where we can refer to a specific class is the object's construction. This way, we both hide data representation and create a possibility of changing 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 have a class that does not implement any interface, then you simply have no choice and must use the 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 also contain some additional methods not existing in the mentioned interface. If and only if your client's code needs to call these extra methods, then you need to refer to the object via this class instead of using its interface.
It is all about providing both hermetization and flexibility. Let's have a look at the Customer
class now:
public class Customer {
private long id;
private String name;
private Date creationDate;
private Date activationDate;
private List < 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 List < ContactPerson > getContactPeople() {
return contactPeople;
}
public void setContactPeople(List < 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 +
'}';
}
}
toString()
It might look trivial to override the default toString()
method, but my experience shows that the way you print your object details is very vital. First of all, there might be some crazy programmer that will write an application reading your logs, parsing them and calculating something — yep, I saw that on production.
Secondly, lots of people have access to our log files and may read some sensitive information. It means that the toString()
method should be implemented with the same attention as an API. It should expose only information that is accessible programmatically.
It is a common mistake to just hit "Generate toString()" in our IDE because it usually creates a method printing all your private
fields and breaks hermetization by publishing all information about data representation. We have one violation of this rule in the Customer
class then. Below, you can see how our toString()
method should look like:
public class Customer {
...
@Override
public String toString() {
return "Customer{" +
"id=" + id +
", name='" + name + '\'' +
", creationDate=" + creationDate +
", active=" + isActive() + //activation date no longer here!
", contactPeople=" + contactPeople +
'}';
}
}
equals() and hashCode()
Another couple of methods common for all objects are equals()
and hashCode()
. They are also strongly connected with OOP principles, as they are part of your API. Each time objects of your class are considered to be compared or play the role of a key in some data structure, and their equality is based on some logical assumptions, then you should override Object's default equals()
method.
It is said that if you can do something inside your class or a library, do it there. Don't force your clients to implement it on their own because you will spread the logic outside of the class and break hermetization. This problem is similar to what we described in the Access Control, Accessors, and Mutators section while moving the activateCustomer()
and isCustomerActive()
methods from the service to the Customer
class.
Of course, you should also remember that overriding equals()
implies the necessity of implementing hashCode()
, too. As soon as you have both methods, your client doesn't have to worry about deciding how they should compare objects or wonder if they can use them in a hash map — you hide (hermetize) it inside your class. Below, you will find out how equals()
and hashCode()
could look like in our example (let's assume that all instance fields define an object's identity).
Please note that the description of the rules that should be followed while implementing equals()
and hashCode()
are out of the scope of this article.
public class ContactPerson {
...
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final ContactPerson that = (ContactPerson) o;
if (id != that.id) return false;
return email != null ? email.equals(that.email) : that.email == null;
}
@Override
public int hashCode() {
int result = (int)(id ^ (id >>> 32));
result = 31 * result + (email != null ? email.hashCode() : 0);
return result;
}
}
public class Customer {
...
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final Customer customer = (Customer) o;
if (id != customer.id) return false;
if (name != null ? !name.equals(customer.name) : customer.name != null) return false;
if (creationDate != null ? !creationDate.equals(customer.creationDate) : customer.creationDate != null) return false;
if (activationDate != null ? !activationDate.equals(customer.activationDate) : customer.activationDate != null) return false;
return contactPeople != null ? contactPeople.equals(customer.contactPeople) : customer.contactPeople == null;
}
@Override
public int hashCode() {
int result = (int)(id ^ (id >>> 32));
result = 31 * result + (name != null ? name.hashCode() : 0);
result = 31 * result + (creationDate != null ? creationDate.hashCode() : 0);
result = 31 * result + (activationDate != null ? activationDate.hashCode() : 0);
result = 31 * result + (contactPeople != null ? contactPeople.hashCode() : 0);
return result;
}
}
Object References
When we operate on objects, we pass their references back and forth, especially when we pass an object as a method parameter or return it as a method's call result. Having a reference to a mutable object, we can easily update its state. That means that if it is a reference to an instance field of some other object, we may break its hermetization. Let's have a look at our example. Imagine a situation, when our client calls the getCreationDate()
method like below:
Date creationDate = customer.getCreationDate();
creationDate.setTime(1504259400000L);
Did we just find a way of changing the state of our Customer
object not even calling any of its mutators? Exactly. One of the possibilities to defend ourselves from this kind of threat is using immutable objects (check out the next section).
Here, we will focus on another option — defensive copying. Defensive copying is about returning copies (references to copies, to be precise) of our mutable instance fields and working on copies of objects passed as constructor or mutator parameters.
Well, in order to secure our creationDate
field, we will produce a copy. Here are two possible solutions:
- Cloning: Cloning is a para-Java mechanism of creating objects without using constructors. There are lots of dangerous things about it that won't be covered in detail here. It is considered not safe and we should avoid it, but I also think that we should be aware of what can be possibly done via this technique. In a big short, it is about calling Object's protected
clone()
method. In most cases, classes that enable cloning provide a public equivalent of clone(). What you need to know is that you should use it for defensive copying only if the class is declaredfinal
(nobody can extend it). Why is that? Because there is a real threat that a subclass overrides theclone()
method, which does both cloning and registering the reference to the original object somewhere, and we will lose hermetization (more about inheritance in a further section). You must be also aware thatclone()
does just make a shallow copy based on an assignment, which means that all instance fields will be initialized with the=
operator. Thus, if the object you are copying has an instance field that is a reference to some other object, this reference will be copied one to one (the instance field of the object's clone will point to the same place in memory — any change within this object may corrupt all clones, unless you implement deep cloning!). Now, please take a look at this sample cloning (don't use it withDate
in real life, asDate
is not afinal
class):public Date getCreationDate() { return (Date) creationDate.clone(); }
- Copying via constructor or factory method (which may be called a copy constructor): This is a quick and safe method, free from threats carried by
clone()
, where you can fully control creation process yourself.public Date getCreationDate() { return new Date(creationDate.getTime()); }
The same problem refers to the contactPeople
collection. Whatever mutable collection you expose to the outer world, anyone can modify it freely without any notice. It means that we should neither return the reference to the collection nor save such a reference to our instance field within a setter. There are plenty of methods to perform the shallow copy of a collection (shallow in this case means that we will create a completely different collection object, but containing the same references to content elements!). Defensive copying for the accessor method is rather straightforward. We could simply use the Collections API
like below:
public List<ContactPerson> getContactPeople() {
return Collections.unmodifiableList(contactPeople);
}
Regarding the mutator, we could either remove it and force our clients to use add/remove methods (addContactPerson
/removeContactPerson
) or make a shallow copy manually:
public void setContactPeople(List < ContactPerson > contactPeople) {
if (contactPeople == null) {
throw new IllegalArgumentException("Contact people list cannot be null");
} else {
this.contactPeople = new ArrayList < > (contactPeople);
}
}
You should see now that the problem we face here is that (despite our defensive copying of the collection) we keep references to all elements and enable our clients to modify them. We could discuss whether a shallow copy provides real hermetization or not, and... it depends.
If we have references to immutable objects (please refer to a further section), we don't need to worry about hermetiaztion violations during copying. Well, immutables don't even need to be copied at all!
If an object is mutable, though, then changing its state means changing (indirectly) the state of all objects containing its reference. Sometimes, when we need to store a reference to a mutable object, we simply cannot copy all its fields. We have a perfect example of that in our code — is there a way of copying the id
field without using reflection?
There is none, as we have the id
field generated automatically inside the constructor, and there is no setter available. Now is the moment where you should ask, "Okay, so why don't we restore setId
methods?"
Personally, I consider it a bad idea. Cloning is a very low-level procedure, and it shouldn't influence our abstraction and hermetization. Thus, to work around this problem we could, for example, create a private constructor
that accepts all fields as parameters and provide our own copy()
method that would call it and return a proper copy (you see, no reflection needed, no hermetization violations). Take a look:
public class ContactPerson {
private long id;
private String email;
private ContactPerson(long id, String email) {
this.id = id;
this.email = email;
}
...
public ContactPerson copy() {
return new ContactPerson(this.id, this.email);
}
}
As soon as we have the ability to make a copy of the elements from our collection, we can easily make deep copies inside the getter and setter:
public List < ContactPerson > getContactPeople() {
return this.contactPeople.stream().map(ContactPerson::copy).collect(toList());
}
public void setContactPeople(List < ContactPerson > contactPeople) {
if (contactPeople == null) {
throw new IllegalArgumentException("Contact people list cannot be null");
} else {
this.contactPeople = contactPeople.stream()
.map(ContactPerson::copy).collect(toList());
}
}
If we have a collection of objects of a type that we have no authority to modify (e.g. as it comes from some external library), we should just perform a shallow copy and advise other programmers with a proper comment not to mutate the state of these objects.
Inheritance
Inheritance breaks hermetization! Every subclass is based on the logic encapsulated in its superclass, right? In order to work properly, a subclass needs to call its parent's super methods, which means it depends on the implementation details of the predecessor. Now a superclass may change, and those changes influence all its subclasses. As a consequence, every alteration might require changes in children classes. Let me show you an example. Let's imagine that we want to store a number of contact people in an instance variable inside a subclass of a Customer
(yes, I know it is just a size()
of our collection, but I want to visualize the problem) — let's call it a SpecialCustomer
.
public class SpecialCustomer extends Customer {
private int numberOfContactPeople = 0;
@Override
public void setContactPeople(List < ContactPerson > contactPeople) {
numberOfContactPeople = contactPeople.size();
super.setContactPeople(contactPeople);
}
@Override
public void addContactPerson(ContactPerson contactPerson) {
super.addContactPerson(contactPerson);
numberOfContactPeople++;
}
public int getNumberOfContactPeople() {
return numberOfContactPeople;
}
}
And here we have a simple test where we create SpecialCustomer
setting a one-element ContactPerson
list:
@Test
public void specialCustomerShouldHaveContactPeopleCounterEqualToOne() {
//given
final ContactPerson contactPerson =
new ContactPerson("Steve", "Harris", "steve@gmail.com");
final List < ContactPerson > contactPeople = new ArrayList < > ();
contactPeople.add(contactPerson);
//when
final SpecialCustomer specialCustomer = new SpecialCustomer();
specialCustomer.setContactPeople(contactPeople);
//then
assertEquals(1, specialCustomer.getNumberOfContactPeople());
}
And here is what we get after running this test:
java.lang.AssertionError:
Expected :1
Actual :2
The reason is that we didn't know that the setContactPeople
method from the Customer
class calls addContactPerson
inside, so our counter gets incremented twice. Hermetization of our subclass is broken by the dependency to its parent and it is a clear example of the strong and dangerous coupling that inheritance brings. Ok, so how do we deal with it?
Well, we could, of course, override every public
method and put the desired logic in a subclass without calling super methods, but this clearly isn't something that inheritance is for, and of course, we might not be aware of, for example, validations that are implemented in our super class.
Another idea might be to use inheritance only when we want to add new methods, not overriding existing ones. Great, but if a method of the same signature is added to the base class, then the problem is back. What if a parent class has some errors? Then we populate all these errors to its children. Okay, I won't keep you in suspense anymore — you should prefer composition over inheritance. We won't discuss this pattern further, but as already you know the threats connected with inheritance, I would rather like to point out that if we see a possibility of breaking hermetization, and we don't think our class should be extendable, we should declare it final
(and we will do so with our example). If it is not the case, then we should declare as many methods final
as possible and document exactly how each method should be overriden properly and hope that other programmers will stick to those rules.
Immutables
We have said it many times before that, when we have immutable objects, we don't have to worry about hermetization issues. Sounds great, huh? What are those immutables, then? As the name implies, these are objects that cannot be modified once they are created. It means that they have one, consistent state throughout their lifecycle, and thus they may be safely shared among numerous threads. In terms of hermetization, it is the essence of hiding and securing internals. Here are the features that immutable classes must provide:
- All instance fields are
private
- No mutators (setters) — there shouldn't be any possibility of changing the state of existing objects
- Any method that is supposed to change a state creates a copy of an object with properly updated fields
- No method can be overriden — it is about dealing with threats carried with inheritance
- All instance fields should be declared
final
, and thus should be initialized inside constructors - Access to all mutable instance fields must be secured with a defensive copying mechanism
As you can see, all those features reflect our previous discussion. Unless there is a strong reason for creating modifiable classes, we should do our best to provide immutable ones. What a wonderful world would it be if we could have our projects built upon only immutables...
But as with everything in this world, it has its drawbacks, too, and we should be aware of them. Now, if an object is immutable, operations that require returning a new instance might be costly in terms of memory. Imagine copying arrays of thousands of elements each time we call the addToYourArray
method! Another side effect is that the "old" instance will be alive as long as something holds its reference (or GC cleans it).
Sometimes, making immutables is even impossible. You may, for example, use JPA and require a no-arg constructor and you simply cannot declare your fields final
and instantiate them at creation time. If we cannot make our classes immutable, we should declare as many fields final as possible to keep hermetization and security at the highest possible level.
When talking about immutables, it is also worth mentioning that immutable objects are desired to be used with so-called Value Objects
. Without digging deeply into the topic, these are the objects that don't have any conceptual identity, and we specify them only by the state of their properties (fields). Keeping value objects immutable combines very good abstraction and encapsulation (by converting a logical value into an object) with hermetization, therefore securing their state (value) from any manipulations from the outside.
There are lots of good examples of immutables in core Java libraries: java.lang.String
, java.math.BigInteger
, and java.time.ZonedDateTime
, for example. You should now know why we didn't do any defensive copy for String
fields. Remember, if you have an option, use immutables! And we will do so here, too. We will throw Date
away in favor of ZonedDateTime
.
What do you think? Can we make our classes purely immutable ones? Let's try it with a ContactPerson
:
public final class ContactPerson {
private final long id;
private final String email;
private ContactPerson(long id, String email) {
this.id = id;
this.email = email;
}
public long getId() {
return id;
}
public String getEmail() {
return email;
}
@Override
public String toString() {
return "ContactPerson{" +
"email='" + email + '\'' +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final ContactPerson that = (ContactPerson) o;
if (id != that.id) return false;
return email != null ? email.equals(that.email) : that.email == null;
}
@Override
public int hashCode() {
int result = (int)(id ^ (id >>> 32));
result = 31 * result + (email != null ? email.hashCode() : 0);
return result;
}
public ContactPerson copy() {
return new ContactPerson(this.id, his.email);
}
public static ContactPerson valueOf(String email) {
return new ContactPerson(Sequence.nextValue(), email);
}
}
- All instance fields
private
- checked - No mutators (setters) - checked
- Any method that is supposed to change a state must create a copy with a properly updated fields - no such methods
- No method can be overriden - checked
- All instance fields
final
- checked - Mutable instance fields must be secured with defensive copying - checked
Success! We made ContactPerson
an immutable class. You can see here that we added a static valueOf
factory method instead of a public constructor
, which gives a convenient way of creating objects (no more new operator) and gives another level of hermetization — the client does not need to know what constructors are used inside.
Now can we do the same to a Customer
class? Yes, but with a few notes. Firstly, if we declare activationDatefinal
, we won't be able to activate the same object — we will need to create a copy. Similarly, we will need to clone the whole customer while adding or removing any contact person. If we are okay with that, let's see how it works:
public class Customer {
private final long id;
private final String name;
private final ZonedDateTime creationDate;
private final ZonedDateTime activationDate;
private final List < ContactPerson > contactPeople;
private Customer(long id, String name, ZonedDateTime creationDate,
ZonedDateTime activationDate, List < 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");
}
this.id = id;
this.name = name;
this.creationDate = creationDate;
this.activationDate = activationDate;
this.contactPeople = new ArrayList < > (contactPeople);
}
public long getId() {
return id;
}
public String getName() {
return name;
}
public ZonedDateTime getCreationDate() {
return creationDate;
}
public List < ContactPerson > getContactPeople() {
return Collections.unmodifiableList(contactPeople);
}
public Customer activate() {
return new Customer(this.id, this.name, this.creationDate,
ZonedDateTime.now(), new ArrayList < > (this.contactPeople));
}
public boolean isActive() {
return this.activationDate != null;
}
public Customer addContactPerson(ContactPerson contactPerson) {
final List < ContactPerson > newContactPersonList =
new ArrayList < > (this.contactPeople);
newContactPersonList.add(contactPerson);
return new Customer(this.id, this.name, this.creationDate,
this.activationDate, newContactPersonList);
}
public Customer removeContactPerson(long contactPersonId) {
final List < ContactPerson > newContactPersonList =
new ArrayList < > (this.contactPeople);
newContactPersonList.removeIf(it - > it.getId() == contactPersonId);
return new Customer(this.id, this.name, this.creationDate,
this.activationDate, newContactPersonList);
}
@Override
public String toString() {
return "Customer{" +
"id=" + id +
", name='" + name + '\'' +
", creationDate=" + creationDate +
", activationDate=" + activationDate +
", contactPeople=" + contactPeople +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final Customer customer = (Customer) o;
if (id != customer.id) return false;
if (name != null ? !name.equals(customer.name) : customer.name != null) return false;
if (creationDate != null ? !creationDate.equals(customer.creationDate) : customer.creationDate != null) return false;
if (activationDate != null ? !activationDate.equals(customer.activationDate) : customer.activationDate != null) return false;
return contactPeople != null ? contactPeople.equals(customer.contactPeople) : customer.contactPeople == null;
}
@Override
public int hashCode() {
int result = (int)(id ^ (id >>> 32));
result = 31 * result + (name != null ? name.hashCode() : 0);
result = 31 * result + (creationDate != null ? creationDate.hashCode() : 0);
result = 31 * result + (activationDate != null ? activationDate.hashCode() : 0);
result = 31 * result + (contactPeople != null ? contactPeople.hashCode() : 0);
return result;
}
public static Customer valueOf(String name, List < ContactPerson > contactPeople) {
return new Customer(Sequence.nextValue(), name, ZonedDateTime.now(),
null, contactPeople);
}
}
- All instance fields
private
- checked - No mutators (setters) - checked
- Any method that is supposed to change a state must create a copy with properly updated fields - checked
- No method can be overriden - checked
- All instance fields
final
- checked - Mutable instance fields must be secured with defensive copying - checked
Please note that in the getContactPeople
method, we got back to a shallow copy solution. This is because ContactPerson
is an immutable class, so we don't need to copy its objects anymore. All right, we have our classes hermetized!
Serialization
It is common to send our objects throughout the network or persist them somehow (in a queue, event store, etc.). In order to convert an object to some unified binary form, we often use the Java serialization mechanism. We are not going to describe how serialization works, but rather point out its connection to hermetization.
Well, what my experience shows is that there is something about serialization that makes people think that the only thing they should do is to implement a Serializable
interface. Unfortunately, it might be illusive. Let me explain.
Deserialization restores the private state of an object. It means that we can treat the readObject
method as another public constructor
. Thus, we cannot trust the external byte stream (like we don't trust our clients) to always contain proper data. If there are any validations implemented in our constructors or setters, they should be applied within a readObject
method, too. It will prevent us from initializing corrupted objects.
Another threat that we are exposed to is that the serialized byte stream might contain so-called back references
to objects that are already inside the stream. If these objects are mutable, then we face the same problem we described in the Object References section. You should know by now that what we should do inside the readObject
method is make a defensive copy of all mutable objects.
In the case of the ContactPerson
class, the default serialized form of an object corresponds to its logical content, and we don't have any references to mutable objects nor validations there. As a result, it is enough to simply implement a Serializable
interface, like below:
public final class ContactPerson implements Serializable {
private static final long serialVersionUID = 1 L;
...
}
In the Customer
class, apart from implementing a Serializable
interface, we need to override the readObject
method and both put appropriate validations and make defensive copies of all instance fields that are mutable.
public class Customer implements Serializable {
private static final long serialVersionUID = 1 L;
private final long id;
private final String name;
private final ZonedDateTime creationDate;
private final ZonedDateTime activationDate;
private List < ContactPerson > contactPeople;
...
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject();
if (contactPeople == null) {
throw new NotSerializableException("Contact people list cannot be null");
}
if (StringUtils.isEmpty(name)) {
throw new NotSerializableException("Name cannot be empty");
}
contactPeople = new ArrayList < > (contactPeople);
}
}
One thing that we needed to do was remove the final
keyword from the contactPeople
field. That is because we are doing defensive copying (a shallow one) after the object is constructed. readObject
works like a constructor, but it isn't one. Here, we partially break one of the rules from immutable object definition, but having a defensive copy mechanism for this field, we will still have a effectively immutable object and safe deserialization, too.
Conclusions
As you can see from this long article, there is a lot of philosophy around OOP concepts, which is not as simple as it might sometimes seems to be. You had a chance to see that all OOP concepts are complementary and overlap with each other. We cannot talk about hermetization without mentioning encapsulation, abstraction, or inheritance. Here are some rules that you should follow to maintain hermetization in your code:
- Create immutables when possible.
- Keep access to instance members as restricted as possible.
- Give your class a
public
access modifier only when you are sure that you want to expose it outside the package. - Declare accessors only to fields you want to expose to the outer world.
- Declare modifiers only to fields that really might be changed in the future and that are not to be set automatically.
- Give your class a behavior so that methods reflect the abstraction, hiding data representation and other implementation details.
- If you have instance fields that are references to mutable objects, provide defensive copying in getters, setters, and constructors.
- Try to keep your class
final
. If you want your class to be extended, declare as many methodsfinal
as possible and document the class clearly. - Always prefer interfaces (or base classes from a class hierarchy) rather than their implementations as types of fields/params/method return values.
- Override the
toString()
method so that it prints information accessible programmatically from the outside of a class — it is like your API. - If your objects are supposed to be compared to each other or used as keys in data structures, provide your own
equals()
andhashCode()
methods. - Prefer composition over inheritance.
- Put proper validations and defensive copying (if needed) inside
readObject
methods while implementing aSerializable
interface.
Published at DZone with permission of Bartłomiej Słota, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments