The value of principles
João Paulo Soares, Software engineer at Vouch.io, shares his thoughts and tips on maintaining principles in software code.
Rules are comfortable. They set expectations. Identify a pattern and then apply the steps the rule describes.
Rules help us set expectations. They define the pattern and inform us of the steps we need to take to meet that expectation. But, depending on the context, there may be exceptions. But, these exceptions shouldn't damage the principles behind our rules.
Here's an example to help illustrate the difference between principles and rules:
Two students at college share a room. They talk:
- Student 1: Hey, is there anything I can do to help you study?
- Student 2: Noise bothers me; silence would help.
- Student 1: But I can't stay silent the entire time.
- Student 2: Ok, so how about being silent from 14h to 18h; that should be enough to help me learn.
Rule: From 14h to 18h.
When coding, we often focus on the rules of writing quality code and forget the goals and principles that drive those rules.
Another concept displayed in this story is the goal (in this case, learning). In software development, our goals might include readability, portability, robustness, security, or even just working code that solves the intended problem. When coding, we often focus on the rules of writing quality code and forget the goals and principles that drive those rules.
In the 1990s, two prominent people, one from MIT and another from Berkeley (but working on Unix), met to discuss operating system issues. The person from MIT was interested in how Unix solved the PC loser-ing problem – how they managed interrupts that they can't hold or mask when a user program invokes a system call to perform a prolonged operation.
Without seeing any code, The person from MIT asked the person from Berkeley how they handled the problem. The person from Berkeley said that the team at Unix was aware of the problem, and the solution was to let the system routine finish. Sometimes it would return an error code that signaled that the system routine had failed to complete its action. Then, a correct user program checked the error code to determine whether to try the system routine again.
The person from MIT didn't like this solution because it wasn't "right." The person from Berekely said that the Unix solution was right because the design philosophy of Unix was simplicity and that the right thing was too complex. Besides, programmers could easily insert this extra test and loop. The person from MIT pointed out that the implementation was simple, but the interface to the functionality was complex. The person from Berkeley answered that Unix had selected the right trade-off – namely, implementation simplicity was more important than interface simplicity. You can find more about this story in Gabriel's Raymond's texts.
In his book, Refactoring, Martin Fowler defines refactoring as "a change made to the internal structure of software to make it easier to understand and cheaper to modify without changing its observable behavior." This definition states that the resulting code should be "easier to understand and cheaper to modify," but what if the goal we're seeking is to increase modularity or improve performance?
While providing many examples of refactorings, which, in themselves, are good examples of rules but, applying rules to your codebase without understanding why you're applying them could be taking you further away from your goal. It is worth noting that while the book provides examples of how to get from A -> B and B -> A, Fowler leaves it to the reader to decide which is better.
Other good resources that I've found for learning more about the value of principles include the book: The Art of Unix Programming (it calls them rules, but they're essentially principles), like the Principle of Optimization: Prototype before polishing, and the Principle of Composition: Design programs to be connected with other programs. SOLID is another good example of principles (at least the SOD part). For tests, I like BDD (behavior-driven development). BDD is often described as a rule – to write tests in the form of GIVEN, WHEN, THEN. But from this rule, we can extract the principle of writing tests as Finite State Machines, i.e., make clear the initial state of the test, the action transitioning one state to another, and the expected state after this action.
There is, of course, the classic Design Patterns. It has good rules for what to do when you see a code outlining a certain pattern, and there are also good examples in how to apply the Design Patterns with simple functions. But, the question is, what's the goal in using the Design Pattern? What do we want to accomplish with it? Is it a more readable code or a better generalization? If we reach the same goal using just functions and the solution is simpler, why wouldn't we do that? The programming language may restrict us, but it's possible to work with high-order functions nowadays in object-oriented languages. In languages like Clojure, which are known to be functional, you can choose to follow a stateful or object-oriented style, so it falls to the developer and the team to assess the context of the rule and their principles to decide what approach to use.
So, with so many principles and even more rules available, how do we choose the right ones to prioritize? Developers and stakeholders should decide on their goals and establish what principles are most important to them. The statement that a code pattern or rule is right or wrong should be viewed cautiously. Start with "why" and think about the driving principles, then you'll be better suited to assess it and decide if it solves your problem.