Learning Notes: Clean Code
“Reading a clean code should make you smile the way a well-crafted music box or well-designed car would.”
Most engineers in the early days of their careers have possibly read or heard about the book: Clean Code: A Handbook of Agile Software Craftsmanship by Robert C. Martin [1]. As one of those aforementioned engineers; I recently read the book, and am sharing my notes to have references for myself in the future.
I’ll iterate over takeaways and add my thoughts where relevant.
Introduction
- Manager expectations: “If I don’t do what my manager says, I’ll be fired” → Probably not. Managers defend schedules; it’s your job to defend code with equal passion. Think doctor-patient: patient says “doctor, hurry - don’t wash your hands, that takes too much time.” Not gonna happen.
Especially inexperienced people might struggle saying no to managers. This analogy perfectly summarizes the dynamic.
- Clean code experience: Reading clean code should make you smile like a well-crafted music box or well-designed car.
This pretty much summarizes what clean code does. While you’re reading the code, it should provide what you’d expect to see.
Naming
- Naming conventions: Classes/objects should have noun/noun phrase names. Avoid verbs.
- Good:
Customer
,WikiPage
,Account
,AddressParser
- Bad:
Verify
- Good:
- It should not be ambigious, it should perfectly describe what it does, and there should be nothing hidden.
- Choose appropriate abstraction level names
- Long names for long scopes
- Name side effects:
create_or_return_oos()
vsget_oos()
# ❌ bad: function name does not describe what it actually does def get_oos(id: str) -> OOS: oos = db.get_oos(id) if not oos: return OOS() return oos
Functions
- Functions should do one thing well, and only that one thing.
# ❌ bad: does multiple things
def process_order(order):
validate(order)
charge_payment(order) # side effect
send_receipt(order) # side effect
return True
# ✅ good: single responsibility
def complete_order(order):
if validate(order):
charge_payment(order)
send_receipt(order)
Stepdown rule: One level of abstraction per function
Use Polymorphism over conditionals. Combine
switch
with Abstract Factory [2].# ❌ bad if animal.type == "DOG": animal.bark() elif animal.type == "CAT": animal.meow() # ✅ good class Animal: def speak(self): pass class Dog(Animal): def speak(self): print("Woof!") class Cat(Animal): def speak(self): print("Meow!") animal.speak()
Fewer functions are better
Avoid flag arguments - use separate functions instead:
# ❌ bad def create_user(name, is_admin): ... # ✅ good def create_admin(name): ... def create_customer(name): ...
Function naming: Use verb-names (
verify(user)
,calculate_total()
)No side effects: Function should only do what it says (overlapping with the Naming chapter)
# ❌ bad: modifies external state def check_password(input_password): if input_password == stored_password: login_attempts = 0 # side effect! return True return False # ✅ good: pure function def verify_password(input_password, correct_password): return input_password == correct_password
Functions should either do something or answer something, not both.
Comments
“Don’t comment bad code — rewrite it.” - Brian W. Kernighan and P.J. Plaugher
# ❌ bad: comment explaining confusing code # check if user is active and over 18 if u['a'] and u['age'] > 18: ... # ✅ good: self-explanatory if user.is_active and user.age > 18: ...
Comments are failures - they compensate for failure to express ourselves in code
- Don’t use comments when you can use a function/variable
- Never comment out code - others won’t delete it
- Use abstract classes over concrete ones to hide unnecessary details
Law of Demeter (minimize object navigation):
# ❌ bad: violates Demeter report.get_user().get_address().format() # ✅ good: direct access report.format_user_address()
Error handling
Use exceptions over return codes:
# ❌ bad if save_file() == ERROR_CODE: ... # ✅ good try: save_file() except IOError as e: handle_error(e)
Don’t return null - throw error or return special case object:
# ❌ bad def find_user(user_id): return users.get(user_id) # ✅ good def find_user(user_id): return users.get(user_id, GuestUser())
Don’t pass null - use assertions
Boundaries
- Good software accommodates change without heavy rework
- Use adapters for third-party services:
# ✅ good: adapter pattern class PaymentAdapter: def process_payment(self, amount): PayPalSDK.charge(amount)
Unit tests
Note: Nowadays, this part is especially useful, as we are accelerating towards AI agents and we need strong verification mechanisms to keep the AI on leash. Tests help you guide AI towards your customized “correct"s.
Three laws of TDD:
- Don’t write production code until you’ve written a failing unit test
- Don’t write more test than needed to fail (compilation failure counts)
- Don’t write more production code than needed to pass the current failing test
Testing philosophies:
- One assert per test
- Single concept per test
F.I.R.S.T principles:
- Fast: Milliseconds execution
- Independent: No test dependencies
- Repeatable: Same results everywhere
- Self-Validating: Automatic pass/fail
- Timely: Write tests first
Classes
Use encapsulation (private variables/utilities)
Classes should be small (measure by responsibilities)
Single Responsibility Principle:
- A class should have only one reason to change
- Organize like toolbox drawers vs junk drawers
# ❌ bad: mixed concerns class User: def save(self): ... def log_activity(self): ... # ✅ good: separated concerns class UserSaver: def save(self, user): ... class ActivityLogger: def log(self, event): ...
Cohesion:
- Few instance variables
- High cohesion when all methods use all variables
# ✅ good: methods use all variables class Rectangle: def __init__(self, width, height): self.width = width self.height = height def area(self): return self.width * self.height def perimeter(self): return 2 * (self.width + self.height)
Systems
Dependency Injection: Apply Inversion of Control (IoC) to dependencies
# ❌ bad: internal dependency creation class ReportService: def __init__(self): self.db = Database() # ✅ good: passed dependency class ReportService: def __init__(self, database): self.db = database
Objects shouldn’t instantiate dependencies - delegate to authoritative mechanism
Use the simplest solution that works
This is indeed really important, because sometimes a person has all the context regarding a specific task. However, the next person might not have enough context, and in case the first person leaves, the whole thing suddenly becomes a burden, and bottleneck.
Emergence
- Kent Beck’s Simple Design Rules:
- Passes all tests
- No duplication
- Expresses programmer intent
- Minimal classes/methods
Concurrency
- “Objects are processing abstractions. Threads are schedule abstractions.” - James O. Coplien
- Concurrency issues appear under stress
- Decoupling strategy: separates “what” from “when”
- Execution models:
- Producer-Consumer: Queue-based resource management
- Readers-Writers: Throughput vs. stale information tradeoffs
- Dining Philosophers: Resource contention problem (threads = philosophers)
- Avoid race conditions: Don’t use multiple methods on shared objects
# ❌ bad: unsafe shared access counter = 0 def increment(): global counter counter += 1 # race condition! # ✅ good: thread-safe with lock from threading import Lock lock = Lock() counter = 0 def safe_increment(): global counter with lock: # exclusive access counter += 1
Smells and Heuristics
Comments
- Inappropriate info: Comments should be technical notes only
- Obsolete comments: Update or delete immediately
- Redundant comments: Remove unnecessary explanations
- Poorly written: Fix or remove unclear comments
- Commented-out code: Delete fearlessly (VCS remembers)
Environment
- Multi-step builds: Building should be single trivial operation
- Multi-step tests: Run all unit tests with one command
Functions
- Too many arguments: Ideal = zero, >3 requires justification
- Output arguments: Change state of owning object instead
- Flag arguments: Indicate multi-purpose functions - eliminate
- Dead functions: Delete unused code
Summary
Boundary conditions: Test all edge cases explicitly
DRY principle: Duplication = missed abstraction opportunity
Vertical separation: Declare variables near usage site
Principle of Least Surprise: Code where readers expect it
Explicit dependencies: No hidden assumptions between modules
Encapsulate conditionals:
# ❌ bad if user.is_authenticated and user.has_premium: # ✅ good if user_has_premium_access(user):
Avoid negative conditionals:
# ❌ bad if not user.is_not_verified: ... # ✅ good if user.is_verified: ...
Function abstraction levels: Descend only one level
High-level configuration: Keep config data at top
Law of Demeter (void transitive navigation): Do not use chain executions such as
.get_this().get_that()
References
[1] Martin, R. C. (2008). Clean Code : a handbook of agile software craftsmanship. http://ci.nii.ac.jp/ncid/BA87924669
[2] Refactoring Guru. (n.d.). Abstract Factory. Refactoring.guru. https://refactoring.guru/design-patterns/abstract-factory