Don’t repeat yourself
Every piece of knowledge must have a single, unambiguous, authoritative representation within a system
— Andy Hunt and Dave Thomas, The Pragmatic Programmer
Car in API
We develop multiple systems that communicate via API in JSON format. The most transferred information are information about cars.
An example of such transferred car
"name": "Arteon Shooting Brake",
We have to express this JSON in objects. We use PHP in our company, but we want to develop modern type-safe code, so we create an objects for this structure (instead of PHP arrays).
We start writing classes, and we see immediately a pattern —objects have often fileds
"key": string , and
"name": string , even the shortName could be expressed by
Excellent result! Our code doesn’t violate the DRY principle, and it is also pretty short.
Inheritance in OOP
We used inheritance to keep our code DRY. Let’s explain deeply what inheritance means.
class Make extends AbstractKeyNameEntity
Make class inherits all properties and behavior of the
AbstractKeyNameEntity. So the
Make class has all protected/public properties as well as all protected/public methods of the parent.
Let’s draw the UML class diagram of our code.
The relation between Make and KeyNameEntity (also Model, Color) expresses
extends. In class diagram it is called inheritance/generalization, and means
Make is a KeyNameEntity
Inheritance means “IS-A” relation. So Make is a KeyNameEntity. Wherever we need to use KeyNameEntity, we can without any restrictions use the Make, because the Make is the KeyNameEntity.
And it starts to be awkward —we never want to use the KeyNameEntity without knowing the concrete class. We can never use Make instead of Model or Color instead of Make.
Inheritance is the strongest relation between classes in OOP, and therefore very difficult to change or refactor. Inheritance should be used only in cases we need to represent IS-A relation - one class is another class.
Traits in PHP
Trait is a tool that imitates multi-inheritance in single-inheritance languages https://www.php.net/manual/en/language.oop5.traits.php. Traits are expressed in UML class diagram as same as inheritance.
Therefore traits express the same relation as inheritance — IS-A relation.
The relation between Car and Make (also Model, Color) means composition. Car is composed of make, model and color. The composition is one of the safest relations in OOP, and means “instance of Make class is owned by an instance of the Car class”, the Car class can therefore safely do anything with related instance. The composition relation causes no problems, it is just nice to have a complete image of terms.
Couple of months later
After couple of month of programming (adding features), the API evolved into
"name": "Arteon Shooting Brake",
Are Make, Model, and Color still the same concepts?
The PHP code looks like
Take a look on a Model. What does it represent? What does it contain? I’d say it contains only ModelFamily. It isn’t obvious that the Model contain more properties, we have to take a look into the KeyNameEntity first.
We had also a trouble — our colleagues used the short name of color in couple of places of our system, but this property doesn’t make sense on color. So we decided to override the method, so our programmers are warned
- Our code is DRY. We don’t duplicate code, and therefore the code is short.
- Almost any change is complicated. When dealing with a Model or Color we have to think “Model is a KeyNameEntity, do I change behavior of the KeyNameEntity by this change? Do I change also behavior of descendants?”
- We have to solve cases that don’t naturally exists, like getting the short name of the color. How to react to such problem? How to name the exception? What error message should we show to the user? And why should such case even ever happen?
- In other words, we created problems by introducing KeyNameEntity, and now we’re solving these problems.
- Problems are caused, because we joined unrelated concepts. And we joined unrelated concepts just because we see similar code.
- The code is too DRY.
Add w̵a̵t̵e̵r̵ code and make it WET again
Let’s forget about the DRY rule. Let’s add code, remove the problematic KeyNameEntity and see what’s the result.
WET: Write Everything Twice
- The code is longer.
- The code is straight-forward to read. All properties are in a particular class, and there is no need to shift attention to another class.
shortNameis not nullable.
- We don’t have to solve
shortNamein the Color problem. This problem disappeared.
- All in all, the code is boring. It’s so obvious, we don’t have to think twice when reading it. And that’s our goal — boring code that has no surprises.
- Class diagram is about twice simpler.
- Make is Make, Model is Model, Color is Color. There is no way we can interchange these concepts, and that is good.
DRY is commonly understood as “don’t repeat the code”, but this is wrong. DRY is about knowledge.
We have an API based application in the Carvago. Frontend in TypeScript, Backend in PHP, API documentation in OpenApi. Every time we make change or add feature, we have to make change in 3 places. And this situation is the correct situation that violates DRY, because we have the knowledge in 3 places.
A solution will be to maintain only the API documentation with Fronend and Backend generating DTOs by this documentation. But such system is a challenge, and will take take time to figure it out properly.
Another typical violation of DRY is computation in PHP entities and the same computation in the database of reports. The computation in database have a sane reason — it operates over many (millions) of records, and using the PHP entities would be impossible. So we violate DRY, and we know about it.
Even this situation can be resolved by using read model and read database that is rebuild from the write model. The computation logic is encapsulated in the write model, and when the data changes, the computed value (from write model) is propagated to the read database.
Just write more code (write everything twice).
✖ It is more code.
✔ Is easy to read and understand.
✔ Is easy to add features.
✔ Don’t have to solve artificially created problems.
✔ Class diagram is easier
✔ Class diagram represents exactly the problematic
✔ The code represents exactly the problematic
The same code representing two different concepts doesn’t mean we violate the DRY.
Traits in Practice
Traits are an interesting way to provide simple/sample implementation of an interface. Programmers have a possibility to implement code by themselves due to the interface, and still can use the help of the trait.
This is nicely done eg. by theleaguephp/oauth2.
Although the class diagram is pretty wild, the benefit is that the library provides basic implementation. When we use the library for the first time, we make it work fast without studying all interfaces. After we confirm this library makes sense for us, we are free to implement interfaces by our own.