Rereading Clean Architecture (Part 2)
A programming paradigm is a way of writing programs. It tells us which code structures to use and when to use them.
Three programming paradigms have emerged: structured programming, object-oriented programming, and functional programming.
The author argues that these approaches do not add weapons to an architect’s arsenal. Architects and programmers already have plenty of weapons; the three paradigms instead impose constraints on their use. That is why they are called paradigms.
Structured Programming
Structured programming restricts and disciplines the direct transfer of program control, especially the unrestricted use of goto statements.
Decomposing Programs
Dijkstra hoped to reason about programs using mathematical proofs: a program would become a Euclidean structure, with new programs assembled from proven structures so that the correctness of the whole could be derived.
He also found that large numbers of goto statements make programs difficult to decompose, while their functions can be expressed entirely with branches and loops.
Structured programming therefore shows that programs can be decomposed. A large problem can be divided into a set of high-level functions, each of which can be divided further into lower-level functions, recursively. More importantly, every resulting function can itself be written using structured programming.
However, programming through formal proof never became mainstream. The scientific method is used more often in practice.
The Scientific Method
Scientific theories and laws can be falsified, but cannot be proved true. Similarly, testing can disprove a program’s correctness but cannot prove it: tests can reveal bugs, but cannot establish that none exist. Using the scientific method, structured programming encourages us to recursively break a program into small, provable functions and then write tests that attempt to show those functions are wrong. If these tests fail to falsify them, we can treat the functions as sufficiently correct and infer the correctness of the overall program.
Object-Oriented Programming
Object-oriented programming restricts and disciplines the indirect transfer of program control.
Specifically, object-oriented programming uses polymorphism to constrain function pointers. This can be understood as limiting the targets a function pointer may reference. For example, polymorphism in Java commonly operates between objects of classes related through inheritance.
The author also regards polymorphism as the defining feature of OOP. It enables dependency inversion:
Dependency inversion introduces interfaces to give us complete control over source-code dependencies without being constrained by the system’s flow of control.
Dependency inversion enables plugin-based development. Inverting the dependencies between business logic and the web UI or database decouples the core business logic from both, turning the UI and database into plugins for that logic.
Object-oriented programming is the ability to control source-code dependencies through polymorphism. This enables architects to build plugin architectures that separate high-level policy components from low-level implementation components. Low-level components can be compiled as plugins and developed and deployed independently of the high-level components.
Functional Programming
Functional programming restricts and disciplines assignment.
In other words, variables in functional programming languages are immutable.
The author makes the following argument, quoted from the book:
“All race conditions, deadlocks, and concurrent update problems arise from mutable variables. If variables never change, races and concurrent updates cannot occur. If lock state is immutable, deadlocks cannot occur either.”
Immutability is feasible if we ignore limits on storage capacity and processing speed. In practice, however, we must account for those limits. To make immutability practical to a useful extent, we need segregation of mutability.
Segregation of Mutability
A common approach is to split an application, or its internal services, into mutable and immutable components. Immutable components perform tasks through pure functions without changing any state. They communicate with one or more nonfunctional, mutable components to modify variable state.
Git provides an example. It records file additions, modifications, and deletions by moving references, while the stored file contents are not actually modified or deleted; new data is added and existing data is retrieved. This is an example of immutability.
Transaction management in MySQL and transactional memory also draw on this idea.
Summary
The three programming paradigms are closely related to software architecture:
- Polymorphism is how we cross architectural boundaries.
- Functional programming is how we discipline and constrain where data is stored and who can access it.
- Structured programming provides the foundation for implementing individual modules.
The three paradigms align with the three major concerns of software architecture: component independence (object-oriented programming), data management (functional programming), and functionality (structured programming).
Further Reflection
All three programming paradigms impose new constraints on programmers. Each limits a way of writing code; none adds a new capability. In other words, programming paradigms tell us what not to do.
(End of Part 2)