How To Choose Between Conflicting Programming Principles

Posted by : on

Category : Architecture

Introduction

Many programming principles exist to help us write better code. While all these principles aim to improve the architecture and readability of our code, we often encounter situations where different principles contradict each other. This can lead programmers to favor certain principles over others in general, without considering the specific context and the situation of the code they are writing.

Programming principles, as their name suggests, are guidelines, not rules. We apply them under certain conditions, depending on the context. However, this is a very generic concept. In this post, I will try to define the specific factors that determine whether a programming principle should be applied. Additionally, I will provide examples of seemingly contradictory programming principles and how to make decisions in such cases.

Why We Have Programming Principles

Before diving into specifics, let’s understand why programming principles exist. When writing a program, we actually write code that serves as a language for another program (the compiler) to transform into an executable. This means our code is a recipe with two audiences.

The first audience is the machine. Our code must translate into a program that is algorithmically correct, meets the requirements, is bug-free, and performs well for its intended use. This program is executed by the machine and used by end-users.

The second audience comprises the readers of our code. This includes ourselves, our team members, any programmers we might ask for help, and anyone who will need to read our code in the future.

The final executable used by end-users is unaffected by programming principles as long as it runs correctly and meets performance requirements. The readers of our code are the ones impacted by the application of programming principles. Depending on our goals for productivity and teamwork, we will choose principles that address specific issues.

Programming principles are essentially solutions to specific problems. There are four main problems to consider when architecting our code, and each principle may address one or more of these. The most important problem to solve is determined by the productivity gains we can achieve. Productivity should be a primary goal for every programmer, and any principle that affects our architecture must, above all else, be a worthwhile time investment.

The Four Problems Programming Principles Solve

Programming principles affect our final architecture and as I mentioned, the problems they are trying to solve belong in one of the four following categories.

Maintainability

Maintainability is the first reason that someone thinks, when he decides to use a principle. It is a primary reason for adhering to programming principles, as it significantly impacts our productivity. It’s not just about modifying parts of our code, but also about adding new features, removing obsolete sections, refactoring for future ease of change, and ensuring readability. Code that is difficult to understand is inherently hard to maintain.

Maintainability, also encompasses the concept of ‘clean code’. The ease with which someone can understand our code is crucial for our team members, future programmers who may work on it, and even our future selves. Additionally, clear code is essential when seeking help. The excuse that we’re writing code only for ourselves and no one else will see it, is not a valid reason to compromise readability. No matter how well we think we remember our code, our future selves will likely forget many details. Moreover, when we seek assistance and need to show part of our code, we are more likely to receive help if our code is readable. For example, asking for help in a forum has a higher chance of getting responses if readers don’t have to “decrypt” our code before attempting to find a solution.

Testability

Testability is another crucial aspect addressed by architectural decisions based on programming principles. This encompasses traditional tests (unit, integration, end-to-end, etc.), but it goes beyond that. Even if someone doesn’t use formal tests, they still need to test their program’s behavior.

Manual testing, where we manually check parts of our code, is also considered testing. Our architecture should facilitate easy and fast manual testing. Depending on the language, frameworks, and libraries we use, we must design our code to allow certain parts to be tested in isolation.

For example, even if we don’t use automatic tests in a game that we make in Unity, we should be able to test in isolation certain parts of it. If we want to test the character controller, but need to add to the scene the entire level in addition to our character, because our character script depend on it, but our level depends on our game manager singleton that also may have some dependencies, we limit our productivity by the sheer amount of time we have to take to set a scene, just for checking our controller script.

Debugging

Debugging is a challenge that every program and programmer encounters. It can be divided into two parts: locating the bug in the code and fixing it.

The ease of finding the bug depends on our code architecture and the type of bug. Multithreaded bugs and issues with parallel execution are notoriously difficult to locate but often simple to fix. Conversely, algorithmic bugs are typically easier to find but harder to resolve.

We need to consider the type of programming we do, and follow principles that create an architecture which will make easier to debug our code. This means structuring our code so that bugs can be quickly identified in small, isolated parts of the codebase or ensuring that fixing a bug won’t require extensive changes to other parts of the code.

Parallel Programming

The final problem our architecture needs to address is one often overlooked by new programmers: parallel programming. Our code architecture should be designed with principles that enable multiple programmers to work on the same codebase simultaneously without interfering with each other.

This means our architecture should allow each programmer to work independently on specific parts of the code. For example, one programmer should be able to work on a class without others affecting their progress, and that part should be easily integrated into the rest of the program. If our codebase contains global variables or, worse, global state, lacks defined contracts, or does not have a clear API, programmers will experience reduced productivity. They will have to wait for changes outside their responsibility, creating bottlenecks in the development process.

Examples Of Situations With Contradictory Programming Principles

The four problems outlined above have solutions in different programming principles. However, not all solutions address every problem equally, and their impact on each problem can vary. Before applying a programming principle that will influence our architecture, the most crucial question to ask is: What problem am I trying to solve ?

The worst thing a programmer can do is to apply principles blindly as if they were rules or, even worse, to favor one principle over others in all situations. For example, saying in general, “I don’t like principle X; I prefer principle Y because it is better for reason Z.” When learning a principle, it’s essential to understand why it was created and what problem it aims to solve.

Below, I provide examples of conflicting programming principles. The choice of which one to follow depends on which of the four problems the team responsible for the codebase is trying to solve. The same program, depending on the team, can have widely different architectures because the situation may require prioritizing a principle that maximizes productivity by addressing a specific problem more effectively than the others.

OCP Vs YAGNI

The Open Closed Principle versus You aren’t gonna need it.

The OCP, allows for easy extensibility of our code, through the use of interfaces which is usually done through the strategy pattern. YAGNI on the other hand, tells us not to try to add things in our codebase that are not needed at the time of writing.

Which one we will use depends on the situation. What are the requirements? Do we expect the to change? How long are we going to have to maintain this codebase? Also, is the particular piece of code prone to bugs? Do we often find ourselves in need of testing it? Debugging it? Do we expect other people to be making use of this code?

ISP Vs Too Many Abstractions

The Interface Segregation Principle versus Too Many Abstractions.

The ISP, is an important principle in the OOP world. Abstractions allow us to define what an object can do, instead of what it is. On the other hand, one of my favorite quotes attributed to David Wheeler is : We can solve any problem by introducing an extra level of indirection, except for the problem of too many levels of indirection

Adding abstractions can reduce the algorithmic cognitive complexity of certain parts of our code. However, this benefit can be outweighed if cognitive complexity increases due to the numerous dependencies between too many abstractions.

Again, what we will choose, depends on the problem we are trying to solve. Do we have difficulties testing or debugging a certain piece of code? Or we have difficulties understanding it because of all the dependencies. Are we the only programmer using it, or do we expect other programmers to depend on it too?

We have to weight the productivity gains each solution will give and act accordingly.

Avoid Temporal Coupling Vs Principle Of Least Astonishment

Temporal coupling is a principle that tells us to encapsulate behaviors that should be run in a certain order. On the other hand, the principle of least astonishment tells us that a system should behave as it is expected, without any surprises.

There are times when we find ourselves needing to choose between two options: having a method call that performs an unexpected action to prevent bugs (such as opening a file before reading from it) or having a method that behaves as expected but requires the programmer to call another method first to ensure its correct execution.

The decision comes again down to the problem we are trying to solve, who will be using that code? Do we have automated tests? Does the first behavior only have to happen before one method, or are there many methods that require it? If a bug does happen because of temporal coupling, how easy will be to find and fix it?

SRP Vs Tell Don’t Ask (Command Query Separation)

The Single Responsibility Principle can be in contrast with the Tell don’t ask principle.

One use of the tell don’t ask principle, is to avoid asking about the state of another object. This obviously can be a violation of the SRP, as the other object may have different reasons to change and by extension our object may have to change too. Again the decision depends on the programmers and their priorities.

Who will be using that type? Do we expect it to change? Does it implement core business logic or exists on the edge of our architecture? What happens if it has a bug, will this bug affect many other parts of our codebase? If we decide that we won’t internally check the state of another object does this introduce temporal coupling?

Law Of Demeter

The law of Demeter, which doesn’t have a very good name, as it is more of a principle than a law (another more appropriate name for it is principle of least knowledge), can be contradicting in certain situations, many principles.

For example, creating a method that encapsulates that knowledge, can oppose YAGNI, as this method will only be used by one class. It can also be responsible for creating interfaces with lots of methods, something that contradicts the ISP, or it can even affect the performance of our program because of too many wrapper methods.

The reason I include the Law of Demeter is that architectural decisions can impact other aspects of our program, including its performance. While programming principles are designed to enhance productivity by addressing the problems mentioned earlier, we must remember that the final program may have specific performance requirements. We should not make our code more complex solely for the sake of performance, especially if that performance is not required by our specifications. Conversely, in performance-critical applications, meeting the performance needs of the end user takes precedence over other considerations.

Inheritance Vs Composition

Inheritance versus composition is a question that exists before the discovery of fire. Those two, along with parameterization are the three ways we have, to create dependencies between our types in object-oriented programming.

There isn’t a straightforward answer to this dilemma, as it depends not only on the specific context but also on the programmer’s experience. My thoughts on this topic might warrant a post of their own. However, I include this discussion here because it offers a crucial lesson. Unlike mistakes in algorithms, choosing the wrong programming principle for architectural decisions can have consequences that only become apparent much later. A decision made today can affect developers weeks or even months down the line.

This leads to an interesting phenomenon: a programmer may logically conclude that a certain principle is preferable over another based on current arguments. Yet, after some time—say, a few months—the decision might no longer seem effective.

When this happens, the programmer might mistakenly conclude that “This principle doesn’t work,” rather than recognizing that “The reasons I initially chose this principle were flawed; I should not use it again for these reasons.”

It’s common to blame external factors when something doesn’t work, especially after a significant amount of time has passed and the original rationale for the decision has been forgotten.

Conclusion

If there’s one key takeaway from this post, it’s that principles are not rules. One should not generally favor one principle over another, nor is there an objective way to determine the best principle solely by examining the codebase. The decision should be based on how applying a principle will enhance the team’s productivity by addressing the problem that affects them the most. As I’ve emphasized in many of my previous posts, code architecture is fundamentally about time investment.

These are my thoughts on programming principles, their application, and how to choose between them. Observant readers may have noticed that I did not mention the KISS principle at all. The reason is that I find KISS to be a poor principle and believe it should not be used as a guiding factor in architectural decisions. I know this might seem controversial, but I promise I will write a post with my thoughts on the subject.

Until then, thank you for reading, and as always, if you have any questions or comments you can use the comments section, or contact me directly via the contact form or by email. Also, if you don’t want to miss any of the new blog posts, you can subscribe to my newsletter or the RSS feed.


Follow me: