Clarifying Abstraction, Encapsulation, and Modularity
Disentangling complementary design principles
Acknowledgements
Huge thanks to the work done by Stephen Clyde and Jorge Edison Lascano. Much of this post stems from what I read in their paper, which I have linked in the references at the bottom. I hope this post serves as a valuable distillation of a portion of their paper. I highly recommend giving it a full read.
The questions I aim to answer:
Why do we want good software design?
What do abstraction, encapsulation, and modularity mean?
How do these principles differ?
Before we dive in, I want to provide some context for why I'm writing this. While reviewing code, I sometimes feel something is off, but find myself struggling to clearly identify and communicate what the exact problem is and how to address it. I recently started reading a book which should help, Refactoring for Software Design Smells. In it, the main categories of design smells are abstraction, encapsulation, modularization (modularity henceforth), and hierarchy (though I won't be talking about this one today). I have a decent understanding of this set of principles, but they seem so intertwined and difficult to talk about without mentioning the others. Thus, I decided that I would do some research into these principles, deepen my understanding, and hopefully clarify their meaning, distinctions, and relations.
Some more prerequisites before getting into the main objective: why should we care about abstraction, encapsulation, and modularity? What are these principles meant to afford us?
What do we want?
High quality software! What characteristics compose high quality software? To name a few, correctness, understandability, testability, maintainability, efficiency, reliability, security, extensibility, openness, interoperability, and re-usability. How do we get these things? By adhering to good software design principles, such as abstraction, encapsulation, and modularity. What makes these software design principles good? People much smarter than me said so!
If an appeal to authority isn’t good enough, let’s take a look at the dictionary definition. A principle is “a fundamental truth or proposition that serves as the foundation for a system of belief or behavior or for a chain of reasoning”. A design principle is a foundational concept which guides a design toward desirable characteristics.
Note: Design patterns are patterns, not principles. Design patterns have trade-offs, and the designer should choose and apply design patterns in a manner which best balances the benefits with the trade-offs. Design patterns are well-used solutions to common problems and are meant to create well-principled software. Blind application of design patterns may result in lower quality software due to the trade-offs outweighing the benefits. Design patterns are implemented not for their own sake, but as a means by which to create high quality software.
Alright, let's get into it!
Individual definitions
Abstraction
An abstraction denotes the essential characteristics of an object that distinguish it from all other kinds of objects and thus provide crisply defined conceptual boundaries, relative to the perspective of the viewer
~ Grady Booch (Object Oriented Design with Applications)
As a process, abstraction is the removal of functionality. Abstraction as a principle in software design is about capturing only the essential details or behavior of a concept. Every component within a software system can be viewed as an abstraction, though the quality of each may vary.
A good abstraction has the following characteristics:
Meaningful identifiers (class, function, variable, package, file, etc. names): the purpose or behavior of an element is clearly conveyed by its name.
Context-aware identifiers: no redundant information exists for an identifier given the context that it's in. If a Car object has a tires property, a method named
getCarTireswould have redundancy. Instead,getTirestakes advantage of the Car context to simplify its name.Completeness: Every functionality essential to the object's use is exposed. For example, with a list abstraction, if add is supported, so too should remove. There are of course exceptions, but it stands as a general guideline.
Sufficiency: Non-essential functionality, that is, functionality that doesn't contribute to the object's main purpose, is not exposed. Looking at the methods exposed by an abstraction, it should be clear what the abstraction does, and the things it does should be cohesive.
Encapsulation
Encapsulation is most often achieved through information hiding (not just data hiding), which is the process of hiding all the secrets of an object that do not contribute to its essential characteristics
~ Grady Booch (Object Oriented Design with Applications)
Encapsulation often involves hiding internal state or data. This serves to protect that state, but also to prevent coupling. If internals are unnecessarily exposed, consumers of such a component may become dependent on these internals. The consequences of this can be a leaky abstraction or hindered modularity.
Well practiced encapsulation may consist of the following:
Scope isolation: Internal elements (data, behavior, structures) should be defined in the narrowest scope possible. If something is needed only in a function, keep it there. If something is needed only in a single class, keep it in that class. My personal pet peeve: having a single constants class which contains static data for many different classes even when these bits of data are not shared.
Programmatic isolation: If available in your language, use appropriate access modifiers to control visibility. If something is internal, enforce it by making it private. If a class is only needed by other classes in the same package, make that class package-private.
Instructed isolation: If something can't be isolated by other means, document how something is meant to be used. While not ideal, it can help to avoid accidental violations of encapsulation.
Modularity
Modularity is the property of a system that has been decomposed into a set of cohesive and loosely coupled modules
~ Grady Booch (Object Oriented Design with Applications)
A design decision, particularly one that is significant and likely to change, should be isolated within one software component. Good modularity has the following characteristics:
Localization of design decisions: a design decision (such as implementing some behavior) should be implemented in one component.
High cohesion: the degree to which elements of one component relate to each other and/or the components primary (ideally single!) responsibility.
Low coupling: being free of unnecessary dependencies and entirely free of hidden dependencies.
Modular reasoning: exists when a developer can reason about the component by only looking the implementation of the component, its public interface, and the public interfaces of any direct dependencies.
Relations to each other
Distinctions
Modularity alone instructs decomposition into components
Abstraction alone instructs exposing the minimal and complete functionality of something
Encapsulation alone instructs hiding internals that clients need not and should not depend on
Abstraction and encapsulation are like opposite sides of the same coin. Here's a quote which may help to distinguish them:
Abstraction focuses on the observable behavior of an object, whereas encapsulation focuses on the implementation that gives rise to this behavior
~ Grady Booch (Object Oriented Design with Applications)
Modularity without abstraction or encapsulation
Concepts are separated into components and the individual design decisions are localized to individual components.
The components do not expose the complete functionality (incomplete abstraction).
The components do expose internals, such as underlying data (missing encapsulation).
Modularity with incomplete abstraction but with encapsulation would be unusable. Missing encapsulation within a component may be to mitigate missing abstraction.
Modularity with good abstraction but missing encapsulation would be usable, but risks creating tight coupling by exposing internals.
Abstraction without modularity or encapsulation
The minimal and complete functionality of a concept is exposed from a component.
Internal details are exposed, risking coupling (missing encapsulation).
Design decisions are not localized to an individual component; some functionality with a mostly or entirely similar is present in two or more components (missing modularity).
Encapsulation without abstraction or modularity
Internals are properly hidden.
Unnecessary behavior is exposed, or necessary behavior is missing, or identifiers are poorly named (poor abstraction).
Design decisions are not localized.
Conclusion
The principles of Abstraction, Encapsulation, and Modularity are not merely theoretical concepts; they are practical tools that, when applied thoughtfully, can improve the quality of software. Each principle possesses its own identity and together form a harmonious and robust foundation for software design.
Abstraction aims to capture and expose only the essential behavior of a concept, thereby reducing complexity and enhancing understandability. Encapsulation serves to protect the internals of a component from unnecessary exposure, helping to prevent problematic coupling. Modularity guides us to separate our software into distinct components, each bearing its own responsibilities and minimizing dependencies, improving flexibility and maintainability.
The journey to master these principles is a challenging and worthwhile one, and I hope that I've managed to provide clarity, or at the very least, cause you to think more deeply about them.

