OOP and procedural programming comparison - part 1
ccarcaci
Posted on May 1, 2023
Introduction
Everything started when I switched from Java to NodeJS. Years ago, it was 2018, I was using Java (and its derived Groovy dialect) to write code. I started studying Typescript and NodeJS to move to a new company.
In the new company, I've found myself writing the same kind of software as in the old one. But, this time, they were more readable, straightforward and better tested.
Some possible reasons for this:
- the new company had a better culture
- I suddenly got better at writing code
- JS/NodeJS was better than Java/Spring
Possibly none of them
- the new company worked even worse in the rush everyday
- I was the same developer working in a language/framework with bare experience
- it is pointless doing such a comparison as universal truth
Maybe, for the kind of software I was writing in both companies the OOP paradigm was not well suited. Maybe, but exaggerated as a hypothesis.
This was the reason I had in my mind to write an article about programming using OOP nowadays. For 5 years.
The story
One chapter of Clean Architecture (R. Martin) is dedicated to programming paradigms. Structured programming (SP), object-oriented programming (OOP) and functional programming (FP). The characterization done in the book in my opinion is the best I have ever seen.
To summarize: at first, there was Assembler (ASM). By using ASM it was possible, and widely used, to jump into the execution flow. In 1968 Dijkstra constrained the execution flow with statements.
Structured programming imposes discipline on direct transfer of control
OOP was born two years before "structured programming" (procedural), in 1966 by Ole Johan Dahl and Kristen Nygaard. OOP moves around the concept of "pointer to function". Languages allow to define a pointer to a function. In OOP this is constrained using polymorphism.
Object-oriented programming imposes discipline on indirect transfer of control
Functional programming was born 30 years before. In 1936 Alonzo Church invented lambda-calculus around immutability, the concept that symbol values do not change.
Functional programming imposes discipline upon assignment
What happened to OOP?
During the '80s, but mostly during the '90s and early 2000s, Java and OOP became the de-facto standard for enterprise software. At the time, software was mostly monolithic, poorly tested, and developed under a strict waterfall approach with no room to improve it.
Java revisions followed the needs of these past times. OOP became the synonym of "Java programming in monolithic architecture".
Software development felt the urgency to step away from this paradigm and moved to SOA, then SAAS and microservices architecture. The trend was to reduce the scope and simplify responsibilities.
Starting from there, the different Java revisions added support to the last programming features and idioms, moving away from its core, pure, OOP nature. And, at the same time, programmers started being bored and overwhelmed by unmaintainable monoliths done in Java. OOP then suffered the association with Java and Java suffered the association with monoliths. OOP suffered because of unmanageable monoliths.
Community perception and personal considerations
In this paragraph, we take a break to talk about how OOP is perceived by the community. The web is full of articles against OOP.
or Like this
If I look at what has been written on the web and in books about OOP, I can find easily a burden of articles on both sides, praise and criticism of OOP and procedural approaches.
The problem with these articles is that they are very long essays that barely show metrics and code comparisons. They are often a stream of consciousness of the author about OOP, programming, and the universe. I can agree or disagree with them because I have a good or bad feeling, but nothing objective. And I didn't want to write the N+1 article about "OOP is good/bad! (in my opinion)".
I'm gonna try to provide some in-depth evidence about the situation. Although I know this analysis is partial. I hope, at least, it is a tiny more objective.
Some objective evidence
In the introduction, are reported just some personal feelings like
they were more readable, straightforward and better tested.
How did I perceive the TS/NodeJS code as more readable, straightforward and better tested?
for the kind of software I was writing
Which kind of software?
Well, I'll try to identify some metrics to measure the code I'll show here. The next paragraphs will list them.
Exercise Call center
The metrics are measured over an OOP exercise taken from Cracking the Coding Interview book reported in Chapter 7 "Object-Oriented Design".
The exercise solutions are available in this Github repo.
I've solved it using Java (OOP) and Typescript+NodeJS (procedural).
Call center OOP solution takeaways
I went using the extension paradigm to provide mocks instead of using Mockito or similar libraries. This is because the mock-providing code tells a lot about the paradigm and the language. For example: how overriding and extension work, how interface definition works and how much verbose is the language.
The approach I've followed is top-down, starting from the "dispatch call" feature.
Call center procedural solution takeaways
Similarly to what I did with OOP solution, and for the same reasons, I preferred not to use the Jest module mock capabilities and, instead, create manual mocks that fulfill the interface definition of the modules (in a Go-like style).
Metrics
I'll try here to give some metrics about "readable, straightforward and better tested".
- Number of lines of code (LOC)
- Number of test lines of code (LOC-t)
- Number of classes (OOP) and modules (NodeJS), entities (E)
- Number of methods (M)
- Indentation levels average (I)
- Average cyclomatic complexity per method (C)
- Number of average dependencies between entities (D)
Measurements
Metric | OOP value | Procedural value |
---|---|---|
LOC | 194 | 312 |
LOC-t | 281 | 499 |
E | 6 | 8 |
M | 15 | 8 |
I | 2.67 | 1.87 |
C | 2.07 | 2.75 |
D | 1 | 1 |
OOP vs procedural approach
As far as I've understood there are some fundamental dichotomies between OOP and the procedural approach.
OOP encapsulates the status into classes and propagates actions.
Procedural propagate status and encapsulate actions into methods.
OOP wraps the status into access and visibility rules.
Procedural makes access rules (methods) to comply with status.
As much as difficult words have been used here we can summarize with "OOP focus on how entities interact, procedural focus on how the status flows between methods" For this reason, in the procedural approach it is very important to identify pure and non-pure functions and always prefer the formers.
Metrics analysis
The core goal of this article is to give some objective evidence about OOP and procedural approaches. The coding exercise proposed here is an OOP-tailored one. We play in the OOP field with the procedural team.
We report here some paragraphs that highlight some traits and comparations between the two approaches.
Conciseness
From the above measurements, it is really clear that OOP outperforms procedural in terms of lines of code. This reflects the possibility to combine objects' behavior resulting in concise instructions.
A great example of this is the respondent selector function. In Java, this is resolved with a single line
respondents.sort(Comparator.comparingInt(CallCenterEmployeeInterface::getManagedCalls));
In procedural this is a fully-fledged function
const selector = (respondents: RespondentType[]): RespondentType | null => {
if (respondents.length <= 0) {
return null
}
const selectedRespondent = respondents.sort((respondent, respondent2) => respondent.calls - respondent2.calls)[0]
return selectedRespondent
}
Readability
To evaluate the readability, what comes into play are the indentation levels average (I) and the cyclomatic complexity (C).
Indentation levels average (I) is better in procedural than OOP. The reason is easy to explain: OOP is based on classes, and their definition adds at least one indentation level. In fact, the difference of I measures is 0.8 which is very close to 1.
To evaluate cyclomatic complexity (C) instead, we should take into account the fact that OOP encapsulation saves conditional statements. The example is around dispatcher and escalation implementation.
To escalate a call in OOP the logic is encapsulated within Employee
class
if(busy) {
System.out.println(name + " is busy, routed to supervisor");
supervisor.pickCall();
return;
}
In the procedural approach, we defined a specific function with some conditional statements
const maybeEscalate = (respondent: RespondentType, logger: LoggerType): RespondentType | null => {
if(respondent.busy && respondent.supervisor === undefined) {
logger.info(`${respondent.name} has no supervisor and is busy, call rejected`)
return null
}
if(respondent.busy) {
logger.info(`${respondent.name} is busy, routed to supervisor`)
return maybeEscalate(respondent.supervisor!, logger)
}
return respondent
}
This affects the cyclomatic complexity for the worst in the procedural approach.
Mental overload
Mental overload is about how many concepts a developer should keep in mind when working. This aspect is crucial when choosing an approach.
A classification of the metrics that influence mental overload could be direct or indirect. Direct ones influence what a developer sees in the editor, indirect ones are related to the project in general and things that are hidden from the developer's view in the editor.
Indirect metrics are the number of entities (E) and the number of average dependencies (D).
Direct metrics are the number of lines of code (LOC, LOC-t), number of methods (M), indentation levels (I) and cyclomatic complexity (C).
In indirect metrics, OOP tends to perform better because some behaviors could be expressed using composition and encapsulated in classes.
Amongst direct metrics, if we consider the number of methods and the indentation levels as something very close to the experience a developer has when writing code, the procedural approach seems to be better. On the other side, cyclomatic complexity and the number of lines of code add more burden to the brain of developers.
From this point of view procedural approach has the advantage to provide step-by-step recipes, for this reason, has higher cyclomatic complexity and more lines of code. And we might consider paying this drawback to have step-by-step cooking-like recipes.
The procedural approach has also the advantage to provide less syntax on the developers' sight because of fewer indentation levels and less number of methods to keep in mind.
Wrap up
To wrap up, what we can say is that if we want very powerful tools to describe complex domains and encapsulate behaviors then OOP is master. It is a great paradigm to provide concise solutions at the cost of hiding behaviors and increases the mental overload of developers.
The fact that recently the web is full of articles against OOP is that encapsulation, domain modeling and bounded context principles have been shifted away from the code to the infrastructure. The use of microservices, lambda functions and decoupled interfacing like API and queues move the mental load from classes to repositories. But, at the same time, providing contracts and regression tests to relieve this. For this reason, developers need a very simple list of instructions executed into their services and a big chunk of the desired behavior is moved to contracts and services intercommunication.
For the reasons measured here, OOP is not best suited for this duty. It forces the developer to think in terms of classes and how to encapsulate a simple behavior into them. When what is needed is a method with a cooking recipe in it. Oftentimes I've found OOP-based microservices that were a bunch of static methods wrapped into well-named classes (modules). Now I can see the reason.
Part 1... Part 2?
The attentive reader noticed that this article is "part 1" something. Yes, there will be part 2.
This article presented some considerations about a simple programming exercise, limited to the syntax and basic approach to the programming.
What is missing here is how OOP and procedural approaches behave in a real-world like case. In part 2, I'll provide an analysis on a very common transactional implementation that involves API, business logic, and DB layers.
Note for the readers
Due to the grievance nature of the topic, and the different positions in the community, I preferred to take a very practical approach providing, measuring and commenting on code.
I will follow the same approach in the comments, I will not respond to comments that are not provided with code and practical considerations. This is because, without some code attached, it will be impossible to separate personal opinions from practical evidence.
Bonus: design patterns
"Integrity is telling myself the truth. And honesty is telling the truth to other people." (S. Johnson)
I've never understood the obsession with design patterns. There are hundreds of design patterns but only a strict subset of them are used daily.
So, from where this obsession came from?
I think design patterns are used for two reasons:
- difficult questions during an interview to make the candidate uncomfortable "Tell me about the Memento pattern, and give me an example where you used it" I was asked once. I've never used it, of course. Next question, please
- academic showcase
Both reasons are worthless.
I dare you to tell me how many times in your professional life you used some patterns like AbstractFactoryBuilder, Composite, or Flyweight. For people that don't keep them fresh in their mind (99% of devs), they are less readable than less engineered, but direct, code.
This means that it's OK to USE design patterns. It's OK to go back to read them and, if there is one that could solve our current problem, use it. Attach a comment to the code pattern for the future reader of your code, that with a high probability will be yourself. But making design patterns a discussion argument or an interview question is dumb.
To be honest, this topic is worth a separate article, comparing the usage of a design pattern VS using a very simple yet naive approach.
Posted on May 1, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.