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.

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

» 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.

» Think about namespaces, module & package structure

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

» Delay abstractions and decisions

Try to postpone abstractions and (architectural) decisions as long as possible. If possible. The delay gives you the opportunity to gain more insight with can (and will) influence your decisions and abstractions..

» 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

» Function length

Functions should fit on a screen, which translates to about 80 lines. Or 100 lines or something like that.

» 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.

» Design code around data structures.

Design your code around your data structures and put effort into getting your data structures right. Once you decide for a data model, 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.

» 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

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, consistency is important!
  • For the actual implementation, consistency is a nice to have. It may be good to stay consistent within a “unit” (class/module/function).

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

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

» API design

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

» 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