Programming Paradigms for Architects
Erratum: First of all, I ended up meeting once again the book “Clean Architecture” by Uncle Bob, a book I read around 2018-2019 which, like every good programming book, has a lot of good information to extract, if you bring critical thinking. I recently wrote a summary of chapters 4, 5 and 6 and decided to share it here.
Most programmers, unfortunately, do not think very critically about programming paradigms. They defend them, actually. If we look at it from a social and anthropological angle, that even makes some sense. But here I would like to analyze the historical and architectural angles. With that, I hope this logic of defending a paradigm loses a bit of its meaning for whoever reads this article.
How mathematics and physics shaped structured programming.
It was March 1952. A few years had passed since the end of World War II and, if you remember a bit of computing history, Alan Turing created the ENIAC to break the ciphers of the German front. At the Mathematical Center of Amsterdam, a young mathematics student named Edsger Dijkstra was starting to work as the first programmer in the Netherlands. He only chose this profession because he judged it to be more intellectually challenging than physics (yes, physics. Newton, Einstein and Oppenheimer are clearly small fry).
As I said, these were the years right after the end of the war, so Dijkstra started his career in the era of vacuum tubes: slow, fragile, with programming done straight in binary or raw assembly. We had the recipe for an edit, compile and test loop that took hours and days.

That is where mathematics comes in. Dijkstra realized that programming is complex and that programmers are not very competent (who would have thought). Because of that, he noticed a recurring problem: a program seemed to work, but one detail that went unnoticed keeps it from working, which is usually discovered after hours or days of compiling and testing. That led him to apply the mathematical theorem of proof.
Put directly, the mathematical proof approach is a proposal to build a Euclidean hierarchy of postulates, theorems, corollaries and lemmas for programmers to use the way mathematicians do.
But what is that?
I am no mathematician, but he basically said programmers could build programs out of proven structures and tie them together with code that would prove them correct.
Yes, he had just started the whole logic of functions, methods, libraries and tests as we know them today.
But while creating this postulate, he realized that one item widely used in the code of the time was harmful to the proposal, and that item is the GOTO statement.
That is because GOTO statements usually broke any flow that could be followed and jumped somewhere else, making it impossible to split things into modules and run those proof tests. In fact, he saw that the good uses of GOTO were the ones corresponding to simple control structures like if/then/else and do/while.
If modules had only these simple controls, it was proven we could split our programs into testable modules.
After publishing a paper defending this idea, the programming community went into crisis. Half the programmers defended Dijkstra while others defended GOTO. As I said above, socially and anthropologically this makes sense. But what should matter to us programmers is the result and, in the end, time showed Dijkstra was right. Today we have practically no GOTO in programming languages and, when we do, it is in a very restricted form.
Physics came in later, when programmers discovered they could not use mathematical proofs to test software. That is because tests in mathematics always treat truth as absolute, that is, they predict the behavior of functions in every scope.
And if you have programmed a bit in your life, you know that is impossible. The very phrase “works on my machine” is the antithesis of that idea.
And programmers solved this by removing the mathematical proof and putting the scientific test in its place.
Science, unlike mathematics, always seeks to contradict the truth in its tests, that is, it seeks the failure.
And this foundation is used by programmers to this day in function tests.
When we write our unit tests, we always carry the thought: does my function work in this specific scenario and proposition?
Object-Oriented Programming
The first thing the book asks is: what is object orientation? And we quickly arrive at three words: Encapsulation, Inheritance and Polymorphism. Let’s break them down.
Encapsulation
The goal of encapsulation is actually to make data and functions private to a specific scope. In OO, that scope is a class.
However, back in C, we already had this capability perfectly:

The users of this lib, let’s call it point.h, have no access to the members of the struct Point. They only have access to the methods in the lib’s header.
Perfect, functional encapsulation without an OO language. Now let’s go to C++:

Because of the C++ compiler, the clients of the header file point.h know about the member variables x and y! We had to add words like public, protected and private later, but that was more of a hack to satisfy the compiler. Later, other languages like Java and C# abolished the header entirely and, if we changed the name of private properties in libraries, we would have to recompile the clients, breaking encapsulation.
Inheritance
If we analyze the main program, we will see that the NamedPoint data structure is the same as Point. It can masquerade as Point because it is a superset of it. This, as far as I know, was used by programmers before OO, and it is even what C++ uses under the hood to implement inheritance.

In other words, there was a trick to do inheritance, and what OO did was bring inheritance in a more convenient way and make multiple inheritance viable.
Polymorphism
Polymorphism always existed before OO languages. Let’s go back to C:

The getchar() function reads data from STDIN, and putchar() writes to STDOUT, but what devices are those?
These functions are polymorphic examples because their behaviors change depending on the device, but the signature does not, just like the style of interfaces in Java, C# and Go.
In the case of these devices, the Unix operating system itself requires every I/O driver to offer five standard functions: open, close, read, write and seek.
If we create a class that has these 5 functions and writes to a file, or an array, for example, it can be implemented as STDIN and STDOUT.
And, as I mentioned, we need to analyze a bit of history here to understand why all operating systems use plugins for devices. At the end of the 1950s, they learned the hard lesson of having to rewrite entire programs, not because the business logic changed, but because we moved from punched cards to magnetic tape. Hence:
Programs must be device independent.
The keyword here is “independent”, because before polymorphism we had a call dependency binding the highest layer of the software to the lowest, starting at main:

For software architects this was very limiting, because there is no option to change the flow of control here. However, with polymorphism, this dependency is broken and things become interesting.

Now, the ML1 dependency points to the interface I in the opposite direction of the flow of control. With this, we can change source code dependencies without affecting the flow of control, and vice versa.
This is called dependency inversion.
One of the things we can do with this new power is completely decouple business rules from the user interface and the database.

With this, our modules can be developed, maintained and compiled at different times and in different ways. This is called independent development and independent deployment.
Functional programming
At the moment I am writing this article, functional programming is on the rise!
As I mentioned in the section on procedural programming, there is a certain favoritism for some paradigms over others. However, honestly, I see many people defending functional programming without truly understanding the benefits and costs associated with it.
For the introduction, let’s compare two programs that print the squares of the first 25 integers, one in Java and another in Lisp, which is a functional language.

Good old Java
Lisp
To make it more readable for those not used to Lisp:

Functional programming in general works by executing functions from the inside out, so read:
(fn (* x x)) This is an anonymous function that calls the multiplication function, passing the input argument twice.
The “range” function returns an infinite list of numbers starting at 0.
That list is passed to “map”, which calls the anonymous function mentioned earlier.
The list of squares is passed to the “take” function, which returns a new list containing only the first 25.
Then “println” prints the result.
I want to highlight something very important here: we create a variable x in each iteration because, in functional programming, variables do not… well… vary.
This concept is called immutability, and it serves to prevent any error that could arise from changing a variable within a scope. With it, access to memory, disk, processing and other resources becomes safer, with no deadlocks, race conditions, and so on. However, this demands more memory and processing; it is, in fact, inversely proportional.
Considering we do not have infinite memory and processing, there are some ways to tune this. One is separating the immutable components from the mutable ones, allowing them to communicate. This is called segregation of mutability.

In the case where we have plenty of storage and processing available, we can reduce the mutable state of our applications. That way, we stop dealing with state and start working with transactions.
In a basic bank account example, instead of querying the bank balance, we work by calculating all the transactions from the origin or from a certain checkpoint. This is called Event Sourcing.
Conclusion
When we coldly analyze the three kinds of paradigms, we realize that we are actually defending disciplines that impose constraints on us. In the book’s own words:
-
Structured programming is discipline imposed upon direct transfer of control.
-
Object-oriented programming is discipline imposed upon indirect transfer of control.
-
Functional programming is discipline imposed upon variable assignment.
All of them impose limitations on us, and none of them adds anything new to our powers. What we learned in half a century is what not to do.
With that, I conclude that software development is not a fast-evolving technology. We are doing things today the same way we did 50 years ago, just in different places, but the essence is the same.
Software is composed only of sequence, selection, iteration and indirection.
Bibliographic reference:
- Clean Architecture - Chapters 4 to 6 - Robert C. Martin.
