Since the very beginning of stored program concept, we learned that software is Programs + Data. It is clear that without data there is no software.
In object-oriented programming we build models that evolve over time, emulating the knowledge we learn by observing the reality we are representing.
However, we manipulate and abuse those changes uncontrollably, violating the only important design principle by generating incomplete (and therefore invalid) representations and propagating the ripple effect with our changes.
In the functional paradigm, this is elegantly achieved by directly forbidding mutations. We can be (a little) less drastic.
Returning to the essential
The great Fred Brooks gave us a few thoughts. Among others, he told us about The myth of pregnant women, he taught us to be suspicious of silver bullets and educated us in the separation of accidental vs. essential complexity.
The essence of an entity of reality is that which makes it be itself and not another.
The entity's accident happens due to temporary situations that despite changing object behavior do not prevent us from noticing we are watching the same entity even though it evolves as we do not bathe twice in the same river.
Being true to our bijection, in our computable model we should be able to distinguish when an object changes in terms of the accidental and prohibit all essential changes (because they would violate that bijection which is our only principle).
Objects should know how to defend themselves against invalid representations. They are the powers against mutants.
Is data ok ?
In most countries, an invoice is a written and unchangeable document. Altering it has criminal consequences similar to what happens with a transaction in a blockchain chain or a ledger.
In this problem domain, immutability is a rule, therefore our simulations must respect it.
However, those of us who have worked in economic, financial or banking domains have built systems that violate these rules systematically with some excuse related to laziness or performance (our favorites).
As a personal anecdote, in one of my first jobs for a large international bank, we modeled financial transactions with an isDataOK attribute that we had as a Boolean flag until we made sure that the transaction was indeed a valid and actionable transaction. This brought us multiple coupling problems on multiple occasions.
Besides, many of those fields remained with null values (instead of modeling the incompleteness or indefiniteness of the data), so we had to spread the code with multiple controls by ifs to validate that some data against nulls.
Thinking about how to build a solution to the problem we were solving at the time, I found the answer on our only axiom: Bijection one by one with reality.
Let's revisit our 90s code:
A hollow class with a lot of attributes and no encapsulation but with a flag (isDataOK) should when it could be safely used. Let's start by hiding the decision to know when it is an actionable movement.
Then let's go on encapsulating the movement's attributes:
This movement is mutable (despite not being so in the real world). We must ensure that it behaves as our observed entity.
Simple, elegant, immutable, without dataOk, always valid, without setters or getters.
Movement is valid from inception, just as it happens in the real world.
Now let's assume that a business rule prevents us from making movements between the same party and counterparty (this happens in the real world).
In our first version this control would be impossible. In the immutable version we only represent real situations, it will be enough to prevent the construction of these objects.
Times are changing
We are going to continue the previous example focusing on the date on which said the transaction was made. In the real world, a date represents a day on an arbitrary calendar.
If we create a movement in bitcoins for May 12, 2020's halving event and we recreate it in our computable model we will have something like this.
But this violates our unique design principle of maintaining a bijection with the real world. Let's be true to our one rule.
We model reality's entities such as a day of a month, a calendar year, and a date, forgetting about arbitrary implementations with integers because bijection and declarativity are more important than performance and laziness.
Let us dwell for a minute on the mutability of a date. One hopes that a date will never mutate because it does not do so in the real world. No non-computer person would ever think of changing a date.
Let us analyze by the method of reduction to the absurd what would happen if we allow a date to change.
Our accredited transaction on the day of halving knows its imputation date. If it changes internally all consecutive blockchains should be recalculated and this is expressly prohibited by the financial domain. It is clear that the date should never mutate.
Is it crystal clear for everybody that a date should not mutate?
Let's review the date class in the most widely used languages in today's industry.
Go: Date is a struct.
Java: Mutable (deprecated).
PHP: Mutable with setters abuse.
Python: Mutable (All attributes are public on Python).
Date problem's domain is probably one of the oldest and best known to humanity. The excuse that these getters are being deprecated speaks about poor initial design in most modern languages.
A possible attack is to reverse the burden of proof. Objects are completely immutable unless otherwise stated. Should they evolve they must always do so in their accidental aspects. Never in their essence. This change should not be coupled to all the other objects that use it.
If an object is complete since its creation, it will always be able to answer to messages. An object must correctly represent the entity since its inception. If we work in a concurrent environment it is essential that the objects are always valid. An object must be immutable if the entity it represents is immutable Most entities are immutable.
These rules keep the model consistently consistent with representation. As a corollary of the demonstration by the absurd we can derive a series of rules:
Objects must be complete since their creation.
Setters and getters must not exist.
Getters should not exist (unless they exist in the real world and then the bijection is valid). It is not the responsibility of any reality entity to reply to a getXXX() message.
Part of the objective of this series of articles is to generate spaces for debate and discussion on software design.
We look forward to comments and suggestions on this article.
This article was published at the same time in Spanish here.