In my blog I write about misconceptions. Encapsulation is something you learn when you study Java and OOP. But it seems that most books fail to truly explain the problems you want to solve with it and those you get by using it. Often it’s just a short chapter or even just a small part about the important concepts of OOP. This is leading to misconceptions and poor understanding of OOP.
(Note: I wrote this before the release of Java 10. Expect that some things are somewhat outdated.)
Why Encapsulation?
You have probably already read about it in your text book. You want to reduce accessibility of your classes to a minimum. Simply because every additional access would only cause problems. Then other code has less possibilities to misuse your object and therefore you get less bugs. And you want to have a clear interface to define what methods there are, so no code has direct access to inner fields. You never know what you need to add later and if some code has direct access, you don’t have a getter and setter to add your code to. With encapsulation you can later change the inner workings in isolation without changing the public API. You could not do that without encapsulation.
Making your class as inaccessible as possible may seem strange. You obviously still need some methods so the API isn’t empty, which would make your class useless. So while you can make a class completely immutable, you still need at least some getters, to make it useful. But you want to provide a clean interface.
So you end up hiding the data and only giving out immutable, copied or wrapped data through getters. The goal is that any change to the returned object is observed is…
- impossible (immutable)
- ignored (copy)
- handled (wrapper)
If you return an array of some inner field directly, you lose that control, since the invoker gets to do this:
someObject.getArray()[4] = "bad";
You can’t prevent the insertion of invalid data when you return a mutable data structure (in this case an array of Strings).
How to get full Encapsulation?
You probably don’t even want 100% encapsulation in many cases. At least not for all of your code, just for some parts of it. That is, unless you have a simple class where you get it without any effort (for example immutable types) or when you just have to because you need it to implement all your requirements.
However, you may want to use a façade. The façade pattern is a software design pattern, which you can use on a high level. It’s about hiding the inner workings of you application. So a complex API is hidden behind a façade. Encapsulation is a similar concept to hide the inner workings of a class and other dependent classes.
What would 100% encapsulation mean and why is it sometimes so hard to achieve? Whenever something happens, the root element must be notified. So even when you alter something you got from some getter this access must be validated by the owner of that object. The array (mentioned above) itself is an object, but it belongs to someObject
. The array doesn’t know this, does not notify anything, and it is mutable. So it’s not really encapsulated. Our object could become invalid. To get a robust system you want all your objects to be valid at all times or at least to be aware of invalid states.
Think of the part of any complex program that allows you to edit the configuration (preferences). So the configuration itself is probably an object, since we are using OOP. You have a class named Configuration
and one instance of it. Maybe just a singleton.
Configurations can be complex, so you probably have many categories. Maybe even subcategories. Just look at the configuration of your IDE. It’s probably a tree structure with some root element. In Eclipse I have “general”, “Java”, “team”, “ant” etc. All of them have subcategories. Whenever I alter some option, Eclipse will probably invoke some setter on the backing object for that subcategory. But how will our singleton instance of Configuration
know about it? What if you set your preferred JDK to some old installation of Java 6 but somewhere else you set the minimum Java version to Java 8. Often such contradictions must be detected and reported when they happen. Usability would suffer if you do not get informed until you click “OK”. And when using synchronized blocks you should make sure that all objects are valid when you leave the block, so all threads always start with a valid state.
In many situations you have a tree of objects, because usually everything just has “children”. But you could end up with a graph, if some object could reference another object. Keep this in mind when you design your classes. Trees, which have no cycles, are much easier to handle than the more general graphs, which may have cycles.
Solution 1: Wrap everything
In any situation you do not want to return arrays ever. Not in a high level API. Instead you want to use List
(i.e. ArrayList
). If you do not want to allow direct manipulation (read only) then you just return Collections.unmodifiableList(result)
.
If you still decide to return an array, it must be a newly created array, or one passed by the caller as a parameter (for example a byte buffer). Then the caller has control over that array and it’s no problem.
But what if the result should be mutable? The solution is to wrap it in a type that is only there to notify the owner on each access. The returned list is really just a wrapper of the actual list and it has additional code to call some validation code of its owner. This way you can throw an exception when some illegal value is to be inserted to prevent invalid state. And you can make sure all operations are properly synchronized so they become thread-safe.
In Java you need to write a lot of code unless you are using some tool to do it for you.
As an example I just want to mention HashMap
, because it actually uses this. I expect that you already have the JDK installed, so just look at the code. A HashMap
has a method keySet()
. The return type is Set
. But it doesn’t just return some set, because it needs to encapsulate all data. So it returns an instance of KeySet
, which is a nonstatic nested class. This KeySet
automatically has a reference to its owner, which is the HashMap
.
Here’s a single line of code to demonstrate how this works:
public final void clear() { HashMap.this.clear(); }
It doesn’t even do anything on itself and it doesn’t need any fields on its own. It just clears the HashMap
and anyone who has a reference to a KeySet
will see the changes because each access to that set uses the backing HasMap
to get the data.
Solution 2: Make all Nodes report to the Root
This isn’t really much different from using wrappers, since methods calls are basically messages to objects. But you could use a framework to manage all notifications and use something more specific than just method calls. Then a node of your object graph can register itself to the framework and also specify which notifications it wants to handle. Every mutation must be reported.
A good example is Document
, which you can use for XML processing in Java. It implements EventTarget
, so you can register event handlers using addEventListener
. Nobody could just add nodes without the document knowing about it. So an instance of Document
is always fully encapsulated.
Solution 3: Hide everything behind a façade
A façade for an XML document could only allow you to search for elements and then return the inner text and a list of attributes. If that is all you need it is obviously much simpler than using Document
(see above). A constructor, that takes a path / url / string, and some methods, like findById
and findByName
is a much cleaner API than one that supports all possible operations. You can even have the constructor private and publish a static factory method in a public interface. Then everything except for one interface is hidden.