Encapsulation

Encapsulation #

Encapsulation is a (vague) principle in object-oriented programming that refers to “bundling” data with the code that operates on it, and restrict access to this code and data from other components of a system.

From Wikipedia: “Essentially, encapsulation prevents external code from being concerned […]”

Each component hides its internal logic by exposing only data and methods that other components may need.

Example. As we saw earlier, in our game, the “view” component (which is in charge of rendering the game on screen) may buffer the game snapshots that it receives from the backend, if these snapshots are received faster than they can be displayed.

As a buffer, this component uses a structure called a queue. This queue is not exposed to other components, because they do not need to see it, and (most importantly) should not modify it. In other words, this queue is an implementation detail, internal to the “view” component.

Encapsulation can have many benefits. Among others:

  1. Easier debugging. If our queue is internal to the “view” component, then we know that it cannot be responsible for the malfunction of another component.
  2. Easier collaboration. Carol may refactor the implementation of the “view” component, knowing that this will not affect Alice, who is currently working on the backend.

This is why a common practice in object-oriented programming consists in hiding all attributes and methods of a new class by default. And make accessible only the ones that need to be (in particular, this is likely to be the default behavior of your IDE).

Encapsulation also largely dictates how libraries are structured. For instance, when you create a String in Java, you do not have access to the internal representation of the string object.

in Java #

Each attribute or method of a class can have an access modifier, which specifies which other classes can access it. For instance, the keywords private and protected below are access modifiers.

private int myAttribute;

protected int myMethod(){
  return 1;
}

Definition. There are four levels of access in Java:

  • private restricts access to the current class,
  • “package-private” relaxes private by also allowing access from the folder of the current class (in Java, a folder for source code is called a package), excluding subfolders,
  • protected relaxes “package-private” by also allowing access from the subclasses of the current class,
  • public does not restrict access.

Warning. There is no keyword for the “package-private” level. Instead, this is the default level for an attribute or method without access modifier. For instance, in the example below, the attribute myAttribute is package-private:

int myAttribute;

Here is a recap table from the Oracle tutorials:

keyword class package subclasses world
private yes no no no
none yes yes no no
protected yes yes yes no
public yes yes yes yes

Warning. A method declared in an interface is (implicitly) public.

Warning. If a method m1 overrides (or implements) a method m2, then m1 must be at least as accessible as m2.

The following program does not compile. Can you see why, and how to fix this?

├── Run.java
└── units
    ├── Unit.java
    └── impl
        └── Unicorn.java
public abstract class Unit {

    static String configFolder = "path/to/config";
}
public class Unicorn extends Unit {

    String name;

    public Unicorn (String name){
        this.name = name;
    }

    public static String getConfigFilePath (){
        return configFolder + "/unicorn.properties";
    }
}
public class Run {

    void testUnicorn(){
        Unicorn myUnicorn = new Unicorn("Storm");
        myUnicorn.name = "Tornado";
    }
}
  • Unicorn.getConfigFilePath tries to access the package-private attribute Unit.configFolder (it should be made protected of public),
  • Run.getConfigFilePath, tries to access the package-private attribute name of myUnicorn (it should be made public).

Hint. Your IDE may suggest how to fix such compilation errors.

To improve encapsulation, it is good practice to restrict access whenever possible (i.e. without compromising compilation).

Hint. As a rule of thumb, in Java:

  • use private by default for all attributes and methods that you create, and
  • if the program does not compile, then use your IDE to relax access.

Encapsulation in this program can be improved. Can you see how?

├── Run.java
└── units
    ├── Unit.java
    └── impl
        └── Unicorn.java
public abstract class Unit {

    public int health;

    public Unit(int health) {
        this.health = health;
    }

    public void attack(Unit defender){
        health -= defender.health;
        defender.health = -health;
    }
}

public class Unicorn extends Unit {

    public Unicorn (){
        super(1);
    }

    @Override
    public void attack(Unit defender){
        regen();
        super.attack(defender);
    }

    public void regen(){
        health += 1;
    }
}
public class Run {

    void testUnicorn(){
        Unicorn u1 = new Unicorn();
        Unicorn u2 = new Unicorn();
        u1.attack(u2);
    }
}
  • Unit.health can be made protected,
  • the constructor of Unit can be made protected,
  • Unit.attack can be made protected,
  • Unicorn.regen can be made private.

Note. The constructor of an abstract class can always be made protected (since it can only be called in the constructor of a subclass).

Getters and setters #

For attributes, the notion of “access” can be refined. An attribute may be:

  • neither visible nor modifiable, or
  • only visible, or
  • only modifiable, or
  • both visible and modifiable.

This can be achieved with private attributes and so-called “getter” and “setter” methods. For instance, in the following class, the attribute health has public visibility but is not modifiable.

public class Unicorn {

    private int health;

    public int getHealth(){
        return health;
    }
}

Conversely, in the following class, the attribute health can be modified but is not visible.

public class Butterfly {

    private int health;

    public void setHealth(int health){
        this.health = health;
    }
}

Hint. Getter and setter methods can be automatically generated by your IDE.

To go further: inheritance and encapsulation #

Composition #

Example (from Effective Java, Item 18).

Consider a class MyHashSet that extends Java’s HashSet functionalities by keeping track of the number of times something has been added to the set (as opposed to the output of HashSet.size(), which returns the number of elements remaining in the set).

This class myHashSet may have a private attribute int counter (initialized to 0) that keeps track of the number of elements added to the set so far. And the class may be implemented by overriding add and addAll in the expected way, i.e.:

public class MyHashSet extends HashSet {

      private int counter;

      ...

      @Override
      public boolean add(E e){
          counter++;
          return super.add(e);
      }

      @Override
      public boolean addAll(Collection<? extends E> c){
          counter += c.size();
          return super.addAll(c);
      }
}

However, this implementation of addAll would count every insertion twice, because the implementation of HashSet.addAll calls the method add (which is overridden in this case).

A design pattern called composition can be used to avoid such unintended effects. Intuitively, instead of extending the original class, use an instance of the original class as a (private) attribute of the new class (e.g. an instance Hashset named set in this example). However, this requires re-implementing all methods of the original class (albeit in a straightforward way), for instance:

public class MyHashSet {

      public boolean isEmpty(){
            return set.isEmpty();
      }
}

Prevent overriding or inheritance #

As show by the example above, in order to improve encapsulation, one may want in some scenarios to forbid overriding a method or extending a class. In Java, this can be enforced with the keyword final, for instance:

public final class NonExtensibleClass {
    ...
}
public class MyClass{

  public final void nonOverridableMethod(){
        ...
  }
}

Warning. In Java, a variable can also be declared final. But this has a different meaning.