Code Quality Essentials

Code quality an essential factor for the long-term success of a software system. At the same time, code quality is in most cases opinion-based and up for discussion. Actually testing for bad code is like testing for obscenity: you know it if you see it. After reading Clean Code I realised that this topic is and always will stay subjective. In the end, Clean Code is basically the (expert) opinion of Uncle Bob Martin from 2008.

Recently people (see e.g. here and here) start rightfully questioning the advice in the book. I could start ranting about Clean Code, too, but this would be too easy. A rant without alternative is just hot air: criticism without being constructive, you cannot learn anything from it.

Instead of looking at Clean Code, I fel in love with “A Philosophy of Software Design” by John Ousterhout. There is actually a

discussion between John Ousterhout and Robert Martin about differences between John’s book “A Philosophy of Software Design” and Bob’s book “Clean Code”.. Definitively worth a read!

What are the guidelines we really need? I’ll attempt to build my personal, highly subjective collection, with references to the sources.

» When to test?

Difficult question. I found some random notes on » The Internet «, Is TDD Dead? and the corresponding HN thread.

My view coincides with Andrew Dalke’s Problems with TDD:

Just use common sense and » good unit testing «, without the extra bells and whistles of TDD.

When to write tests?

  • Automated tests after (or during) code development, never before.
  • Unit tests should test the API of a cohesive piece of software, something with a clear boundary (aka the unit).
  • Use tests to hunt down edge cases
  • Adding a new class is not the trigger for a test, but the trigger is implementing a requirement (source)

One test per (desired) external behavior of the unit, plus one test for every bug.

See also

» Stay away from (tactical) DDD and hexagonal architecture

DDD (Domain Driven Design) is a niche »technology«, it fits for complex enterprise systems, where domain logic is challenging. In particular, ubiquitous language and bounded contexts are very useful concepts. They are part of the strategic design.

But the rest almost always leads to over-engineering. I’ve seen it over and over again. If you are in a microservice architecture, you have already relatively small services. They don’t warrant the huge overhead of applying (tactical) DDD or hexagonal architecture. Keeping everything consistent (see also consistency) comes at a huge cost. Also many engineers are not sufficiently educated in applying the principles correctly.

If in a monolith, strive for »the modular monolith«. Modules make the code manageable, not DDD and also not hexagonal architecture.

However, it is not black and white, if you find a concept useful, use it. But don’t cargo-cult.

» Design it simple, not easy

Shortcuts = easy

Simple = effort to make it consistent and simple to use.

Especially in the initial design phase of a feature or system, this approach decreases the headache in the later stages considerably.

» Rough design up-front

I attack architecture by making a rough design up-front. Invest some (perhaps considerable) thinking time into the architectural design. Designs can be iterated fast, written code not. But don’t go too much into detail, just sketch it out. If you add to many details, your design gets outdated fast and confounded by (needless) details.

Conceptual Integrity can help in the design process.

» One level of abstraction

You should not mix different levels of abstraction in one function. Either the function performs low-level or high-level tasks, but not both. Good to see if you zoom out and look at the syntax colour distribution (cq, nocq).

» No side effects

Minimise functions with side-effects (cq, nocq).

  • that output arguments are to be avoided in favour of return values.
  • if you want, you can categorise functions into commands, which do something, or queries, which answer something, but not both.

» Delay abstractions and decisions

Try to delay abstractions and (architectural) decisions. Let it settle. If possible. The delay gives you the opportunity to gain more insight which can (and will) influence your decisions and abstractions. This is especially true for DRY: you might want to prevent code duplication and eagerly create an abstraction. You have to resist this urge for a while.

» Function length

Functions should fit on a screen to see everything in one go. This translates to about 80 lines. Or 100 lines or something like that.

» Design code around data structures.

Design your code around your data structures and data needs. Put effort into getting your data structures right. Don’t freestyle it. Ask yourself:

Which workflow step needs what data in what quantity

Once you decide for a data model and data dependencies, be aware that it will be expensive to change it afterwards. Spend extra time and iterations into design. Changes in design are cheap, changes in implementation not.

Show me your code and conceal your data structures, and I shall continue to be mystified. Show me your data structures, and I won’t usually need your code; it’ll be obvious. – Eric Raymond

Smart data structures and dumb code works a lot better than the other way around. – Eric Raymond

git actually has a simple design, with stable and reasonably well-documented data structures. In fact, I’m a huge proponent of designing your code around the data, rather than the other way around, and I think it’s one of the reasons git has been fairly successful […] I will, in fact, claim that the difference between a bad programmer and a good one is whether he considers his code or his data structures more important. (aka “Bad programmers worry about the code. Good programmers worry about data structures and their relationships.”) – Linus Torvalds

To communicate your ideas to business stakeholders, use whatever gets the message across, even if it is Excel or hand-drawn diagrams.

» Coding conventions deterioate over time

If you have coding conventions, make sure they are kept intact.

Pseudoexample:

In front-end development the CSS styling approach BEM is a convention and often not enforced, so over time, as team and devs are changing, it will get watered down.

How to deal with this?

  • Educate: have documentation ready and put effort into onboarding new devs.
  • Enfoce: Use tooling to encode core architecture, style and intent in the linting rules (take care to not overdo it).

» Dependencies

Keep dependencies small, this will keep your code-base maintainable and resilient.

» Develop the critical path first (or early on)

Start with the difficult stuff, but strip it down to the essential functionality. When you get the difficult (aka critical path) running, you have a proof of concept and details or features can be added easily. The critical path needs to be a fundamental goal of a project, it does not make sense to spend time on difficult non-fundamental goals.

Connected to the idea of »Riskiest Assumptions«

It is crucial to validate your riskiest assumptions before you decide to invest more time and money in your idea

Combine with: Rough design up-front and make it simple, not easy.

» Consistency

My own guideline is »Global organisation, local chaos.«

Every dev in the team should be able to understand the code, independent of consistency.

And to quote one stackoverflow comment:

TL;DR implementation consistency is where “little minds” get their hobgoblin on. nsfyn55

A »hobgoblin« in this context refers to a famous quote by Ralph Waldo Emerson:

A foolish consistency is the hobgoblin of little minds.

Another quote from the same comment:

Consistency is of paramount importance.

…but it comes with a caveat…

You and you coworker are likely obsessing over the wrong sort of consistency – nsfyn55

  • Function names should be descriptive, verb phrases. Choose them carefully (cq, nocq).
  • APIs are like diamonds, they are forever » consistency comes in handy.
  • For the actual implementation, consistency is a nice to have. It may be good to stay consistent within a “unit” (class/module/function).

There is a balance between staying consistent and improving code. It is up to the team to decide on what (best practice or improvement) should be introduced and what not.

Keep in mind that consistency is expensive.

(Just to clarify the scope of the word »improvement«: improving code does not mean »let’s use a different framework!«. These type of changes are a whole new ball game.)

» Composition over Inheritance

Prefer composition over inheritance. Almost always.

I consider implementing an interface and using mixins as part of the composition-toolkit.

» API design

APIs, like diamonds, are forever. So think twice before changing it.

Also, I’m careful with the robustness principle (Postel’s law)!

Be strict when sending and tolerant when receiving can lead to

  • a »shadow API« will emerge: undocumented assumptions manifested in your code that might break at any time and are hard to debug (see Hyrum’s Law)
  • a considerable increase in parsing complexity of the receiving side

For me it also relates to Hyrum’s Law:

With a sufficient number of users of an API, it does not matter what you promise in the contract: all observable behaviours of your system will be depended on by somebody.

If you try to keep the »observable behaviours« small, you’ll have less headache later on.

» Think about namespaces, module & package structure

Module/package structure benefits from being thought out instead of creating a new package/module ad-hoc.

» Impure Functional Programming works best

Some concepts are difficult to realise in one paradigm, so don’t try to force it. Just switch the paradigm temporarily. Examples

» Cohesion

Ignore the single resposibility principle, strive for (functional) cohesion instead.

cohesion refers to the degree to which the elements inside a module belong together. Functional cohesion is when parts of a module are grouped because they all contribute to a single well-defined task of the module.

coupling is the degree of interdependence between software modules

Coupling vs. Cohesion