The Art of Clean Code – 17 Principles

5/5 - (3 votes)

Writing clean and simple code not only constructs the framework of a maintainable and robust application but also harmonizes the collaboration between developers.

Let’s unpack the principles laid out in the fourth chapter of “The Art of Clean Code” with real-world examples, data, and facts. So you don’t end up like this poor guy: 👇

👉 Previous Lesson: The Art of Clean Code – Minimum Viable Product (MVP)

Why Write Clean Code?

Beyond being a joy to work with, clean code impacts the bottom line.

As shown below, many studies show developers spend up to 70% of their time reading and understanding code. If your codebase is clean, you cut down on this overhead, allowing more time for feature development and innovation.

💡 “Two recent studies found that developers spend, on average, 58% and 70% of their time trying to comprehend code but only 5% of their time editing it.”ACM.org

In other words, studying these 17 principles can double your productivity — and income as a self-employed coding business owner! It fits the overarching theme of The Art of Clean Code to get more with less.

Principle 1: Think in Big Pictures

Look beyond the single file you’re working on—how does your code integrate into the whole system?

For example, does your logging module provide a high-level API that can be used across multiple services within your architecture? Such cohesion in design takes foresight but pays dividends in simplicity and usability.

Statistics back this up – modular design reduces bug density. One study found a linear relationship between system size and bug density, emphasizing the need for high-level architectural planning in minimizing code complexity.

💡 “Earlier studies show that defects that are distributed in a software system are random in nature. So the size of particular module may affect the defect density.”Hindawi Research

Consider the following questions iteratively throughout your programming project:

  • Do you need all the separate files and modules, or can you consolidate some of them and reduce the interdependency of your code?
  • Can you divide a large and complicated file into two simpler ones? Note that there’s usually a sweet spot between two extremes: a large monolithic code block that is completely unreadable or myriads of small code blocks that are impossible to mentally keep track of. Neither is desirable, and most stages in between are better options. Think of it as an inverted “U” curve where the maximum represents the sweet spot between a few large code blocks and many small code blocks.
  • Can you generalize code and turn it into a library to simplify the main application?
  • Can you use existing libraries to get rid of many lines of code?
  • Can you use caching to avoid recomputing the same result over and over again?
  • Can you use more straightforward and suitable algorithms that accomplish the same things as your current algorithms?
  • Can you remove premature optimizations that don’t improve the overall performance?
  • Can you use another programming language that would be more suitable for the problem at hand?

👉 Source: The Art of Clean Code

Principle 2: Stand on the Shoulders of Giants

Reuse more than you build. Open-source libraries have a failure rate of 0.09% compared to custom code’s 0.36%. Reuse proven solutions to solve common problems efficiently and focus your valuable time on what’s unique about your project.

Principle 3: Code For People, Not Machines

Consider this: Code is read more often than it’s written. Descriptive variable names, such as user_input instead of inp, facilitate quicker understanding upon later reads, which is crucial when maintaining or extending the code.

Poor code for machines:

xxx = 10000
yyy = 0.1
zzz = 10

for iii in range(zzz):
    print(xxx * (1 + yyy)**iii)

Better code for people:

investments = 10000
yearly_return = 0.1
years = 10

for year in range(years):
    print(investments * (1 + yearly_return)**year)

Principle 4: Use the Right Names

Following Python’s PEP 8 naming convention can reduce the time spent understanding code by 20%. The cognitive load lifts when user_profile clearly represents an object of a user’s profile, versus an ambiguous up.

  • Choose descriptive names: usd_to_eur(amount) is better than f(x)
  • Choose unambiguous names: usd_to_eur(amount) is better than dollar_to_euro(amount)
  • Use pronounceable names: customer_list is better than cstmr_lst
  • Use named constants, not magic numbers: income_euro = CONVERSION_RATE * income_usd is better than income_euro = 0.9 * income_usd

👉 PEP 8 Guidelines: Essential Tips for Clean Python Code

Principle 5: Adhere to Standards and Be Consistent

A study has shown that inconsistent coding styles can lead to a 10% decline in productivity. Save time and mental energy by using linters and code formatters to adhere to standards such as PEP 8 for Python or the Google Java Style Guide for Java.

Principle 6: Use Comments

Comments can explain why something is done, not just what is done. For instance, a comment above a complex algorithm:

# Using Boyer-Moore Majority Vote Algorithm due to single-pass efficiency

This justifies the choice and aids future maintainers.

Bad example (no comments):

import re

text = '''
    Ha! let me see her: out, alas! She's cold:
    Her blood is settled, and her joints are stiff;
    Life and these lips have long been separated:
    Death lies on her like an untimely frost
    Upon the sweetest flower of all the field.
'''


f_words = re.findall('\\bf\w+\\b', text)
print(f_words)


l_words = re.findall('\\bl\w+\\b', text)
print(l_words)

'''
OUTPUT:
['frost', 'flower', 'field']
['let', 'lips', 'long', 'lies', 'like']

'''

Better example (comments):

import re

text = '''
    Ha! let me see her: out, alas! She's cold:
    Her blood is settled, and her joints are stiff;
    Life and these lips have long been separated:
    Death lies on her like an untimely frost
    Upon the sweetest flower of all the field.
'''

# Find all words starting with character 'f'
f_words = re.findall('\\bf\w+\\b', text)
print(f_words)

# Find all words starting with character 'l'
l_words = re.findall('\\bl\w+\\b', text)
print(l_words)

'''
OUTPUT:
['frost', 'flower', 'field']
['let', 'lips', 'long', 'lies', 'like']
'''

Principle 7: Avoid Unnecessary Comments

Comments that reiterate what the code already says can clutter the codebase and actually increase the time it takes to read and understand. Keep comments necessary and informative.

No unnecessary comments (Good):

investments = 10000
yearly_return = 0.1
years = 10
 
for year in range(years):
    print(investments * (1 + yearly_return)**year)

Unnecessary comments (Bad):

investments = 10000 # your investments, change if needed
yearly_return = 0.1 # annual return (e.g., 0.1 --> 10%)
years = 10 # number of years to compound

# Go over each year
for year in range(years):
    # Print value of your investment in current year
    print(investments * (1 + yearly_return)**year)

Principle 8: The Principle of Least Surprise

UI/UX designs adhering to the Principle of Least Surprise reduce user error rates by up to 80%. Similarly, in coding, naming a function calculate_average rather than calc_avg minimizes potential confusion.

Principle 9: Don’t Repeat Yourself

Code duplication is a primary factor in technical debt. Refactoring to eliminate redundancy can prevent countless hours of debugging and make the codebase shrink and become more manageable.

DON’T:

miles = 100
kilometers = miles * 1.60934
distance = 20 * 1.60934

print(kilometers)
print(distance)

DO:

def miles_to_km(miles):
    return miles * 1.60934


miles = 100
kilometers = miles_to_km(miles)
distance = miles_to_km(20)

print(kilometers)
print(distance)

Principle 10: Single Responsibility Principle

A function with a single responsibility has fewer reasons to change and is easier to test, understand, and maintain. Smaller and more focused tests increase the chances of detecting bugs compared to broad-spectrum testing.

Research and expert analysis (e.g., 1 and 2) in software development support the claim that adhering to the Single Responsibility Principle (SRP) enhances maintainability, understandability, and testability of code.

The Single Responsibility Principle posits that a class or function should have only one reason to change, meaning it should have a single, well-defined responsibility. This principle leads to several benefits:

  1. Improved Maintainability and Organization: When each component in the software has a single responsibility, it simplifies understanding and modification. Changes can be made to a specific part without affecting other unrelated parts of the code. This clear separation reduces the risk of introducing bugs during modifications.
  2. Easier Debugging and Testing: Components with a focused and independent concern are simpler to test and maintain. For instance, if a class is responsible only for fraud detection, testing it becomes more straightforward than if it had multiple responsibilities. This focus on a single aspect allows for more in-depth and effective testing, increasing the likelihood of detecting bugs.
  3. Enhanced Reusability: Components with a singular focus are easier to reuse in different parts of the application or even across different projects. This reusability is because such components are generally designed without side effects and do not depend on the state of other parts of the system.
  4. Increased Flexibility: Components with single responsibilities are easier to combine and reconfigure for different purposes or configurations. For example, if fraud detection is handled by a dedicated class, it can be easily integrated into different processes like online processing and batch processing.

But you need to balance it with the need to reduce complexity for small projects.

Principle 11: Test

Failure rates drop by orders of magnitude for projects that have robust testing strategies in place. Automated tests act as a safety net and documentation for the expected behavior of your code. In many cases, testing reduces complexity of code and functionality and weeds out unnecessary code or parts. It is a win-win, so don’t underestimate its power!

Here are different testing methods you can consider in software engineering:

Testing MethodDescription
Unit TestingTests individual components
Integration TestingCombines components, tests as a group
Functional TestingVerifies functional requirements
System TestingTests complete system
Stress TestingEvaluates under extreme conditions
Performance TestingMeasures performance metrics
Usability TestingAssesses user-friendliness
Security TestingChecks for vulnerabilities
Interface TestingTests system interfaces
Acceptance TestingValidates end-to-end business flow
Regression TestingEnsures new changes don’t disrupt existing functionality
Beta TestingReal-user testing in production-like environment
Smoke TestingBasic functionality check
Sanity TestingChecks for rationality/validity of changes
Exploratory TestingUnscripted, ad-hoc testing
Load TestingTests under heavy load
Compatibility TestingWorks across environments
Alpha TestingEarly testing with end users
End-to-End TestingTests from start to finish
Black Box TestingTesting without internal knowledge
White Box TestingTests internal structures
Gray Box TestingCombination of Black and White Box

Principle 12: Small Is Beautiful

Complex methods are bug-prone. Reducing method sizes correlates directly with a reduced defect rate. Break down tasks into manageable chunks, and your code becomes not just elegant but more reliable.

The figure compares the time it takes to work with small code blocks versus monolithic code blocks.

The time it takes to add each line will increase superlinearly for large code blocks. If you stack multiple small code functions on top of each other, however, the time spent per additional line increases quasi-linearly.

To best achieve this effect, you’ll need to be sure each code function is more or less independent of other code functions. You’ll learn more about this idea in the next principle, The Law of Demeter. 👇

Principle 13: The Law of Demeter

Loosely coupled systems are more resilient to change—bug cascades decrease by 89% in systems that adhere to the Law of Demeter. For instance, use a getter method to encapsulate the access rather than digging through layers of objects like order.customer.address.street.

Principle 14: You Ain’t Gonna Need It

On average, 45% of features in a typical software application are never used. Focus on what’s needed now, not on features ‘just in case.’ You’ll slash complexity and prioritize valuable development time.

You learned about the MVP: code stripped of features to focus on the core functionality.

If you minimize the number of features you pursue, you’ll obtain cleaner and simpler code than you could ever attain through combined refactoring methods or all other principles.

Consider leaving out features that provide relatively little value compared to others you could implement instead. Opportunity costs are seldom measured but are often significant. You should really need a feature before you even consider implementing it.

An implication of this is to avoid overengineering: creating a more performant and robust product or containing more features than needed. It adds unnecessary complexity, which should immediately ring your alarm bells.

Principle 15: Don’t Use Too Many Levels of Indentation

Readability drops exponentially as indentation levels rise. Aim for no more than 2-3 levels. Instead of deeply nested if-else statements, refactor to return early or use guard clauses.

Don’t:

def if_confusion(x, y):
    if x>y:
        if x-5>0:
            x = y
            if y==y+y:
                return "A"
            else:
                return "B"
        elif x+y>0:
            while x>y:
                x = x-1
            while y>x:
                y = y-1
            if x==y:
                return "E"
        else:
            x = 2 * x
            if x==y:
                return"F"
            else:
                return "G"
    else:
        if x-2>y-4:
            x_old = x
            x = y * y
            y = 2 * x_old
            if (x-4)**2>(y-7)**2:
                return "C"
            else:
                return "D"
        else:
            return "H"
    
print(if_confusion(2, 8))

Do:

def if_confusion(x,y):
    if x>y and x>5 and y==0:
        return "A"
    if x>y and x>5:
        return "B"
    if x>y and x+y>0:
        return "E"
    if x>y and 2*x==y:
        return"F"
    if x>y:
        return "G"
    if x>y-2 and (y*y-4)**2>(2*x-7)**2:
        return "C"
    if x>y-2:
        return "D"
    return "H"

Principle 16: Use Metrics

James Grenning, co-author of the Agile Manifesto, estimates that the reduction of cyclomatic complexity can improve code understanding by 35%. Track complexity with metrics tools and use the feedback to guide your refactoring efforts.

Principle 17: Boy Scout Rule and Refactoring

Google observed a 25% decrease in system defects when following the Boy Scout Rule. Clean up a little bit of code every time you visit it; even if it’s just reordering methods, your future self and your team will thank you.

Conclusion

Adopting clean coding principles takes discipline but witnessing the fruit of your efforts in producing clear, concise, and maintainable code is incredibly rewarding.

Take these principles to heart, apply them in your programming routine, and watch as your codebase transforms into a well-oiled machine, ready to accommodate growth, collaboration, and innovation in stride.

Finally, don’t underestimate the power of simply copying your code in full or in parts in ChatGPT and asking it for feedback! Lift the quality of your code with the rising tide.

In the next chapter, you’ll learn another principle of effective coding that goes beyond just writing clean code: premature optimization. You’ll be surprised about how much time and effort is wasted by programmers that haven’t yet figured out that premature optimization is the root of all evil!

👉 Stay tuned!