Each programming paradigm has its fundamental pillars. In the case of object-oriented programming (OOP), the main ones are: inheritance, polymorphism, abstraction, and encapsulation. Here, I'll bring some elements related to encapsulation.
Variables (Attributes)
In most programming languages based on the object-oriented paradigm, it's common to model classes with private (private) or protected (protected) attributes to restrict direct access. However, a frequent mistake is making attributes private while also providing getters and setters for all of them. This practice weakens encapsulation because, in practice, there's little difference between accessing an attribute directly or through simple methods.
It's important to note that access modifier visibility varies between languages. In Java, for example, a member with protected can be accessed by classes in the same package (package-private) and by subclasses, regardless of the package. In Kotlin, protected limits access only to subclasses, preventing other classes in the same package from accessing it directly.
Therefore, when modeling classes, it's essential to understand the specifics of your chosen language to ensure efficient encapsulation that's coherent with the system's architecture.
public class Food {
private String id;
private String description;
private Product product;
private LocalDate received;
private LocalDate sampled;
public String getId() {
return id;
}
public String getDescription() {
return description;
}
public String getProduct() {
return product;
}
public String getReceived() {
return received;
}
public String getSampled() {
return sampled;
}
public void setId(String id) {
this.id = id;
}
public void setDescription(String description) {
this.description = description;
}
public void setProduct(Product product) {
this.product = product;
}
public void setReceived(LocalDate received) {
this.received = received;
}
public void setSampled(LocalDate sampled) {
this.sampled = sampled;
}
}Methods (Behaviors)
Adding methods to an object goes beyond simply getting or modifying attribute values. But you might be thinking: I get it, but how do I initialize my object?
There are several ways to do this, and the most common is using a constructor. But perhaps you'll say: Manoel, I don't always have all the information when creating the object. Will I need to pass null for attributes that don't have values yet?
The quick answer is: not necessarily. You can, for example, create multiple constructors (we'll dive deeper into this when discussing polymorphism). Another widely used approach is the Builder design pattern, one of the GoF (Gang of Four) patterns, which I particularly enjoy applying.
The true purpose of encapsulation goes beyond simply creating methods to change values and hide variables. Its goal is to provide behaviors that modify the object's state in a controlled manner. This is especially critical in complex domains, where correct behavior modeling directly impacts the system's integrity and consistency.
Encapsulation and Behavior Modeling
Since OOP aims to simulate the real world through objects, let's look at an example:
Imagine a Person class that has a state represented by the attributes energy and fat percentage. If the person's energy is low, how can we change it? Would it be enough to call setEnergy(100)?
In the real world, this doesn't make sense. Instead, we trigger an action: eating. The method eat(Food food) encapsulates this action. What happens inside it doesn't need to be known by whoever calls it, but we know that, in the end, the Person object's energy will be adjusted.
public class Person {
private static final int MAX_ENERGY = 100;
private static final double KCAL_PER_KG_FAT = 7700.0;
private int energy;
private double fat;
public Person(int energy, double fat) {
this.energy = Math.min(energy, MAX_ENERGY);
this.fat = Math.max(0, fat);
}
public void eat(Food food) {
if (isOverload(food)) {
fat += energyOverloadedToFat(food);
energy = MAX_ENERGY;
} else {
energy = Math.min(MAX_ENERGY, energy + food.getEnergy());
}
}
public void exercise(Modality modality) {
int energySpent = (int) modality.getExpendedCalories();
energy = Math.max(0, energy - energySpent);
double fatCalories = modality.getExpendedCalories() * 0.5;
double fatBurned = fatCalories / KCAL_PER_KG_FAT;
fat = Math.max(0, fat - fatBurned);
}
private double energyOverloadedToFat(Food food) {
double excessEnergy = Math.max(0, food.getEnergy() - (MAX_ENERGY - energy));
return excessEnergy / KCAL_PER_KG_FAT;
}
private boolean isOverload(Food food) {
return energy + food.getEnergy() > MAX_ENERGY;
}
}Encapsulation in Practice
A person's energy is a body state, while eating is a behavior. Encapsulation ensures that energy cannot be directly changed. Instead, only appropriate methods can modify this state in a controlled manner.
Furthermore, this action can trigger other effects. If the person consumes more energy than they spend, the excess will be stored as fat. However, the fat percentage attribute cannot be modified directly. When eating, we notice that excess energy increases the fat percentage. And how do we reduce it? We need to adopt another behavior, like running, for example.
In other words, the body has methods that represent actions and attributes that represent states. State modification occurs internally, as a consequence of actions, and not through direct attribute manipulation.
Deliberately exposing attributes for direct modification compromises encapsulation, making the system more fragile and prone to inconsistencies.
The Importance of Encapsulation in Code Design
When we ignore the power of encapsulation, two common problems arise:
1️⃣ Primitive Obsession: Occurs when we neglect the essence of an attribute and, instead of encapsulating it in a more representative object, we directly use primitive types. This can result in less expressive code that's harder to maintain. This is a relevant topic I'll explore in a future article.
2️⃣ Anemic Entities: Occurs when classes act only as data containers, containing only getters and setters, without real behavior. In Domain-Driven Design (DDD), this type of modeling weakens the domain because it shifts business logic to external services, making the code less expressive and harder to evolve.
Good code design makes the most of the language's resources. In object-oriented programming, this means applying encapsulation correctly, limiting direct access to attributes and creating behavior-rich entities.
DDD goes further by encouraging the creation of models that faithfully reflect the real-world domain. By using a ubiquitous language, aligned with business experts, we ensure that the code accurately represents the system's essential concepts and rules. Thus, entities cease to be mere data structures and begin to capture the meaning and intent of the domain.
Properly modeling classes not only ensures the integrity of business rules but also makes the system more expressive, cohesive, and sustainable in the long term.
Conclusion
Encapsulation goes far beyond simply making attributes private. Its true power lies in creating behaviors that ensure domain consistency.
By applying encapsulation correctly and modeling rich entities, we ensure that our code not only works but faithfully represents the rules and concepts of the real world. This strengthens system design, improves its maintainability, and reduces errors resulting from inconsistent modifications to object state.
If you enjoyed this content, stay tuned for the next article, where I'll explore more about
🔗 Want more content about leadership, code design, OOP, and DDD?
Follow me on social media for more programming discussions! 🚀