How we are supposed to use OOP
Or what is the Law of Demeter?
The Meme
A few days back, I saw a meme related to real-life reflection in the form of source code. One thing immediately caught my eye — The Law of Demeter is violated in this snippet. Wanna know more? Let's dive into this example.
Domain definition
The first and most important thing would be to define our domain. From this example, we can see a few key things that will be important later:
- We are modeling some real-life processes that reflect people and their interactions.
- Each person has hobbies (piano), memberships (gym), a job, assets (car, watch), a home, tattoos, a hairstyle, friends, etc.
- Certain life events (midlife crises) can trigger changes in a person.
Existing code analysis
Now, let's analyze the given snippet line by line and see what can be wrong there. I will not focus on minor details, such as naming, code style, etc. Also, I will not take into account pseudo code like NormallStuff.
The main goal of this analysis is to see how the 'carl' entity is used and how this usage could evolve.
Age Calculation
Firstly, we see that age is calculated based on the date of birth. Given our domain, we might expect age to be used in different scenarios, such as other age-related events like retirement. But with the given approach, we must always calculate age manually, leading to accumulated code duplications.
Exposed collection properties
Collections, such as Memberships, Assets, Tattoos, and Friends, can be modified by any owner of the 'carl' instance. In practice, we can't enforce any domain rules over those collection modifications. Or, if we would try to implement them, we would have to either create a separate domain service or copy-paste domain rules to all places with 'carl' entity usage.
For example:
- If 'carl' has any preferences for his memberships, we would have to enforce those preferences directly in the loop.
- When 'carl' exposes his assets collection to the general public, anyone can erase assets collection the same way friends are cleared.
I guess now you can imagine the magnitude of problems we could get by exposing mutable collections. So, let's move to the next issue.
Exposed properties with rich behavior
By exposing property' Home,' we again allow unauthorized access to protected behaviors. For example, we can create ridiculous surprises like this:
Analysis Summary
With existed codebase, our entities are protected only by a thin level of trust to codebase owners. There are no effective ways to define domain rules and ensure all entities' clients are exposed to them. Coincidently, some of the problems described above can be solved by following The Law of Demeter.
Principles overview
The Law Of Demeter
The main idea of this principle is to limit coupling between objects by limiting their dependency on closest friends.
For example, a car consists of many components connected to each other. Humans drive a vehicle by using a simplified interface, allowing the car to solve all other driving-related challenges. When you want to accelerate, you are adding extra pressure on the acceleration pedal, but you are not controlling fuel-air mixture richness yourself.
In other words, it's preferable to interact with other objects using a simplified interface and not manual interaction with internal object components.
Here, we call Carl's method to add an asset to his collection, which would effectively hide all other options from us.
Tell Don't Ask
Another great principle that correlates with The Law Of Demeter is TellDontAsk. In general, this means we need to tell objects what they need to do instead of requesting objects' data and changing it down the road.
Tell-Don’t-Ask is a principle that helps people remember that object-orientation is about bundling data with the functions that operate on that data. It reminds us that rather than asking an object for data and acting on that data, we should instead tell an object what to do. This encourages to move behavior into an object to go with the data.
- Martin Fowler
For our case, we would tell Carl to renovate his Home instead of invoking renovation ourselves.
Now we can run all business rules in the RenovateHome method before jumping to renovation.
Let's make it better.
I will review two options for managing Carl's transition to a midlife crisis. The first option will only fix minor problems and keep the existing while-loop. The second would be more sophisticated and give Carl much more behavior than he currently has.
Option 1: we are controlling Carl's preferences
Let's replace property calls with specialized methods and see how they will change our loop.
First of all, we can appreciate the fact that now it's not possible to manipulate Carl's assets without his will, as the assets collection is not exposed.
More than that, we can enforce additional business rules inside Carl's class:
This change on its own will already reduce a lot of code duplications related to enforcing business rules. However, we can go even further and limit our interaction with Carl to a single operation.
Option 2: we are allowing Carl to decide his fate (preferable)
This option will encapsulate the logic relevant to Carl's case. We will only instruct Carl to enter a midlife crisis when applicable.
Now, it's up to Carl's internal logic to decide whether it's possible to have a crisis now or not. More than that, now we need only two properties and one method of carl, which will drastically reduce possible misuse and reinforce your SOLID principles. Let's take a look at the EnterMidlifeCrisis method itself.
Firstly, we can no longer execute all these actions at a random point in time. This method will ensure that right now entity is in the proper state for a transition. Also, Carl's class has complete control over this behavior, meaning that all business rules related to Carl's preferences are already built-in, so we don't need to double-check them.
Conclusion
We've just finalized a code review for a meme. What a time to be alive, huh?
Shortly, don't hesitate to connect your data and behavior. You will easily avoid things like code duplications or missed business rules. More than that, when your entities allow uncontrolled state changes, you have an infinite opportunity for strange bugs. These opportunities will be happily (and unintentionally) exploited by engineers from your team.
I hope this article gave you an alternative view of your current problems. Feel free to tell your story here, and let's see what we can do.
Check out my other articles.
Are you confused with Dispose Pattern? Here is a 5-minute explanation for the most common use cases.
Wanna know how much LINQ to objects is slower than conventional for-loop? I've measured that for you.
References
All links listed here are non-affiliate.
The screenshots above are made from the source code that can be found here:
Useful articles:
- https://martinfowler.com/articles/mocksArentStubs.html
- https://martinfowler.com/bliki/TellDontAsk.html
- https://www2.ccs.neu.edu/research/demeter/demeter-method/LawOfDemeter/general-formulation.html
Fancy Syntax Highlight provided by ReSharper.