Encapsulation

I believe this is the most important aspect of Object-Oriented Design (OOD) and Programming (OOP). I also believe this is the most disregarded. It is disregarded because it is not fully understood. It is my opinion that this aspect of OOP is misunderstood because beginner programmers focus in learning WHAT encapsulation is, and not why it is important or needed. This article aims to explain why encapsulation is important and how to achieve true encapsulation.

Although anyone could look up its definition in a dictionary, I would like to start by defining the word 'encapsulation.' Encapsulation simply means to enclose something in. To completely cover or hide something especially so that it will not touch (or come in contact with) anything else. In the real world, there are many reasons to encapsulate. For example, certain medication come in the form of capsules. The real value of medicinal capsules in in the medicine that it encloses. To make a long story short, medicinal capsules were developed to improve the delivery and effectiveness of powder and liquid medications. One could argue that in OOP, encapsulation does improve the delivery of data. However, the main reason why encapsulation is needed is to "hide" the data. To be more precise, data is hidden in order to protect its integrity. In other words, our goal is to hide the data in order to prevent accidental modification. So, in text books and other articles, you might see the term "encapsulation" and "data-hidding" used interchangeably.

Now that you have learned WHY encapsulation is needed, you need to understand how to achieve it. In programming classes you might have learned that data members of a class should be made private and that you should provide public accessor and mutator functions to access those data members. This is not real encapsulation. Imagine you have a treasure chest full of gold and gems. The treasure chest is locked so that no one could have access to the chest. However, you decide to make copies of the key and give it to every living soul. How protected would your treasure be if you do that? Not very well protected. By providing public accessor and mutator methods, you are giving the entire world access to your data. Since encapsulation means "information hidding", you have really achieved nothing by taking this approach. That said, not everything in the original statement is incorrect. You do want to make the data members (variables) of a class private. The question now is, how could you provide access to the data without compromising it? In order to provide proper access to data members of a class, you have to keep several things in mind. The most important is to code to minimize mutability. This topic I will discuss in a future blog. Immutable object will make your code more secure and stable. But sometimes, immutability is not practical. Take for example, a traffic light. If you were to model a class to emulate this real-world entity, you must have the ability to change the object's state. Sometimes, the light will be green. Other times, the light will be yellow, or red. This means that you will need functions that will change the value of some (or all) of the variables in this class.

In order to protect information, one must know all of the possible ways to modify its value. One obvious way to modify the value of a data member is through mutator methods.


import java.util.ArrayList;
import java.util.Collection;

public class EncapsulationDemo
{
  private String name;
  Collection<String> gems = new ArrayList<String>();
 
  public static void main(String[] args)
  {
    EncapsulationDemo obj1 = new EncapsulationDemo();
    String name = "Joe";

    obj1.setName(name);
    name = "Jane";
    System.out.println(obj1.getName());

    Collection<String> other = new ArrayList<String>();
    other.add("Diamond");
    other.add("Ruby");
    other.add("Sapphire");

    obj1.addGem("Opal");
    obj1.addGem("Emerald");
    obj1.addGem("Amethyst");

    obj1.getGems().addAll(other); // Modification through accessor method
    other.clear();

    System.out.print("My gems: ");
    for (String gem : obj1.getGems())
    {
      System.out.print(gem + "; ");
    }
  }

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

  public String getName()
  {
    return name;
  }

  public void addGem(String gem)
  {
    gems.add(gem);
  }

  public void addGems(Collection<String> gems)
  {
    this.gems.addAll(gems);
  }

  public Collection<String> getGems()
  {
    return gems;
  }
}

In the above example, the setName(String name) method allows external entities to modify the class internal value of the variable "name." This could be desirable. The output of the code above is as follows:


Joe
My gems: Opal; Emerald; Amethyst; Diamond; Ruby; Sapphire;


On the other hand, getName() returns the value of name, and there is no harm to the internal value of name because String objects are immutable. However, consider the getGems() method. Something more sinister is about to happen here. Many developers make the mistake of thinking that there is no harm in providing an accessor method like in the first case. This is certainly not the case with Java Collection classes. In Java, objects are references; pointers to the area in memory where the value of the object resides. Once a client of the class obtains this reference, it is very easy to change the internal values of this collection without the providing class knowing anything about it; and this is definitely undesirable.


obj1.getGems().addAll(other);

This line of the code above was able to modify the internals of the EncapsulationDemo object using an accessor method.

The question still remains: How do we achieve true encapsulation without compromising the data? We can conclude from the example above:

  1. All data members of a class should be private.
  2. Provide accessor and mutator members on a case-by-case basis. Not all data member need mutator methods.
  3. Beware of accessor method for Java Collection objects.
But, if we need to access the data, and some accessor methods can compromise the integrity of the data held by an object, how do we provide a safe accessor method? This last piece of the puzzle can be achieved in three different ways. The first way is to store values in immutable objects like String objects. Since the value of an immutable object cannot be changed once it is created, it is safe to give external entities access to these data members. For objects that are not immutable, the best way to resolve this is to make a copy of the object and return the copy. We can apply this strategy to the getGems() method. So, instead of returning the original list, a copy of the original list shall be returned.

  public Collection<String> getGems()
  {
    Collection<<String> copy = Collections.unmodifiableList((List<? extends String>) gems);
    return copy;
  }

With this change, an exception will be thrown when the obj1.getGems().addAll(other); line is executed because the list being returned is unmodifiable.


run:

Exception in thread "main" java.lang.UnsupportedOperationException
at java.util.Collections$UnmodifiableCollection.addAll(Unknown Source)
at demo.EncapsulationDemo.main(EncapsulationDemo.java:38)


However, if a modifiable copy is needed, there are two techniques to accomplish this. One is to make the elements of the list Cloneable (implement the Cloneable marker interface), or by adding a copy constructor to the element class, and add a new instance to the list which is a copy of the element of the original list. String class has a Copy Constructor. So I will illustrate this using the second approach.


  public Collection<String> getGems()
  {
    Collection<String> copy = new ArrayList<String>(gems.size());
    for (String element : gems)
    {
      copy.add(element);
    }
    return copy;
  }

This time, even though the list was "modified", the output reflects that only the copy was modified. The original list remained unchanged as you can see from the output below.


Joe
My gems: Opal; Emerald; Amethyst;


In conclusion, have determined that in order to have true encapsulation, you must:

  1. Make all data members of a class private.
  2. Provide accessor and mutator members on a case-by-case basis. Not all data member need mutator methods.
  3. Accessor methods should return a primitive value, an immutable object, or a copy of a mutable object. In the case of Java Collection, you should return an unmodifiable collection, or a new instance of a collection object that is the same size of the original, and that it contains immutable elements or copies of the original elements.

Comments

Popular posts from this blog

Implementing Interfaces with Java Records

Exception Handling: File CRUD Operations Example

Combining State and Singleton Patterns to Create a State-Machine