Tuesday, July 2, 2019

Messaging - the essence of OOP

Note: This article is highly influenced by the work of Sandi Metz. Her books and lectures helped to fill in the missing pieces in my current understanding of the object-oriented design.

Probably we all heard about following job interview question:
"What are the four pillars of object-oriented programming?"

The expected answer usually is: Encapsulation, Polymorphism, Abstraction, and Inheritance.

These are really important concepts and I'm sure that every developer knows something about them.
Still, when we open some legacy code base, the code usually doesn't look object-oriented at all.
The fact that we are programming in C#, Java or some other object-oriented programming language does not guarantee that we are writing object-oriented code. In my experience it's the opposite: developers are often wrapping procedures in .cs or .java files.

The main problem with procedures is that they don't scale. Adding new features or changing behavior in large and complicated procedural code can be really time-consuming and stressful. I'm sure you already faced this kind of nightmare in your career.

So, if we consider that we know a lot about OOP and that we are practicing it for decades now, why are we still writing procedural code? Does that mean that OOP is bad? Well, maybe. But maybe we are missing something important.

Let's go back to the roots and look at a couple of quotes by Allan Key, the inventor of OOP:

“I thought of objects being like biological cells and/or individual computers on a network, only able to communicate with messages”

“The key in making great and growable systems is much more to design how its modules communicate rather than what their internal properties and behaviors should be.”

“I’m sorry that I long ago coined the term “objects” for this topic because it gets many people to focus on the lesser idea.”

“The big idea is “messaging””

Why is messaging so important and what did Allan Kay have in mind exactly?



Figure 1.

On Figure 1. we can see the message passing process. Object A is sending a message to the object B. Object A is called the sending object and object B the receiving object.
So, you may ask yourself: what is the difference between a method call and sending a message? There are a lot of discussions on the internet about this topic. To me, technically, there is no difference, but the term of sending a message represents much better the idea that the correct method will be selected and we do not know in advance which one will be executed.
One more subjective reason in favor of message passing terminology is that it makes me focus on communication between objects rather than focusing on types and internal properties of those objects.

Open closed principle (OCP)

OCP is one of the main principles of OOP which states that a class should be open for extension and closed for modification. In terms of reducing the cost of change, this is the perfect scenario. However, in practice, it can be really hard to achieve. The reason for this is simple: we can't predict the future. In order to be able to extend the class, we need to detect points of variation but we can't be sure which changes will come in the future. To me, detection of points of variation is one of the most, if not the most important part of the design process, because it leads us to more stable and maintainable solutions. Focusing on messages can't help us to predict the points of variation, but it can help us to detect them in the process of design and implement them at the appropriate moment.

Enough with the theory, let's look at an example.

The domain of the problem that we are solving is the party organization process. Our main goal is to organize parties for our customers.

The starting point could look like this:

The Party class is receiving Technician dependency as the parameter of the OrganizeBy method.
If we look closer we can see that Party class sends three messages to the Technician. Party knows that Technician is setting up speakers, mixer and lighting. Based on these messages we can conclude that the Party class knows too much about Technician. It knows how Technician is doing his job. Whenever Technician changes the way of setting up the equipment, Party needs to change also. This is bad design and we should hide how Technician is preparing a party. When we want to hide details, we introduce an abstraction, right? So, we can introduce ITechnician interface and the code now looks like this:

Is that better? No, because interfaces are not abstractions by themselves. We are still sending messages that are too concrete, only now, we are sending them to ITechnician. We should probably try to hide details by sending a more abstract message:

Notice that I removed the ITechnician interface and returned to the concrete Technician as an argument. This is better, but let's make our domain more interesting. We have a new requirement for a chef that needs to make food for the party and a disc jockey (DJ) that should create a song list.


By implementing new requirements, we broke OCP. We changed the implementation of the Party class in order to add new behavior. This is often hard to avoid, but at least every change that we are making should take us closer to that goal. One more time, let's examine the messages. Based on the message examination, we can conclude that Technician, Chef, and DiscJockey are participating in the organization of a party. So, we can rename the messages in the following manner:

Now, we are sending Organize message to all dependencies. All three dependencies are organizers of a party. This was an important step because we just discovered a role: the Organizer role. We can implement that role with the role interface IOrganizer. Now, OrganizeBy method can look like this:

OrganizeBy method is receiving a collection of the organizers as the parameter. Next, we are iterating through the collection of organizers and depending on the type of the organizer, we are sending the correct message. This is again really bad design. Whenever we want to add a new organizer we need to again change Party class so we didn't achieve anything in terms of reducing the cost of change. We should again do the closer examination of the messages that we are sending. There are two different messages: organizer.Organize(Equipment) and organizer.Organize(Guests). Instead of passing Equipment and Guests, we should try to pass the Party class as the parameter of the Organize message. The Party class becomes:

What we achieved here is that now we can introduce any new organizer for a party and we don't need to change the Party class. In the context of organizing the party, OCP is respected. This example does not suggest any kind of a pattern where you should pass a message sending object as a parameter to its receivers. It's just an example of how concentrating on messages that are passed between objects, we can achieve a proper object-oriented design.

In the context of the Party class, everything looks fine. But let's take a closer look at one of the organizers.

There is a problem here. Technician is sending Equipment message to the Party but it can also send Guests or even send the OrganizeBy message. Interface segregation principle is broken, and also, Technician and other organizers are forever bound to the organization of the Party class. In Ruby, for example, we can say that we have interface segregation out of the box, but in C# we need to put more effort in order to achieve it. Remember that we discovered the Organizer role earlier? Well, the Organizer is a role, but there is one more hidden role: Organizable. Party is organizable in the context of Equipment and Guests. So we can define two interfaces: IEquipmentOrganizable and IGuestsOrganizable.

Now we can implement the Party class like this:

The organizer interface becomes:

And organizers are:

Interface segregation is respected and organizers can organize basically everything that is organizable.

Abstractions

There are many definitions of what abstraction really is, but my favorite is by Robert C. Martin: 

"Abstraction is the elimination of the irrelevant and the amplification of the essential". 

Even if this may not sound clear at first, it's actually quite simple. Remember when we introduced IOrganizer interface? We eliminated the irrelevant details and amplified the essential in the context of party organization. Abstractions are about hiding irrelevant details. Let's go a few steps backward and look at the implementation of Technician and DiscJockey classes.


The usual process that I encountered is that people are trying to extract an abstraction out of objects. I'm also guilty of that process in the past. So, when we look at Technician and Chef, can we define a proper abstraction? Well, it depends on what we want to abstract. In the context of the party organization, in my opinion, it's impossible to extract a proper abstraction.

Remember that IOrganizer abstraction was resulting from analysis of the message passing process between Party and its dependencies and not as a consequence of some extraction of common behavior from objects. The main point here is that abstractions are in messages, not in objects. When you take into account that abstraction discovery is probably one of the most critical (and hardest) parts of the OOP design, then we can realize how extremely important this approach is. 

Conclusion

Software design is about how we organize our code. Object-oriented programming should make our life easier in terms of responding to constant changes. OOP design should be more maintainable and flexible compared to the procedural one. The behavior of the system in the OOP world should be changed by the composition of objects and not by changing procedural parts of the code.

With all this said, by respecting OOP principles, we should be able to provide greater value to our stakeholders in terms of ability to respond to change.

In this article, we didn't cover some design pattern that we should use on a daily basis. The intention was to try to understand how switching our mindset to focus on messages between objects can help us deal with the complex parts of the object-oriented design. 

I will sum up with a quote by Sandi Metz: "You don't send messages because you have objects, you have objects because you send messages."