clean code
- All developers should feel like they have agency to write code.
- All developers should feel like they have agency to contribute code.
- A pull-request (PR) is an opportunity to mentor or advise on code quality.
History
The term "Clean Code" comes from Robert Martin's seminal agile software development book, "Clean Code". From the description:
Even bad code can function. But if code isn’t clean, it can bring a development organization to its knees. Every year, countless hours and significant resources are lost because of poorly written code. But it doesn’t have to be that way.
Clean code is presented as a set of suggestions for:
- how to format code for readability
- how to write good names, functions, objects, and a classes
- how to implement error handling without obscuring code logic
- how to unit test and practice test-driven development
- how to transform bad code into clean code (refactoring)
This book is in most must-read lists for software development, usually alongside "Pragmatic Programmer" and "Code Complete", which cover much of the same material.
Objectives
The objective of clean code is to achieve a codebase that is demonstrative of the values of readability, expressiveness, simplicity, portability and testability. A Clean codebase is one that can easily be refactored at any point, and largely maps to the business problem domain. Coming from an Agile background, Clean Code assumes software is never perfect and always flawed, but by adopting certain values software developers can, more often than not, look at their codebase without cringing. A clean codebase empowers the ability to effectively review code, talk about code, make improvements to code gradually and incrementally. We can easily remove bits we no longer need, or replace them with better bits, without introducing risk to our well-tested system. A clean codebase will have more cohesion, less bugs, and over time, less technical debt :)
https://blog.codinghorror.com/we-make-shitty-software-with-bugs/
https://blog.codinghorror.com/on-the-meaning-of-coding-horror/
The overwhelming TLDR is that Clean Code is "easy to read and easy to refactor". As responsible software developers we ought to strive to write as little code as possible. The code that is written is therefore necessary and worthy of the unit tests, assertions, and time that developers inevitably put into caring for it. There are many principles in software development that express this point in different ways:
- KISS (Keep It Simple, Stupid)
- YAGNI (You Ain't Gonna Need It)
- DRY (Don't Repeat Yourself)
- SPOT (Single Point of Truth)
- Once and Only Once
- Worse is Better
Readable
- Code is read much more often than it is written. Readable code is cost-effective across a software team. Put in the effort to write code that is as easy as possible to read.
- Reading code should be pleasant. If the code looks awkward, there is likely a language construct that can be used to improve the expressiveness. Ask around!
- Smaller is better, so long as you aren't sacrificing readability of the code.
- No premature optimizations
- No complex functions with complicated branching scenarios
- No deeply indented code. Validate inputs and fail fast, then perform the one thing you're designed to do.
- Throw defined and expected exceptions that conform to known business domain cases.
- If a function is doing three things, it should be refactored into four functions, and each should have a matching test.
- Delete commented-out code, dead code, cruft code, and clutter. There's no need to keep large blocks of commented-out or unused code when using a VCS. The codebase should also mirror exactly how things are, not how things "will be" or "were". Keep those things in history, tags, and branches. Bad code begets bad code, as described in Broken windows theory.
Expressive
- Readable code is expressive and self-documenting. As a general practice this can render many comments and documentation redundant.
- Write code in the same way that you speak and be optimally verbose.
- Give meaningful variable names, function names, and class names.
- Don't use abbreviations, and try to use ubiquitous language as much as possible.
- Mirror real-world language and sentences: if (dog.isHungry()) giveSnackTo(dog)
- Instead of comments, use assertions.
- Encapsulate conditional statements in a meaningful function name: if if (bigDogisValidSpecial(obj, 4) > if (x % 4 === 5 && obj instanceof BigDog && obj.special === 4)
- Prefer positive over negative conditionals; they're much easier to grok: if (isAGoodDog()) giveTreat() > if (!isABadDog()) giveTreat()
Testable
- Classes should have a clear intent (writer, reader, provider, manager, etc)
- Functions should have a single clear intent: findMeASuitableMate(), calculateDogTax()
- Functions should have an obvious return value and type: getAPrettyDog()
- Functions should not return null unless it's explicitely obvious findPrettyDogOrNull()
- Functions should have no side-effects (getters get, setters set, mutators mutate. Never set in a get, never get in a set, never mutate in a set, etc)
- Methods should have as few side effects as possible, ideally none.
- Code should be encapsulated and loosely-coupled
- If it's "hard to test" or if you need to "pull in the world" in order to run your test, the technical design was not granular enough and should be reexamined.
- Your tests should only match up to the code you wrote! Don't test other peoples code, or third-party library code, or framework code :)
Rules of thumb
We're largely concerned with Pareto principle applications, so we're okay with making some rules of thumb that by and large should be followed consistently in our codebases:
- If a function has more than 30 lines it's should likely be several functions.
- If a class has more than 300 lines it should likely be refactored into several classes that each handle specific functionality.
- No code indentation beyond 2 levels (or three in reasonable cases).
- One statement per line.
- Some duplication is ok. Too much is not.
- You've extracted too much if your function name is longer than the contents of the function :)
Code Paradigms (advanced)
- Declarative > Functional > Object Oriented > Imperative
Declarative
Expresses the logic a computation (what do) without describing it's control flow (how do)
- configuration over integration
- example: grunt configuration
- example: routing logic
- example: sequal syntax
- example: css syntax
- react components also have a strong declarative nature
Functional
Relies on pure functions which can be:
- easily tested
- easily composed
- easily reused
- easily chained
- easily optimized
- allowing for very expressive and terse code
Functional programming enables clean code at higher levels of abstraction through the development of "declarative interfaces"
Object-oriented
Classes allow for expressive coding patterns, but tend to grow well beyond their use (God object anti-pattern). Object-oriented code is the dominant paradigm in software and game programming, but requires constant refactoring to keep the classes encapsulated, cohesive, decoupled, and testable. "Often the proper OO way of doing things ends up being a productivity tax", so look where you can leverage other paradigms.
Imperative
Imperative is the only paradigm that uses conditional branching operators, since imperative is statements that effect state (control/computation). Before writing code that branches, attempt to find a way to solve the problem with a declarative or functional approach.
Code Orthagonality (advanced)
Orthagonality is one of the most important properties in coding maintainable and compact complex systems. Orthagonality is basically what we talk about when we are refering to code being "loosely" or "tightly" coupled. Orthagonality is the degree to which we achieve separation and expose an interface between our encapsulated code and code elsewhere in the system. This is especially significant when considering coding green-field capabilities.
- Hexagonal architecture
- Ports and Adapters
Object-Oriented Code (advanced)
All PR's containing object-oriented code should follow SOLID Principles. In many cases these may not apply, but it's important to be able to notice when they do.
- Single Responsibility principle
- Open/Closed principle
- Liskov Substitution principle
- Interface segregation principle
- Dependency inversion principle
Code Refactoring (advanced)
Without changing the functionality of the program, it is possible to improve the code of a program by refactoring. The code can become more readable, testable, expressive. Refactoring should not break unit tests, and should not be attempted without first having a suite of unit-tests to prove that the refactoring has not introduced risk or bugs into the system. Typical refactoring operations include:
- change the name of a variable, class, or interface
- change the name of a method or function
- change the name of an argument in a function/method definition
- convert an inner class to a top-level class definition
- convert a static function into a class
- convert inline code into a method or function
- leverage common libraries
- adopt well-known patterns
- refactor for orthagonality
- refactor for low/high cohesion
- throw exceptions instead of return codes or null
- throw as early as possible to help detection of the stack trace
- methods to do one thing only – loops, exception handling should all be in sub-methods
- prefer fewer arguments to methods – refactor methods to do less with fewer arguments
- catch specific exceptions in places where you can handle them in a significant way
- never return null
Common Code Smells
Code smells are something that your nose starts picking up once you've become familiar with Clean Code. Code smells don't necessarily indicate a big problem, but they're worth consideration.
- rigidity
- fragility
- immobility
- needless complexity
- needless repetition
- viscocity of design, where the introduction of code debt (quick fix or poor design) requires less effort than refactoring the existing code
- code that could be easier to read
- code that is too terse
- code that could be easier to understand
- code has few affordances (meaningful variable names, method names)
- code doesn't match up with problem domain or ubiquitous language
- many imports
- many exports
- many dependencies
- many arguments
How to Learn Clean Code
- Pair program with a senior programmer on a technical design
- Assign a senior programmer to review your PR
- Read books (Clean Code, Code Complete, Pragmatic Programmer)
- Practice TDD (familiarity with tests influence writing testable code)
And so on...
Do one thing: https://blog.codinghorror.com/curlys-law-do-one-thing/
Tips from Pragmatic Programmer: https://pragprog.com/the-pragmatic-programmer/extracts/tips
Common software patterns: https://sourcemaking.com/design_patterns
- Creational patterns (Factory, Prototype, Singleton, Pool)
- Structural patterns (Adapter, Bridge, Decorator, Facade, Proxy)
- Behavioural patterns (State Machine, Strategy, Visitor)