In this article, we talk briefly about the three programming paradigms that are widely used: structured programming, object-oriented programming, and functional programming.
Structured Programming
Structured programming was discovered by Edsger W. Dijkstra in 1968, and it was the first widely adopted programming paradigm.
The idea of structured programming is that all programs can be constructed from three basic structures: sequence, selection (such as if
and else
), and iteration (such as for
and while
loops). It means that any program can be recursively decomposed into a set of small functions, and each small function can be proven correct mathematically.
However, most programmers don’t consider it beneficial to go through the heavy process to formally prove each and every small function correct. Therefore, in practice, we use tests to prove the incorrectness of those functions. If the tests fail to prove incorrectness, we deem the functions are correct.
Object-Oriented Programming
Object-oriented programming was first discovered by Ole Johan Dahl and Kristen Nygaard in 1966. However, it didn’t gain much traction until the invention of C++ and Java.
The biggest benefit of object-oriented programming is to provide a safe and easy to way to do polymorphism through inheritance, making it easier to decouple components of a big system. For example, we can develop and deploy high-level components (such as business rules) independently of lower-level components (such as UI or database).
An Example
Assume we have the following interface and classes in a library (lower-level components):
interface Shape {
fun draw()
}
class Rectangle: Shape {
override fun draw() { ... }
}
class Square: Shape {
override fun draw() { ... }
}
In the application (higher-level components), we can decide which Shape
to use at runtime by simply passing a different instance:
fun doSomethingWithShape(shape: Shape) {
shape.draw()
...
}
Note that this is nothing new. We can achieve the same functionality e.g. by passing function pointers in C. However, function pointers are difficult to use and error prone. For example, in the OO world, you can only call the draw()
function of a subclass of Shape
, and you can’t mix draw()
with printArea()
. But with function pointers, the compiler can’t distinguish between draw(Shape*)
and printArea(Shape*)
.
So, it’s easy and safe to use, but how does this make it easy to decouple?
Imagine that we need to support a new shape, Oval
.
That’s easy! There is no need to touch the original library, and all that you need is to add the new class to your application (e.g. when you don’t want to pay library developer for the additional feature), or to a new library (e.g. when you’re selling it as a plugin):
class Oval: Shape {
override fun draw() { ... }
}
There is also no need to change the doSomethingWithShape()
function. Simply passing a reference of an Oval
will make it work.
See? Your business logic (the doSomethingWithShape()
function) can be independently developed and deployed of your low-level details (the shapes library).
Functional Programming
Functional programming was invented by John McCarthy in 1958, based on Alonzo Church’s lambda calculus in 1936. However, it only starts to gain traction quite recently.
The idea of functional programming is to eliminate mutable variables. To better understand this, let’s check the example to print the first 10 integers in Java:
for (int i = 0; i < 10; ++i) {
System.out.println(i);
}
If we do it in a functional way, it could be something like below in Kotlin:
repeat(10) { i -> println(i) }
In the Java code, there is one mutable variable: i
. If any code inside the for
block changes it by accident, the behavior will be different. However, in the Kotlin code, i
is an immutable val
, so you can’t accidentally change it. This makes the code more robust.
It is true that this is easy to manage in a short program like this. But what if the code is not trivial, or multi-threading needs to be supported? If we have fully eliminated mutable variables, we will no longer have problems like race conditions, deadlocks, etc. However, fully functional code requires more storage and processing power. Therefore, a good architecture is to find the balance between them, and segregate components with mutable variables into modules that are appropriately protected.
Conclusion
All of the three paradigms were invented more than half a century ago, meaning the essence of software hasn’t changed that much for decades. As software engineers, we need to master them, and use them as tools to improve the architecture.