clean-code
Use this skill when reviewing, writing, or refactoring code for cleanliness and maintainability following Robert C. Martin's (Uncle Bob) Clean Code principles. Triggers on code review, refactoring, naming improvements, function decomposition, applying SOLID principles, writing clean tests with TDD, identifying code smells, or improving error handling. Covers Clean Code, SOLID, and test-driven development.
engineering clean-coderefactoringsolidcode-reviewtddbest-practicesWhat is clean-code?
Use this skill when reviewing, writing, or refactoring code for cleanliness and maintainability following Robert C. Martin's (Uncle Bob) Clean Code principles. Triggers on code review, refactoring, naming improvements, function decomposition, applying SOLID principles, writing clean tests with TDD, identifying code smells, or improving error handling. Covers Clean Code, SOLID, and test-driven development.
clean-code
clean-code is a production-ready AI agent skill for claude-code, gemini-cli, openai-codex, and 1 more. Reviewing, writing, or refactoring code for cleanliness and maintainability following Robert C. Martin's (Uncle Bob) Clean Code principles.
Quick Facts
| Field | Value |
|---|---|
| Category | engineering |
| Version | 0.1.0 |
| Platforms | claude-code, gemini-cli, openai-codex, mcp |
| License | MIT |
How to Install
- Make sure you have Node.js installed on your machine.
- Run the following command in your terminal:
npx skills add AbsolutelySkilled/AbsolutelySkilled --skill clean-code- The clean-code skill is now available in your AI coding agent (Claude Code, Gemini CLI, OpenAI Codex, etc.).
Overview
Clean Code is a set of principles and practices from Robert C. Martin (Uncle Bob) for writing software that is readable, maintainable, and expressive. The core idea is that code is read far more often than it is written, so optimizing for readability is optimizing for productivity. This skill covers the Clean Code book's principles, SOLID object-oriented design, and test-driven development - giving an agent the judgment to write, review, and refactor code the way a disciplined craftsman would.
Tags
clean-code refactoring solid code-review tdd best-practices
Platforms
- claude-code
- gemini-cli
- openai-codex
- mcp
Related Skills
Pair clean-code with these complementary skills:
Frequently Asked Questions
What is clean-code?
Use this skill when reviewing, writing, or refactoring code for cleanliness and maintainability following Robert C. Martin's (Uncle Bob) Clean Code principles. Triggers on code review, refactoring, naming improvements, function decomposition, applying SOLID principles, writing clean tests with TDD, identifying code smells, or improving error handling. Covers Clean Code, SOLID, and test-driven development.
How do I install clean-code?
Run npx skills add AbsolutelySkilled/AbsolutelySkilled --skill clean-code in your terminal. The skill will be immediately available in your AI coding agent.
What AI agents support clean-code?
This skill works with claude-code, gemini-cli, openai-codex, mcp. Install it once and use it across any supported AI coding agent.
Maintainers
Generated from AbsolutelySkilled
SKILL.md
Clean Code
Clean Code is a set of principles and practices from Robert C. Martin (Uncle Bob) for writing software that is readable, maintainable, and expressive. The core idea is that code is read far more often than it is written, so optimizing for readability is optimizing for productivity. This skill covers the Clean Code book's principles, SOLID object-oriented design, and test-driven development - giving an agent the judgment to write, review, and refactor code the way a disciplined craftsman would.
When to use this skill
Trigger this skill when the user:
- Asks to review code for quality, readability, or maintainability
- Wants to refactor a function, class, or module to be cleaner
- Needs help naming variables, functions, or classes
- Asks about SOLID principles or how to apply them
- Wants to decompose a large function or class
- Asks to identify code smells or technical debt
- Wants to write tests using TDD (red-green-refactor)
- Needs to improve error handling patterns
Do NOT trigger this skill for:
- Performance optimization (clean code prioritizes readability, not speed)
- Architecture decisions at the system level (use clean-architecture skills instead)
Key principles
The Boy Scout Rule - Leave the code cleaner than you found it. Every commit is an opportunity to improve a name, extract a helper, or remove dead code.
Readability is king - Code should read like well-written prose. If a reader needs to pause and re-read a line, that line needs work. Clever code is bad code.
Single Responsibility at every level - Every function does one thing. Every class has one reason to change. Every module has one area of responsibility.
Express intent, don't document it - The code itself should explain what and why. Comments that explain "what" the code does indicate the code failed to communicate. Reserve comments for "why" something non-obvious was chosen.
Small is beautiful - Functions should be 5-20 lines. Classes should be small enough to describe in one sentence. Files should fit a mental model.
Core concepts
Clean Code rests on a hierarchy of concerns, from the smallest unit to the largest:
Names are the most fundamental tool. A good name eliminates the need for
comments, makes intent obvious, and prevents misuse. Names should be
intention-revealing, pronounceable, and searchable. See references/naming-guide.md.
Functions are the building blocks. Each function should do one thing, operate at one level of abstraction, and have as few arguments as possible. The "stepdown rule" means code reads top-to-bottom like a newspaper - high-level summary first, details below.
SOLID principles govern class and module design. They prevent rigid, fragile code
that breaks in unexpected places when changed. See references/solid-principles.md.
Code smells are surface indicators of deeper structural problems. Recognizing
smells is the first step to refactoring. See references/code-smells.md.
Tests are the safety net that enables fearless refactoring. TDD (test-driven
development) ensures tests exist before code and that code is only as complex as
needed. See references/tdd.md.
Common tasks
Review code for Clean Code violations
Walk through the code looking for violations in this priority order:
- Naming - Are names intention-revealing? Can you understand the code without reading comments?
- Function size - Any function over 20 lines? Does it do more than one thing?
- Abstraction levels - Does the function mix high-level logic with low-level detail?
- Duplication - Any copy-paste code or structural duplication?
- Error handling - Are errors handled with exceptions, not return codes? Any null returns?
Before (violations):
// Check if user can access the resource
function check(u, r) {
if (u != null) {
if (u.role == 'admin') {
return true;
} else if (u.perms != null) {
for (let i = 0; i < u.perms.length; i++) {
if (u.perms[i].rid == r.id && u.perms[i].level >= 2) {
return true;
}
}
}
}
return false;
}After (clean):
function canUserAccessResource(user, resource) {
if (!user) return false;
if (user.isAdmin()) return true;
return user.hasPermissionFor(resource, Permission.READ);
}Refactor a long function
Apply the Extract Method pattern. Identify clusters of lines that operate at the same level of abstraction and give them a name.
Before:
def process_order(order):
# validate
if not order.items:
raise ValueError("Empty order")
if not order.customer:
raise ValueError("No customer")
for item in order.items:
if item.quantity <= 0:
raise ValueError(f"Invalid quantity for {item.name}")
# calculate totals
subtotal = sum(item.price * item.quantity for item in order.items)
tax = subtotal * 0.08
shipping = 5.99 if subtotal < 50 else 0
total = subtotal + tax + shipping
# charge
payment = gateway.charge(order.customer.payment_method, total)
if not payment.success:
raise PaymentError(payment.error)
# send confirmation
send_email(order.customer.email, "Order confirmed", f"Total: ${total:.2f}")After:
def process_order(order):
validate_order(order)
total = calculate_total(order)
charge_customer(order.customer, total)
send_confirmation(order.customer, total)Each extracted function is independently readable, testable, and reusable.
Improve naming
Apply these rules by entity type:
| Entity | Rule | Bad | Good |
|---|---|---|---|
| Boolean | Should read as a true/false question | flag, status |
isActive, hasPermission |
| Function | Verb + noun, describes action | data(), process() |
fetchUserProfile(), validateEmail() |
| Class | Noun, describes what it is | Manager, Processor |
EmailSender, InvoiceCalculator |
| Collection | Plural noun | list, data |
activeUsers, pendingOrders |
| Constant | Screaming snake case, self-documenting | 86400 |
SECONDS_PER_DAY = 86400 |
See references/naming-guide.md for the full guide.
Apply SOLID principles
When a class is hard to change, test, or reuse, check it against SOLID:
- Single Responsibility - Does this class have more than one reason to change? Split it.
- Open/Closed - Can you extend behavior without modifying existing code? Use polymorphism.
- Liskov Substitution - Can subtypes replace their parent without breaking things?
- Interface Segregation - Are clients forced to depend on methods they don't use? Split the interface.
- Dependency Inversion - Do high-level modules depend on low-level details? Inject abstractions.
See references/solid-principles.md for detailed examples and when NOT to apply each.
Write clean tests with TDD
Follow the red-green-refactor cycle:
- Red - Write a failing test that defines the desired behavior
- Green - Write the minimum code to make it pass
- Refactor - Clean up both production and test code
Tests should follow the FIRST principles (Fast, Independent, Repeatable, Self-validating, Timely) and use the Arrange-Act-Assert structure.
See references/tdd.md for the full guide.
Clean up error handling
Replace error codes with exceptions. Never return or pass null.
Before:
public int withdraw(Account account, int amount) {
if (account == null) return -1;
if (amount > account.getBalance()) return -2;
account.debit(amount);
return 0;
}
// Caller: if (withdraw(acct, 100) == -2) { ... }After:
public void withdraw(Account account, int amount) {
Objects.requireNonNull(account, "Account must not be null");
if (amount > account.getBalance()) {
throw new InsufficientFundsException(account, amount);
}
account.debit(amount);
}
// Caller: try { withdraw(acct, 100); } catch (InsufficientFundsException e) { ... }Prefer unchecked (runtime) exceptions. Checked exceptions violate the Open/Closed Principle - a new exception in a low-level function forces signature changes up the entire call chain.
Anti-patterns / common mistakes
| Mistake | Why it's wrong | What to do instead |
|---|---|---|
| Over-abstracting | Creating interfaces, factories, and layers for simple problems adds complexity without value | Only abstract when you have a concrete second use case, not "just in case" |
| Comment-phobia | Deleting ALL comments including genuinely useful "why" explanations | Remove "what" comments, keep "why" comments. Regex explanations and business rule context are valuable |
| Tiny function obsession | Breaking code into dozens of 2-line functions destroys readability | Extract when a block has a clear name and purpose, not just because it's "long" |
| Dogmatic SOLID | Creating an interface for every class, even with one implementation | Apply SOLID when you feel the pain of rigidity, not preemptively everywhere |
| Magic refactoring | Refactoring without tests, hoping nothing breaks | Always have test coverage before refactoring. Write tests first if they don't exist |
| Naming paralysis | Names so long they hurt readability (AbstractSingletonProxyFactoryBean) |
Names should be proportional to scope. Loop variable i is fine; module-level needs more |
| TDD cargo-culting | Testing implementation details (private methods, mock internals) | Test behavior and public contracts, not implementation. Tests should survive refactoring |
Gotchas
Refactoring without a safety net - Extract Method and Rename refactors look safe but break things when the surrounding code has implicit coupling, side effects, or no test coverage. Always ensure tests cover the behavior being refactored before making any structural change - even a "trivial" rename.
Over-decomposing into micro-functions - Splitting a 40-line function into 15 two-line helpers makes individual pieces shorter but the flow incomprehensible. Extract only when the extracted block has a name that is more informative than reading the code itself. Length is not the trigger; clarity is.
Applying SOLID to one-off utilities - Adding an interface for a class that has exactly one implementation "to follow Dependency Inversion" introduces indirection without value. Apply SOLID principles when you feel friction from rigidity or testability problems, not preemptively as a ritual.
Comments explaining what, not why - After a refactor, leftover "what" comments that now contradict the code are worse than no comments. They mislead future readers. Delete any comment that describes the operation of code that has since been renamed or restructured to be self-explanatory.
TDD on implementation, not behavior - Writing tests that call private methods or assert on internal state means the tests break every refactor, defeating the purpose of having tests. Test only through public interfaces and observable outputs; the test should survive any internal restructuring.
References
For detailed content on specific topics, read the relevant file from references/:
references/solid-principles.md- Each SOLID principle with examples and when NOT to applyreferences/code-smells.md- Catalog of smells with refactoring moves to fix eachreferences/tdd.md- Three laws of TDD, red-green-refactor, test design patternsreferences/naming-guide.md- Detailed naming rules by entity type with examples
Only load a references file if the current task requires deep detail on that topic.
References
code-smells.md
Code Smells & Refactoring Moves
A code smell is a surface indication of a deeper structural problem. Smells don't necessarily mean the code is wrong - they signal that refactoring might improve the design. Always use judgment: not every smell needs fixing.
Function-level smells
Long Function
- Symptom: Function exceeds 20 lines or requires comments to separate sections
- Refactoring: Extract Method - pull each section into a named function
- Signal phrases: "this function does X, then Y, then Z"
Too Many Arguments
- Symptom: Function takes 3+ arguments
- Refactoring:
- 0 args (niladic) - ideal
- 1 arg (monadic) - good
- 2 args (dyadic) - acceptable
- 3 args (triadic) - needs justification
- 4+ args - introduce a parameter object or builder
# Bad: 5 arguments
def create_user(name, email, age, role, department): ...
# Good: parameter object
def create_user(user_data: CreateUserRequest): ...Flag Arguments
- Symptom: Boolean parameter that makes the function do two different things
- Refactoring: Split into two functions with clear names
// Bad
render(data, true) // what does true mean?
// Good
renderForPrint(data)
renderForScreen(data)Side Effects
- Symptom: Function name suggests it does X but it also secretly does Y
- Refactoring: Either rename to reflect all effects or extract the side effect
Dead Code
- Symptom: Unreachable code, unused variables, commented-out blocks
- Refactoring: Delete it. Version control has the history if you need it back.
Class-level smells
Large Class (God Object)
- Symptom: Class has too many instance variables, too many methods, or does too many unrelated things
- Refactoring: Extract Class - group related fields and methods into new classes
- Test: Can you describe the class's purpose in one sentence without "and"?
Feature Envy
- Symptom: A method uses more data from another class than from its own
- Refactoring: Move Method - put the method in the class whose data it uses most
# Bad: OrderPrinter is envious of Order's data
class OrderPrinter:
def format(self, order):
return f"{order.customer.name}: {order.total()} ({len(order.items)} items)"
# Better: Move the formatting to Order
class Order:
def summary(self):
return f"{self.customer.name}: {self.total()} ({len(self.items)} items)"Data Clumps
- Symptom: The same group of variables appears together in multiple places
- Refactoring: Extract a class or named tuple for the group
// Bad: start_x, start_y, end_x, end_y appear together everywhere
function drawLine(startX: number, startY: number, endX: number, endY: number) {}
// Good: introduce Point
function drawLine(start: Point, end: Point) {}Primitive Obsession
- Symptom: Using primitive types (string, int) for domain concepts
- Refactoring: Replace with a value object
// Bad
String phoneNumber = "555-1234";
String zipCode = "90210";
// Good
PhoneNumber phone = new PhoneNumber("555-1234"); // validates format
ZipCode zip = new ZipCode("90210"); // validates formatRefused Bequest
- Symptom: Subclass inherits methods/properties it doesn't want or use
- Refactoring: Replace inheritance with composition, or push unused methods down to siblings that actually need them
Structural smells
Duplicated Code
- Symptom: Same or similar logic in two or more places
- Refactoring:
- Same class: Extract Method
- Sibling classes: Extract to parent or shared utility
- Unrelated classes: Extract to a standalone function
- Warning: Don't extract prematurely. Two occurrences might be coincidental similarity. Three is a pattern.
Divergent Change
- Symptom: One class is modified for many different reasons
- Refactoring: Split the class by responsibility (apply SRP)
Shotgun Surgery
- Symptom: One change requires small edits across many classes
- Refactoring: Consolidate the scattered logic into one class
Speculative Generality
- Symptom: Abstract classes, interfaces, parameters, or hooks that exist "just in case" but have only one implementation
- Refactoring: Remove it. You Aren't Gonna Need It (YAGNI). Add abstraction when the second use case actually appears.
Inappropriate Intimacy
- Symptom: Two classes access each other's private details excessively
- Refactoring: Move methods and fields to reduce coupling, or introduce a mediator between them
Comment smells
Redundant Comment
// Bad: adds nothing the code doesn't already say
i++; // increment iMandated Comment
// Bad: forced Javadoc that just restates the signature
/** Sets the name. @param name the name */
public void setName(String name) { this.name = name; }Commented-Out Code
# Bad: dead code hiding in comments
# total = calculate_legacy_total(order)
total = calculate_total(order)Delete it. Git has history.
Journal Comments
// Bad: changelog in the file
// 2024-01-15 - Added validation
// 2024-02-01 - Fixed null checkThat's what git log is for.
Refactoring decision guide
- Is there a test for this code? If no, write one first. Never refactor without tests.
- What smell do I see? Name it specifically.
- What's the minimal refactoring move? Don't restructure the world.
- Does the refactoring make the code more readable? If not, revert it.
- Do the tests still pass? If not, the refactoring introduced a bug.
naming-guide.md
Naming Guide
Names are the most powerful tool for making code readable. A good name eliminates the need for comments, makes intent obvious, and prevents misuse. Invest time in names - it pays dividends every time someone reads the code.
Universal rules
Use intention-revealing names
The name should answer: why does this exist, what does it do, how is it used?
# Bad: requires a comment to understand
d = 86400 # elapsed time in seconds
# Good: name reveals intent
SECONDS_PER_DAY = 86400Use pronounceable names
If you can't say it out loud, it's hard to discuss in code reviews and meetings.
// Bad
Date genymdhms; // generation year-month-day-hour-minute-second
// Good
Date generationTimestamp;Use searchable names
Single-letter names and numeric constants are hard to grep. The length of a name should be proportional to the size of its scope.
// Bad: searching for "5" finds thousands of results
if (items.length > 5) { ... }
// Good: searchable and self-documenting
const MAX_ITEMS_PER_PAGE = 5;
if (items.length > MAX_ITEMS_PER_PAGE) { ... }Avoid encodings and prefixes
Don't embed type information, scope, or notation in names.
// Bad: Hungarian notation, member prefixes
let strName: string;
let m_count: number;
let IShapeFactory: interface;
// Good: let the type system handle types
let name: string;
let count: number;
interface ShapeFactory { ... }Avoid mental mapping
Readers shouldn't have to mentally translate a name into a concept they already know.
# Bad: reader must remember that 'r' means 'urlRewritten'
r = rewrite_url(original)
# Good: no mapping needed
rewritten_url = rewrite_url(original)Exception: Single-letter variables are fine in small scopes where the convention
is universal: i, j for loop indices; x, y for coordinates; e for
exception in a catch block.
Rules by entity type
Variables
| Guideline | Bad | Good |
|---|---|---|
| Noun or noun phrase | calc |
totalPrice |
| Specific, not generic | data, info, temp |
userProfile, orderDetails |
| No noise words | nameString, accountData |
name, account |
| Plural for collections | userList |
users |
Booleans
Booleans should read as true/false questions using prefixes like is, has,
can, should, was:
| Bad | Good |
|---|---|
active |
isActive |
permission |
hasPermission |
visible |
isVisible |
open |
canOpen or isOpen |
Avoid negated boolean names - they cause double negatives:
# Bad: double negative is confusing
if not is_not_found: ...
# Good: positive name
if is_found: ...Functions and methods
Functions should be verbs or verb phrases. The name should describe the action.
| Bad | Good |
|---|---|
data() |
fetchUserData() |
process() |
calculateTax() |
handle() |
routeRequest() |
check() |
validateEmail() |
make() |
createInvoice() |
Accessors, mutators, and predicates follow conventions:
getName() // accessor (get + noun)
setName(val) // mutator (set + noun)
isActive() // predicate (is + adjective)
hasPermission() // predicate (has + noun)Classes
Classes should be nouns or noun phrases. Never verbs.
| Bad | Good |
|---|---|
Process |
PaymentProcessor |
Manage |
UserManager |
Run |
TaskRunner |
Avoid meaningless suffixes that add no information:
| Avoid | Prefer |
|---|---|
UserInfo |
User |
AccountData |
Account |
OrderObject |
Order |
Exception: Suffixes that communicate a design pattern are valuable:
EmailSender, OrderRepository, PaymentStrategy, UserFactory.
Constants
Use SCREAMING_SNAKE_CASE. The name should explain the meaning, not just the value.
# Bad: the value is obvious, the meaning is not
FIVE = 5
SIXTY = 60
# Good: explains meaning
MAX_RETRY_ATTEMPTS = 5
SESSION_TIMEOUT_SECONDS = 60Enums
Enum type should be singular; members should be SCREAMING_SNAKE_CASE:
enum OrderStatus {
PENDING,
PROCESSING,
SHIPPED,
DELIVERED,
CANCELLED,
}Scope-length rule
The length of a name should be proportional to the scope in which it's used:
| Scope | Name length | Example |
|---|---|---|
| Loop (2-3 lines) | 1 character | i, x |
| Lambda / closure | Short | user, item |
| Local variable | Short to medium | activeUsers |
| Function parameter | Medium | targetUserId |
| Class field | Medium to long | connectionTimeout |
| Module constant | Long and descriptive | MAX_CONCURRENT_CONNECTIONS |
| Global / exported | Very descriptive | DEFAULT_SESSION_TIMEOUT_SECONDS |
Naming consistency
Pick one word per concept
Choose one synonym and use it everywhere. Don't mix:
fetch/retrieve/get/load- pick onecreate/make/build/generate- pick onecontroller/manager/handler- pick one
Use domain language
Use the terms your business domain uses (Domain-Driven Design's "ubiquitous language"). If the business says "policy," don't call it "rule" in code.
# Bad: invented terminology
class DiscountRule:
def applies_to(self, purchase): ...
# Good: matches business language
class PricingPolicy:
def applies_to(self, order): ...Renaming checklist
When improving names in existing code:
- Identify the name's scope and all usages
- Choose a name that passes the "read aloud" test
- Use your IDE's rename refactoring (not find-and-replace)
- Verify tests still pass
- Check that no external API contracts are broken
solid-principles.md
SOLID Principles
The five SOLID principles guide class and module design toward code that is easy to change, test, and extend. Apply them when you feel the pain of rigidity - not preemptively everywhere.
S - Single Responsibility Principle (SRP)
A class should have only one reason to change.
This means one actor (stakeholder or group) owns each class. When two different business concerns live in the same class, a change for one concern risks breaking the other.
Before (violates SRP)
class Employee:
def calculate_pay(self): # owned by accounting
...
def generate_report(self): # owned by reporting
...
def save_to_database(self): # owned by DBA/infra
...After (SRP applied)
class PayCalculator:
def calculate_pay(self, employee): ...
class EmployeeReporter:
def generate_report(self, employee): ...
class EmployeeRepository:
def save(self, employee): ...When NOT to apply
- Tiny scripts or one-off utilities - splitting a 30-line script into 3 classes adds friction without value.
- Data classes / DTOs - a class that just holds data fields doesn't need splitting even if multiple consumers read from it.
O - Open/Closed Principle (OCP)
Software entities should be open for extension but closed for modification.
You should be able to add new behavior without changing existing, tested code. Achieve this through polymorphism, strategy pattern, or plugin architectures.
Before (violates OCP)
function calculateShipping(order: Order): number {
if (order.type === 'standard') return 5.99;
if (order.type === 'express') return 12.99;
if (order.type === 'overnight') return 24.99;
// Every new shipping type requires modifying this function
}After (OCP applied)
interface ShippingStrategy {
calculate(order: Order): number;
}
class StandardShipping implements ShippingStrategy {
calculate(order: Order) { return 5.99; }
}
class ExpressShipping implements ShippingStrategy {
calculate(order: Order) { return 12.99; }
}
// New types are added by creating new classes, not modifying existing code
function calculateShipping(order: Order, strategy: ShippingStrategy): number {
return strategy.calculate(order);
}When NOT to apply
- When you have 2-3 cases that rarely change - a simple if/switch is clearer than a strategy pattern with one implementation per case.
- Internal utilities where you control all callers - just change the code directly.
L - Liskov Substitution Principle (LSP)
Subtypes must be substitutable for their base types without altering correctness.
If code works with a base class, it must work identically with any subclass.
Violations show up as instanceof checks or broken assumptions in calling code.
Classic violation: Square/Rectangle
class Rectangle {
void setWidth(int w) { this.width = w; }
void setHeight(int h) { this.height = h; }
int area() { return width * height; }
}
class Square extends Rectangle {
void setWidth(int w) { this.width = w; this.height = w; } // breaks LSP
void setHeight(int h) { this.width = h; this.height = h; } // breaks LSP
}
// Calling code assumes width and height are independent - Square breaks that
Rectangle r = new Square();
r.setWidth(5);
r.setHeight(10);
assert r.area() == 50; // FAILS: area is 100Fix
Don't make Square extend Rectangle. Use a Shape interface with an area() method
that each implements independently.
When NOT to apply
- LSP is rarely "over-applied" - it's more of a diagnostic. If your subtypes work correctly everywhere the parent is used, you're fine. Don't create elaborate type hierarchies just to prove LSP compliance.
I - Interface Segregation Principle (ISP)
Clients should not be forced to depend on methods they do not use.
Fat interfaces that serve multiple clients force unnecessary coupling. Split them into focused, role-specific interfaces.
Before (violates ISP)
interface Worker {
work(): void;
eat(): void;
sleep(): void;
attendMeeting(): void;
}
// A Robot worker is forced to implement eat() and sleep()
class Robot implements Worker {
work() { /* ... */ }
eat() { throw new Error('Robots do not eat'); } // forced stub
sleep() { throw new Error('Robots do not sleep'); }
attendMeeting() { /* ... */ }
}After (ISP applied)
interface Workable { work(): void; }
interface Feedable { eat(): void; }
interface Restable { sleep(): void; }
interface Meetable { attendMeeting(): void; }
class Robot implements Workable, Meetable {
work() { /* ... */ }
attendMeeting() { /* ... */ }
}When NOT to apply
- Don't split an interface that has a single consumer into micro-interfaces of one method each. ISP solves the problem of multiple consumers with different needs.
- Standard library or framework interfaces (e.g.
Iterable) shouldn't be split even if some methods go unused.
D - Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules. Both should depend on abstractions.
The business logic (policy) should define the interface it needs. Infrastructure (database, HTTP, filesystem) implements that interface and is injected.
Before (violates DIP)
class OrderService:
def __init__(self):
self.db = MySQLDatabase() # hard-coded dependency
self.mailer = SmtpEmailSender() # hard-coded dependency
def place_order(self, order):
self.db.save(order)
self.mailer.send(order.customer.email, "Order placed")After (DIP applied)
class OrderService:
def __init__(self, repository: OrderRepository, notifier: Notifier):
self.repository = repository
self.notifier = notifier
def place_order(self, order):
self.repository.save(order)
self.notifier.notify(order.customer, "Order placed")Now OrderService depends on abstractions (OrderRepository, Notifier) that
it defines. Tests can inject fakes; production wires in real implementations.
When NOT to apply
- Simple scripts or small applications where you control the full stack and dependency injection adds ceremony without testability benefit.
- When there will only ever be one implementation (e.g. wrapping a single third-party SDK) - inject the concrete class directly; add the interface when a second implementation actually appears.
Applying SOLID pragmatically
Use this decision heuristic:
- Feel the pain first - Don't apply SOLID preemptively. Wait until a change is hard, a test is awkward, or a class is confusing.
- Identify which principle is violated - The symptom tells you the principle:
- Hard to change without breaking other things -> SRP or OCP
- Subclass doesn't work where parent is expected -> LSP
- Forced to implement empty methods -> ISP
- Can't test without real database/network -> DIP
- Apply the minimum fix - Don't restructure the whole codebase. Fix the one class or interface causing the problem.
- Verify with tests - The refactoring should make tests easier to write, not harder. If you need more mocks after applying SOLID, you may have over-applied.
tdd.md
Test-Driven Development (TDD)
TDD is a discipline where tests drive the design of the code. You write the test first, watch it fail, make it pass with minimal code, then refactor. This cycle produces code that is only as complex as needed and always has test coverage.
The Three Laws of TDD
- You may not write production code until you have written a failing test.
- You may not write more of a test than is sufficient to fail (compilation failures count as failures).
- You may not write more production code than is sufficient to pass the currently failing test.
These laws create a tight feedback loop: red-green-refactor cycles of 1-5 minutes.
Red-Green-Refactor
Red
Write a test that describes the behavior you want. Run it. It must fail. If it passes, either the behavior already exists or the test is wrong.
def test_new_account_has_zero_balance():
account = Account()
assert account.balance == 0Green
Write the simplest, most naive code that makes the test pass. Don't optimize. Don't handle edge cases you haven't tested yet. Just make it green.
class Account:
def __init__(self):
self.balance = 0Refactor
With the test green, clean up both production and test code. Remove duplication, improve names, extract methods. The tests protect you - if you break something, you'll know immediately.
What makes a clean test
FIRST Principles
| Principle | Meaning |
|---|---|
| Fast | Tests should run in milliseconds. Slow tests don't get run. |
| Independent | Tests must not depend on each other's execution order. |
| Repeatable | Same result every time, in any environment. No external dependencies. |
| Self-validating | Pass or fail - no manual log inspection to determine the result. |
| Timely | Written just before the production code, not after. |
Arrange-Act-Assert (AAA)
Every test should have exactly three sections:
def test_deposit_increases_balance():
# Arrange
account = Account()
# Act
account.deposit(100)
# Assert
assert account.balance == 100Keep each section small. If Arrange is long, extract a factory or builder. If Act is more than one line, the API might be too complex.
One assert per concept
A test should verify one behavioral concept. Multiple asserts are fine if they all verify the same concept from different angles:
# Good: one concept (user creation) verified from multiple angles
def test_create_user():
user = create_user("Alice", "alice@test.com")
assert user.name == "Alice"
assert user.email == "alice@test.com"
assert user.is_active is True# Bad: two unrelated concepts in one test
def test_user_operations():
user = create_user("Alice", "alice@test.com")
assert user.name == "Alice" # concept 1: creation
user.deactivate()
assert user.is_active is False # concept 2: deactivationTest naming
Tests should read as behavior specifications. Use this pattern:
test_<action>_<scenario>_<expected_result>Examples:
test_withdraw_with_sufficient_funds_reduces_balancetest_withdraw_exceeding_balance_raises_insufficient_fundstest_new_account_has_zero_balance
Avoid generic names like test_withdraw_1, test_edge_case, or test_bug_fix.
What to test
Test behavior, not implementation
# Bad: tests implementation detail (internal list structure)
def test_add_item():
cart = Cart()
cart.add(item)
assert cart._items[0] == item # reaching into private state
# Good: tests observable behavior
def test_add_item():
cart = Cart()
cart.add(item)
assert cart.item_count() == 1
assert cart.contains(item)Test boundaries, not internals
Focus tests on:
- Public API - the contract other code depends on
- Edge cases - empty inputs, zero, negative, maximum values
- Error paths - what happens when things go wrong
- Business rules - the logic that encodes domain requirements
Don't test:
- Private methods (test them through the public API)
- Framework behavior (trust that your ORM saves correctly)
- Trivial getters/setters (unless they have logic)
- Implementation details that could change during refactoring
Test doubles
Use the right kind of test double for the situation:
| Double | Purpose | Example |
|---|---|---|
| Stub | Returns canned answers | payment_gateway.charge() -> Success |
| Mock | Verifies interactions | verify(mailer).send_called_with(email) |
| Fake | Working implementation (simplified) | In-memory database |
| Spy | Records calls for later assertion | spy.call_count == 3 |
Prefer stubs over mocks. Mocks create tight coupling to implementation. If you refactor how a method achieves its result, mock-heavy tests break even though behavior is unchanged.
Prefer fakes for infrastructure. An in-memory repository is better than mocking every database call.
Common TDD mistakes
| Mistake | Problem | Fix |
|---|---|---|
| Writing tests after code | Loses the design feedback TDD provides | Commit to red-green-refactor |
| Testing private methods | Couples tests to implementation | Test through public API |
| Too many mocks | Tests break on refactoring, test nothing real | Use fakes, test fewer layers |
| Skipping the refactor step | Accumulates mess in both test and production code | Refactor is not optional |
| Tests that mirror implementation | assert mock.method_called_with(x, y, z) for every line |
Test outcomes, not call sequences |
| Not running tests frequently | Long feedback loops, harder to find what broke | Run after every small change |
Frequently Asked Questions
What is clean-code?
Use this skill when reviewing, writing, or refactoring code for cleanliness and maintainability following Robert C. Martin's (Uncle Bob) Clean Code principles. Triggers on code review, refactoring, naming improvements, function decomposition, applying SOLID principles, writing clean tests with TDD, identifying code smells, or improving error handling. Covers Clean Code, SOLID, and test-driven development.
How do I install clean-code?
Run npx skills add AbsolutelySkilled/AbsolutelySkilled --skill clean-code in your terminal. The skill will be immediately available in your AI coding agent.
What AI agents support clean-code?
clean-code works with claude-code, gemini-cli, openai-codex, mcp. Install it once and use it across any supported AI coding agent.