If you asked me what’s the one thing you can learn that will improve your code quality a lot, I would say encapsulation.
Encapsulation is a concept, not a feature
There are countless of articles that explain encapsulation as a feature in OOP (object-oriented programming), but the truth is encapsulation is not necessarily related to OOP. Most articles will show you that encapsulation is about defining your class’s fields as private. While it’s a way to encapsulate your data, it’s not what encapsulation is really about.
So, what’s encapsulation?
Encapsulation is a concept. It’s the idea of hiding your module’s implementation details from the rest of the world.
But what do I mean by a module? And what’s the rest of the world here? And why do we want to hide its implementation details? I’ll answer all of this in this article.
What’s a module?
I don’t mean an ES module here. By a module I mean anything that can contain some implementation code.
In JavaScript we have: ES modules, classes, and functions.
The two worlds of a module
For each type of these modules, there are two worlds: the inner world and the outer world.
The inner world is what you see when working inside that module. When you are in the inner world, you see all the implementation details (or secrets) of that module. You see how all the different pieces of that module works.
The outer world is what you see when you are using that module. That can be a module you created or installed as a third-party library. But when you are in the outer world of that module, you don’t care who defined it, or most importantly, how it works.
Now that you know that there are two worlds for each module type, let me describe the worlds of each one.
The two worlds of a function
The inner world of a function contains:
- The function name
- The function parameter list
- The return value of the function (if there is any)
- The function implementation code
The outer world of a function contains:
- The function name
- The function parameter list
- The return value of the function (if there is any)
So when you are using a function, you know its name, the parameters it takes, and what it returns, but you don’t know how it works.
The two worlds of a class
The inner world of a class contains:
- The class name
- The class constructor parameters
- All private fields
- All public fields
- All private methods
- All public methods
The outer world of a class contains:
- The class name
- The class constructor parameters
- All public fields
- All public methods
So when you are using a class, you know everything about it except its private fields and methods—that’s why defining a private field is taught as encapsulation.
Since a method is a function, then the same two-world rules for a function apply on a method.
The two worlds of an ES module
The inner world of an ES module contains:
- All variables
- All functions
- All classes
The outer world of an ES module contains:
- Exported variables
- Exported functions
- Exported classes
So the user of the module only sees the exported data—whether they are variables, functions, or classes.
Why encapsulation?
This is the most important question to ask, but I couldn’t answer it before explaining the two-world concept first.
The most important thing encapsulation achieves is decoupling. Coupling is how much one part of the code is dependent on others.
High coupling is bad because changing one part will affect other parts; consequently, it will break some existing code, and you will have errors and bugs.
Encapsulation helps you decouple your code by hiding details. Let me show you how with a code example.
A function example
Let’s say you have a shuffleArray
function that shuffles an array, and you are using it somewhere in your code.
function shuffleArray(array) {
// code to shuffle the array
// then return the result array
}
function main() {
const exampleArray = ['a', 'b', 'c', 'd', 'e']
const shuffledArray = shuffleArray(exampleArray)
console.log(shuffledArray)
}
main()
The main
function here is the outer world for the shuffleArray
function. In main
, I don’t know how shuffling an array works, but I needed to know the name of the function, what parameters it takes, and what it returns.
If parts of the outer world of shuffleArray
changes, then I have to change the code in main
; for example, if the function name has been changed to shuffleAnArray
, then I have to change main
code to use the new function name.
However, if the hidden stuff in shuffleArray
have changed (which is the body of the function), then main
would not be affected—assuming these changes still shuffles an array correctly.
With that in mind, you can refactor and improve how shuffleArray
function works without breaking other parts of your code. A good case where you would need to update the implementation of shuffleArray
is to improve its performance—because maybe you’ve found a better shuffling algorithm.
This is a very simple example, but in most real-world projects, you’ll have more complex functions; refactoring the implementation of these functions will be much more beneficial. An example would be extracting some common parts of a function. If the function still does the same thing (your tests will confirm this), then the rest of your app will not break, but you’ll end up with a better code.
Classes and ES modules
The above example was for encapsulation in functions, but the same idea applies for classes and ES modules.
The outer world of a class doesn’t care how its internal pieces work; it just needs to get the same result. The reason it’s better to always set your class fields to private is because the outer world doesn’t need to know how these fields are used. It should just communicate with its public API—which is its public methods. So, as long as the outer world sees the class the same, then the outer world of that class doesn’t need to change.
ES modules are the same as classes; the outer world of it just cares about its exported parts—whether they are classes, functions, or variables. If the exported parts are the same, then the user of these modules would not care if the way its internal parts work has changed. This gives the author of these modules an opportunity to improve their internal design to make them work faster or easier to change in the future, for example.
Conclusion
Encapsulation is the idea of hiding the internal parts of modules. Hiding them will decouple how these modules work from their callers. When a part of your code is decoupled, then you can change it without affecting other parts in your code; this will give you the confidence to improve how your modules work without worrying about breaking something.
That’s why encapsulation is the best thing to learn to improve your code quality.