The Assault on Object-Oriented Programming (OOP) 🔫🧐
One of the frequent mistakes among us programmers is to give in too easily to the temptation of not putting the right behaviors in the right places.
Hey, dear programmer! Remember the first time you dove into the world of Object-Oriented Programming? Maybe it was an epiphany, or maybe just a slight confusion 😂. Either way, it's a journey many of us share. Let's get back to basics and rediscover the heart of OOP.
Remember the first time you created a class, with its attributes and methods, and suddenly your code seemed to come alive? It was as if you were modeling the real world in your code editor. However, along the way, as we delve deeper into design patterns, architecture, and optimizations, we sometimes forget the essence, the basics. I'd like to start by revisiting the pillars.
Fundamental Principles 🔍
At the heart of OOP, we have the pillars: Encapsulation, Polymorphism, Inheritance, and Abstraction. It might seem basic to you, but how often do we find ourselves applying them almost automatically, without truly reflecting on their purpose? When we encapsulate, we're not just "hiding" data; we're setting boundaries, creating a contract for how our object interacts with the world. Polymorphism isn't just about multiple forms, but about crafting adaptable code that can handle the unexpected. Inheritance isn't just code reuse; it's about establishing a meaningful hierarchical relationship. And abstraction allows us to focus on what's truly important, discarding unnecessary details.
Data and Behaviors 🏃🏻🎲
To deeply understand OOP and how it differs from other programming methodologies, it's essential to understand the relationship between these two concepts.
Data 🎲
Data represents the information or the "attributes" of an object. In simple terms, they are the details or features of that object. For example, think of a car. What comes to mind when you try to describe it? Perhaps you think about its color, model, brand, top speed, and so on. In the context of OOP, all these features are the "data" of the car object.
public class Car
{
public string Brand { get; set; }
public string Model { get; set; }
public string Color { get; set; }
public int TopSpeed { get; set; }
}
Behaviors 🤪
If data are the features that describe an object, behaviors are the actions that this object can perform. Going back to the car example, what does a car do? It can accelerate, brake, turn on, turn off, etc. These actions that a car can execute are its behaviors.
public class Car
{
public string Brand { get; set; }
public string Model { get; set; }
public string Color { get; set; }
public int MaxSpeed { get; set; }
public int CurrentSpeed { get; set; }
public void Accelerate(int amount)
{
CurrentSpeed += amount;
if (CurrentSpeed > MaxSpeed)
CurrentSpeed = MaxSpeed;
}
public void Brake()
{
CurrentSpeed = 0;
}
public void Start()
{
// Logic to start the car
}
public void ShutDown()
{
// Logic to shut down the car
}
}
"Here, 'Accelerate', 'Brake', 'Turn On', and 'Turn Off' are methods that represent the behaviors of the Car object.
Relationship Between Data and Behaviors 🫶🏼
One of the beauties of OOP (Object-Oriented Programming) is that it allows us to encapsulate data and behaviors together into a single unit called an 'object'. This isn't just a design choice, but a way to reflect the real world in our code.
Think about yourself. You have attributes (data) such as name, age, height, etc., but you also have behaviors: you can walk, talk, think, etc. OOP captures this essence by allowing us to model objects in our code in the same way we see and interact with objects in the real world.
But how are we reflecting OOP in the projects we're working on daily? Let's talk about that."
Demystifying the Separation of Data and Behaviors 🤨
There is a belief that separating data and behaviors in our classes is the "correct" or "purest" way to practice OOP. Let's debunk this notion with an example:
// Data
public class ProductData
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public int StockQuantity { get; set; }
}
// Behaviors
public class ProductOperations
{
public void ApplyDiscount(ProductData product, decimal discountValue)
{
// Risk 1: Without validation logic in the context of ProductData
product.Price -= discountValue;
}
public void Sell(ProductData product, int quantity)
{
// Risk 2: Changes outside of ProductData control
product.StockQuantity -= quantity;
}
}
At first glance, separating data representation (as we see in Product
) from the associated behaviors (like in ProductOperations
) might seem like a brilliant idea. "A place for everything," we think 😂. But, upon further examination, we realize that this approach can hide risks and complexities. Let's dive in and understand together.
Unwrapping the Domains 🎁
The central idea of OOP (Object-Oriented Programming) is to encapsulate data and behaviors together. However, when we separate them, the business rules, which should be firmly rooted in the domain, begin to leak out. The result is business logic that becomes scattered, making the code harder to track and maintain.
Let's consider an example from a banking system:
public class CheckingAccount
{
public decimal Balance { get; set; }
}
public class CheckingAccountService
{
public void Withdraw(CheckingAccount account, decimal amount)
{
if (amount <= 0)
throw new ArgumentException("Invalid withdrawal amount.");
if (account.Balance < amount)
throw new InvalidOperationException("Insufficient balance.");
account.Balance -= amount;
}
}
In this example, CheckingAccount
only contains data, while CheckingAccountService
contains all the logic. At first glance, it might seem clean. However, if we need to implement new business rules, such as fees or differentiated withdrawal limits, the Service can become bloated and complex. Let's quickly discuss the risks.
Orchestrators who know a lot
By shifting business logic from the domain to "services" or "orchestrators", we fall into a trap. These orchestrators become responsible for much more than they should, going against the single responsibility principle.
In complex systems, we might end up with orchestrators that have multiple responsibilities, dealing with the logic of various different entities, making the code dense and highly coupled.
The Direct Impact on Maintenance and Extensibility
With business rules outside of the domain and located in the orchestrators, the code becomes less intuitive. Moreover, when introducing new features or changing existing rules, you find yourself having to modify the orchestrators, which could lead to undesired side effects.
Consider a new rule: the bank's premium customers can withdraw even with insufficient funds up to an overdraft limit. Implementing this in the current CurrentAccountService would be problematic, as you would have to introduce logic to check if a customer is premium, what their overdraft limit is, etc. The Service would grow in complexity and become harder to maintain.
The Risk of Violating Encapsulation ⚠️
Encapsulation is a cornerstone of OOP. By moving business logic outside the domain, we are exposing details that should be hidden. This not only makes the code less secure but also more prone to errors, as developers might make changes to the data directly, bypassing the business logic.
Risks Associated with "Data Bag" Classes 🚨
Data Bag classes (or "Data Structures") are classes that essentially just store data without providing any specific functionality or behavior. Structurally, they often only contain public fields (or properties with trivial getters and setters) and no significant methods.
Violation of SRP: Without a clear responsibility, it's easy to add random functionalities, contradicting the Single Responsibility Principle.
Compromised Maintainability: Logic scattered throughout the code makes the system less flexible to changes.
Weak Encapsulation: By exposing data without control or validation, they go against a central pillar of OOP.
Problematic Reusability: The lack of behaviors makes reusability more prone to logic duplication.
In summary, although they seem simple, "Data Bag" classes can cause unintentional complexity, making a system less cohesive and harder to maintain.
Orchestrators, Services, and the Safeguarding of the Domain 🤵♂️
When we talk about software architecture, especially in a world dominated by complex and interconnected systems, orchestrators and services often emerge as the main characters on stage. However, while these components are crucial for ensuring the smooth operation of a system, they should not overshadow or harm the essence of the domain. Let's dive deeply into the role and purpose of these agents and how they relate to the domain.
Orchestrators: Maestros of Workflow
Imagine an orchestra. You have several musicians, each with a different instrument, ready to create harmonious music. However, without a conductor to coordinate and direct, you would have only a cacophony of sounds. Similarly, in complex systems, orchestrators act as conductors.
The orchestrators are responsible for coordinating different services, ensuring that they work together harmoniously. They understand the sequence, dependencies, and high-level business logic. For example, when processing an online order, an orchestrator might coordinate services like payment, inventory, shipping, and customer notification.
But what's the relationship of orchestrators to the domain?
The distinction between domain and orchestrators is vital for the clarity and integrity of software architecture. The domain reflects the core of business logic: its rules, constraints, and relations. Put simply, it is the essence and definition of "what" the system does in terms of business rules.
On the other hand, orchestrators take care of coordination and execution. They define "how" these domain rules are put into practice within a specific flow or process. Orchestrators deal with the sequence, the interaction between components, and how data flows through the system.
The mistake lies in allowing orchestrators to delve too deeply into business logic, rather than just orchestrating. When this happens, we have orchestrators becoming part of the domain and taking on responsibilities that should belong to the domain itself. This entanglement complicates maintenance and clarity, as separating business logic from coordination becomes challenging.
In summary, while the domain focuses on the "what," orchestrators focus on the "how." Mixing the two can obscure the clarity of architecture and make the system harder to evolve and maintain. We will discuss this in more depth soon.
Services: Bridges to Specialized Features 🌉
Services, on the other hand, are like the individual musicians in the orchestra. Each service is specialized and focused. They perform specific tasks, such as processing a payment or checking inventory. In a well-designed architecture, services are consumer agnostic. They don't care who's calling them, be it an orchestrator, another service, or even a user interface.
However, it's vital to ensure that services don't become repositories for domain logic. If we start embedding business rules within a service, it quickly becomes monolithic and hard to manage. Instead, services should communicate with the domain to make decisions based on business rules.
UseCases and the Dance with the Domain 💃🏼
Now, how do use cases fit into this? They serve as a guide. By clearly defining what a system should do, use cases help shape the domain. They act as a bridge between business requirements and technical implementation.
In software and systems engineering, a use case is a list of actions or event steps that typically define the interactions between a role (known in the Unified Modeling Language as an actor) and a system to achieve a goal. - General and Business Definition.
The definition above doesn't light up the eyes of any programmer, so let's turn to the book Clean Architecture:
These use cases orchestrate the flow of data to and from the entities and direct these entities to use their Critical Business Rules to achieve the use case's goals. - Book Clean Architecture.
I would like to discuss the above sentence from the book further.
"These use cases orchestrate the data flow to and from entities..."
A use case refers to a specific action or set of actions that a system can perform. For example, in an e-commerce system, a use case might be "Place Order". This use case would be responsible for orchestrating various operations, such as checking an item's availability, processing the payment, and updating the inventory.
When it is said that use cases "orchestrate the data flow to and from entities", it means they manage the interaction and movement of information between various parts of the system.
"Entities" refer to key objects or concepts in the business domain. In the context of e-commerce, entities might be Customer, Order, Product, etc.
"...and guide these entities to use their Critical Business Rules to achieve the objectives of the use case."
The "Critical Business Rules" are the principles, constraints, and logics that govern the operation of a business or system. For example, a business rule in our e-commerce example might require that a customer can only purchase a product if it is available in stock.
When a use case "guides these entities to use their Critical Business Rules", it is instructing or guiding the entities to apply these specific rules in order to achieve the objective of the use case. In the "Place Order" example, the use case might guide the Order entity to apply business rules to check product availability and, if available, proceed with other steps such as stock deduction and payment processing.
When developing systems, it is crucial to regularly reference these use cases to ensure the domain remains pure. Without this reference, there's a risk of "domain leakage", where technical decisions start to distort or dilute the business rules.
Example 1: In an online store, a use case might be "Buy Product". This use case would encompass all the steps that a user (actor) would take to search for a product, add it to the cart, provide shipping and payment information, and finally confirm the purchase.
Example 2: In a bank, a use case could be "Transfer Money". The user, in this case, would go through steps of selecting a source account, specifying the amount, providing destination account details, and confirming the transfer.
Relationship between UseCases and Domain Rules 🫱🏼🫲🏼
Confusion arises when the lines between UseCases and domain rules start to blur. UseCases should handle coordination and sequencing of actions, while domain rules are concerned with underlying business logic and how data is handled and manipulated.
In the "Transfer Money" example, the UseCase should coordinate the sequence of events (select accounts, specify amounts, confirm, etc.), but the exact rules of how the money is transferred, balance checks, fees applied, etc., reside in the domain and should be encapsulated within relevant entities and aggregates, like BankAccount.
Avoiding Domain Rule Leakage into UseCases
The "leakage" occurs when we start moving business logic and domain rules from the domain into the UseCases. This is problematic because:
Violates Cohesion: The system loses its cohesion, and business logic becomes scattered, making it hard to maintain and understand.
Testability: UseCases overloaded with business rules become harder to test since you will have to mock and set up many more conditions.
Reusability: Since business logic is mixed with action coordination, it becomes almost impossible to reuse it in other contexts without duplication.
Let's imagine a simple scenario of an order system where we have the need to apply a discount to the order based on specific criteria.
// Domain Class
public class Order
{
public double TotalValue { get; set; }
public double Discount { get; set; }
}
// UseCase with domain rule leakage
public class ApplyDiscountUseCase
{
public void Execute(Order order, double discountPercentage)
{
if (order.TotalValue > 100 && discountPercentage > 0 && discountPercentage <= 10)
{
order.Discount = order.TotalValue * (discountPercentage / 100);
}
}
}
In this example, we can see that the logic for applying the discount, which is clearly a business rule, has been placed in the UseCase. This is a leak, because the business rules that should be contained in the domain are now in the UseCase.
To avoid the leak:
Clarity in Requirements: When writing or reviewing UseCases, make sure it describes the sequence of actions and interactions, and doesn't delve into business logic.
Rich Domain: Advocate for a rich domain, where entities and aggregates have behaviors, not just data. They should encapsulate business rules and related logic.
Delegate Responsibilities: When implementing a UseCase, upon encountering a need to apply a business rule, delegate that responsibility to the domain.
A Small Reflection 😄
We might be tempted to place business rules in the UseCase for several reasons:
Lack of Understanding: We might not clearly understand the distinction between domain rules and the coordination of actions that a UseCase should carry out.
Immediate Simplicity: At first glance, it might seem simpler to put the logic directly into the UseCase. After all, it's "just one line of code," right? But as the system grows, this approach quickly becomes unsustainable.
Now let's correct our example:
// Refactored Domain Class
public class Order
{
public double TotalAmount { get; private set; }
public double Discount { get; private set; }
public Order(double totalAmount)
{
TotalAmount = totalAmount;
}
public void ApplyDiscount(double discountPercentage)
{
if (TotalAmount > 100 && discountPercentage > 0 && discountPercentage <= 10)
{
Discount = TotalAmount * (discountPercentage / 100);
}
}
}
// Refactored UseCase
public class ApplyDiscountUseCase
{
public void Execute(Order order, double discountPercentage)
{
order.ApplyDiscount(discountPercentage);
}
}
In this way, the system keeps the core business logic centralized in the entities (promoting cohesion and reusability) while allowing the UseCases to manage the complexity of coordinating multiple entities and operations to achieve a specific goal.
The Silent Assault on Encapsulation! 🚨🔫
This pillar is often seen as a kind of "best practice" 😂, but what's alarming is that over time, this vital principle has been misunderstood or, in some cases, completely overlooked.
Understanding encapsulation as merely "hiding" data is not only superficial but also a misguided perception. Encapsulation is not about hiding, but about protecting.
Consider the DNA of a cell: it isn't "hidden" within the nucleus just for the sake of it. It's kept there to protect it from harmful external influences. Similarly, encapsulation in OOP safeguards data from being manipulated in ways that might harm the program's integrity.
Let's explore a common example in C# to illustrate the point:
public class Car
{
public string Model;
public int Year;
public double Price;
}
In this simple Car class, all members are public. This means that any external code can directly modify the Model, Year, and Price without any restriction.
var myCar = new Car();
myCar.Model = "XYZ";
myCar.Year = -1500; // Negative year?
myCar.Price = -10000; // Negative price?
See the issues here? We can easily assign a negative year and a negative price to the car, which is logically incorrect. Proper encapsulation could protect us from such inconsistencies.
Now, you might ask: why does this happen? Why do some developers overlook proper encapsulation?
Part of the problem might be haste. In agile development environments, where rapid delivery is often prioritized, there might be a lean towards writing code "that just works", without adequately considering structure or design. In other cases, it might be a lack of understanding or appreciation for the depth of the concept.
Another part of the problem is education. Often, OOP concepts are taught in a very theoretical manner. Without relevant practical examples, encapsulation might seem like an unnecessary abstraction. "Why should I bother hiding this?" might be a common question.
But, with that mindset, we miss the true purpose. Without encapsulation, our software becomes fragile. They become prone to errors, as more and more code starts interacting with parts that should be protected. This isn't just a theoretical issue - it can lead to real bugs, system crashes, and endless hours of debugging.
There's more. The lack of proper encapsulation makes the code less reusable. When the Car
class is designed without adequate protections, it becomes a vulnerable entity in our code. Thus, any part of the code, anywhere in the system, can change its state, often in unintended or unforeseen ways. This is especially problematic since a class should not just represent data, but also ensure that this data remains consistent and valid at all times.
On the other hand, good encapsulation allows developers to modify the internal logic of a class or component without affecting the consumers of that class. It acts like a contract: "Don't worry about what happens inside. I promise to provide what you expect, as long as you interact with me on the terms I've set". By correctly encapsulating a class, you're providing a clear "interface" to other developers - a kind of "instruction manual".
When we stray from these principles, we're not just assaulting the integrity of OOP, but compromising the quality of the software we produce. We're sacrificing robustness, maintainability, and in many cases, security.
Let's take a fresh look at the given example and refactor it appropriately, prioritizing correct encapsulation.
Refactoring with Encapsulation in Mind 🧠
public class Car
{
private string _model;
private int _year;
private double _price;
public string Model
{
get { return _model; }
set
{
if (string.IsNullOrEmpty(value))
throw new ArgumentException("The model cannot be empty or null.");
_model = value;
}
}
public int Year
{
get { return _year; }
set
{
if (value < 1885) // The first car was invented in 1885.
throw new ArgumentOutOfRangeException("The year cannot be earlier than 1885.");
_year = value;
}
}
public double Price
{
get { return _price; }
set
{
if (value < 0)
throw new ArgumentException("The price cannot be negative.");
_price = value;
}
}
}
When a programmer tries to instantiate a Car and assigns invalid values to these members, he will get an error. This error acts as a guide, telling the developer that something is not right and needs to be fixed.
var myCar = new Car();
myCar.Model = "XYZ";
myCar.Year = -1500; // Error! Value outside acceptable range.
myCar.Price = -10000; // Error! Negative value is not accepted.
Now, encapsulation acts as a wall, protecting the internal logic of the class and ensuring that it remains consistent and logical. And this, in essence, is what OOP strives to achieve: creating robust and reusable building blocks that can be combined in complex ways to build larger and more complex systems, without compromising the integrity or functionality of those blocks.
Furthermore, by focusing on proper encapsulation, we are also making our code more maintainable. In the future, if we need to change how the price is determined or if we need to add additional logic to the car model, we can do this without affecting the code that uses our Car class.
You might be thinking, "But this is very verbose!" Indeed! But in Csharp, we can use a less verbose syntax:
public string Model
{
get => _model;
set => _model = string.IsNullOrEmpty(value)
? throw new ArgumentException("The model cannot be empty or null.")
: value;
}
Nothing stops you from using the popular library for validations in C#, FluentValidation. It allows you to define validation rules in a fluent and declarative manner. But be careful not to get addicted to it! 😂
The Assault on OOP! 🔫
When we separate data and behaviors in our coding, we are compromising one of the fundamental pillars of object-oriented programming: encapsulation. OOP, at its core, is about the union of data and the behaviors that operate on this data within a single cohesive entity - the object.
At the heart of this "assault" is a disconnect between the fundamental principles of object orientation and the daily practices adopted by developers. While OOP, in its essence, promotes clarity, cohesion, and modularity, these deviations often result in code that is anything but that. Many software projects lean towards separating data and behaviors. It's like an assault on the pillars. It's like saying: “Hand over all those behaviors now!”. And this might not seem so harmful at first... but over time, it becomes a huge problem!
I'll discuss a few reasons why separating data and behaviors can be considered an "assault" on the true nature of OOP:
Violation of Encapsulation: As we discussed extensively, by separating behaviors from data, you are essentially making all data public, allowing them to be altered from anywhere in the code. This violates the encapsulation principle, which aims to protect the internal state of an object.
Loss of Cohesion: In OOP, we aim to create cohesive classes, where each class has a clear and well-defined responsibility. When we separate behaviors from data, classes become less cohesive, and their real responsibility becomes less clear.
Increased Coupling: When behaviors are separated from data, the chances of unwanted coupling between classes increase. Classes containing behaviors need to know the internal structure of data classes, making the system more fragile to changes.
Maintenance Challenges: Suppose you want to change the way certain data is stored or computed. If behaviors are in a separate class, you will need to review both classes, and possibly many others, to ensure all interactions between them still function as expected.
Compromised Testability: Classes that are purely data or purely behavior are harder to test in isolation. In OOP, a well-defined object can be easily instantiated and tested under various conditions. However, if behavior is separated from data, tests might require the creation of many mocks, becoming less clear and more error-prone.
Counterintuitive for Domain Modeling: When we model software, we often think in terms of real-world entities and their interactions. For example, a "Car" can "Accelerate" or "Brake". It's natural to combine these concepts into a single Car class that has Accelerate() and Brake() methods. Separating data and behaviors here would go against our intuitive understanding of the problem.
Reversing the "assault" is not an easy task. It requires reeducation, a reassessment of priorities, and, most importantly, a renewed understanding and appreciation of the fundamental principles of OOP. Now it would be interesting to be honest about the current state of software engineering!
The Point of Equilibrium
It's important to seek balance. Not all classes in our application need to be rich in behaviors. In reality, there are scenarios where having simple classes, or "data bags," is not only acceptable but also appropriate.
Take, for instance, the DTOs (Data Transfer Objects). They are often used in application layers to transfer data between subsystems, especially when interacting with interfaces, databases, or external services. The simplicity and predictability of DTOs are their greatest strengths. They are clear, straightforward, and most of the time, they don't have any associated logic. They are there to transport data, and they do it very well.
There are several projects or services where the main focus is on the transit, storage, or presentation of data, and sophisticated business rules or behaviors are not required. Let's look at some scenarios where this happens:
Basic CRUD Services: Some services are purely meant to create, read, update, and delete records from a database or another data source. In such cases, the complexity surrounding business logic is minimal, and the primary concern is ensuring efficient data access.
Proxies and Gateways: These are intermediary services that simply pass requests from one point to another. They may handle data in transit but generally do not impose business rules.
Caches and Temporary Storages: Systems that act as caches or temporary storages are more concerned with the speed and efficiency of data storage and retrieval than with business logic.
Data Transformers: There are services whose main task is to transform data from one format to another (e.g., XML to JSON or vice versa). The logic here is in the transformation, not in the business rules.
Simple Front-end Applications: Some presentation applications or dashboards merely display data from an API or database. The actual processing and business logic are contained elsewhere, and the front-end application primarily serves as a viewing layer.
Logs and Monitoring: Services that collect, store, and perhaps visualize logs and metrics generally don't require complex business logic. They are concerned with capturing and storing events as they occur.
However, when we start entering the territory of corporate systems - those large systems that manage critical operations and whose data needs to be handled with utmost precision - the story changes. These systems often have complex rules and nuances that need to be carefully orchestrated. In such scenarios, relying on classes that are just "data bags" can become risky. This is because they do not offer the capability to ensure data integrity on their own.
Here, the importance of behaviors stands out. Behaviors ensure that entities remain in a valid state, according to business rules.
Deviations from the Essence... 😕
The journey of object-oriented programming (OOP) has been winding and full of twists and turns. From its inception to its real-world application, many have interpreted and reinvented OOP in ways that don't always align with its core essence. Let's delve into some of the reasons why this is happening. I won't cite too many so as not to make the post longer than it already is 😅.
A significant reason for this drift is the rapid pace of the tech industry. New languages, frameworks, and tools emerge almost daily, each with its abstractions and patterns. For a developer, staying updated is crucial, but it's also easy to get drawn into the "new" and "performance-centric", forgetting the foundational bases and principles. Amid this avalanche of novelties, the principles of OOP might seem a bit "old-fashioned" or "traditional" to some.
Another consideration is the constant pressure for productivity and quick delivery. In many work environments, the focus is on delivering quickly rather than delivering with quality and according to well-established principles. In this setting, it's tempting to take shortcuts. Instead of thinking carefully about an object's design and cohesion, it's easier just to split data and behaviors, for instance. The famous "It works" becomes a mantra, even if it later morphs into a massive technical debt that the team will have to confront down the line.
Moreover, the collaborative nature of programming can, paradoxically, lead to issues. In large teams, not everyone will have the same level of understanding or appreciation for OOP. If a team member isn't fully onboard or simply doesn't get it, they might start writing code that deviates from pure object orientation. Without rigorous code reviews or clear standards in place, these discrepancies can sneak into the codebase and become the norm rather than the exception.
There's also a broader cultural issue at play. We live in an era of instant gratification. We want immediate results. And with so many tools at our disposal, we often feel like we should use all of them. This can lead to massive overengineering, where a straightforward solution based on solid OOP principles is overshadowed by layers upon layers of complexity. Sometimes, it's a classic case of not seeing the forest for the trees. We become so wrapped up in the minutiae of the code that we forget the bigger picture: creating software that addresses real problems efficiently and maintainably.
Additionally, education in computer science itself might be partly to blame. Many courses emphasize algorithms and data structures but not necessarily the principles and practices of good software design. A programmer might leave college or a course knowing how to create a binary tree but not necessarily how to design a cohesive and well-structured object-oriented system.
What to do?
Awareness is the first step. In a world where speed often surpasses sustainability, it is essential to pause and reflect on the approaches we are adopting. We must ask ourselves: "Am I truly utilizing OOP to its fullest potential? Or am I just wearing its costumes while neglecting its essence?".
Cultivating a genuine understanding and appreciation for object-oriented programming requires more than just knowing how to create a class or understand inheritance. One needs to understand the underlying philosophy, the desire to model the real world in a logical and intuitive way, so that the software can mirror reality as naturally as possible.
Furthermore, in the workplace, it is crucial to establish a culture where design quality is not sacrificed for speedy delivery. This might involve difficult and possibly unpopular discussions about timelines and scope, but in the long run, well-designed, object-oriented code will save time, money, and a lot of headaches.
The advantage of OOP is that, when properly implemented, it can make code more readable, maintainable, and adaptable. Solutions are modeled more closely to the problem domain, making them more intuitive for developers and stakeholders. In contrast, when we deviate from the foundational principles of OOP, we risk losing these benefits.
Instead of being seen as outdated or restrictive, OOP should be valued as the powerful tool it is, capable of bringing clarity to chaos and ordering complex systems for easy domain understanding.
Thus, with every line of code we write, with every object we model, we should strive to honor the original vision of OOP while also remaining open to innovations and changes. And above all, we must remember that at the heart of any technology is humanity.
This post was inspired by Leonardo Leitão from Cod3r, in his YouTube video, link at the end. It's very much worth checking out!
I recommend reading Mauricio Aniche's book as well:
Simple Object Oriented Design: A guide to creating clean, maintainable code.
That's it for this post! Thank you for reading to the end; if this post was helpful, please share! Until next time! 😄