Skip to content

Clean Code

Alexander Graul edited this page Dec 22, 2023 · 5 revisions

Clean Code

Programs must be written for people to read, and only incidentally for machines to execute. --Harold Abelson and Gerald Jay Sussman, Structure and Interpretation of Computer Progams (1984)

Writing computer programs that are easy to understand, change, expand, and replace is hard. This topic is as old as programming, yet no silver-bullet has been found. Many helpful resources try to advice us on how to design our progams to keep them maintainable. This page serves as a starting point.

Naming

Symbols (variables, functions, methods, classes, modules, etc.) should be clear, descriptive and consitent. These attributes remove cognitive load on the programmer and makes it easier to understand different parts of the program.

SOLID Principles for Object-oriented Design

A collection of principles that helps with structuring object-oriented programs.

Single Responsibility principle

A module should only be responsible to one, and only one, actor

An actor can be a person or a group of persons with the same role. A different way to word this principle is "group things that change for the same reason seperately from things that change for other reasons".

Open Closed principle

Software entities should be open for extension, but closed for modification

These two attributes sound like they are conflicting. The solution is to use interfaces that are not changed, so that e.g. class A can depend on interface I instead of class B. When class B is changed/replaced, class A does not need to be changed.

Liskov Substition principle

Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.

This principle states that subclasses must not break the API of the parent class.

Interface Segregation principle

Clients should not be forced to depend upon interfaces that they do not use.

This principle states that interfaces should be small, and all implementations fullfill the complete interface. If an interface gets too big, it should be split.

Dependency Inversion principle

A. High-level modules should not import anything from low-level modules. Both should depend on abstractions (e.g., interfaces). B. Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.

High-level modules directly interacting with low-level modules makes it hard to change the low-level modules. On top of that, it also makes testing the high-level module hard. When both instead depend on an interface, the low-level module can be changed without the high-level module noticing (Open Closed principle). Testing is easier since the high-level module can use a test-dummy in place of the real low-level module.

Incremental Development

Uyuni has currently roughly one million lines of source code between the Java and Python code base in the main repository. It would be herculean task to improve the readability and maintainability of everything at once. Not only would that alone take months, if not years, it might not last either. Programs are modified all the time, the only way to stay on top of the changes is by adapting an incremental-improvements mindset. This mindset is best summarized by the boy scout rule:

Always leave the code you're editing a little better than you found it.

Style Guides and Formatting

Every developer has their own preferences. Working together, there is a simple but important rule:

Consistent style is more important than personal preferences.

Newer programming languages, such as Go, even come with their own formatter to avoid formatting discussions. In Uyuni we use different programming languages and each of them has their own idiosyncracies. Therefore, code style and tooling is different for each language.

Chunking & Aliasing

One technique to reduce cognitive load on programmers is to reduce the number of items one needs to hold in their head at once. Humans only have a "L1 cache" size of 7 ± 2.

Chunking is the act of splitting parts of your programs in chunks (e.g. refactor into functions). Each chunk can be small enough to only require 7 ± 2 items, making it easier to keep everything in the programmer's head. The second part is aliasing, or giving chunks names. Once understood, aliases don't take up any space in the "human L1 cache".

Raymond Hettinger did a great job explaining and showing this technique at PyBay 2019 (The Mental Game of Python).

Clone this wiki locally