Photo by Trent Pickering on Unsplash
Ask a software developer, what is ‘good code?’ They will likely answer something along the lines of, “it’s clean or elegant code.”
It’s likely this person will further list SOLID principles and design patterns as a way to legitimize cleanliness or elegance.
I think the way this relationship dynamic is interpreted is wrong. What I mean is that SOLID and design patterns do not simply imply good code. It’s actually the other way around. Good code precedes them. But it goes much deeper than that.
As humans, we divide reality across arbitrary boundaries in relation to a context or point of view. These divisions are used to speak and relate to each other in a correct way.
Where context defines correctness, and the expectations we project, on a boundary. What I mean is that the same tree is “seen” differently by a kid and a botanist.
These boundaries are abstractions on which we project expectations. This is where object thinking comes into play.
Its goal is to model a computer to satisfy these expectations. It equates objects to boundaries. As boundaries, objects are the elemental way to divide reality.
This is where object thinking’s superiority lies in respect of procedural thinking. Indeed, the latter wants to model every boundary with data and algorithms. In other words, it tries to reduce an arbitrary boundary to another arbitrary boundary.
Procedural thinking models boundaries to reflect computers. While object thinking models computers to reflect boundaries.
This is the reason why the ‘object way’ beats the procedural way. I’m not saying that the latter is absolutely wrong. I’m saying that it’s better when boundaries are defined by data and algorithms.
A sage once said: “a demo is worth more than thousands of words”. So, here we are.
The example comes with real example of a request from a customer. In this context, he’s a newsletter manager.
At first, he said that users should be able to subscribe to the newsletter. We asked in which way. He said that they must provide an email.
Using object thinking we can translate this expectation in this way:
interface Newsletter {
Newsletter subscribe(Email anEmail);
}
So, given the boundary Newsletter a user can subscribe to it with anEmail. The goal is to satisfy user expectations (in this context) through interfaces.
Then he said that a user can also unsubscribe in the same way:
interface Newsletter {
Newsletter subscribe(Email anEmail);
Newsletter unsubscribe(Email anEmail);
}
Obviously, he also expressed the need to broadcast news to subscribers. Then we asked if he needs to authenticate in some way.
He took it for granted. And he furthermore explained that he expects to access a protected area with a passphrase. Then we discovered that, that news describes a fact with words.
So, we can reflect these expectations in this way:
interface Newsletter {
Newsletter subscribe(Email anEmail);
Newsletter unsubscribe(Email anEmail);
ProtectedArea protectedArea(Passphrase aPassphrase);
}
interface Passphrase {
Boolean correct(String passphrase);
}
interface ProtectedArea {
ProtectedArea broadcast(News news);
}
interface News {
String fact();
}
This means that Newsletter provides access to a ProtectedArea. And the latter broadcasts News.
After some time wanted to add the possibility to subscribe to the newsletter with a telephone number. So, we asked if anEmail and aPhoneNumber are references through which to send text to a subscriber.
He agreed, and he pointed out that they also identify subscribers. Then, he expressed the needs to count subscribers.
This is a way to satisfy these expectations. I added some simple classes as examples:
interface Newsletter {
Newsletter subscribe(Reference aReference);
Newsletter unsubscribe(Reference aReference);
ProtectedArea protectedArea(Passphrase aPassphrase);
}
interface Reference {
String id();
Reference send(String text);
}
class Email implements Reference {
// implements method that sends email
}
class TelephoneNumber implements Reference {
// implements method that sends SMS
}
interface Passphrase {
Boolean correct(String passphrase);
}
class SHA512Passphrase implements Passphrase {
// implements method to verify correctness
}
interface ProtectedArea {
ProtectedArea broadcast(News news);
Subscribers subscribers();
}
interface News {
String fact();
}
interface Subscribers {
Long count();
}
At this point, he seems fulfilled (until the next needs).
I started this article by saying that SOLID and design patterns do not imply good code. At most, good code implies them.
How ‘good’ your code is, depends only on how much it adheres and reflects the domain. After all, we code for this reason.
But when we introduce extraneous abstractions we reduce the goodness.
Inevitably. It isn’t important if they respect SOLID principles, a design pattern, or whatever best practice. Because they’re extraneous they don’t intrinsically exist in the domain. So, they should not be in the code.
For this reason, thanks to its goal, object thinking has a great impact on good code. As I said it aims to model the computer to reflect the domain we are facing.
In this optic, we start first from a domain understanding. Perhaps patterns will emerge. Spontaneously.