Silence render-disposed spam, unbreak bundled skills, drop board wake
Three independently ugly bugs that all fed the same log flood:
* Electron's `WebFrameMain.send` writes every "Render frame was disposed"
to stderr via `console.error`. When the worker fans a second
agent-event past a reloading renderer, we produced thousands of
identical lines and stalled the main loop on stderr I/O. Added a
`mainFrame.detached` guard so the send is short-circuited before
Electron's own catch block fires, and installed a targeted filter in
`suppress-warnings.ts` (now actually imported from the entry point)
so the specific `console.error("Error sending from webFrameMain:"…)`
noise never reaches the terminal. Any other payload still surfaces.
* `dispatchAgentMessage` accepted the human-reader sentinel as a wake
target when an agent replied to `__board__` — `wakeAgentIfPossible`
then called `db.getAgent("__board__")`, threw, and the dispatcher
kept repeating on every send. Strip `HUMAN_SENDER_ID` at
`resolveMessageWakeTargets`, and keep a defense-in-depth guard in the
heartbeat so a stray caller (future automation fan-out) cannot
reintroduce the crash.
* Pi's skills loader rejects any SKILL.md whose frontmatter `name`
diverges from its parent directory. The four bundled `my-writing-style`
copies declared `name: writing-style-skill`, so every install emitted
a warning and dropped the skill. They also lived as orphan gitlinks
(mode 160000 without .gitmodules), and the global `*.md` gitignore
hid every other SKILL.md from version control. Fixed the frontmatter
to match the dir, converted the dangling submodules to plain trees,
and allowlisted `builtin-skills/**/*.md` so a fresh clone carries the
catalogue instead of silently losing it.
This commit is contained in:
1 parent
7a9a6779b0
commit
e74dafcd36
287 files changed
+59858
-10
No files matched your search
@@ -0,0 +1,215 @@
|
||||
---
|
||||
name: ai-coding-discipline
|
||||
description: >
|
||||
Mandatory coding discipline rules that prevent common AI coding anti-patterns.
|
||||
MUST be loaded for ALL code writing, editing, reviewing, bug fixing, and testing tasks.
|
||||
Trigger on: writing code, editing code, fixing bugs, writing tests, implementing features,
|
||||
refactoring, code review, creating functions, adding error handling, debugging.
|
||||
This skill enforces fail-fast principles, proper error propagation, meaningful tests,
|
||||
and disciplined debugging workflows. Always active when Claude writes or modifies code.
|
||||
---
|
||||
|
||||
# AI Coding Discipline
|
||||
|
||||
> These rules override default AI coding tendencies. Follow them in ALL code you write or modify.
|
||||
|
||||
---
|
||||
|
||||
## Rule 1: No Silent Fallbacks
|
||||
|
||||
**Never use fallback values to mask data that should not be missing.**
|
||||
|
||||
```typescript
|
||||
// FORBIDDEN — hides upstream bugs
|
||||
const price = product?.price ?? 0;
|
||||
const userName = user?.name || "Unknown";
|
||||
|
||||
// CORRECT — fail fast when data contract is violated
|
||||
if (product.price == null) {
|
||||
throw new Error(`Product ${product.id} is missing price`);
|
||||
}
|
||||
const price = product.price;
|
||||
```
|
||||
|
||||
**When fallbacks ARE acceptable:**
|
||||
- User-facing display with explicit design intent (e.g., avatar placeholder)
|
||||
- Optional configuration with documented defaults
|
||||
- External input parsing where absence is a valid state
|
||||
|
||||
**Checklist before writing `??`, `||`, or `?.`:**
|
||||
1. Can this value legitimately be null/undefined at this point?
|
||||
2. If it is null, will the fallback produce a correct result downstream?
|
||||
3. Would a thrown error help me find a bug faster?
|
||||
|
||||
If the answer to #3 is yes, throw instead of falling back.
|
||||
|
||||
---
|
||||
|
||||
## Rule 2: No Catch-All try/catch in Business Logic
|
||||
|
||||
**Business logic functions must NOT wrap everything in try/catch. Let errors propagate naturally.**
|
||||
|
||||
```typescript
|
||||
// FORBIDDEN — swallows all errors into a useless null
|
||||
async function createOrder(data: OrderInput) {
|
||||
try {
|
||||
const user = await getUser(data.userId);
|
||||
const coupon = await validateCoupon(data.couponCode);
|
||||
const order = await saveOrder({ ...data, discount: coupon.value });
|
||||
return order;
|
||||
} catch (error) {
|
||||
console.log('Error creating order:', error);
|
||||
return null; // caller gets null, has no idea what failed
|
||||
}
|
||||
}
|
||||
|
||||
// CORRECT — let errors bubble up, catch at the boundary
|
||||
async function createOrder(data: OrderInput) {
|
||||
const user = await getUser(data.userId);
|
||||
const coupon = await validateCoupon(data.couponCode);
|
||||
const order = await saveOrder({ ...data, discount: coupon.value });
|
||||
return order;
|
||||
}
|
||||
|
||||
// Catch ONLY at the API/controller boundary
|
||||
app.post('/orders', async (req, res) => {
|
||||
try {
|
||||
const order = await createOrder(req.body);
|
||||
res.json(order);
|
||||
} catch (error) {
|
||||
logger.error('Order creation failed', { error, body: req.body });
|
||||
res.status(500).json({ error: 'Order creation failed' });
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**Where try/catch IS appropriate:**
|
||||
- API route handlers / controller boundaries
|
||||
- Top-level event handlers (message queues, cron jobs)
|
||||
- Operations where partial failure is expected and recovery is defined (e.g., batch processing with per-item error handling)
|
||||
- Specific, named error types you intend to handle differently
|
||||
|
||||
**Never catch `Error` just to return `null`, `undefined`, `false`, or an empty object.**
|
||||
|
||||
---
|
||||
|
||||
## Rule 3: Tests Must Fail When Code Breaks
|
||||
|
||||
**Every test must verify specific business outcomes. A test that passes when the core logic is deleted is worthless.**
|
||||
|
||||
```typescript
|
||||
// FORBIDDEN — passes even if processOrder returns garbage
|
||||
test('should process order', async () => {
|
||||
const result = await processOrder(mockOrder);
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
|
||||
// CORRECT — verifies exact business behavior
|
||||
test('should calculate total with 10% discount', async () => {
|
||||
const result = await processOrder({
|
||||
...mockOrder,
|
||||
discount: 0.1,
|
||||
});
|
||||
expect(result.totalAmount).toBe(900); // 1000 * 0.9
|
||||
expect(result.discountAmount).toBe(100);
|
||||
expect(result.status).toBe('confirmed');
|
||||
});
|
||||
```
|
||||
|
||||
**Test quality checklist:**
|
||||
1. If I delete the function body, does this test fail? If not, the test is useless.
|
||||
2. Am I testing behavior or just testing that "something exists"?
|
||||
3. Do my assertions check concrete values, not just truthiness?
|
||||
|
||||
**Banned weak assertions** (unless testing existence is the actual requirement):
|
||||
- `toBeDefined()`, `toBeTruthy()`, `toBeFalsy()` as sole assertion
|
||||
- `toHaveLength(expect.any(Number))`
|
||||
- `expect(result).not.toBeNull()` without further value checks
|
||||
|
||||
---
|
||||
|
||||
## Rule 4: No Hardcoded Lookup-Table Implementations
|
||||
|
||||
**Never implement business logic by hardcoding return values that match test cases.**
|
||||
|
||||
```typescript
|
||||
// FORBIDDEN — fake implementation that "fits" the tests
|
||||
function calculateDiscount(amount: number, level: string): number {
|
||||
if (amount === 1000 && level === 'gold') return 100;
|
||||
if (amount === 500 && level === 'silver') return 25;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// CORRECT — real logic
|
||||
function calculateDiscount(amount: number, level: string): number {
|
||||
const rates: Record<string, number> = { gold: 0.1, silver: 0.05, bronze: 0.02 };
|
||||
const rate = rates[level] ?? 0;
|
||||
return amount * rate;
|
||||
}
|
||||
```
|
||||
|
||||
**How to prevent this:**
|
||||
- Use diverse test data: multiple amounts, edge cases, boundary values
|
||||
- Add property-based / fuzzy tests where appropriate
|
||||
- Test with values NOT in the original spec to catch lookup-table fakes
|
||||
|
||||
---
|
||||
|
||||
## Rule 5: Red-Green Testing (TDD Order)
|
||||
|
||||
**When fixing a bug, always write the failing test FIRST, then fix the code.**
|
||||
|
||||
```
|
||||
Correct order:
|
||||
1. Discover bug
|
||||
2. Write a test that reproduces the bug
|
||||
3. Run the test — confirm it FAILS (red)
|
||||
4. Fix the code
|
||||
5. Run the test — confirm it PASSES (green)
|
||||
```
|
||||
|
||||
**Why this matters:** If you fix the code first and then write a test, you can never be sure the test would have caught the bug. The test might pass for the wrong reason.
|
||||
|
||||
**Never skip step 3.** Seeing the test go from red to green is the proof that the test is valid.
|
||||
|
||||
---
|
||||
|
||||
## Rule 6: Never Remove Debug Logs During a Fix
|
||||
|
||||
**When debugging, debug logs are removed ONLY after the human confirms the fix works.**
|
||||
|
||||
```
|
||||
FORBIDDEN workflow:
|
||||
1. Human asks to add debug logs
|
||||
2. AI adds logs
|
||||
3. Human runs code, shares log output
|
||||
4. AI "finds the problem", applies fix AND removes debug logs in the same edit
|
||||
5. Fix doesn't work — logs are gone, must re-add them
|
||||
|
||||
CORRECT workflow:
|
||||
1. Human asks to add debug logs
|
||||
2. AI adds logs
|
||||
3. Human runs code, shares log output
|
||||
4. AI applies fix ONLY — debug logs stay untouched
|
||||
5. Human verifies the fix
|
||||
6. Human decides when to remove debug logs (or asks AI to remove them)
|
||||
```
|
||||
|
||||
**Rule: Never touch debug/diagnostic logs in the same commit as a fix. They are separate concerns.**
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference: Self-Check Before Submitting Code
|
||||
|
||||
Before finalizing any code change, run through this checklist:
|
||||
|
||||
| Check | Question |
|
||||
|-------|----------|
|
||||
| Fallbacks | Did I use `??` or `\|\|` to hide a value that should never be missing? |
|
||||
| Error handling | Did I add try/catch in business logic that should just let errors propagate? |
|
||||
| Test strength | Would my tests still pass if I deleted the implementation? |
|
||||
| Test honesty | Did I hardcode values to match test cases instead of implementing real logic? |
|
||||
| TDD order | For bug fixes: did I see the test fail before applying the fix? |
|
||||
| Debug logs | Am I removing diagnostic logs in the same change as the fix? |
|
||||
|
||||
If any answer is "yes", revise before proceeding.
|
||||
@@ -0,0 +1,518 @@
|
||||
---
|
||||
name: api-design-principles
|
||||
description: Master REST and GraphQL API design principles to build intuitive, scalable, and maintainable APIs that delight developers. Use when designing new APIs, reviewing API specifications, or establishing API design standards.
|
||||
---
|
||||
|
||||
# API Design Principles
|
||||
|
||||
Master REST and GraphQL API design principles to build intuitive, scalable, and maintainable APIs that delight developers and stand the test of time.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Designing new REST or GraphQL APIs
|
||||
- Refactoring existing APIs for better usability
|
||||
- Establishing API design standards for your team
|
||||
- Reviewing API specifications before implementation
|
||||
- Migrating between API paradigms (REST to GraphQL, etc.)
|
||||
- Creating developer-friendly API documentation
|
||||
- Optimizing APIs for specific use cases (mobile, third-party integrations)
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### 1. RESTful Design Principles
|
||||
|
||||
**Resource-Oriented Architecture**
|
||||
|
||||
- Resources are nouns (users, orders, products), not verbs
|
||||
- Use HTTP methods for actions (GET, POST, PUT, PATCH, DELETE)
|
||||
- URLs represent resource hierarchies
|
||||
- Consistent naming conventions
|
||||
|
||||
**HTTP Methods Semantics:**
|
||||
|
||||
- `GET`: Retrieve resources (idempotent, safe)
|
||||
- `POST`: Create new resources
|
||||
- `PUT`: Replace entire resource (idempotent)
|
||||
- `PATCH`: Partial resource updates
|
||||
- `DELETE`: Remove resources (idempotent)
|
||||
|
||||
### 2. GraphQL Design Principles
|
||||
|
||||
**Schema-First Development**
|
||||
|
||||
- Types define your domain model
|
||||
- Queries for reading data
|
||||
- Mutations for modifying data
|
||||
- Subscriptions for real-time updates
|
||||
|
||||
**Query Structure:**
|
||||
|
||||
- Clients request exactly what they need
|
||||
- Single endpoint, multiple operations
|
||||
- Strongly typed schema
|
||||
- Introspection built-in
|
||||
|
||||
### 3. API Versioning Strategies
|
||||
|
||||
**URL Versioning:**
|
||||
|
||||
```
|
||||
/api/v1/users
|
||||
/api/v2/users
|
||||
```
|
||||
|
||||
**Header Versioning:**
|
||||
|
||||
```
|
||||
Accept: application/vnd.api+json; version=1
|
||||
```
|
||||
|
||||
**Query Parameter Versioning:**
|
||||
|
||||
```
|
||||
/api/users?version=1
|
||||
```
|
||||
|
||||
## REST API Design Patterns
|
||||
|
||||
### Pattern 1: Resource Collection Design
|
||||
|
||||
```python
|
||||
# Good: Resource-oriented endpoints
|
||||
GET /api/users # List users (with pagination)
|
||||
POST /api/users # Create user
|
||||
GET /api/users/{id} # Get specific user
|
||||
PUT /api/users/{id} # Replace user
|
||||
PATCH /api/users/{id} # Update user fields
|
||||
DELETE /api/users/{id} # Delete user
|
||||
|
||||
# Nested resources
|
||||
GET /api/users/{id}/orders # Get user's orders
|
||||
POST /api/users/{id}/orders # Create order for user
|
||||
|
||||
# Bad: Action-oriented endpoints (avoid)
|
||||
POST /api/createUser
|
||||
POST /api/getUserById
|
||||
POST /api/deleteUser
|
||||
```
|
||||
|
||||
### Pattern 2: Pagination and Filtering
|
||||
|
||||
```python
|
||||
from typing import List, Optional
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
class PaginationParams(BaseModel):
|
||||
page: int = Field(1, ge=1, description="Page number")
|
||||
page_size: int = Field(20, ge=1, le=100, description="Items per page")
|
||||
|
||||
class FilterParams(BaseModel):
|
||||
status: Optional[str] = None
|
||||
created_after: Optional[str] = None
|
||||
search: Optional[str] = None
|
||||
|
||||
class PaginatedResponse(BaseModel):
|
||||
items: List[dict]
|
||||
total: int
|
||||
page: int
|
||||
page_size: int
|
||||
pages: int
|
||||
|
||||
@property
|
||||
def has_next(self) -> bool:
|
||||
return self.page < self.pages
|
||||
|
||||
@property
|
||||
def has_prev(self) -> bool:
|
||||
return self.page > 1
|
||||
|
||||
# FastAPI endpoint example
|
||||
from fastapi import FastAPI, Query, Depends
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@app.get("/api/users", response_model=PaginatedResponse)
|
||||
async def list_users(
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
status: Optional[str] = Query(None),
|
||||
search: Optional[str] = Query(None)
|
||||
):
|
||||
# Apply filters
|
||||
query = build_query(status=status, search=search)
|
||||
|
||||
# Count total
|
||||
total = await count_users(query)
|
||||
|
||||
# Fetch page
|
||||
offset = (page - 1) * page_size
|
||||
users = await fetch_users(query, limit=page_size, offset=offset)
|
||||
|
||||
return PaginatedResponse(
|
||||
items=users,
|
||||
total=total,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
pages=(total + page_size - 1) // page_size
|
||||
)
|
||||
```
|
||||
|
||||
### Pattern 3: Error Handling and Status Codes
|
||||
|
||||
```python
|
||||
from fastapi import HTTPException, status
|
||||
from pydantic import BaseModel
|
||||
|
||||
class ErrorResponse(BaseModel):
|
||||
error: str
|
||||
message: str
|
||||
details: Optional[dict] = None
|
||||
timestamp: str
|
||||
path: str
|
||||
|
||||
class ValidationErrorDetail(BaseModel):
|
||||
field: str
|
||||
message: str
|
||||
value: Any
|
||||
|
||||
# Consistent error responses
|
||||
STATUS_CODES = {
|
||||
"success": 200,
|
||||
"created": 201,
|
||||
"no_content": 204,
|
||||
"bad_request": 400,
|
||||
"unauthorized": 401,
|
||||
"forbidden": 403,
|
||||
"not_found": 404,
|
||||
"conflict": 409,
|
||||
"unprocessable": 422,
|
||||
"internal_error": 500
|
||||
}
|
||||
|
||||
def raise_not_found(resource: str, id: str):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail={
|
||||
"error": "NotFound",
|
||||
"message": f"{resource} not found",
|
||||
"details": {"id": id}
|
||||
}
|
||||
)
|
||||
|
||||
def raise_validation_error(errors: List[ValidationErrorDetail]):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail={
|
||||
"error": "ValidationError",
|
||||
"message": "Request validation failed",
|
||||
"details": {"errors": [e.dict() for e in errors]}
|
||||
}
|
||||
)
|
||||
|
||||
# Example usage
|
||||
@app.get("/api/users/{user_id}")
|
||||
async def get_user(user_id: str):
|
||||
user = await fetch_user(user_id)
|
||||
if not user:
|
||||
raise_not_found("User", user_id)
|
||||
return user
|
||||
```
|
||||
|
||||
### Pattern 4: HATEOAS (Hypermedia as the Engine of Application State)
|
||||
|
||||
```python
|
||||
class UserResponse(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
email: str
|
||||
_links: dict
|
||||
|
||||
@classmethod
|
||||
def from_user(cls, user: User, base_url: str):
|
||||
return cls(
|
||||
id=user.id,
|
||||
name=user.name,
|
||||
email=user.email,
|
||||
_links={
|
||||
"self": {"href": f"{base_url}/api/users/{user.id}"},
|
||||
"orders": {"href": f"{base_url}/api/users/{user.id}/orders"},
|
||||
"update": {
|
||||
"href": f"{base_url}/api/users/{user.id}",
|
||||
"method": "PATCH"
|
||||
},
|
||||
"delete": {
|
||||
"href": f"{base_url}/api/users/{user.id}",
|
||||
"method": "DELETE"
|
||||
}
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
## GraphQL Design Patterns
|
||||
|
||||
### Pattern 1: Schema Design
|
||||
|
||||
```graphql
|
||||
# schema.graphql
|
||||
|
||||
# Clear type definitions
|
||||
type User {
|
||||
id: ID!
|
||||
email: String!
|
||||
name: String!
|
||||
createdAt: DateTime!
|
||||
|
||||
# Relationships
|
||||
orders(first: Int = 20, after: String, status: OrderStatus): OrderConnection!
|
||||
|
||||
profile: UserProfile
|
||||
}
|
||||
|
||||
type Order {
|
||||
id: ID!
|
||||
status: OrderStatus!
|
||||
total: Money!
|
||||
items: [OrderItem!]!
|
||||
createdAt: DateTime!
|
||||
|
||||
# Back-reference
|
||||
user: User!
|
||||
}
|
||||
|
||||
# Pagination pattern (Relay-style)
|
||||
type OrderConnection {
|
||||
edges: [OrderEdge!]!
|
||||
pageInfo: PageInfo!
|
||||
totalCount: Int!
|
||||
}
|
||||
|
||||
type OrderEdge {
|
||||
node: Order!
|
||||
cursor: String!
|
||||
}
|
||||
|
||||
type PageInfo {
|
||||
hasNextPage: Boolean!
|
||||
hasPreviousPage: Boolean!
|
||||
startCursor: String
|
||||
endCursor: String
|
||||
}
|
||||
|
||||
# Enums for type safety
|
||||
enum OrderStatus {
|
||||
PENDING
|
||||
CONFIRMED
|
||||
SHIPPED
|
||||
DELIVERED
|
||||
CANCELLED
|
||||
}
|
||||
|
||||
# Custom scalars
|
||||
scalar DateTime
|
||||
scalar Money
|
||||
|
||||
# Query root
|
||||
type Query {
|
||||
user(id: ID!): User
|
||||
users(first: Int = 20, after: String, search: String): UserConnection!
|
||||
|
||||
order(id: ID!): Order
|
||||
}
|
||||
|
||||
# Mutation root
|
||||
type Mutation {
|
||||
createUser(input: CreateUserInput!): CreateUserPayload!
|
||||
updateUser(input: UpdateUserInput!): UpdateUserPayload!
|
||||
deleteUser(id: ID!): DeleteUserPayload!
|
||||
|
||||
createOrder(input: CreateOrderInput!): CreateOrderPayload!
|
||||
}
|
||||
|
||||
# Input types for mutations
|
||||
input CreateUserInput {
|
||||
email: String!
|
||||
name: String!
|
||||
password: String!
|
||||
}
|
||||
|
||||
# Payload types for mutations
|
||||
type CreateUserPayload {
|
||||
user: User
|
||||
errors: [Error!]
|
||||
}
|
||||
|
||||
type Error {
|
||||
field: String
|
||||
message: String!
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 2: Resolver Design
|
||||
|
||||
```python
|
||||
from typing import Optional, List
|
||||
from ariadne import QueryType, MutationType, ObjectType
|
||||
from dataclasses import dataclass
|
||||
|
||||
query = QueryType()
|
||||
mutation = MutationType()
|
||||
user_type = ObjectType("User")
|
||||
|
||||
@query.field("user")
|
||||
async def resolve_user(obj, info, id: str) -> Optional[dict]:
|
||||
"""Resolve single user by ID."""
|
||||
return await fetch_user_by_id(id)
|
||||
|
||||
@query.field("users")
|
||||
async def resolve_users(
|
||||
obj,
|
||||
info,
|
||||
first: int = 20,
|
||||
after: Optional[str] = None,
|
||||
search: Optional[str] = None
|
||||
) -> dict:
|
||||
"""Resolve paginated user list."""
|
||||
# Decode cursor
|
||||
offset = decode_cursor(after) if after else 0
|
||||
|
||||
# Fetch users
|
||||
users = await fetch_users(
|
||||
limit=first + 1, # Fetch one extra to check hasNextPage
|
||||
offset=offset,
|
||||
search=search
|
||||
)
|
||||
|
||||
# Pagination
|
||||
has_next = len(users) > first
|
||||
if has_next:
|
||||
users = users[:first]
|
||||
|
||||
edges = [
|
||||
{
|
||||
"node": user,
|
||||
"cursor": encode_cursor(offset + i)
|
||||
}
|
||||
for i, user in enumerate(users)
|
||||
]
|
||||
|
||||
return {
|
||||
"edges": edges,
|
||||
"pageInfo": {
|
||||
"hasNextPage": has_next,
|
||||
"hasPreviousPage": offset > 0,
|
||||
"startCursor": edges[0]["cursor"] if edges else None,
|
||||
"endCursor": edges[-1]["cursor"] if edges else None
|
||||
},
|
||||
"totalCount": await count_users(search=search)
|
||||
}
|
||||
|
||||
@user_type.field("orders")
|
||||
async def resolve_user_orders(user: dict, info, first: int = 20) -> dict:
|
||||
"""Resolve user's orders (N+1 prevention with DataLoader)."""
|
||||
# Use DataLoader to batch requests
|
||||
loader = info.context["loaders"]["orders_by_user"]
|
||||
orders = await loader.load(user["id"])
|
||||
|
||||
return paginate_orders(orders, first)
|
||||
|
||||
@mutation.field("createUser")
|
||||
async def resolve_create_user(obj, info, input: dict) -> dict:
|
||||
"""Create new user."""
|
||||
try:
|
||||
# Validate input
|
||||
validate_user_input(input)
|
||||
|
||||
# Create user
|
||||
user = await create_user(
|
||||
email=input["email"],
|
||||
name=input["name"],
|
||||
password=hash_password(input["password"])
|
||||
)
|
||||
|
||||
return {
|
||||
"user": user,
|
||||
"errors": []
|
||||
}
|
||||
except ValidationError as e:
|
||||
return {
|
||||
"user": None,
|
||||
"errors": [{"field": e.field, "message": e.message}]
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 3: DataLoader (N+1 Problem Prevention)
|
||||
|
||||
```python
|
||||
from aiodataloader import DataLoader
|
||||
from typing import List, Optional
|
||||
|
||||
class UserLoader(DataLoader):
|
||||
"""Batch load users by ID."""
|
||||
|
||||
async def batch_load_fn(self, user_ids: List[str]) -> List[Optional[dict]]:
|
||||
"""Load multiple users in single query."""
|
||||
users = await fetch_users_by_ids(user_ids)
|
||||
|
||||
# Map results back to input order
|
||||
user_map = {user["id"]: user for user in users}
|
||||
return [user_map.get(user_id) for user_id in user_ids]
|
||||
|
||||
class OrdersByUserLoader(DataLoader):
|
||||
"""Batch load orders by user ID."""
|
||||
|
||||
async def batch_load_fn(self, user_ids: List[str]) -> List[List[dict]]:
|
||||
"""Load orders for multiple users in single query."""
|
||||
orders = await fetch_orders_by_user_ids(user_ids)
|
||||
|
||||
# Group orders by user_id
|
||||
orders_by_user = {}
|
||||
for order in orders:
|
||||
user_id = order["user_id"]
|
||||
if user_id not in orders_by_user:
|
||||
orders_by_user[user_id] = []
|
||||
orders_by_user[user_id].append(order)
|
||||
|
||||
# Return in input order
|
||||
return [orders_by_user.get(user_id, []) for user_id in user_ids]
|
||||
|
||||
# Context setup
|
||||
def create_context():
|
||||
return {
|
||||
"loaders": {
|
||||
"user": UserLoader(),
|
||||
"orders_by_user": OrdersByUserLoader()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### REST APIs
|
||||
|
||||
1. **Consistent Naming**: Use plural nouns for collections (`/users`, not `/user`)
|
||||
2. **Stateless**: Each request contains all necessary information
|
||||
3. **Use HTTP Status Codes Correctly**: 2xx success, 4xx client errors, 5xx server errors
|
||||
4. **Version Your API**: Plan for breaking changes from day one
|
||||
5. **Pagination**: Always paginate large collections
|
||||
6. **Rate Limiting**: Protect your API with rate limits
|
||||
7. **Documentation**: Use OpenAPI/Swagger for interactive docs
|
||||
|
||||
### GraphQL APIs
|
||||
|
||||
1. **Schema First**: Design schema before writing resolvers
|
||||
2. **Avoid N+1**: Use DataLoaders for efficient data fetching
|
||||
3. **Input Validation**: Validate at schema and resolver levels
|
||||
4. **Error Handling**: Return structured errors in mutation payloads
|
||||
5. **Pagination**: Use cursor-based pagination (Relay spec)
|
||||
6. **Deprecation**: Use `@deprecated` directive for gradual migration
|
||||
7. **Monitoring**: Track query complexity and execution time
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- **Over-fetching/Under-fetching (REST)**: Fixed in GraphQL but requires DataLoaders
|
||||
- **Breaking Changes**: Version APIs or use deprecation strategies
|
||||
- **Inconsistent Error Formats**: Standardize error responses
|
||||
- **Missing Rate Limits**: APIs without limits are vulnerable to abuse
|
||||
- **Poor Documentation**: Undocumented APIs frustrate developers
|
||||
- **Ignoring HTTP Semantics**: POST for idempotent operations breaks expectations
|
||||
- **Tight Coupling**: API structure shouldn't mirror database schema
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
# API Design Checklist
|
||||
|
||||
## Pre-Implementation Review
|
||||
|
||||
### Resource Design
|
||||
|
||||
- [ ] Resources are nouns, not verbs
|
||||
- [ ] Plural names for collections
|
||||
- [ ] Consistent naming across all endpoints
|
||||
- [ ] Clear resource hierarchy (avoid deep nesting >2 levels)
|
||||
- [ ] All CRUD operations properly mapped to HTTP methods
|
||||
|
||||
### HTTP Methods
|
||||
|
||||
- [ ] GET for retrieval (safe, idempotent)
|
||||
- [ ] POST for creation
|
||||
- [ ] PUT for full replacement (idempotent)
|
||||
- [ ] PATCH for partial updates
|
||||
- [ ] DELETE for removal (idempotent)
|
||||
|
||||
### Status Codes
|
||||
|
||||
- [ ] 200 OK for successful GET/PATCH/PUT
|
||||
- [ ] 201 Created for POST
|
||||
- [ ] 204 No Content for DELETE
|
||||
- [ ] 400 Bad Request for malformed requests
|
||||
- [ ] 401 Unauthorized for missing auth
|
||||
- [ ] 403 Forbidden for insufficient permissions
|
||||
- [ ] 404 Not Found for missing resources
|
||||
- [ ] 422 Unprocessable Entity for validation errors
|
||||
- [ ] 429 Too Many Requests for rate limiting
|
||||
- [ ] 500 Internal Server Error for server issues
|
||||
|
||||
### Pagination
|
||||
|
||||
- [ ] All collection endpoints paginated
|
||||
- [ ] Default page size defined (e.g., 20)
|
||||
- [ ] Maximum page size enforced (e.g., 100)
|
||||
- [ ] Pagination metadata included (total, pages, etc.)
|
||||
- [ ] Cursor-based or offset-based pattern chosen
|
||||
|
||||
### Filtering & Sorting
|
||||
|
||||
- [ ] Query parameters for filtering
|
||||
- [ ] Sort parameter supported
|
||||
- [ ] Search parameter for full-text search
|
||||
- [ ] Field selection supported (sparse fieldsets)
|
||||
|
||||
### Versioning
|
||||
|
||||
- [ ] Versioning strategy defined (URL/header/query)
|
||||
- [ ] Version included in all endpoints
|
||||
- [ ] Deprecation policy documented
|
||||
|
||||
### Error Handling
|
||||
|
||||
- [ ] Consistent error response format
|
||||
- [ ] Detailed error messages
|
||||
- [ ] Field-level validation errors
|
||||
- [ ] Error codes for client handling
|
||||
- [ ] Timestamps in error responses
|
||||
|
||||
### Authentication & Authorization
|
||||
|
||||
- [ ] Authentication method defined (Bearer token, API key)
|
||||
- [ ] Authorization checks on all endpoints
|
||||
- [ ] 401 vs 403 used correctly
|
||||
- [ ] Token expiration handled
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
- [ ] Rate limits defined per endpoint/user
|
||||
- [ ] Rate limit headers included
|
||||
- [ ] 429 status code for exceeded limits
|
||||
- [ ] Retry-After header provided
|
||||
|
||||
### Documentation
|
||||
|
||||
- [ ] OpenAPI/Swagger spec generated
|
||||
- [ ] All endpoints documented
|
||||
- [ ] Request/response examples provided
|
||||
- [ ] Error responses documented
|
||||
- [ ] Authentication flow documented
|
||||
|
||||
### Testing
|
||||
|
||||
- [ ] Unit tests for business logic
|
||||
- [ ] Integration tests for endpoints
|
||||
- [ ] Error scenarios tested
|
||||
- [ ] Edge cases covered
|
||||
- [ ] Performance tests for heavy endpoints
|
||||
|
||||
### Security
|
||||
|
||||
- [ ] Input validation on all fields
|
||||
- [ ] SQL injection prevention
|
||||
- [ ] XSS prevention
|
||||
- [ ] CORS configured correctly
|
||||
- [ ] HTTPS enforced
|
||||
- [ ] Sensitive data not in URLs
|
||||
- [ ] No secrets in responses
|
||||
|
||||
### Performance
|
||||
|
||||
- [ ] Database queries optimized
|
||||
- [ ] N+1 queries prevented
|
||||
- [ ] Caching strategy defined
|
||||
- [ ] Cache headers set appropriately
|
||||
- [ ] Large responses paginated
|
||||
|
||||
### Monitoring
|
||||
|
||||
- [ ] Logging implemented
|
||||
- [ ] Error tracking configured
|
||||
- [ ] Performance metrics collected
|
||||
- [ ] Health check endpoint available
|
||||
- [ ] Alerts configured for errors
|
||||
|
||||
## GraphQL-Specific Checks
|
||||
|
||||
### Schema Design
|
||||
|
||||
- [ ] Schema-first approach used
|
||||
- [ ] Types properly defined
|
||||
- [ ] Non-null vs nullable decided
|
||||
- [ ] Interfaces/unions used appropriately
|
||||
- [ ] Custom scalars defined
|
||||
|
||||
### Queries
|
||||
|
||||
- [ ] Query depth limiting
|
||||
- [ ] Query complexity analysis
|
||||
- [ ] DataLoaders prevent N+1
|
||||
- [ ] Pagination pattern chosen (Relay/offset)
|
||||
|
||||
### Mutations
|
||||
|
||||
- [ ] Input types defined
|
||||
- [ ] Payload types with errors
|
||||
- [ ] Optimistic response support
|
||||
- [ ] Idempotency considered
|
||||
|
||||
### Performance
|
||||
|
||||
- [ ] DataLoader for all relationships
|
||||
- [ ] Query batching enabled
|
||||
- [ ] Persisted queries considered
|
||||
- [ ] Response caching implemented
|
||||
|
||||
### Documentation
|
||||
|
||||
- [ ] All fields documented
|
||||
- [ ] Deprecations marked
|
||||
- [ ] Examples provided
|
||||
- [ ] Schema introspection enabled
|
||||
+583
@@ -0,0 +1,583 @@
|
||||
# GraphQL Schema Design Patterns
|
||||
|
||||
## Schema Organization
|
||||
|
||||
### Modular Schema Structure
|
||||
|
||||
```graphql
|
||||
# user.graphql
|
||||
type User {
|
||||
id: ID!
|
||||
email: String!
|
||||
name: String!
|
||||
posts: [Post!]!
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
user(id: ID!): User
|
||||
users(first: Int, after: String): UserConnection!
|
||||
}
|
||||
|
||||
extend type Mutation {
|
||||
createUser(input: CreateUserInput!): CreateUserPayload!
|
||||
}
|
||||
|
||||
# post.graphql
|
||||
type Post {
|
||||
id: ID!
|
||||
title: String!
|
||||
content: String!
|
||||
author: User!
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
post(id: ID!): Post
|
||||
}
|
||||
```
|
||||
|
||||
## Type Design Patterns
|
||||
|
||||
### 1. Non-Null Types
|
||||
|
||||
```graphql
|
||||
type User {
|
||||
id: ID! # Always required
|
||||
email: String! # Required
|
||||
phone: String # Optional (nullable)
|
||||
posts: [Post!]! # Non-null array of non-null posts
|
||||
tags: [String!] # Nullable array of non-null strings
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Interfaces for Polymorphism
|
||||
|
||||
```graphql
|
||||
interface Node {
|
||||
id: ID!
|
||||
createdAt: DateTime!
|
||||
}
|
||||
|
||||
type User implements Node {
|
||||
id: ID!
|
||||
createdAt: DateTime!
|
||||
email: String!
|
||||
}
|
||||
|
||||
type Post implements Node {
|
||||
id: ID!
|
||||
createdAt: DateTime!
|
||||
title: String!
|
||||
}
|
||||
|
||||
type Query {
|
||||
node(id: ID!): Node
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Unions for Heterogeneous Results
|
||||
|
||||
```graphql
|
||||
union SearchResult = User | Post | Comment
|
||||
|
||||
type Query {
|
||||
search(query: String!): [SearchResult!]!
|
||||
}
|
||||
|
||||
# Query example
|
||||
{
|
||||
search(query: "graphql") {
|
||||
... on User {
|
||||
name
|
||||
email
|
||||
}
|
||||
... on Post {
|
||||
title
|
||||
content
|
||||
}
|
||||
... on Comment {
|
||||
text
|
||||
author {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Input Types
|
||||
|
||||
```graphql
|
||||
input CreateUserInput {
|
||||
email: String!
|
||||
name: String!
|
||||
password: String!
|
||||
profileInput: ProfileInput
|
||||
}
|
||||
|
||||
input ProfileInput {
|
||||
bio: String
|
||||
avatar: String
|
||||
website: String
|
||||
}
|
||||
|
||||
input UpdateUserInput {
|
||||
id: ID!
|
||||
email: String
|
||||
name: String
|
||||
profileInput: ProfileInput
|
||||
}
|
||||
```
|
||||
|
||||
## Pagination Patterns
|
||||
|
||||
### Relay Cursor Pagination (Recommended)
|
||||
|
||||
```graphql
|
||||
type UserConnection {
|
||||
edges: [UserEdge!]!
|
||||
pageInfo: PageInfo!
|
||||
totalCount: Int!
|
||||
}
|
||||
|
||||
type UserEdge {
|
||||
node: User!
|
||||
cursor: String!
|
||||
}
|
||||
|
||||
type PageInfo {
|
||||
hasNextPage: Boolean!
|
||||
hasPreviousPage: Boolean!
|
||||
startCursor: String
|
||||
endCursor: String
|
||||
}
|
||||
|
||||
type Query {
|
||||
users(first: Int, after: String, last: Int, before: String): UserConnection!
|
||||
}
|
||||
|
||||
# Usage
|
||||
{
|
||||
users(first: 10, after: "cursor123") {
|
||||
edges {
|
||||
cursor
|
||||
node {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Offset Pagination (Simpler)
|
||||
|
||||
```graphql
|
||||
type UserList {
|
||||
items: [User!]!
|
||||
total: Int!
|
||||
page: Int!
|
||||
pageSize: Int!
|
||||
}
|
||||
|
||||
type Query {
|
||||
users(page: Int = 1, pageSize: Int = 20): UserList!
|
||||
}
|
||||
```
|
||||
|
||||
## Mutation Design Patterns
|
||||
|
||||
### 1. Input/Payload Pattern
|
||||
|
||||
```graphql
|
||||
input CreatePostInput {
|
||||
title: String!
|
||||
content: String!
|
||||
tags: [String!]
|
||||
}
|
||||
|
||||
type CreatePostPayload {
|
||||
post: Post
|
||||
errors: [Error!]
|
||||
success: Boolean!
|
||||
}
|
||||
|
||||
type Error {
|
||||
field: String
|
||||
message: String!
|
||||
code: String!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
createPost(input: CreatePostInput!): CreatePostPayload!
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Optimistic Response Support
|
||||
|
||||
```graphql
|
||||
type UpdateUserPayload {
|
||||
user: User
|
||||
clientMutationId: String
|
||||
errors: [Error!]
|
||||
}
|
||||
|
||||
input UpdateUserInput {
|
||||
id: ID!
|
||||
name: String
|
||||
clientMutationId: String
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
updateUser(input: UpdateUserInput!): UpdateUserPayload!
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Batch Mutations
|
||||
|
||||
```graphql
|
||||
input BatchCreateUserInput {
|
||||
users: [CreateUserInput!]!
|
||||
}
|
||||
|
||||
type BatchCreateUserPayload {
|
||||
results: [CreateUserResult!]!
|
||||
successCount: Int!
|
||||
errorCount: Int!
|
||||
}
|
||||
|
||||
type CreateUserResult {
|
||||
user: User
|
||||
errors: [Error!]
|
||||
index: Int!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
batchCreateUsers(input: BatchCreateUserInput!): BatchCreateUserPayload!
|
||||
}
|
||||
```
|
||||
|
||||
## Field Design
|
||||
|
||||
### Arguments and Filtering
|
||||
|
||||
```graphql
|
||||
type Query {
|
||||
posts(
|
||||
# Pagination
|
||||
first: Int = 20
|
||||
after: String
|
||||
|
||||
# Filtering
|
||||
status: PostStatus
|
||||
authorId: ID
|
||||
tag: String
|
||||
|
||||
# Sorting
|
||||
orderBy: PostOrderBy = CREATED_AT
|
||||
orderDirection: OrderDirection = DESC
|
||||
|
||||
# Searching
|
||||
search: String
|
||||
): PostConnection!
|
||||
}
|
||||
|
||||
enum PostStatus {
|
||||
DRAFT
|
||||
PUBLISHED
|
||||
ARCHIVED
|
||||
}
|
||||
|
||||
enum PostOrderBy {
|
||||
CREATED_AT
|
||||
UPDATED_AT
|
||||
TITLE
|
||||
}
|
||||
|
||||
enum OrderDirection {
|
||||
ASC
|
||||
DESC
|
||||
}
|
||||
```
|
||||
|
||||
### Computed Fields
|
||||
|
||||
```graphql
|
||||
type User {
|
||||
firstName: String!
|
||||
lastName: String!
|
||||
fullName: String! # Computed in resolver
|
||||
posts: [Post!]!
|
||||
postCount: Int! # Computed, doesn't load all posts
|
||||
}
|
||||
|
||||
type Post {
|
||||
likeCount: Int!
|
||||
commentCount: Int!
|
||||
isLikedByViewer: Boolean! # Context-dependent
|
||||
}
|
||||
```
|
||||
|
||||
## Subscriptions
|
||||
|
||||
```graphql
|
||||
type Subscription {
|
||||
postAdded: Post!
|
||||
|
||||
postUpdated(postId: ID!): Post!
|
||||
|
||||
userStatusChanged(userId: ID!): UserStatus!
|
||||
}
|
||||
|
||||
type UserStatus {
|
||||
userId: ID!
|
||||
online: Boolean!
|
||||
lastSeen: DateTime!
|
||||
}
|
||||
|
||||
# Client usage
|
||||
subscription {
|
||||
postAdded {
|
||||
id
|
||||
title
|
||||
author {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Scalars
|
||||
|
||||
```graphql
|
||||
scalar DateTime
|
||||
scalar Email
|
||||
scalar URL
|
||||
scalar JSON
|
||||
scalar Money
|
||||
|
||||
type User {
|
||||
email: Email!
|
||||
website: URL
|
||||
createdAt: DateTime!
|
||||
metadata: JSON
|
||||
}
|
||||
|
||||
type Product {
|
||||
price: Money!
|
||||
}
|
||||
```
|
||||
|
||||
## Directives
|
||||
|
||||
### Built-in Directives
|
||||
|
||||
```graphql
|
||||
type User {
|
||||
name: String!
|
||||
email: String! @deprecated(reason: "Use emails field instead")
|
||||
emails: [String!]!
|
||||
|
||||
# Conditional inclusion
|
||||
privateData: PrivateData @include(if: $isOwner)
|
||||
}
|
||||
|
||||
# Query
|
||||
query GetUser($isOwner: Boolean!) {
|
||||
user(id: "123") {
|
||||
name
|
||||
privateData @include(if: $isOwner) {
|
||||
ssn
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Directives
|
||||
|
||||
```graphql
|
||||
directive @auth(requires: Role = USER) on FIELD_DEFINITION
|
||||
|
||||
enum Role {
|
||||
USER
|
||||
ADMIN
|
||||
MODERATOR
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
deleteUser(id: ID!): Boolean! @auth(requires: ADMIN)
|
||||
updateProfile(input: ProfileInput!): User! @auth
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Union Error Pattern
|
||||
|
||||
```graphql
|
||||
type User {
|
||||
id: ID!
|
||||
email: String!
|
||||
}
|
||||
|
||||
type ValidationError {
|
||||
field: String!
|
||||
message: String!
|
||||
}
|
||||
|
||||
type NotFoundError {
|
||||
message: String!
|
||||
resourceType: String!
|
||||
resourceId: ID!
|
||||
}
|
||||
|
||||
type AuthorizationError {
|
||||
message: String!
|
||||
}
|
||||
|
||||
union UserResult = User | ValidationError | NotFoundError | AuthorizationError
|
||||
|
||||
type Query {
|
||||
user(id: ID!): UserResult!
|
||||
}
|
||||
|
||||
# Usage
|
||||
{
|
||||
user(id: "123") {
|
||||
... on User {
|
||||
id
|
||||
email
|
||||
}
|
||||
... on NotFoundError {
|
||||
message
|
||||
resourceType
|
||||
}
|
||||
... on AuthorizationError {
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Errors in Payload
|
||||
|
||||
```graphql
|
||||
type CreateUserPayload {
|
||||
user: User
|
||||
errors: [Error!]
|
||||
success: Boolean!
|
||||
}
|
||||
|
||||
type Error {
|
||||
field: String
|
||||
message: String!
|
||||
code: ErrorCode!
|
||||
}
|
||||
|
||||
enum ErrorCode {
|
||||
VALIDATION_ERROR
|
||||
UNAUTHORIZED
|
||||
NOT_FOUND
|
||||
INTERNAL_ERROR
|
||||
}
|
||||
```
|
||||
|
||||
## N+1 Query Problem Solutions
|
||||
|
||||
### DataLoader Pattern
|
||||
|
||||
```python
|
||||
from aiodataloader import DataLoader
|
||||
|
||||
class PostLoader(DataLoader):
|
||||
async def batch_load_fn(self, post_ids):
|
||||
posts = await db.posts.find({"id": {"$in": post_ids}})
|
||||
post_map = {post["id"]: post for post in posts}
|
||||
return [post_map.get(pid) for pid in post_ids]
|
||||
|
||||
# Resolver
|
||||
@user_type.field("posts")
|
||||
async def resolve_posts(user, info):
|
||||
loader = info.context["loaders"]["post"]
|
||||
return await loader.load_many(user["post_ids"])
|
||||
```
|
||||
|
||||
### Query Depth Limiting
|
||||
|
||||
```python
|
||||
from graphql import GraphQLError
|
||||
|
||||
def depth_limit_validator(max_depth: int):
|
||||
def validate(context, node, ancestors):
|
||||
depth = len(ancestors)
|
||||
if depth > max_depth:
|
||||
raise GraphQLError(
|
||||
f"Query depth {depth} exceeds maximum {max_depth}"
|
||||
)
|
||||
return validate
|
||||
```
|
||||
|
||||
### Query Complexity Analysis
|
||||
|
||||
```python
|
||||
def complexity_limit_validator(max_complexity: int):
|
||||
def calculate_complexity(node):
|
||||
# Each field = 1, lists multiply
|
||||
complexity = 1
|
||||
if is_list_field(node):
|
||||
complexity *= get_list_size_arg(node)
|
||||
return complexity
|
||||
|
||||
return validate_complexity
|
||||
```
|
||||
|
||||
## Schema Versioning
|
||||
|
||||
### Field Deprecation
|
||||
|
||||
```graphql
|
||||
type User {
|
||||
name: String! @deprecated(reason: "Use firstName and lastName")
|
||||
firstName: String!
|
||||
lastName: String!
|
||||
}
|
||||
```
|
||||
|
||||
### Schema Evolution
|
||||
|
||||
```graphql
|
||||
# v1 - Initial
|
||||
type User {
|
||||
name: String!
|
||||
}
|
||||
|
||||
# v2 - Add optional field (backward compatible)
|
||||
type User {
|
||||
name: String!
|
||||
email: String
|
||||
}
|
||||
|
||||
# v3 - Deprecate and add new field
|
||||
type User {
|
||||
name: String! @deprecated(reason: "Use firstName/lastName")
|
||||
firstName: String!
|
||||
lastName: String!
|
||||
email: String
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices Summary
|
||||
|
||||
1. **Nullable vs Non-Null**: Start nullable, make non-null when guaranteed
|
||||
2. **Input Types**: Always use input types for mutations
|
||||
3. **Payload Pattern**: Return errors in mutation payloads
|
||||
4. **Pagination**: Use cursor-based for infinite scroll, offset for simple cases
|
||||
5. **Naming**: Use camelCase for fields, PascalCase for types
|
||||
6. **Deprecation**: Use `@deprecated` instead of removing fields
|
||||
7. **DataLoaders**: Always use for relationships to prevent N+1
|
||||
8. **Complexity Limits**: Protect against expensive queries
|
||||
9. **Custom Scalars**: Use for domain-specific types (Email, DateTime)
|
||||
10. **Documentation**: Document all fields with descriptions
|
||||
+408
@@ -0,0 +1,408 @@
|
||||
# REST API Best Practices
|
||||
|
||||
## URL Structure
|
||||
|
||||
### Resource Naming
|
||||
|
||||
```
|
||||
# Good - Plural nouns
|
||||
GET /api/users
|
||||
GET /api/orders
|
||||
GET /api/products
|
||||
|
||||
# Bad - Verbs or mixed conventions
|
||||
GET /api/getUser
|
||||
GET /api/user (inconsistent singular)
|
||||
POST /api/createOrder
|
||||
```
|
||||
|
||||
### Nested Resources
|
||||
|
||||
```
|
||||
# Shallow nesting (preferred)
|
||||
GET /api/users/{id}/orders
|
||||
GET /api/orders/{id}
|
||||
|
||||
# Deep nesting (avoid)
|
||||
GET /api/users/{id}/orders/{orderId}/items/{itemId}/reviews
|
||||
# Better:
|
||||
GET /api/order-items/{id}/reviews
|
||||
```
|
||||
|
||||
## HTTP Methods and Status Codes
|
||||
|
||||
### GET - Retrieve Resources
|
||||
|
||||
```
|
||||
GET /api/users → 200 OK (with list)
|
||||
GET /api/users/{id} → 200 OK or 404 Not Found
|
||||
GET /api/users?page=2 → 200 OK (paginated)
|
||||
```
|
||||
|
||||
### POST - Create Resources
|
||||
|
||||
```
|
||||
POST /api/users
|
||||
Body: {"name": "John", "email": "john@example.com"}
|
||||
→ 201 Created
|
||||
Location: /api/users/123
|
||||
Body: {"id": "123", "name": "John", ...}
|
||||
|
||||
POST /api/users (validation error)
|
||||
→ 422 Unprocessable Entity
|
||||
Body: {"errors": [...]}
|
||||
```
|
||||
|
||||
### PUT - Replace Resources
|
||||
|
||||
```
|
||||
PUT /api/users/{id}
|
||||
Body: {complete user object}
|
||||
→ 200 OK (updated)
|
||||
→ 404 Not Found (doesn't exist)
|
||||
|
||||
# Must include ALL fields
|
||||
```
|
||||
|
||||
### PATCH - Partial Update
|
||||
|
||||
```
|
||||
PATCH /api/users/{id}
|
||||
Body: {"name": "Jane"} (only changed fields)
|
||||
→ 200 OK
|
||||
→ 404 Not Found
|
||||
```
|
||||
|
||||
### DELETE - Remove Resources
|
||||
|
||||
```
|
||||
DELETE /api/users/{id}
|
||||
→ 204 No Content (deleted)
|
||||
→ 404 Not Found
|
||||
→ 409 Conflict (can't delete due to references)
|
||||
```
|
||||
|
||||
## Filtering, Sorting, and Searching
|
||||
|
||||
### Query Parameters
|
||||
|
||||
```
|
||||
# Filtering
|
||||
GET /api/users?status=active
|
||||
GET /api/users?role=admin&status=active
|
||||
|
||||
# Sorting
|
||||
GET /api/users?sort=created_at
|
||||
GET /api/users?sort=-created_at (descending)
|
||||
GET /api/users?sort=name,created_at
|
||||
|
||||
# Searching
|
||||
GET /api/users?search=john
|
||||
GET /api/users?q=john
|
||||
|
||||
# Field selection (sparse fieldsets)
|
||||
GET /api/users?fields=id,name,email
|
||||
```
|
||||
|
||||
## Pagination Patterns
|
||||
|
||||
### Offset-Based Pagination
|
||||
|
||||
```python
|
||||
GET /api/users?page=2&page_size=20
|
||||
|
||||
Response:
|
||||
{
|
||||
"items": [...],
|
||||
"page": 2,
|
||||
"page_size": 20,
|
||||
"total": 150,
|
||||
"pages": 8
|
||||
}
|
||||
```
|
||||
|
||||
### Cursor-Based Pagination (for large datasets)
|
||||
|
||||
```python
|
||||
GET /api/users?limit=20&cursor=eyJpZCI6MTIzfQ
|
||||
|
||||
Response:
|
||||
{
|
||||
"items": [...],
|
||||
"next_cursor": "eyJpZCI6MTQzfQ",
|
||||
"has_more": true
|
||||
}
|
||||
```
|
||||
|
||||
### Link Header Pagination (RESTful)
|
||||
|
||||
```
|
||||
GET /api/users?page=2
|
||||
|
||||
Response Headers:
|
||||
Link: <https://api.example.com/users?page=3>; rel="next",
|
||||
<https://api.example.com/users?page=1>; rel="prev",
|
||||
<https://api.example.com/users?page=1>; rel="first",
|
||||
<https://api.example.com/users?page=8>; rel="last"
|
||||
```
|
||||
|
||||
## Versioning Strategies
|
||||
|
||||
### URL Versioning (Recommended)
|
||||
|
||||
```
|
||||
/api/v1/users
|
||||
/api/v2/users
|
||||
|
||||
Pros: Clear, easy to route
|
||||
Cons: Multiple URLs for same resource
|
||||
```
|
||||
|
||||
### Header Versioning
|
||||
|
||||
```
|
||||
GET /api/users
|
||||
Accept: application/vnd.api+json; version=2
|
||||
|
||||
Pros: Clean URLs
|
||||
Cons: Less visible, harder to test
|
||||
```
|
||||
|
||||
### Query Parameter
|
||||
|
||||
```
|
||||
GET /api/users?version=2
|
||||
|
||||
Pros: Easy to test
|
||||
Cons: Optional parameter can be forgotten
|
||||
```
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
### Headers
|
||||
|
||||
```
|
||||
X-RateLimit-Limit: 1000
|
||||
X-RateLimit-Remaining: 742
|
||||
X-RateLimit-Reset: 1640000000
|
||||
|
||||
Response when limited:
|
||||
429 Too Many Requests
|
||||
Retry-After: 3600
|
||||
```
|
||||
|
||||
### Implementation Pattern
|
||||
|
||||
```python
|
||||
from fastapi import HTTPException, Request
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
class RateLimiter:
|
||||
def __init__(self, calls: int, period: int):
|
||||
self.calls = calls
|
||||
self.period = period
|
||||
self.cache = {}
|
||||
|
||||
def check(self, key: str) -> bool:
|
||||
now = datetime.now()
|
||||
if key not in self.cache:
|
||||
self.cache[key] = []
|
||||
|
||||
# Remove old requests
|
||||
self.cache[key] = [
|
||||
ts for ts in self.cache[key]
|
||||
if now - ts < timedelta(seconds=self.period)
|
||||
]
|
||||
|
||||
if len(self.cache[key]) >= self.calls:
|
||||
return False
|
||||
|
||||
self.cache[key].append(now)
|
||||
return True
|
||||
|
||||
limiter = RateLimiter(calls=100, period=60)
|
||||
|
||||
@app.get("/api/users")
|
||||
async def get_users(request: Request):
|
||||
if not limiter.check(request.client.host):
|
||||
raise HTTPException(
|
||||
status_code=429,
|
||||
headers={"Retry-After": "60"}
|
||||
)
|
||||
return {"users": [...]}
|
||||
```
|
||||
|
||||
## Authentication and Authorization
|
||||
|
||||
### Bearer Token
|
||||
|
||||
```
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
|
||||
|
||||
401 Unauthorized - Missing/invalid token
|
||||
403 Forbidden - Valid token, insufficient permissions
|
||||
```
|
||||
|
||||
### API Keys
|
||||
|
||||
```
|
||||
X-API-Key: your-api-key-here
|
||||
```
|
||||
|
||||
## Error Response Format
|
||||
|
||||
### Consistent Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "VALIDATION_ERROR",
|
||||
"message": "Request validation failed",
|
||||
"details": [
|
||||
{
|
||||
"field": "email",
|
||||
"message": "Invalid email format",
|
||||
"value": "not-an-email"
|
||||
}
|
||||
],
|
||||
"timestamp": "2025-10-16T12:00:00Z",
|
||||
"path": "/api/users"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Status Code Guidelines
|
||||
|
||||
- `200 OK`: Successful GET, PATCH, PUT
|
||||
- `201 Created`: Successful POST
|
||||
- `204 No Content`: Successful DELETE
|
||||
- `400 Bad Request`: Malformed request
|
||||
- `401 Unauthorized`: Authentication required
|
||||
- `403 Forbidden`: Authenticated but not authorized
|
||||
- `404 Not Found`: Resource doesn't exist
|
||||
- `409 Conflict`: State conflict (duplicate email, etc.)
|
||||
- `422 Unprocessable Entity`: Validation errors
|
||||
- `429 Too Many Requests`: Rate limited
|
||||
- `500 Internal Server Error`: Server error
|
||||
- `503 Service Unavailable`: Temporary downtime
|
||||
|
||||
## Caching
|
||||
|
||||
### Cache Headers
|
||||
|
||||
```
|
||||
# Client caching
|
||||
Cache-Control: public, max-age=3600
|
||||
|
||||
# No caching
|
||||
Cache-Control: no-cache, no-store, must-revalidate
|
||||
|
||||
# Conditional requests
|
||||
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
|
||||
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"
|
||||
→ 304 Not Modified
|
||||
```
|
||||
|
||||
## Bulk Operations
|
||||
|
||||
### Batch Endpoints
|
||||
|
||||
```python
|
||||
POST /api/users/batch
|
||||
{
|
||||
"items": [
|
||||
{"name": "User1", "email": "user1@example.com"},
|
||||
{"name": "User2", "email": "user2@example.com"}
|
||||
]
|
||||
}
|
||||
|
||||
Response:
|
||||
{
|
||||
"results": [
|
||||
{"id": "1", "status": "created"},
|
||||
{"id": null, "status": "failed", "error": "Email already exists"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Idempotency
|
||||
|
||||
### Idempotency Keys
|
||||
|
||||
```
|
||||
POST /api/orders
|
||||
Idempotency-Key: unique-key-123
|
||||
|
||||
If duplicate request:
|
||||
→ 200 OK (return cached response)
|
||||
```
|
||||
|
||||
## CORS Configuration
|
||||
|
||||
```python
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["https://example.com"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
```
|
||||
|
||||
## Documentation with OpenAPI
|
||||
|
||||
```python
|
||||
from fastapi import FastAPI
|
||||
|
||||
app = FastAPI(
|
||||
title="My API",
|
||||
description="API for managing users",
|
||||
version="1.0.0",
|
||||
docs_url="/docs",
|
||||
redoc_url="/redoc"
|
||||
)
|
||||
|
||||
@app.get(
|
||||
"/api/users/{user_id}",
|
||||
summary="Get user by ID",
|
||||
response_description="User details",
|
||||
tags=["Users"]
|
||||
)
|
||||
async def get_user(
|
||||
user_id: str = Path(..., description="The user ID")
|
||||
):
|
||||
"""
|
||||
Retrieve user by ID.
|
||||
|
||||
Returns full user profile including:
|
||||
- Basic information
|
||||
- Contact details
|
||||
- Account status
|
||||
"""
|
||||
pass
|
||||
```
|
||||
|
||||
## Health and Monitoring Endpoints
|
||||
|
||||
```python
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
return {
|
||||
"status": "healthy",
|
||||
"version": "1.0.0",
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
@app.get("/health/detailed")
|
||||
async def detailed_health():
|
||||
return {
|
||||
"status": "healthy",
|
||||
"checks": {
|
||||
"database": await check_database(),
|
||||
"redis": await check_redis(),
|
||||
"external_api": await check_external_api()
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,113 @@
|
||||
---
|
||||
name: clean-architecture
|
||||
description: Clean Architecture principles and best practices from Robert C. Martin's book. This skill should be used when designing software systems, reviewing code structure, or refactoring applications to achieve better separation of concerns. Triggers on tasks involving layers, boundaries, dependency direction, entities, use cases, or system architecture.
|
||||
---
|
||||
|
||||
# Clean Architecture Best Practices
|
||||
|
||||
Comprehensive guide to Clean Architecture principles for designing maintainable, testable software systems. Based on Robert C. Martin's "Clean Architecture: A Craftsman's Guide to Software Structure and Design." Contains 42 rules across 8 categories, prioritized by architectural impact.
|
||||
|
||||
## When to Apply
|
||||
|
||||
Reference these guidelines when:
|
||||
- Designing new software systems or modules
|
||||
- Structuring dependencies between layers
|
||||
- Defining boundaries between business logic and infrastructure
|
||||
- Reviewing code for architectural violations
|
||||
- Refactoring coupled systems toward cleaner structure
|
||||
|
||||
## Rule Categories by Priority
|
||||
|
||||
| Priority | Category | Impact | Prefix |
|
||||
|----------|----------|--------|--------|
|
||||
| 1 | Dependency Direction | CRITICAL | `dep-` |
|
||||
| 2 | Entity Design | CRITICAL | `entity-` |
|
||||
| 3 | Use Case Isolation | HIGH | `usecase-` |
|
||||
| 4 | Component Cohesion | HIGH | `comp-` |
|
||||
| 5 | Boundary Definition | MEDIUM-HIGH | `bound-` |
|
||||
| 6 | Interface Adapters | MEDIUM | `adapt-` |
|
||||
| 7 | Framework Isolation | MEDIUM | `frame-` |
|
||||
| 8 | Testing Architecture | LOW-MEDIUM | `test-` |
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### 1. Dependency Direction (CRITICAL)
|
||||
|
||||
- [`dep-inward-only`](references/dep-inward-only.md) - Source dependencies point inward only
|
||||
- [`dep-interface-ownership`](references/dep-interface-ownership.md) - Interfaces belong to clients not implementers
|
||||
- [`dep-no-framework-imports`](references/dep-no-framework-imports.md) - Avoid framework imports in inner layers
|
||||
- [`dep-data-crossing-boundaries`](references/dep-data-crossing-boundaries.md) - Use simple data structures across boundaries
|
||||
- [`dep-acyclic-dependencies`](references/dep-acyclic-dependencies.md) - Eliminate cyclic dependencies between components
|
||||
- [`dep-stable-abstractions`](references/dep-stable-abstractions.md) - Depend on stable abstractions not volatile concretions
|
||||
|
||||
### 2. Entity Design (CRITICAL)
|
||||
|
||||
- [`entity-pure-business-rules`](references/entity-pure-business-rules.md) - Entities contain only enterprise business rules
|
||||
- [`entity-no-persistence-awareness`](references/entity-no-persistence-awareness.md) - Entities must not know how they are persisted
|
||||
- [`entity-encapsulate-invariants`](references/entity-encapsulate-invariants.md) - Encapsulate business invariants within entities
|
||||
- [`entity-value-objects`](references/entity-value-objects.md) - Use value objects for domain concepts
|
||||
- [`entity-rich-not-anemic`](references/entity-rich-not-anemic.md) - Build rich domain models not anemic data structures
|
||||
|
||||
### 3. Use Case Isolation (HIGH)
|
||||
|
||||
- [`usecase-single-responsibility`](references/usecase-single-responsibility.md) - Each use case has one reason to change
|
||||
- [`usecase-input-output-ports`](references/usecase-input-output-ports.md) - Define input and output ports for use cases
|
||||
- [`usecase-orchestrates-not-implements`](references/usecase-orchestrates-not-implements.md) - Use cases orchestrate entities not implement business rules
|
||||
- [`usecase-no-presentation-logic`](references/usecase-no-presentation-logic.md) - Use cases must not contain presentation logic
|
||||
- [`usecase-explicit-dependencies`](references/usecase-explicit-dependencies.md) - Declare all dependencies explicitly in constructor
|
||||
- [`usecase-transaction-boundary`](references/usecase-transaction-boundary.md) - Use case defines the transaction boundary
|
||||
|
||||
### 4. Component Cohesion (HIGH)
|
||||
|
||||
- [`comp-screaming-architecture`](references/comp-screaming-architecture.md) - Structure should scream the domain not the framework
|
||||
- [`comp-common-closure`](references/comp-common-closure.md) - Group classes that change together
|
||||
- [`comp-common-reuse`](references/comp-common-reuse.md) - Avoid forcing clients to depend on unused code
|
||||
- [`comp-reuse-release-equivalence`](references/comp-reuse-release-equivalence.md) - Release components as cohesive units
|
||||
- [`comp-stable-dependencies`](references/comp-stable-dependencies.md) - Depend in the direction of stability
|
||||
|
||||
### 5. Boundary Definition (MEDIUM-HIGH)
|
||||
|
||||
- [`bound-humble-object`](references/bound-humble-object.md) - Use humble objects at architectural boundaries
|
||||
- [`bound-partial-boundaries`](references/bound-partial-boundaries.md) - Use partial boundaries when full separation is premature
|
||||
- [`bound-boundary-cost-awareness`](references/bound-boundary-cost-awareness.md) - Weigh boundary cost against ignorance cost
|
||||
- [`bound-main-component`](references/bound-main-component.md) - Treat main as a plugin to the application
|
||||
- [`bound-defer-decisions`](references/bound-defer-decisions.md) - Defer framework and database decisions
|
||||
- [`bound-service-internal-architecture`](references/bound-service-internal-architecture.md) - Services must have internal clean architecture
|
||||
|
||||
### 6. Interface Adapters (MEDIUM)
|
||||
|
||||
- [`adapt-controller-thin`](references/adapt-controller-thin.md) - Keep controllers thin
|
||||
- [`adapt-presenter-formats`](references/adapt-presenter-formats.md) - Presenters format data for the view
|
||||
- [`adapt-gateway-abstraction`](references/adapt-gateway-abstraction.md) - Gateways hide external system details
|
||||
- [`adapt-mapper-translation`](references/adapt-mapper-translation.md) - Use mappers to translate between layers
|
||||
- [`adapt-anti-corruption-layer`](references/adapt-anti-corruption-layer.md) - Build anti-corruption layers for external systems
|
||||
|
||||
### 7. Framework Isolation (MEDIUM)
|
||||
|
||||
- [`frame-domain-purity`](references/frame-domain-purity.md) - Domain layer has zero framework dependencies
|
||||
- [`frame-orm-in-infrastructure`](references/frame-orm-in-infrastructure.md) - Keep ORM usage in infrastructure layer
|
||||
- [`frame-web-in-infrastructure`](references/frame-web-in-infrastructure.md) - Web framework concerns stay in interface layer
|
||||
- [`frame-di-container-edge`](references/frame-di-container-edge.md) - Dependency injection containers live at the edge
|
||||
- [`frame-logging-abstraction`](references/frame-logging-abstraction.md) - Abstract logging behind domain interfaces
|
||||
|
||||
### 8. Testing Architecture (LOW-MEDIUM)
|
||||
|
||||
- [`test-tests-are-architecture`](references/test-tests-are-architecture.md) - Tests are part of the system architecture
|
||||
- [`test-testable-design`](references/test-testable-design.md) - Design for testability from the start
|
||||
- [`test-layer-isolation`](references/test-layer-isolation.md) - Test each layer in isolation
|
||||
- [`test-boundary-verification`](references/test-boundary-verification.md) - Verify architectural boundaries with tests
|
||||
|
||||
## How to Use
|
||||
|
||||
Read individual reference files for detailed explanations and code examples:
|
||||
|
||||
- [Section definitions](references/_sections.md) - Category structure and impact levels
|
||||
- [Rule template](assets/templates/_template.md) - Template for adding new rules
|
||||
|
||||
## Reference Files
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| [references/_sections.md](references/_sections.md) | Category definitions and ordering |
|
||||
| [assets/templates/_template.md](assets/templates/_template.md) | Template for new rules |
|
||||
| [metadata.json](metadata.json) | Version and reference information |
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
title: Rule Title in Imperative Form
|
||||
impact: CRITICAL|HIGH|MEDIUM-HIGH|MEDIUM|LOW-MEDIUM|LOW
|
||||
impactDescription: quantified impact, e.g., "prevents cascade failures", "2-10× testability improvement"
|
||||
tags: prefix, technique1, technique2, concept
|
||||
---
|
||||
|
||||
## Rule Title in Imperative Form
|
||||
|
||||
Brief explanation (1-3 sentences) of WHY this matters for software architecture. Focus on the architectural implications and cascade effects.
|
||||
|
||||
**Incorrect (description of problem/cost):**
|
||||
|
||||
```language
|
||||
// Code example showing the anti-pattern
|
||||
// Include comments on problematic lines explaining the cost
|
||||
```
|
||||
|
||||
**Correct (description of benefit/solution):**
|
||||
|
||||
```language
|
||||
// Code example showing the correct pattern
|
||||
// Minimal diff from incorrect - same variable names, structure
|
||||
```
|
||||
|
||||
**Alternative (when applicable):**
|
||||
|
||||
```language
|
||||
// Alternative approach for specific contexts
|
||||
```
|
||||
|
||||
**When NOT to use this pattern:**
|
||||
- Exception or edge case 1
|
||||
- Exception or edge case 2
|
||||
|
||||
**Benefits:**
|
||||
- Concrete benefit 1
|
||||
- Concrete benefit 2
|
||||
|
||||
Reference: [Source Title](URL)
|
||||
@@ -0,0 +1,46 @@
|
||||
# Sections
|
||||
|
||||
This file defines all sections, their ordering, impact levels, and descriptions.
|
||||
The section ID (in parentheses) is the filename prefix used to group rules.
|
||||
|
||||
---
|
||||
|
||||
## 1. Dependency Direction (dep)
|
||||
|
||||
**Impact:** CRITICAL
|
||||
**Description:** The Dependency Rule is the architectural foundation. Source code dependencies must point inward only; violations cascade failures across all layers.
|
||||
|
||||
## 2. Entity Design (entity)
|
||||
|
||||
**Impact:** CRITICAL
|
||||
**Description:** Enterprise business rules must be framework-agnostic, stable, and completely independent of databases, UI, and external systems.
|
||||
|
||||
## 3. Use Case Isolation (usecase)
|
||||
|
||||
**Impact:** HIGH
|
||||
**Description:** Application-specific business rules orchestrate entities without leaking infrastructure details or depending on presentation concerns.
|
||||
|
||||
## 4. Component Cohesion (comp)
|
||||
|
||||
**Impact:** HIGH
|
||||
**Description:** Components grouped by business capability enable independent deployment, parallel team development, and controlled change propagation.
|
||||
|
||||
## 5. Boundary Definition (bound)
|
||||
|
||||
**Impact:** MEDIUM-HIGH
|
||||
**Description:** Architectural boundaries isolate volatile from stable elements; Humble Objects maximize testability by separating hard-to-test from easy-to-test code.
|
||||
|
||||
## 6. Interface Adapters (adapt)
|
||||
|
||||
**Impact:** MEDIUM
|
||||
**Description:** Controllers, presenters, and gateways translate between use cases and external systems without leaking implementation details.
|
||||
|
||||
## 7. Framework Isolation (frame)
|
||||
|
||||
**Impact:** MEDIUM
|
||||
**Description:** Frameworks are details, not architecture. Business logic must never import, reference, or depend on framework-specific types.
|
||||
|
||||
## 8. Testing Architecture (test)
|
||||
|
||||
**Impact:** LOW-MEDIUM
|
||||
**Description:** Tests are architectural components. Layer isolation enables fast unit tests, while boundaries enable targeted integration tests.
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
---
|
||||
title: Build Anti-Corruption Layers for External Systems
|
||||
impact: MEDIUM
|
||||
impactDescription: protects domain from external model pollution
|
||||
tags: adapt, anti-corruption, external, isolation
|
||||
---
|
||||
|
||||
## Build Anti-Corruption Layers for External Systems
|
||||
|
||||
When integrating with external systems that have different models, build an anti-corruption layer (ACL) that translates between their model and yours. Never let external concepts pollute your domain.
|
||||
|
||||
**Incorrect (external model leaks into domain):**
|
||||
|
||||
```java
|
||||
// Domain polluted with Stripe's model
|
||||
public class Order {
|
||||
private String id;
|
||||
private List<OrderItem> items;
|
||||
// Stripe-specific fields in domain
|
||||
private String stripeCustomerId;
|
||||
private String stripePaymentIntentId;
|
||||
private String stripePaymentMethodId;
|
||||
|
||||
public void processPayment(Stripe stripe) {
|
||||
// Domain directly calls Stripe
|
||||
PaymentIntent intent = PaymentIntent.create(
|
||||
PaymentIntentCreateParams.builder()
|
||||
.setCustomer(this.stripeCustomerId)
|
||||
.setPaymentMethod(this.stripePaymentMethodId)
|
||||
.setAmount(this.calculateTotal().cents())
|
||||
.setCurrency("usd")
|
||||
.build()
|
||||
);
|
||||
this.stripePaymentIntentId = intent.getId();
|
||||
}
|
||||
}
|
||||
|
||||
// If we switch to PayPal, Order must change
|
||||
```
|
||||
|
||||
**Correct (anti-corruption layer isolates external model):**
|
||||
|
||||
```java
|
||||
// domain/Order.java - Pure domain, no external concepts
|
||||
public class Order {
|
||||
private OrderId id;
|
||||
private CustomerId customerId;
|
||||
private List<OrderItem> items;
|
||||
private PaymentStatus paymentStatus;
|
||||
|
||||
public Money calculateTotal() {
|
||||
return items.stream()
|
||||
.map(OrderItem::lineTotal)
|
||||
.reduce(Money.zero(), Money::add);
|
||||
}
|
||||
|
||||
public void markPaid(PaymentReference reference) {
|
||||
this.paymentStatus = PaymentStatus.paid(reference);
|
||||
}
|
||||
}
|
||||
|
||||
// domain/ports/PaymentProcessor.java - Domain-defined port
|
||||
public interface PaymentProcessor {
|
||||
PaymentResult charge(CustomerId customer, Money amount);
|
||||
}
|
||||
|
||||
// Domain-defined value objects
|
||||
public record PaymentResult(PaymentReference reference, PaymentStatus status) {}
|
||||
public record PaymentReference(String value) {}
|
||||
|
||||
// infrastructure/stripe/StripePaymentProcessor.java - ACL
|
||||
public class StripePaymentProcessor implements PaymentProcessor {
|
||||
private final CustomerMappingRepository customerMapping;
|
||||
|
||||
public PaymentResult charge(CustomerId customerId, Money amount) {
|
||||
// Translate domain CustomerId to Stripe customer
|
||||
String stripeCustomerId = customerMapping.getStripeId(customerId);
|
||||
|
||||
// Call Stripe with their model
|
||||
PaymentIntent intent = PaymentIntent.create(
|
||||
PaymentIntentCreateParams.builder()
|
||||
.setCustomer(stripeCustomerId)
|
||||
.setAmount(amount.cents())
|
||||
.setCurrency("usd")
|
||||
.setConfirm(true)
|
||||
.build()
|
||||
);
|
||||
|
||||
// Translate Stripe response back to domain model
|
||||
return new PaymentResult(
|
||||
new PaymentReference(intent.getId()),
|
||||
translateStatus(intent.getStatus())
|
||||
);
|
||||
}
|
||||
|
||||
private PaymentStatus translateStatus(String stripeStatus) {
|
||||
return switch (stripeStatus) {
|
||||
case "succeeded" -> PaymentStatus.COMPLETED;
|
||||
case "processing" -> PaymentStatus.PENDING;
|
||||
case "requires_action" -> PaymentStatus.REQUIRES_ACTION;
|
||||
default -> PaymentStatus.FAILED;
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Domain model remains clean and business-focused
|
||||
- Switch payment providers by implementing new ACL
|
||||
- External API changes isolated to ACL
|
||||
|
||||
Reference: [Anti-Corruption Layer Pattern](https://docs.microsoft.com/en-us/azure/architecture/patterns/anti-corruption-layer)
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
---
|
||||
title: Keep Controllers Thin
|
||||
impact: MEDIUM
|
||||
impactDescription: simplifies testing, isolates HTTP concerns
|
||||
tags: adapt, controller, thin, http
|
||||
---
|
||||
|
||||
## Keep Controllers Thin
|
||||
|
||||
Controllers should only handle HTTP concerns: parsing requests, validating input format, calling use cases, and formatting responses. No business logic.
|
||||
|
||||
**Incorrect (fat controller with business logic):**
|
||||
|
||||
```python
|
||||
class OrderController:
|
||||
@app.route('/orders', methods=['POST'])
|
||||
def create_order(self):
|
||||
data = request.json
|
||||
|
||||
# Input validation - OK in controller
|
||||
if not data.get('items'):
|
||||
return jsonify({'error': 'Items required'}), 400
|
||||
|
||||
# Business logic - WRONG in controller
|
||||
customer = db.query(Customer).get(data['customer_id'])
|
||||
if not customer.is_active:
|
||||
return jsonify({'error': 'Inactive customer'}), 400
|
||||
|
||||
total = 0
|
||||
for item in data['items']:
|
||||
product = db.query(Product).get(item['product_id'])
|
||||
if product.stock < item['quantity']:
|
||||
return jsonify({'error': f'{product.name} out of stock'}), 400
|
||||
total += product.price * item['quantity']
|
||||
|
||||
# More business logic
|
||||
if total > customer.credit_limit:
|
||||
return jsonify({'error': 'Exceeds credit limit'}), 400
|
||||
|
||||
order = Order(
|
||||
customer_id=customer.id,
|
||||
items=data['items'],
|
||||
total=total
|
||||
)
|
||||
db.session.add(order)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'order_id': order.id}), 201
|
||||
```
|
||||
|
||||
**Correct (thin controller delegates to use case):**
|
||||
|
||||
```python
|
||||
class OrderController:
|
||||
def __init__(self, create_order_use_case: CreateOrderUseCase):
|
||||
self.create_order = create_order_use_case
|
||||
|
||||
@app.route('/orders', methods=['POST'])
|
||||
def create(self):
|
||||
# Parse HTTP request
|
||||
data = request.json
|
||||
|
||||
# Validate request format (not business rules)
|
||||
if not data.get('items'):
|
||||
return jsonify({'error': 'Items required'}), 400
|
||||
|
||||
# Build command
|
||||
command = CreateOrderCommand(
|
||||
customer_id=data['customer_id'],
|
||||
items=[
|
||||
OrderItemCommand(p['product_id'], p['quantity'])
|
||||
for p in data['items']
|
||||
]
|
||||
)
|
||||
|
||||
# Delegate to use case
|
||||
try:
|
||||
result = self.create_order.execute(command)
|
||||
return jsonify({'order_id': result.order_id}), 201
|
||||
|
||||
except CustomerInactiveError:
|
||||
return jsonify({'error': 'Customer account inactive'}), 400
|
||||
except InsufficientStockError as e:
|
||||
return jsonify({'error': f'{e.product_name} out of stock'}), 400
|
||||
except CreditLimitExceededError:
|
||||
return jsonify({'error': 'Order exceeds credit limit'}), 400
|
||||
```
|
||||
|
||||
**Controller responsibilities:**
|
||||
- Parse HTTP request to command/query objects
|
||||
- Validate request format (required fields present)
|
||||
- Call appropriate use case
|
||||
- Map exceptions to HTTP status codes
|
||||
- Format response
|
||||
|
||||
**Use case responsibilities:**
|
||||
- Business validation (credit limits, stock)
|
||||
- Business logic (calculations, state changes)
|
||||
- Orchestrate entities and repositories
|
||||
|
||||
Reference: [Clean Architecture - Controllers](https://www.oreilly.com/library/view/clean-architecture-a/9780134494272/ch22.xhtml)
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
---
|
||||
title: Gateways Hide External System Details
|
||||
impact: MEDIUM
|
||||
impactDescription: enables vendor switching, simplifies testing
|
||||
tags: adapt, gateway, external, abstraction
|
||||
---
|
||||
|
||||
## Gateways Hide External System Details
|
||||
|
||||
Database gateways, API gateways, and service gateways are polymorphic interfaces that hide external system details. The use case layer talks to abstractions; infrastructure implements them.
|
||||
|
||||
**Incorrect (use case knows external system details):**
|
||||
|
||||
```go
|
||||
func (uc *ProcessRefundUseCase) Execute(orderId string) error {
|
||||
order := uc.db.QueryRow("SELECT * FROM orders WHERE id = $1", orderId)
|
||||
|
||||
// Direct Stripe API knowledge in use case
|
||||
stripe.Key = os.Getenv("STRIPE_KEY")
|
||||
params := &stripe.RefundParams{
|
||||
PaymentIntent: stripe.String(order.PaymentIntentId),
|
||||
Amount: stripe.Int64(order.Total),
|
||||
}
|
||||
_, err := refund.New(params)
|
||||
if err != nil {
|
||||
// Stripe-specific error handling
|
||||
if stripeErr, ok := err.(*stripe.Error); ok {
|
||||
if stripeErr.Code == stripe.ErrorCodeChargeAlreadyRefunded {
|
||||
return ErrAlreadyRefunded
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
uc.db.Exec("UPDATE orders SET status = 'refunded' WHERE id = $1", orderId)
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (gateway abstracts external system):**
|
||||
|
||||
```go
|
||||
// application/ports/PaymentGateway.go
|
||||
type PaymentGateway interface {
|
||||
Refund(paymentId string, amount Money) (RefundResult, error)
|
||||
}
|
||||
|
||||
type RefundResult struct {
|
||||
RefundId string
|
||||
Status RefundStatus
|
||||
}
|
||||
|
||||
// application/ports/OrderRepository.go
|
||||
type OrderRepository interface {
|
||||
FindById(id OrderId) (*Order, error)
|
||||
Save(order *Order) error
|
||||
}
|
||||
|
||||
// application/usecases/ProcessRefundUseCase.go
|
||||
type ProcessRefundUseCase struct {
|
||||
orders OrderRepository
|
||||
payments PaymentGateway
|
||||
}
|
||||
|
||||
func (uc *ProcessRefundUseCase) Execute(orderId string) error {
|
||||
order, err := uc.orders.FindById(OrderId(orderId))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result, err := uc.payments.Refund(order.PaymentId, order.Total)
|
||||
if err != nil {
|
||||
return err // Gateway translates Stripe errors to domain errors
|
||||
}
|
||||
|
||||
order.MarkRefunded(result.RefundId)
|
||||
return uc.orders.Save(order)
|
||||
}
|
||||
|
||||
// infrastructure/StripePaymentGateway.go
|
||||
type StripePaymentGateway struct {
|
||||
client *stripe.Client
|
||||
}
|
||||
|
||||
func (g *StripePaymentGateway) Refund(paymentId string, amount Money) (RefundResult, error) {
|
||||
params := &stripe.RefundParams{
|
||||
PaymentIntent: stripe.String(paymentId),
|
||||
Amount: stripe.Int64(amount.Cents()),
|
||||
}
|
||||
|
||||
refund, err := g.client.Refunds.New(params)
|
||||
if err != nil {
|
||||
return RefundResult{}, g.translateError(err)
|
||||
}
|
||||
|
||||
return RefundResult{
|
||||
RefundId: refund.ID,
|
||||
Status: g.translateStatus(refund.Status),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (g *StripePaymentGateway) translateError(err error) error {
|
||||
// Convert Stripe-specific errors to domain errors
|
||||
if stripeErr, ok := err.(*stripe.Error); ok {
|
||||
switch stripeErr.Code {
|
||||
case stripe.ErrorCodeChargeAlreadyRefunded:
|
||||
return ErrAlreadyRefunded
|
||||
}
|
||||
}
|
||||
return ErrPaymentFailed
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Switch from Stripe to Adyen without touching use cases
|
||||
- Test use cases with mock gateways
|
||||
- External API changes isolated to gateway implementation
|
||||
|
||||
Reference: [Clean Architecture - Database Gateways](https://www.oreilly.com/library/view/clean-architecture-a/9780134494272/ch22.xhtml)
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
---
|
||||
title: Use Mappers to Translate Between Layers
|
||||
impact: MEDIUM
|
||||
impactDescription: decouples domain from persistence and presentation models
|
||||
tags: adapt, mapper, translation, layers
|
||||
---
|
||||
|
||||
## Use Mappers to Translate Between Layers
|
||||
|
||||
Mappers translate between domain entities and external representations (database rows, API responses, DTOs). Each layer has its own model; mappers bridge them.
|
||||
|
||||
**Incorrect (domain entity used everywhere):**
|
||||
|
||||
```typescript
|
||||
// Domain entity directly serialized to JSON and stored in DB
|
||||
class User {
|
||||
id: string
|
||||
email: string
|
||||
passwordHash: string // Exposed in API response!
|
||||
createdAt: Date
|
||||
lastLogin: Date
|
||||
preferences: UserPreferences
|
||||
roles: Role[]
|
||||
|
||||
toJSON() {
|
||||
return { ...this } // Leaks everything
|
||||
}
|
||||
}
|
||||
|
||||
// Controller returns entity directly
|
||||
app.get('/users/:id', (req, res) => {
|
||||
const user = userRepo.findById(req.params.id)
|
||||
res.json(user) // passwordHash in response!
|
||||
})
|
||||
|
||||
// ORM couples domain to database schema
|
||||
@Entity()
|
||||
class User {
|
||||
@PrimaryColumn() id: string
|
||||
@Column() email: string
|
||||
// Domain entity is now a database entity
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (dedicated models per layer with mappers):**
|
||||
|
||||
```typescript
|
||||
// domain/User.ts - Pure domain entity
|
||||
class User {
|
||||
constructor(
|
||||
readonly id: UserId,
|
||||
private email: Email,
|
||||
private passwordHash: PasswordHash,
|
||||
private roles: Set<Role>
|
||||
) {}
|
||||
|
||||
hasRole(role: Role): boolean {
|
||||
return this.roles.has(role)
|
||||
}
|
||||
}
|
||||
|
||||
// infrastructure/persistence/UserEntity.ts - Database model
|
||||
interface UserRow {
|
||||
id: string
|
||||
email: string
|
||||
password_hash: string
|
||||
roles: string // JSON array in DB
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
// infrastructure/persistence/UserMapper.ts
|
||||
class UserMapper {
|
||||
toDomain(row: UserRow): User {
|
||||
return new User(
|
||||
new UserId(row.id),
|
||||
Email.create(row.email),
|
||||
new PasswordHash(row.password_hash),
|
||||
new Set(JSON.parse(row.roles))
|
||||
)
|
||||
}
|
||||
|
||||
toPersistence(user: User): UserRow {
|
||||
return {
|
||||
id: user.id.value,
|
||||
email: user.email.value,
|
||||
password_hash: user.passwordHash.value,
|
||||
roles: JSON.stringify([...user.roles]),
|
||||
updated_at: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// interface/dto/UserResponse.ts - API response model
|
||||
interface UserResponse {
|
||||
id: string
|
||||
email: string
|
||||
roles: string[]
|
||||
// No passwordHash!
|
||||
}
|
||||
|
||||
// interface/mappers/UserResponseMapper.ts
|
||||
class UserResponseMapper {
|
||||
toResponse(user: User): UserResponse {
|
||||
return {
|
||||
id: user.id.value,
|
||||
email: user.email.value,
|
||||
roles: [...user.roles]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Database schema changes don't affect domain
|
||||
- API can evolve independently of domain model
|
||||
- Sensitive data never accidentally exposed
|
||||
|
||||
Reference: [Data Mapper Pattern](https://martinfowler.com/eaaCatalog/dataMapper.html)
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
---
|
||||
title: Presenters Format Data for the View
|
||||
impact: MEDIUM
|
||||
impactDescription: enables multiple view formats from same use case
|
||||
tags: adapt, presenter, formatting, view-model
|
||||
---
|
||||
|
||||
## Presenters Format Data for the View
|
||||
|
||||
Presenters accept data from use cases and format it for presentation. They create view models with strings, booleans, and pre-formatted values - nothing left for the view to compute.
|
||||
|
||||
**Incorrect (view does formatting):**
|
||||
|
||||
```tsx
|
||||
// View component does formatting - logic spread across UI
|
||||
function InvoiceView({ invoice }: { invoice: Invoice }) {
|
||||
// Formatting logic in view
|
||||
const formattedDate = new Date(invoice.dueDate).toLocaleDateString('en-US', {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
})
|
||||
|
||||
const formattedTotal = new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: invoice.currency
|
||||
}).format(invoice.total / 100)
|
||||
|
||||
const statusColor = invoice.status === 'paid' ? 'green'
|
||||
: invoice.status === 'overdue' ? 'red'
|
||||
: 'yellow'
|
||||
|
||||
const daysOverdue = invoice.status === 'overdue'
|
||||
? Math.floor((Date.now() - new Date(invoice.dueDate).getTime()) / 86400000)
|
||||
: 0
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span style={{ color: statusColor }}>{invoice.status.toUpperCase()}</span>
|
||||
<span>Due: {formattedDate}</span>
|
||||
<span>Total: {formattedTotal}</span>
|
||||
{daysOverdue > 0 && <span>{daysOverdue} days overdue</span>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (presenter formats, view renders):**
|
||||
|
||||
```tsx
|
||||
// presenter/InvoicePresenter.ts
|
||||
interface InvoiceViewModel {
|
||||
invoiceNumber: string
|
||||
status: string
|
||||
statusColor: 'green' | 'yellow' | 'red'
|
||||
dueDate: string
|
||||
total: string
|
||||
overdueMessage: string | null
|
||||
}
|
||||
|
||||
class InvoicePresenter {
|
||||
present(invoice: InvoiceResult, locale: string): InvoiceViewModel {
|
||||
return {
|
||||
invoiceNumber: `INV-${invoice.id.padStart(6, '0')}`,
|
||||
status: invoice.status.toUpperCase(),
|
||||
statusColor: this.getStatusColor(invoice.status),
|
||||
dueDate: this.formatDate(invoice.dueDate, locale),
|
||||
total: this.formatMoney(invoice.total, invoice.currency, locale),
|
||||
overdueMessage: this.getOverdueMessage(invoice)
|
||||
}
|
||||
}
|
||||
|
||||
private getStatusColor(status: string): 'green' | 'yellow' | 'red' {
|
||||
const colors = { paid: 'green', pending: 'yellow', overdue: 'red' }
|
||||
return colors[status] || 'yellow'
|
||||
}
|
||||
|
||||
private formatDate(date: Date, locale: string): string {
|
||||
return new Intl.DateTimeFormat(locale, {
|
||||
month: 'long', day: 'numeric', year: 'numeric'
|
||||
}).format(date)
|
||||
}
|
||||
|
||||
private getOverdueMessage(invoice: InvoiceResult): string | null {
|
||||
if (invoice.status !== 'overdue') return null
|
||||
const days = this.calculateDaysOverdue(invoice.dueDate)
|
||||
return `${days} day${days !== 1 ? 's' : ''} overdue`
|
||||
}
|
||||
}
|
||||
|
||||
// view/InvoiceView.tsx - Humble, just renders
|
||||
function InvoiceView({ vm }: { vm: InvoiceViewModel }) {
|
||||
return (
|
||||
<div>
|
||||
<span style={{ color: vm.statusColor }}>{vm.status}</span>
|
||||
<span>Due: {vm.dueDate}</span>
|
||||
<span>Total: {vm.total}</span>
|
||||
{vm.overdueMessage && <span>{vm.overdueMessage}</span>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Formatting logic tested without UI framework
|
||||
- Same use case serves different locales via different presenter configs
|
||||
- View components trivially simple, easy to redesign
|
||||
|
||||
Reference: [Clean Architecture - Presenters and Humble Objects](https://www.oreilly.com/library/view/clean-architecture-a/9780134494272/ch23.xhtml)
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
---
|
||||
title: Weigh Boundary Cost Against Ignorance Cost
|
||||
impact: MEDIUM-HIGH
|
||||
impactDescription: optimizes architectural investment, prevents both over and under engineering
|
||||
tags: bound, cost, tradeoffs, pragmatism
|
||||
---
|
||||
|
||||
## Weigh Boundary Cost Against Ignorance Cost
|
||||
|
||||
Boundaries are expensive to create and maintain, but ignoring needed boundaries becomes very expensive later. Continuously evaluate where the cost of implementing is less than the cost of ignoring.
|
||||
|
||||
**Incorrect (boundary everywhere - over-engineering):**
|
||||
|
||||
```java
|
||||
// Overkill for a simple CRUD app with 3 entities
|
||||
|
||||
// 9 interfaces for 3 entities
|
||||
interface UserRepository { }
|
||||
interface UserService { }
|
||||
interface UserPresenter { }
|
||||
interface ProductRepository { }
|
||||
interface ProductService { }
|
||||
interface ProductPresenter { }
|
||||
interface OrderRepository { }
|
||||
interface OrderService { }
|
||||
interface OrderPresenter { }
|
||||
|
||||
// 9 implementations
|
||||
class SqlUserRepository implements UserRepository { }
|
||||
class UserServiceImpl implements UserService { }
|
||||
class UserPresenterImpl implements UserPresenter { }
|
||||
// ... 6 more classes
|
||||
|
||||
// 3 factories
|
||||
class UserFactory { }
|
||||
class ProductFactory { }
|
||||
class OrderFactory { }
|
||||
|
||||
// Result: 100 files for functionality that could be 20 files
|
||||
// Maintenance burden exceeds benefit for small team
|
||||
```
|
||||
|
||||
**Incorrect (no boundaries - under-engineering):**
|
||||
|
||||
```java
|
||||
// Dangerous for a complex domain with multiple teams
|
||||
|
||||
class GodService {
|
||||
public void handleEverything(Request req) {
|
||||
// 2000 lines mixing:
|
||||
// - User authentication
|
||||
// - Order processing
|
||||
// - Payment handling
|
||||
// - Email notifications
|
||||
// - Report generation
|
||||
}
|
||||
}
|
||||
|
||||
// Result: Every change risks breaking unrelated features
|
||||
// No team can work independently
|
||||
```
|
||||
|
||||
**Correct (boundaries where they matter):**
|
||||
|
||||
```java
|
||||
// Evaluate each potential boundary:
|
||||
|
||||
// HIGH VALUE BOUNDARY: External payment provider
|
||||
// - Changes frequently (provider updates API)
|
||||
// - Risk of vendor lock-in
|
||||
// - Different team might own this
|
||||
interface PaymentGateway {
|
||||
PaymentResult charge(Money amount, PaymentMethod method);
|
||||
}
|
||||
|
||||
// MEDIUM VALUE BOUNDARY: Database access
|
||||
// - Might migrate databases
|
||||
// - Enables testing without DB
|
||||
interface OrderRepository {
|
||||
void save(Order order);
|
||||
Order findById(OrderId id);
|
||||
}
|
||||
|
||||
// LOW VALUE - SKIP FOR NOW: Presenter/View split
|
||||
// - Single frontend, single team
|
||||
// - No plans to support multiple UIs
|
||||
// Just put formatting in React components for now
|
||||
// Add boundary later if needed
|
||||
|
||||
// SKIP: Separate microservices
|
||||
// - Team is 5 people
|
||||
// - Deployment is monolithic anyway
|
||||
// - Network boundary adds latency and complexity for no benefit
|
||||
```
|
||||
|
||||
**Decision Framework:**
|
||||
| Factor | Add Boundary | Skip Boundary |
|
||||
|--------|-------------|---------------|
|
||||
| Change frequency | High | Low |
|
||||
| Team ownership | Multiple teams | Single team |
|
||||
| External dependency | Yes | No |
|
||||
| Testing difficulty | Hard without boundary | Easy anyway |
|
||||
| Current pain | Evident | Hypothetical |
|
||||
|
||||
Reference: [Clean Architecture - The Cost of Boundaries](https://www.oreilly.com/library/view/clean-architecture-a/9780134494272/ch25.xhtml)
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
---
|
||||
title: Defer Framework and Database Decisions
|
||||
impact: MEDIUM-HIGH
|
||||
impactDescription: preserves optionality, reduces early commitment risk
|
||||
tags: bound, deferral, decisions, optionality
|
||||
---
|
||||
|
||||
## Defer Framework and Database Decisions
|
||||
|
||||
A good architecture allows major decisions about frameworks, databases, and delivery mechanisms to be deferred until the last responsible moment. The longer you wait, the more information you have.
|
||||
|
||||
**Incorrect (early commitment to specifics):**
|
||||
|
||||
```typescript
|
||||
// Day 1: "Let's use Prisma with PostgreSQL and Next.js"
|
||||
|
||||
// domain/Order.ts - Coupled to Prisma from the start
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
export async function createOrder(data: OrderInput) {
|
||||
// Business logic intertwined with Prisma
|
||||
const order = await prisma.order.create({
|
||||
data: {
|
||||
items: {
|
||||
create: data.items.map(item => ({
|
||||
productId: item.productId,
|
||||
quantity: item.quantity,
|
||||
price: item.price
|
||||
}))
|
||||
},
|
||||
total: data.items.reduce((sum, i) => sum + i.price * i.quantity, 0)
|
||||
},
|
||||
include: { items: true }
|
||||
})
|
||||
|
||||
return order
|
||||
}
|
||||
|
||||
// 6 months later: "PostgreSQL doesn't scale for our read patterns,
|
||||
// we need DynamoDB" - Massive rewrite required
|
||||
```
|
||||
|
||||
**Correct (defer database decision):**
|
||||
|
||||
```typescript
|
||||
// Day 1: Focus on business rules, defer database choice
|
||||
|
||||
// domain/Order.ts - Pure business logic
|
||||
export class Order {
|
||||
private items: OrderItem[] = []
|
||||
|
||||
addItem(product: Product, quantity: number): void {
|
||||
if (quantity <= 0) throw new InvalidQuantityError()
|
||||
this.items.push(new OrderItem(product, quantity))
|
||||
}
|
||||
|
||||
calculateTotal(): Money {
|
||||
return this.items.reduce(
|
||||
(sum, item) => sum.add(item.lineTotal()),
|
||||
Money.zero()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// application/ports/OrderRepository.ts - Interface only
|
||||
export interface OrderRepository {
|
||||
save(order: Order): Promise<void>
|
||||
findById(id: OrderId): Promise<Order | null>
|
||||
}
|
||||
|
||||
// For now: Simple in-memory implementation for testing
|
||||
// infrastructure/InMemoryOrderRepository.ts
|
||||
export class InMemoryOrderRepository implements OrderRepository {
|
||||
private orders = new Map<string, Order>()
|
||||
|
||||
async save(order: Order): Promise<void> {
|
||||
this.orders.set(order.id.value, order)
|
||||
}
|
||||
}
|
||||
|
||||
// Later, when you know more: Add real database
|
||||
// infrastructure/PrismaOrderRepository.ts
|
||||
// OR
|
||||
// infrastructure/DynamoOrderRepository.ts
|
||||
// OR
|
||||
// infrastructure/MongoOrderRepository.ts
|
||||
|
||||
// The domain never changes regardless of database choice
|
||||
```
|
||||
|
||||
**Decisions worth deferring:**
|
||||
- Database vendor (PostgreSQL vs MongoDB vs DynamoDB)
|
||||
- ORM choice (Prisma vs TypeORM vs raw SQL)
|
||||
- Web framework (Express vs Fastify vs Hono)
|
||||
- Message queue (RabbitMQ vs Kafka vs SQS)
|
||||
- Cache provider (Redis vs Memcached)
|
||||
|
||||
**Benefits:**
|
||||
- Learn requirements before committing
|
||||
- Prototype faster with simple implementations
|
||||
- Switch vendors without domain rewrites
|
||||
|
||||
Reference: [Clean Architecture - The Database is a Detail](https://www.oreilly.com/library/view/clean-architecture-a/9780134494272/ch30.xhtml)
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
---
|
||||
title: Use Humble Objects at Architectural Boundaries
|
||||
impact: MEDIUM-HIGH
|
||||
impactDescription: maximizes testable code, isolates hard-to-test parts
|
||||
tags: bound, humble-object, testing, boundaries
|
||||
---
|
||||
|
||||
## Use Humble Objects at Architectural Boundaries
|
||||
|
||||
The Humble Object pattern separates hard-to-test behaviors from easy-to-test behaviors. The "humble" part contains minimal logic and is hard to test; the substantial logic goes in a testable component.
|
||||
|
||||
**Incorrect (logic mixed with hard-to-test framework code):**
|
||||
|
||||
```typescript
|
||||
// React component with business logic
|
||||
function OrderSummary({ orderId }: Props) {
|
||||
const [order, setOrder] = useState<Order | null>(null)
|
||||
const [discount, setDiscount] = useState<number>(0)
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`/api/orders/${orderId}`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
setOrder(data)
|
||||
// Business logic in component - hard to test
|
||||
if (data.items.length > 5) {
|
||||
setDiscount(data.total * 0.1)
|
||||
} else if (data.customer.tier === 'gold') {
|
||||
setDiscount(data.total * 0.05)
|
||||
}
|
||||
})
|
||||
}, [orderId])
|
||||
|
||||
const finalTotal = order ? order.total - discount : 0
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span>Subtotal: ${order?.total}</span>
|
||||
<span>Discount: ${discount}</span>
|
||||
<span>Total: ${finalTotal}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Testing requires React Testing Library + mocked fetch
|
||||
```
|
||||
|
||||
**Correct (humble view + testable presenter):**
|
||||
|
||||
```typescript
|
||||
// Presenter - pure function, easy to test
|
||||
interface OrderViewModel {
|
||||
subtotal: string
|
||||
discount: string
|
||||
total: string
|
||||
hasDiscount: boolean
|
||||
}
|
||||
|
||||
function presentOrder(order: Order): OrderViewModel {
|
||||
const discount = calculateDiscount(order)
|
||||
const finalTotal = order.total - discount
|
||||
|
||||
return {
|
||||
subtotal: formatCurrency(order.total),
|
||||
discount: formatCurrency(discount),
|
||||
total: formatCurrency(finalTotal),
|
||||
hasDiscount: discount > 0
|
||||
}
|
||||
}
|
||||
|
||||
function calculateDiscount(order: Order): number {
|
||||
if (order.items.length > 5) return order.total * 0.1
|
||||
if (order.customer.tier === 'gold') return order.total * 0.05
|
||||
return 0
|
||||
}
|
||||
|
||||
// Humble view - no logic, just renders data
|
||||
function OrderSummary({ viewModel }: { viewModel: OrderViewModel }) {
|
||||
return (
|
||||
<div>
|
||||
<span>Subtotal: {viewModel.subtotal}</span>
|
||||
{viewModel.hasDiscount && <span>Discount: {viewModel.discount}</span>}
|
||||
<span>Total: {viewModel.total}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Container handles data fetching - also humble
|
||||
function OrderSummaryContainer({ orderId }: Props) {
|
||||
const { data: order } = useQuery(['order', orderId], fetchOrder)
|
||||
if (!order) return <Loading />
|
||||
return <OrderSummary viewModel={presentOrder(order)} />
|
||||
}
|
||||
|
||||
// Test presenter without React
|
||||
test('applies bulk discount for 6+ items', () => {
|
||||
const order = { items: [1,2,3,4,5,6], total: 100, customer: { tier: 'standard' } }
|
||||
const vm = presentOrder(order)
|
||||
expect(vm.discount).toBe('$10.00')
|
||||
})
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Business logic tested with simple unit tests
|
||||
- UI tests only verify rendering, not logic
|
||||
- 90%+ of logic covered by fast tests
|
||||
|
||||
Reference: [Clean Architecture - Humble Object Pattern](https://www.oreilly.com/library/view/clean-architecture-a/9780134494272/ch23.xhtml)
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
---
|
||||
title: Treat Main as a Plugin to the Application
|
||||
impact: MEDIUM-HIGH
|
||||
impactDescription: enables multiple configurations, simplifies testing
|
||||
tags: bound, main, composition-root, plugin
|
||||
---
|
||||
|
||||
## Treat Main as a Plugin to the Application
|
||||
|
||||
The Main component is the lowest-level, dirtiest component. It creates all factories, strategies, and global facilities, then hands control to high-level abstractions. Treat it as a plugin that can be swapped.
|
||||
|
||||
**Incorrect (Main mixed with application logic):**
|
||||
|
||||
```go
|
||||
// main.go
|
||||
func main() {
|
||||
db := connectDatabase()
|
||||
|
||||
// Business logic in main
|
||||
http.HandleFunc("/orders", func(w http.ResponseWriter, r *http.Request) {
|
||||
var order Order
|
||||
json.NewDecoder(r.Body).Decode(&order)
|
||||
|
||||
// Validation logic
|
||||
if order.Total < 0 {
|
||||
http.Error(w, "Invalid total", 400)
|
||||
return
|
||||
}
|
||||
|
||||
// Direct database calls
|
||||
db.Exec("INSERT INTO orders ...")
|
||||
|
||||
// Response formatting
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
||||
})
|
||||
|
||||
http.ListenAndServe(":8080", nil)
|
||||
}
|
||||
// Cannot test without starting HTTP server
|
||||
// Cannot run with different database
|
||||
```
|
||||
|
||||
**Correct (Main as composition root):**
|
||||
|
||||
```go
|
||||
// main.go - Production entry point
|
||||
func main() {
|
||||
app := bootstrap(ProductionConfig{
|
||||
DatabaseURL: os.Getenv("DATABASE_URL"),
|
||||
Port: os.Getenv("PORT"),
|
||||
})
|
||||
app.Run()
|
||||
}
|
||||
|
||||
// bootstrap.go - Composition root
|
||||
func bootstrap(config Config) *Application {
|
||||
// Create infrastructure
|
||||
db := postgres.NewConnection(config.DatabaseURL)
|
||||
|
||||
// Create repositories (implement domain interfaces)
|
||||
orderRepo := postgres.NewOrderRepository(db)
|
||||
userRepo := postgres.NewUserRepository(db)
|
||||
|
||||
// Create use cases (depend on interfaces)
|
||||
placeOrder := usecases.NewPlaceOrderUseCase(orderRepo)
|
||||
getOrders := usecases.NewGetOrdersUseCase(orderRepo)
|
||||
|
||||
// Create controllers (call use cases)
|
||||
orderController := controllers.NewOrderController(placeOrder, getOrders)
|
||||
|
||||
// Wire up HTTP routes
|
||||
router := http.NewRouter()
|
||||
router.POST("/orders", orderController.Create)
|
||||
router.GET("/orders", orderController.List)
|
||||
|
||||
return &Application{
|
||||
Router: router,
|
||||
Port: config.Port,
|
||||
}
|
||||
}
|
||||
|
||||
// main_test.go - Test entry point
|
||||
func TestMain(t *testing.T) {
|
||||
app := bootstrap(TestConfig{
|
||||
DatabaseURL: "memory://", // In-memory for tests
|
||||
Port: "0", // Random port
|
||||
})
|
||||
// Test against app
|
||||
}
|
||||
|
||||
// main_dev.go - Development entry point
|
||||
func main() {
|
||||
app := bootstrap(DevConfig{
|
||||
DatabaseURL: "localhost:5432",
|
||||
Port: "3000",
|
||||
})
|
||||
app.RunWithHotReload()
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Swap database, logging, configuration per environment
|
||||
- Integration tests use real wiring with test doubles
|
||||
- Dev/staging/prod share same composition logic
|
||||
|
||||
Reference: [Clean Architecture - The Main Component](https://www.oreilly.com/library/view/clean-architecture-a/9780134494272/ch26.xhtml)
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
---
|
||||
title: Use Partial Boundaries When Full Separation is Premature
|
||||
impact: MEDIUM-HIGH
|
||||
impactDescription: prepares for future without over-engineering now
|
||||
tags: bound, partial-boundaries, pragmatism, yagni
|
||||
---
|
||||
|
||||
## Use Partial Boundaries When Full Separation is Premature
|
||||
|
||||
When you anticipate needing a boundary but the cost of full separation is too high, implement a partial boundary. This prepares for future separation without premature over-engineering.
|
||||
|
||||
**Incorrect (no boundary when one might be needed):**
|
||||
|
||||
```python
|
||||
# Tightly coupled - if we ever need to split, massive refactor
|
||||
class ReportService:
|
||||
def generate_sales_report(self, start_date, end_date):
|
||||
# Direct database access
|
||||
conn = psycopg2.connect(DATABASE_URL)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT * FROM sales
|
||||
WHERE date BETWEEN %s AND %s
|
||||
""", (start_date, end_date))
|
||||
|
||||
# Direct PDF generation
|
||||
pdf = FPDF()
|
||||
for row in cursor.fetchall():
|
||||
pdf.cell(200, 10, txt=str(row))
|
||||
return pdf.output()
|
||||
|
||||
# Cannot split report generation from data access without rewriting
|
||||
```
|
||||
|
||||
**Correct (partial boundary - same component, prepared for split):**
|
||||
|
||||
```python
|
||||
# Strategy 1: Facade pattern (simplest partial boundary)
|
||||
class ReportingFacade:
|
||||
"""Single entry point, internal implementation can be refactored later."""
|
||||
|
||||
def generate_sales_report(self, request: ReportRequest) -> Report:
|
||||
data = self._fetch_data(request)
|
||||
return self._format_report(data, request.format)
|
||||
|
||||
def _fetch_data(self, request):
|
||||
# Could become a separate DataAccess component later
|
||||
return self.repository.get_sales(request.start, request.end)
|
||||
|
||||
def _format_report(self, data, format):
|
||||
# Could become a separate ReportRenderer component later
|
||||
if format == 'pdf':
|
||||
return PdfRenderer().render(data)
|
||||
return CsvRenderer().render(data)
|
||||
|
||||
|
||||
# Strategy 2: Interface with single implementation
|
||||
class SalesDataProvider(Protocol):
|
||||
def get_sales(self, start: date, end: date) -> list[Sale]: ...
|
||||
|
||||
class PostgresSalesProvider:
|
||||
def get_sales(self, start: date, end: date) -> list[Sale]:
|
||||
# Implementation here
|
||||
|
||||
# Both live in same component, but interface enables future split
|
||||
# When split needed: move interface to one component, impl to another
|
||||
|
||||
|
||||
# Strategy 3: One-dimensional boundary
|
||||
# Skip reciprocal interface - simpler but weaker protection
|
||||
class ReportGenerator:
|
||||
def __init__(self, data_provider: SalesDataProvider):
|
||||
self.data_provider = data_provider # Depends on abstraction
|
||||
|
||||
# SalesDataProvider implementation knows about ReportGenerator
|
||||
# Not fully decoupled, but good enough for now
|
||||
```
|
||||
|
||||
**When to use partial boundaries:**
|
||||
- You suspect a boundary will be needed, but not yet
|
||||
- Team is small and deployment is unified
|
||||
- Cost of full boundary outweighs current benefits
|
||||
|
||||
Reference: [Clean Architecture - Partial Boundaries](https://www.oreilly.com/library/view/clean-architecture-a/9780134494272/ch24.xhtml)
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
---
|
||||
title: Services Must Have Internal Clean Architecture
|
||||
impact: MEDIUM-HIGH
|
||||
impactDescription: microservices don't replace architecture, they require it
|
||||
tags: bound, services, microservices, internal
|
||||
---
|
||||
|
||||
## Services Must Have Internal Clean Architecture
|
||||
|
||||
Breaking a monolith into microservices doesn't solve architectural problems. Each service still needs internal architecture. Services are a deployment option, not an architecture.
|
||||
|
||||
**Incorrect (microservices as architecture replacement):**
|
||||
|
||||
```text
|
||||
"We use microservices architecture"
|
||||
|
||||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||
│ order-service │ │ user-service │ │ payment-service │
|
||||
│ │ │ │ │ │
|
||||
│ routes.js │ │ routes.js │ │ routes.js │
|
||||
│ database.js │ │ database.js │ │ database.js │
|
||||
│ helpers.js │ │ helpers.js │ │ helpers.js │
|
||||
│ │ │ │ │ │
|
||||
│ (No layers, │ │ (No layers, │ │ (No layers, │
|
||||
│ no boundaries,│ │ no boundaries,│ │ no boundaries,│
|
||||
│ just smaller │ │ just smaller │ │ just smaller │
|
||||
│ messes) │ │ messes) │ │ messes) │
|
||||
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||
|
||||
# Result: Distributed monolith
|
||||
# All the downsides of microservices (network, deployment, consistency)
|
||||
# None of the benefits (each service still a tangled mess)
|
||||
```
|
||||
|
||||
**Correct (clean architecture within each service):**
|
||||
|
||||
```text
|
||||
┌───────────────────────────────────────────────────────────┐
|
||||
│ order-service │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ domain/ │ │
|
||||
│ │ Order.ts OrderLine.ts OrderStatus.ts │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ application/ │ │
|
||||
│ │ PlaceOrderUseCase.ts │ │
|
||||
│ │ ports/ │ │
|
||||
│ │ OrderRepository.ts │ │
|
||||
│ │ PaymentGateway.ts ← calls payment-service │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ infrastructure/ │ │
|
||||
│ │ PostgresOrderRepository.ts │ │
|
||||
│ │ HttpPaymentGateway.ts → payment-service API │ │
|
||||
│ │ KafkaEventPublisher.ts │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ interface/ │ │
|
||||
│ │ OrderController.ts │ │
|
||||
│ │ OrderEventHandler.ts │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
└───────────────────────────────────────────────────────────┘
|
||||
|
||||
# Each service has:
|
||||
# - Domain layer with business rules
|
||||
# - Application layer with use cases and ports
|
||||
# - Infrastructure layer implementing ports
|
||||
# - Clean dependency direction within service
|
||||
```
|
||||
|
||||
**Cross-service communication:**
|
||||
```typescript
|
||||
// application/ports/PaymentGateway.ts
|
||||
interface PaymentGateway {
|
||||
charge(amount: Money, method: PaymentMethod): Promise<PaymentResult>
|
||||
}
|
||||
|
||||
// infrastructure/HttpPaymentGateway.ts
|
||||
class HttpPaymentGateway implements PaymentGateway {
|
||||
async charge(amount: Money, method: PaymentMethod): Promise<PaymentResult> {
|
||||
// Calls payment-service over HTTP
|
||||
// Service boundary is an infrastructure detail
|
||||
const response = await fetch('http://payment-service/charge', { ... })
|
||||
return this.mapResponse(response)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Services testable without other services
|
||||
- Can extract or merge services without rewriting business logic
|
||||
- Network boundaries don't compromise internal architecture
|
||||
|
||||
Reference: [Clean Architecture - Services: Great and Small](https://www.oreilly.com/library/view/clean-architecture-a/9780134494272/ch27.xhtml)
|
||||
@@ -0,0 +1,62 @@
|
||||
---
|
||||
title: Group Classes That Change Together
|
||||
impact: HIGH
|
||||
impactDescription: localizes change impact, reduces deployment risk
|
||||
tags: comp, common-closure, cohesion, change
|
||||
---
|
||||
|
||||
## Group Classes That Change Together
|
||||
|
||||
Classes that change for the same reasons and at the same times should be in the same component. This is the Common Closure Principle (CCP) - the Single Responsibility Principle for components.
|
||||
|
||||
**Incorrect (classes that change together are separated):**
|
||||
|
||||
```text
|
||||
src/
|
||||
├── entities/
|
||||
│ └── Invoice.ts # Changes when billing rules change
|
||||
├── repositories/
|
||||
│ └── InvoiceRepository.ts # Changes when billing rules change
|
||||
├── services/
|
||||
│ └── InvoiceService.ts # Changes when billing rules change
|
||||
├── validators/
|
||||
│ └── InvoiceValidator.ts # Changes when billing rules change
|
||||
└── mappers/
|
||||
└── InvoiceMapper.ts # Changes when billing rules change
|
||||
|
||||
# A billing rule change touches 5 directories
|
||||
# Risk of forgetting one, inconsistent changes
|
||||
```
|
||||
|
||||
**Correct (classes that change together are grouped):**
|
||||
|
||||
```text
|
||||
src/
|
||||
├── billing/
|
||||
│ ├── domain/
|
||||
│ │ ├── Invoice.ts
|
||||
│ │ ├── InvoiceLine.ts
|
||||
│ │ ├── InvoiceRules.ts
|
||||
│ │ └── InvoiceValidator.ts
|
||||
│ ├── application/
|
||||
│ │ ├── CreateInvoiceUseCase.ts
|
||||
│ │ └── ports/
|
||||
│ │ └── InvoiceRepository.ts
|
||||
│ └── infrastructure/
|
||||
│ ├── PostgresInvoiceRepository.ts
|
||||
│ └── InvoiceMapper.ts
|
||||
├── payments/
|
||||
│ └── ... # Changes for different reasons
|
||||
└── shipping/
|
||||
└── ... # Changes for different reasons
|
||||
|
||||
# Billing rule change isolated to billing/
|
||||
# Single PR, single review, single deployment
|
||||
```
|
||||
|
||||
**How to identify change reasons:**
|
||||
- "When X business rule changes, which classes change?"
|
||||
- "When the Y team requests a feature, which code changes?"
|
||||
- Group by actor/stakeholder, not by technical layer
|
||||
|
||||
Reference: [Clean Architecture - Common Closure Principle](https://www.oreilly.com/library/view/clean-architecture-a/9780134494272/ch13.xhtml)
|
||||
@@ -0,0 +1,71 @@
|
||||
---
|
||||
title: Avoid Forcing Clients to Depend on Unused Code
|
||||
impact: HIGH
|
||||
impactDescription: reduces unnecessary recompilation and deployment
|
||||
tags: comp, common-reuse, dependencies, coupling
|
||||
---
|
||||
|
||||
## Avoid Forcing Clients to Depend on Unused Code
|
||||
|
||||
Classes used together should be in the same component. Classes not used together should be in separate components. Forcing a client to depend on things it doesn't use creates unnecessary coupling.
|
||||
|
||||
**Incorrect (monolithic utility package):**
|
||||
|
||||
```java
|
||||
// utils/src/main/java/com/app/utils/
|
||||
├── StringUtils.java
|
||||
├── DateUtils.java
|
||||
├── FileUtils.java
|
||||
├── HttpUtils.java
|
||||
├── CryptoUtils.java
|
||||
├── ImageUtils.java // Depends on ImageMagick
|
||||
└── PdfUtils.java // Depends on iText
|
||||
|
||||
// pom.xml for utils module
|
||||
<dependencies>
|
||||
<dependency>imagemagick</dependency> <!-- 50MB -->
|
||||
<dependency>itext-pdf</dependency> <!-- 20MB -->
|
||||
<dependency>bouncycastle</dependency> <!-- 10MB -->
|
||||
</dependencies>
|
||||
|
||||
// A service that only needs StringUtils
|
||||
// must now depend on all 80MB of transitive dependencies
|
||||
```
|
||||
|
||||
**Correct (split by usage pattern):**
|
||||
|
||||
```java
|
||||
// string-utils/
|
||||
├── StringUtils.java
|
||||
└── pom.xml // No heavy dependencies
|
||||
|
||||
// date-utils/
|
||||
├── DateUtils.java
|
||||
└── pom.xml
|
||||
|
||||
// image-processing/
|
||||
├── ImageUtils.java
|
||||
├── ImageResizer.java
|
||||
└── pom.xml // ImageMagick dependency only here
|
||||
|
||||
// pdf-generation/
|
||||
├── PdfUtils.java
|
||||
├── PdfBuilder.java
|
||||
└── pom.xml // iText dependency only here
|
||||
|
||||
// crypto/
|
||||
├── CryptoUtils.java
|
||||
├── HashingService.java
|
||||
└── pom.xml // BouncyCastle dependency only here
|
||||
|
||||
// Services import only what they need
|
||||
// order-service depends on string-utils, date-utils (2MB)
|
||||
// report-service depends on string-utils, pdf-generation (22MB)
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Clients only pull dependencies they actually use
|
||||
- Smaller deployment artifacts
|
||||
- Changes to image processing don't redeploy order service
|
||||
|
||||
Reference: [Clean Architecture - Common Reuse Principle](https://www.oreilly.com/library/view/clean-architecture-a/9780134494272/ch13.xhtml)
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
---
|
||||
title: Release Components as Cohesive Units
|
||||
impact: HIGH
|
||||
impactDescription: enables version tracking, supports parallel team development
|
||||
tags: comp, release, versioning, cohesion
|
||||
---
|
||||
|
||||
## Release Components as Cohesive Units
|
||||
|
||||
Classes grouped into a component should be releasable together. A component should have a version number, release notes, and clear documentation. This is the Reuse/Release Equivalence Principle (REP).
|
||||
|
||||
**Incorrect (arbitrary grouping):**
|
||||
|
||||
```text
|
||||
shared-lib/
|
||||
├── src/
|
||||
│ ├── auth/
|
||||
│ │ ├── JwtValidator.ts
|
||||
│ │ └── PermissionChecker.ts
|
||||
│ ├── logging/
|
||||
│ │ └── Logger.ts
|
||||
│ ├── email/
|
||||
│ │ └── EmailSender.ts
|
||||
│ └── payment/
|
||||
│ └── StripeClient.ts
|
||||
└── package.json // version 1.2.3
|
||||
|
||||
# Version 1.2.4 fixes JWT bug
|
||||
# But users of EmailSender must also upgrade
|
||||
# Changelog unclear which features affect which users
|
||||
```
|
||||
|
||||
**Correct (cohesive releasable components):**
|
||||
|
||||
```text
|
||||
packages/
|
||||
├── auth/
|
||||
│ ├── src/
|
||||
│ │ ├── JwtValidator.ts
|
||||
│ │ ├── PermissionChecker.ts
|
||||
│ │ └── index.ts
|
||||
│ ├── CHANGELOG.md # Auth-specific changes
|
||||
│ └── package.json # @company/auth v2.1.0
|
||||
├── logging/
|
||||
│ ├── src/
|
||||
│ │ └── Logger.ts
|
||||
│ ├── CHANGELOG.md
|
||||
│ └── package.json # @company/logging v1.0.3
|
||||
├── email/
|
||||
│ ├── src/
|
||||
│ │ └── EmailSender.ts
|
||||
│ ├── CHANGELOG.md
|
||||
│ └── package.json # @company/email v1.5.0
|
||||
└── payments/
|
||||
├── src/
|
||||
│ └── StripeClient.ts
|
||||
├── CHANGELOG.md
|
||||
└── package.json # @company/payments v3.0.0
|
||||
|
||||
# Clear ownership: auth team owns @company/auth
|
||||
# Independent versioning: JWT fix only bumps auth
|
||||
# Semantic versioning: breaking change in payments doesn't affect others
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Teams release independently on their own schedule
|
||||
- Version numbers communicate compatibility
|
||||
- Changelogs specific to what users of each component care about
|
||||
|
||||
Reference: [Clean Architecture - Reuse/Release Equivalence Principle](https://www.oreilly.com/library/view/clean-architecture-a/9780134494272/ch13.xhtml)
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
---
|
||||
title: Structure Should Scream the Domain Not the Framework
|
||||
impact: HIGH
|
||||
impactDescription: enables understanding at a glance, reveals intent
|
||||
tags: comp, screaming-architecture, structure, domain
|
||||
---
|
||||
|
||||
## Structure Should Scream the Domain Not the Framework
|
||||
|
||||
The folder structure should communicate what the system does, not what framework it uses. Looking at the top-level directories should reveal the business domain.
|
||||
|
||||
**Incorrect (framework-oriented structure):**
|
||||
|
||||
```text
|
||||
src/
|
||||
├── controllers/
|
||||
│ ├── UserController.ts
|
||||
│ ├── OrderController.ts
|
||||
│ └── ProductController.ts
|
||||
├── services/
|
||||
│ ├── UserService.ts
|
||||
│ ├── OrderService.ts
|
||||
│ └── ProductService.ts
|
||||
├── repositories/
|
||||
│ ├── UserRepository.ts
|
||||
│ ├── OrderRepository.ts
|
||||
│ └── ProductRepository.ts
|
||||
├── models/
|
||||
│ ├── User.ts
|
||||
│ ├── Order.ts
|
||||
│ └── Product.ts
|
||||
└── utils/
|
||||
└── helpers.ts
|
||||
|
||||
# This screams "MVC framework" not "e-commerce system"
|
||||
```
|
||||
|
||||
**Correct (domain-oriented structure):**
|
||||
|
||||
```text
|
||||
src/
|
||||
├── ordering/
|
||||
│ ├── domain/
|
||||
│ │ ├── Order.ts
|
||||
│ │ ├── OrderLine.ts
|
||||
│ │ └── OrderStatus.ts
|
||||
│ ├── application/
|
||||
│ │ ├── PlaceOrderUseCase.ts
|
||||
│ │ ├── CancelOrderUseCase.ts
|
||||
│ │ └── ports/
|
||||
│ │ ├── OrderRepository.ts
|
||||
│ │ └── PaymentGateway.ts
|
||||
│ └── infrastructure/
|
||||
│ ├── PostgresOrderRepository.ts
|
||||
│ └── StripePaymentGateway.ts
|
||||
├── inventory/
|
||||
│ ├── domain/
|
||||
│ ├── application/
|
||||
│ └── infrastructure/
|
||||
├── customers/
|
||||
│ ├── domain/
|
||||
│ ├── application/
|
||||
│ └── infrastructure/
|
||||
└── shared/
|
||||
└── kernel/
|
||||
├── Money.ts
|
||||
└── EntityId.ts
|
||||
|
||||
# This screams "e-commerce with ordering, inventory, customers"
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- New developers understand the domain immediately
|
||||
- Related code lives together, enabling focused changes
|
||||
- Frameworks become implementation details, not organizing principles
|
||||
|
||||
Reference: [Screaming Architecture](https://blog.cleancoder.com/uncle-bob/2011/09/30/Screaming-Architecture.html)
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
---
|
||||
title: Depend in the Direction of Stability
|
||||
impact: HIGH
|
||||
impactDescription: prevents unstable components from destabilizing dependents
|
||||
tags: comp, stability, dependencies, direction
|
||||
---
|
||||
|
||||
## Depend in the Direction of Stability
|
||||
|
||||
Components should depend on components that are more stable than themselves. A stable component (many dependents, few dependencies) should not depend on an unstable component (few dependents, many dependencies).
|
||||
|
||||
**Incorrect (stable component depends on unstable):**
|
||||
|
||||
```text
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ core-domain │
|
||||
│ (Used by 50 services, should be very stable) │
|
||||
│ │
|
||||
│ import { formatDate } from 'ui-helpers' // WRONG! │
|
||||
│ import { sendMetrics } from 'analytics' // WRONG! │
|
||||
└──────────────────────┬──────────────────────────────┘
|
||||
│
|
||||
┌───────────────┼───────────────┐
|
||||
▼ ▼ ▼
|
||||
ui-helpers analytics reporting
|
||||
(Changes (Changes (Changes
|
||||
weekly) monthly) often)
|
||||
|
||||
# When ui-helpers changes, core-domain might break
|
||||
# When core-domain breaks, all 50 services break
|
||||
```
|
||||
|
||||
**Correct (depend toward stability):**
|
||||
|
||||
```text
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ core-domain │
|
||||
│ (Used by 50 services, zero external dependencies) │
|
||||
│ │
|
||||
│ - Only depends on language primitives │
|
||||
│ - Defines interfaces, not implementations │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
▲
|
||||
│
|
||||
┌───────────────┼───────────────┐
|
||||
│ │ │
|
||||
ui-helpers analytics reporting
|
||||
│ │ │
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ external-dependencies │
|
||||
│ (date-fns, axios, lodash - maintained externally) │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
|
||||
# Unstable components depend on stable core
|
||||
# Core never breaks due to UI changes
|
||||
```
|
||||
|
||||
**Stability Metric:**
|
||||
```text
|
||||
Instability = Outgoing Dependencies / (Incoming + Outgoing)
|
||||
```
|
||||
- I = 0: Maximally stable (many dependents, no dependencies)
|
||||
- I = 1: Maximally unstable (no dependents, many dependencies)
|
||||
|
||||
Dependencies should flow from high I to low I.
|
||||
|
||||
Reference: [Clean Architecture - Stable Dependencies Principle](https://www.oreilly.com/library/view/clean-architecture-a/9780134494272/ch14.xhtml)
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
---
|
||||
title: Eliminate Cyclic Dependencies Between Components
|
||||
impact: CRITICAL
|
||||
impactDescription: prevents ripple effects, enables independent deployment
|
||||
tags: dep, cycles, acyclic, components
|
||||
---
|
||||
|
||||
## Eliminate Cyclic Dependencies Between Components
|
||||
|
||||
The dependency graph must be a Directed Acyclic Graph (DAG). Cycles create ripple effects where changes propagate unpredictably through the system.
|
||||
|
||||
**Incorrect (cyclic dependency):**
|
||||
|
||||
```typescript
|
||||
// modules/orders/OrderService.ts
|
||||
import { CustomerService } from '../customers/CustomerService'
|
||||
|
||||
export class OrderService {
|
||||
constructor(private customers: CustomerService) {}
|
||||
|
||||
async createOrder(customerId: string) {
|
||||
const customer = await this.customers.findById(customerId)
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
// modules/customers/CustomerService.ts
|
||||
import { OrderService } from '../orders/OrderService' // Cycle!
|
||||
|
||||
export class CustomerService {
|
||||
constructor(private orders: OrderService) {}
|
||||
|
||||
async getCustomerWithOrders(customerId: string) {
|
||||
const orders = await this.orders.findByCustomer(customerId)
|
||||
// ...
|
||||
}
|
||||
}
|
||||
// Neither module can be deployed or tested independently
|
||||
```
|
||||
|
||||
**Correct (break cycle with dependency inversion):**
|
||||
|
||||
```typescript
|
||||
// modules/orders/ports/CustomerProvider.ts
|
||||
export interface CustomerProvider {
|
||||
findById(id: string): Promise<Customer>
|
||||
}
|
||||
|
||||
// modules/orders/OrderService.ts
|
||||
import { CustomerProvider } from './ports/CustomerProvider'
|
||||
|
||||
export class OrderService {
|
||||
constructor(private customers: CustomerProvider) {}
|
||||
|
||||
async createOrder(customerId: string) {
|
||||
const customer = await this.customers.findById(customerId)
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
// modules/customers/CustomerService.ts
|
||||
// No import from orders module
|
||||
|
||||
export class CustomerService implements CustomerProvider {
|
||||
// Implements the interface defined in orders module
|
||||
}
|
||||
|
||||
// modules/customers/adapters/OrderAdapter.ts
|
||||
import { OrderService } from '../../orders/OrderService'
|
||||
|
||||
export class CustomerOrderAdapter {
|
||||
constructor(private orders: OrderService) {}
|
||||
|
||||
async getOrdersForCustomer(customerId: string) {
|
||||
return this.orders.findByCustomer(customerId)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Alternative (extract shared abstraction):**
|
||||
|
||||
Create a new component that both depend on, breaking the cycle into a DAG.
|
||||
|
||||
Reference: [Clean Architecture - Acyclic Dependencies Principle](https://www.oreilly.com/library/view/clean-architecture-a/9780134494272/ch14.xhtml)
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
---
|
||||
title: Use Simple Data Structures Across Boundaries
|
||||
impact: CRITICAL
|
||||
impactDescription: prevents coupling between layers
|
||||
tags: dep, boundaries, dto, data-transfer
|
||||
---
|
||||
|
||||
## Use Simple Data Structures Across Boundaries
|
||||
|
||||
Data crossing architectural boundaries should be simple, isolated data structures. Never pass entities, database rows, or framework objects across boundaries.
|
||||
|
||||
**Incorrect (entity crosses boundary):**
|
||||
|
||||
```python
|
||||
# domain/entities/user.py
|
||||
class User:
|
||||
def __init__(self, id, email, password_hash, created_at):
|
||||
self.id = id
|
||||
self.email = email
|
||||
self.password_hash = password_hash # Sensitive data
|
||||
self.created_at = created_at
|
||||
|
||||
# interface_adapters/controllers/user_controller.py
|
||||
class UserController:
|
||||
def get_user(self, user_id):
|
||||
user = self.use_case.get_user(user_id)
|
||||
return jsonify(user.__dict__) # Entity exposed to HTTP layer, leaks password_hash
|
||||
```
|
||||
|
||||
**Correct (DTOs cross boundaries):**
|
||||
|
||||
```python
|
||||
# application/dto/user_response.py
|
||||
@dataclass
|
||||
class UserResponse:
|
||||
id: str
|
||||
email: str
|
||||
member_since: str # Formatted for presentation
|
||||
|
||||
# application/usecases/get_user.py
|
||||
class GetUserUseCase:
|
||||
def execute(self, user_id: str) -> UserResponse:
|
||||
user = self.repository.find_by_id(user_id)
|
||||
return UserResponse(
|
||||
id=user.id,
|
||||
email=user.email,
|
||||
member_since=user.created_at.strftime("%B %Y")
|
||||
)
|
||||
|
||||
# interface_adapters/controllers/user_controller.py
|
||||
class UserController:
|
||||
def get_user(self, user_id):
|
||||
response = self.use_case.execute(user_id)
|
||||
return jsonify(asdict(response)) # Only safe, formatted data
|
||||
```
|
||||
|
||||
**When NOT to use this pattern:**
|
||||
- Within the same architectural layer, entities can flow freely
|
||||
- Performance-critical paths may need optimized data transfer
|
||||
|
||||
Reference: [Clean Architecture - Crossing Boundaries](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
---
|
||||
title: Interfaces Belong to Clients Not Implementers
|
||||
impact: CRITICAL
|
||||
impactDescription: enables true dependency inversion
|
||||
tags: dep, interfaces, dependency-inversion, ownership
|
||||
---
|
||||
|
||||
## Interfaces Belong to Clients Not Implementers
|
||||
|
||||
Interfaces should be defined in the layer that uses them, not the layer that implements them. The client owns the abstraction; the implementation adapts to it.
|
||||
|
||||
**Incorrect (interface defined next to implementation):**
|
||||
|
||||
```java
|
||||
// infrastructure/persistence/UserRepository.java
|
||||
public interface UserRepository {
|
||||
User findById(String id);
|
||||
void save(User user);
|
||||
}
|
||||
|
||||
// infrastructure/persistence/PostgresUserRepository.java
|
||||
public class PostgresUserRepository implements UserRepository {
|
||||
// Implementation
|
||||
}
|
||||
|
||||
// application/usecases/CreateUserUseCase.java
|
||||
import infrastructure.persistence.UserRepository; // Use case imports from infrastructure!
|
||||
|
||||
public class CreateUserUseCase {
|
||||
private final UserRepository repository;
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (interface defined where it's used):**
|
||||
|
||||
```java
|
||||
// application/ports/output/UserRepository.java
|
||||
public interface UserRepository {
|
||||
User findById(String id);
|
||||
void save(User user);
|
||||
}
|
||||
|
||||
// application/usecases/CreateUserUseCase.java
|
||||
import application.ports.output.UserRepository; // Same layer import
|
||||
|
||||
public class CreateUserUseCase {
|
||||
private final UserRepository repository; // No infrastructure dependency
|
||||
}
|
||||
|
||||
// infrastructure/persistence/PostgresUserRepository.java
|
||||
import application.ports.output.UserRepository; // Infrastructure depends on application
|
||||
|
||||
public class PostgresUserRepository implements UserRepository {
|
||||
// Implementation adapts to the port
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** This is the essence of the Dependency Inversion Principle. The high-level module defines what it needs; low-level modules conform to that contract.
|
||||
|
||||
Reference: [Clean Architecture - Chapter 11: DIP](https://www.oreilly.com/library/view/clean-architecture-a/9780134494272/ch11.xhtml)
|
||||
@@ -0,0 +1,69 @@
|
||||
---
|
||||
title: Source Dependencies Point Inward Only
|
||||
impact: CRITICAL
|
||||
impactDescription: prevents cascade failures across all layers
|
||||
tags: dep, dependency-rule, layers, architecture
|
||||
---
|
||||
|
||||
## Source Dependencies Point Inward Only
|
||||
|
||||
The Dependency Rule states that source code dependencies can only point inward toward higher-level policies. Inner circles must never reference outer circles.
|
||||
|
||||
**Incorrect (inner layer imports from outer layer):**
|
||||
|
||||
```typescript
|
||||
// domain/entities/Order.ts - ENTITY LAYER
|
||||
import { OrderRepository } from '../../infrastructure/OrderRepository'
|
||||
import { EmailService } from '../../infrastructure/EmailService'
|
||||
|
||||
export class Order {
|
||||
constructor(
|
||||
private repo: OrderRepository, // Changes to repo implementation break Order
|
||||
private email: EmailService
|
||||
) {}
|
||||
|
||||
async complete() {
|
||||
await this.repo.save(this)
|
||||
await this.email.notify(this.customerId) // Cannot test without email server
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (inner layer defines interface, outer layer implements):**
|
||||
|
||||
```typescript
|
||||
// domain/entities/Order.ts - ENTITY LAYER
|
||||
export interface OrderPersistence {
|
||||
save(order: Order): Promise<void>
|
||||
}
|
||||
|
||||
export interface NotificationPort {
|
||||
notify(customerId: string): Promise<void>
|
||||
}
|
||||
|
||||
export class Order {
|
||||
constructor(
|
||||
private repo: OrderPersistence,
|
||||
private email: NotificationPort
|
||||
) {}
|
||||
|
||||
async complete() {
|
||||
await this.repo.save(this)
|
||||
await this.email.notify(this.customerId)
|
||||
}
|
||||
}
|
||||
|
||||
// infrastructure/OrderRepository.ts - INFRASTRUCTURE LAYER
|
||||
import { Order, OrderPersistence } from '../domain/entities/Order'
|
||||
|
||||
export class OrderRepository implements OrderPersistence {
|
||||
async save(order: Order): Promise<void> { /* DB implementation */ }
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Inner layers remain stable when outer layers change
|
||||
- Business rules can be tested without infrastructure
|
||||
- Infrastructure can be swapped without touching domain code
|
||||
|
||||
Reference: [The Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
---
|
||||
title: Avoid Framework Imports in Inner Layers
|
||||
impact: CRITICAL
|
||||
impactDescription: prevents framework lock-in, enables 10× faster unit tests
|
||||
tags: dep, frameworks, imports, isolation
|
||||
---
|
||||
|
||||
## Avoid Framework Imports in Inner Layers
|
||||
|
||||
Entities and use cases must never import framework-specific types. Framework dependencies in inner layers create tight coupling that makes testing slow and migration impossible.
|
||||
|
||||
**Incorrect (use case imports framework types):**
|
||||
|
||||
```csharp
|
||||
// Application/UseCases/ProcessPaymentUseCase.cs
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
public class ProcessPaymentUseCase
|
||||
{
|
||||
private readonly DbContext _context; // EF Core dependency
|
||||
private readonly IHttpContextAccessor _http; // ASP.NET dependency
|
||||
|
||||
public async Task Execute(PaymentRequest request)
|
||||
{
|
||||
var userId = _http.HttpContext.User.Identity.Name;
|
||||
var payment = JsonConvert.DeserializeObject<Payment>(request.Data);
|
||||
_context.Payments.Add(payment);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (use case depends only on abstractions):**
|
||||
|
||||
```csharp
|
||||
// Application/UseCases/ProcessPaymentUseCase.cs
|
||||
// No framework imports
|
||||
|
||||
public class ProcessPaymentUseCase
|
||||
{
|
||||
private readonly IPaymentRepository _payments;
|
||||
private readonly ICurrentUserProvider _currentUser;
|
||||
|
||||
public async Task Execute(PaymentCommand command)
|
||||
{
|
||||
var userId = _currentUser.GetUserId();
|
||||
var payment = new Payment(command.Amount, command.Currency, userId);
|
||||
await _payments.Save(payment);
|
||||
}
|
||||
}
|
||||
|
||||
// Infrastructure/Persistence/EfPaymentRepository.cs
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
public class EfPaymentRepository : IPaymentRepository
|
||||
{
|
||||
private readonly DbContext _context;
|
||||
// Framework usage isolated to infrastructure
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Use case tests run without framework bootstrapping
|
||||
- Framework can be upgraded or replaced independently
|
||||
- Business logic remains readable without framework noise
|
||||
|
||||
Reference: [Clean Architecture - Frameworks are Details](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
---
|
||||
title: Depend on Stable Abstractions Not Volatile Concretions
|
||||
impact: CRITICAL
|
||||
impactDescription: reduces change frequency by 5-10×
|
||||
tags: dep, stability, abstractions, volatility
|
||||
---
|
||||
|
||||
## Depend on Stable Abstractions Not Volatile Concretions
|
||||
|
||||
The most flexible systems depend on abstractions, not concretions. Volatile concrete classes are under active development and change frequently; depending on them propagates instability.
|
||||
|
||||
**Incorrect (depending on volatile concrete class):**
|
||||
|
||||
```go
|
||||
// services/notification.go
|
||||
package services
|
||||
|
||||
import (
|
||||
"myapp/infrastructure/email/sendgrid"
|
||||
"myapp/infrastructure/sms/twilio"
|
||||
)
|
||||
|
||||
type NotificationService struct {
|
||||
email *sendgrid.Client // Concrete SendGrid client
|
||||
sms *twilio.Client // Concrete Twilio client
|
||||
}
|
||||
|
||||
func (n *NotificationService) NotifyUser(userID string, message string) {
|
||||
// When SendGrid API changes, this service must change
|
||||
// When migrating to AWS SES, this service must change
|
||||
n.email.SendWithTemplate("notify", message)
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (depending on stable interface):**
|
||||
|
||||
```go
|
||||
// domain/ports/notification.go
|
||||
package ports
|
||||
|
||||
type EmailSender interface {
|
||||
Send(to string, subject string, body string) error
|
||||
}
|
||||
|
||||
type SMSSender interface {
|
||||
Send(to string, message string) error
|
||||
}
|
||||
|
||||
// services/notification.go
|
||||
package services
|
||||
|
||||
import "myapp/domain/ports"
|
||||
|
||||
type NotificationService struct {
|
||||
email ports.EmailSender // Stable abstraction
|
||||
sms ports.SMSSender // Stable abstraction
|
||||
}
|
||||
|
||||
func (n *NotificationService) NotifyUser(userID string, message string) {
|
||||
// Service is immune to email provider changes
|
||||
n.email.Send(userID, "Notification", message)
|
||||
}
|
||||
|
||||
// infrastructure/sendgrid/client.go
|
||||
package sendgrid
|
||||
|
||||
type Client struct { /* ... */ }
|
||||
|
||||
func (c *Client) Send(to, subject, body string) error {
|
||||
// Concrete implementation can change freely
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** Depending on stable concretions (like standard library classes) is acceptable. Focus inversion on volatile, actively-developed modules.
|
||||
|
||||
Reference: [Clean Architecture - Stable Abstractions Principle](https://www.oreilly.com/library/view/clean-architecture-a/9780134494272/ch11.xhtml)
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
---
|
||||
title: Encapsulate Business Invariants Within Entities
|
||||
impact: CRITICAL
|
||||
impactDescription: prevents invalid state, centralizes validation
|
||||
tags: entity, invariants, validation, encapsulation
|
||||
---
|
||||
|
||||
## Encapsulate Business Invariants Within Entities
|
||||
|
||||
Business rules that must always be true (invariants) should be enforced by the entity itself. External code should not be able to put an entity into an invalid state.
|
||||
|
||||
**Incorrect (invariants scattered outside entity):**
|
||||
|
||||
```python
|
||||
# application/usecases/transfer_money.py
|
||||
class TransferMoneyUseCase:
|
||||
def execute(self, from_id, to_id, amount):
|
||||
from_account = self.repo.find(from_id)
|
||||
to_account = self.repo.find(to_id)
|
||||
|
||||
# Validation scattered in use case
|
||||
if from_account.balance < amount:
|
||||
raise InsufficientFundsError()
|
||||
if amount <= 0:
|
||||
raise InvalidAmountError()
|
||||
if from_account.status != 'active':
|
||||
raise AccountInactiveError()
|
||||
|
||||
from_account.balance -= amount # Direct field mutation
|
||||
to_account.balance += amount
|
||||
|
||||
# Another use case might forget these checks!
|
||||
```
|
||||
|
||||
**Correct (invariants enforced by entity):**
|
||||
|
||||
```python
|
||||
# domain/entities/account.py
|
||||
class Account:
|
||||
def __init__(self, id: AccountId, balance: Money, status: AccountStatus):
|
||||
self._id = id
|
||||
self._balance = balance
|
||||
self._status = status
|
||||
|
||||
def withdraw(self, amount: Money) -> None:
|
||||
self._ensure_active()
|
||||
self._ensure_positive_amount(amount)
|
||||
self._ensure_sufficient_funds(amount)
|
||||
self._balance = self._balance.subtract(amount)
|
||||
|
||||
def deposit(self, amount: Money) -> None:
|
||||
self._ensure_active()
|
||||
self._ensure_positive_amount(amount)
|
||||
self._balance = self._balance.add(amount)
|
||||
|
||||
def _ensure_active(self) -> None:
|
||||
if self._status != AccountStatus.ACTIVE:
|
||||
raise AccountInactiveError(self._id)
|
||||
|
||||
def _ensure_positive_amount(self, amount: Money) -> None:
|
||||
if amount.is_zero_or_negative():
|
||||
raise InvalidAmountError(amount)
|
||||
|
||||
def _ensure_sufficient_funds(self, amount: Money) -> None:
|
||||
if self._balance.less_than(amount):
|
||||
raise InsufficientFundsError(self._id, self._balance, amount)
|
||||
|
||||
# application/usecases/transfer_money.py
|
||||
class TransferMoneyUseCase:
|
||||
def execute(self, from_id, to_id, amount):
|
||||
from_account = self.repo.find(from_id)
|
||||
to_account = self.repo.find(to_id)
|
||||
|
||||
from_account.withdraw(amount) # Invariants guaranteed
|
||||
to_account.deposit(amount)
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Impossible to create invalid account state
|
||||
- Validation rules documented in one place
|
||||
- Every use case gets consistent validation automatically
|
||||
|
||||
Reference: [Domain-Driven Design - Aggregates](https://martinfowler.com/bliki/DDD_Aggregate.html)
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
---
|
||||
title: Entities Must Not Know How They Are Persisted
|
||||
impact: CRITICAL
|
||||
impactDescription: enables database migration without domain changes
|
||||
tags: entity, persistence, database, ignorance
|
||||
---
|
||||
|
||||
## Entities Must Not Know How They Are Persisted
|
||||
|
||||
Entities should have no awareness of databases, ORMs, or storage mechanisms. Persistence is an infrastructure detail that must not leak into the domain.
|
||||
|
||||
**Incorrect (entity aware of persistence):**
|
||||
|
||||
```java
|
||||
// domain/entities/Product.java
|
||||
@Entity
|
||||
@Table(name = "products")
|
||||
public class Product {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "product_name", nullable = false)
|
||||
private String name;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "category_id")
|
||||
private Category category;
|
||||
|
||||
@Version
|
||||
private Long version;
|
||||
|
||||
// Entity is coupled to JPA, cannot exist without Hibernate
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (persistence-ignorant entity):**
|
||||
|
||||
```java
|
||||
// domain/entities/Product.java
|
||||
public class Product {
|
||||
private final ProductId id;
|
||||
private String name;
|
||||
private CategoryId categoryId;
|
||||
private Money price;
|
||||
|
||||
public Product(ProductId id, String name, CategoryId categoryId, Money price) {
|
||||
this.id = id;
|
||||
this.name = validateName(name);
|
||||
this.categoryId = categoryId;
|
||||
this.price = price;
|
||||
}
|
||||
|
||||
public void updatePrice(Money newPrice, PricingPolicy policy) {
|
||||
if (!policy.allowsPrice(newPrice)) {
|
||||
throw new InvalidPriceException(newPrice);
|
||||
}
|
||||
this.price = newPrice;
|
||||
}
|
||||
|
||||
private String validateName(String name) {
|
||||
if (name == null || name.isBlank()) {
|
||||
throw new InvalidProductNameException();
|
||||
}
|
||||
return name.trim();
|
||||
}
|
||||
}
|
||||
|
||||
// infrastructure/persistence/jpa/JpaProductEntity.java
|
||||
@Entity
|
||||
@Table(name = "products")
|
||||
class JpaProductEntity {
|
||||
@Id private Long id;
|
||||
@Column private String name;
|
||||
// ORM mapping isolated to infrastructure
|
||||
}
|
||||
|
||||
// infrastructure/persistence/jpa/ProductMapper.java
|
||||
class ProductMapper {
|
||||
Product toDomain(JpaProductEntity entity) { /* ... */ }
|
||||
JpaProductEntity toJpa(Product product) { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Switch from SQL to NoSQL without touching domain
|
||||
- Entity tests don't require database setup
|
||||
- Domain model expresses business concepts, not database schema
|
||||
|
||||
Reference: [Clean Architecture - Database is a Detail](https://www.oreilly.com/library/view/clean-architecture-a/9780134494272/ch30.xhtml)
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
---
|
||||
title: Entities Contain Only Enterprise Business Rules
|
||||
impact: CRITICAL
|
||||
impactDescription: enables reuse across applications, prevents coupling
|
||||
tags: entity, business-rules, domain, purity
|
||||
---
|
||||
|
||||
## Entities Contain Only Enterprise Business Rules
|
||||
|
||||
Entities encapsulate enterprise-wide business rules that would exist regardless of automation. They must not contain application-specific logic, persistence code, or UI concerns.
|
||||
|
||||
**Incorrect (entity mixed with infrastructure concerns):**
|
||||
|
||||
```typescript
|
||||
class Invoice {
|
||||
id: string
|
||||
items: LineItem[]
|
||||
status: InvoiceStatus
|
||||
dueDate: Date
|
||||
|
||||
calculateTotal(): Money {
|
||||
return this.items.reduce((sum, item) => sum.add(item.amount), Money.zero())
|
||||
}
|
||||
|
||||
isOverdue(): boolean {
|
||||
return this.status === InvoiceStatus.Unpaid && new Date() > this.dueDate
|
||||
}
|
||||
|
||||
markPaid(): void {
|
||||
this.status = InvoiceStatus.Paid
|
||||
database.invoices.update(this) // Entity cannot be tested without database
|
||||
emailService.send(this.customerEmail, 'Payment received') // Coupled to email system
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (entity contains only business rules):**
|
||||
|
||||
```typescript
|
||||
class Invoice {
|
||||
id: string
|
||||
items: LineItem[]
|
||||
status: InvoiceStatus
|
||||
dueDate: Date
|
||||
|
||||
calculateTotal(): Money {
|
||||
return this.items.reduce((sum, item) => sum.add(item.amount), Money.zero())
|
||||
}
|
||||
|
||||
isOverdue(): boolean {
|
||||
return this.status === InvoiceStatus.Unpaid && new Date() > this.dueDate
|
||||
}
|
||||
|
||||
markPaid(): void {
|
||||
if (this.status !== InvoiceStatus.Unpaid) {
|
||||
throw new InvalidOperationError('Invoice already processed')
|
||||
}
|
||||
this.status = InvoiceStatus.Paid
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Entity can be used in billing system, reporting system, mobile app
|
||||
- Business rules tested without database or email setup
|
||||
- Rules documented in one place, not scattered across application
|
||||
|
||||
Reference: [Clean Architecture - Entities](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
---
|
||||
title: Build Rich Domain Models Not Anemic Data Structures
|
||||
impact: HIGH
|
||||
impactDescription: centralizes business logic, prevents scattered rules
|
||||
tags: entity, domain-model, anemic, behavior
|
||||
---
|
||||
|
||||
## Build Rich Domain Models Not Anemic Data Structures
|
||||
|
||||
Entities should contain behavior, not just data. Anemic domain models push business logic into services, scattering rules and duplicating validation.
|
||||
|
||||
**Incorrect (anemic domain model):**
|
||||
|
||||
```java
|
||||
// domain/entities/Subscription.java
|
||||
public class Subscription {
|
||||
private String id;
|
||||
private String planId;
|
||||
private LocalDate startDate;
|
||||
private LocalDate endDate;
|
||||
private String status;
|
||||
|
||||
// Only getters and setters - no behavior
|
||||
public String getStatus() { return status; }
|
||||
public void setStatus(String status) { this.status = status; }
|
||||
public LocalDate getEndDate() { return endDate; }
|
||||
public void setEndDate(LocalDate endDate) { this.endDate = endDate; }
|
||||
}
|
||||
|
||||
// application/services/SubscriptionService.java
|
||||
public class SubscriptionService {
|
||||
public void cancel(Subscription sub) {
|
||||
if (!sub.getStatus().equals("active")) {
|
||||
throw new IllegalStateException("Cannot cancel");
|
||||
}
|
||||
sub.setStatus("cancelled");
|
||||
sub.setEndDate(LocalDate.now());
|
||||
}
|
||||
|
||||
public void renew(Subscription sub, int months) {
|
||||
if (!sub.getStatus().equals("active")) {
|
||||
throw new IllegalStateException("Cannot renew");
|
||||
}
|
||||
sub.setEndDate(sub.getEndDate().plusMonths(months));
|
||||
}
|
||||
// Logic scattered across many services
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (rich domain model):**
|
||||
|
||||
```java
|
||||
// domain/entities/Subscription.java
|
||||
public class Subscription {
|
||||
private final SubscriptionId id;
|
||||
private final PlanId planId;
|
||||
private final LocalDate startDate;
|
||||
private LocalDate endDate;
|
||||
private SubscriptionStatus status;
|
||||
|
||||
public void cancel() {
|
||||
ensureActive();
|
||||
this.status = SubscriptionStatus.CANCELLED;
|
||||
this.endDate = LocalDate.now();
|
||||
}
|
||||
|
||||
public void renew(Period extension) {
|
||||
ensureActive();
|
||||
if (extension.getMonths() < 1) {
|
||||
throw new InvalidExtensionPeriodException(extension);
|
||||
}
|
||||
this.endDate = this.endDate.plus(extension);
|
||||
}
|
||||
|
||||
public boolean isExpired() {
|
||||
return LocalDate.now().isAfter(this.endDate);
|
||||
}
|
||||
|
||||
public boolean canUpgradeTo(Plan newPlan) {
|
||||
return this.status == SubscriptionStatus.ACTIVE
|
||||
&& newPlan.isUpgradeFrom(this.planId);
|
||||
}
|
||||
|
||||
private void ensureActive() {
|
||||
if (this.status != SubscriptionStatus.ACTIVE) {
|
||||
throw new InactiveSubscriptionException(this.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// application/usecases/CancelSubscriptionUseCase.java
|
||||
public class CancelSubscriptionUseCase {
|
||||
public void execute(SubscriptionId id) {
|
||||
Subscription sub = repository.findById(id);
|
||||
sub.cancel(); // Business logic in entity
|
||||
repository.save(sub);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Business rules live with the data they operate on
|
||||
- Impossible to forget validation when manipulating data
|
||||
- Entity documents its own capabilities and constraints
|
||||
|
||||
Reference: [Anemic Domain Model Anti-pattern](https://martinfowler.com/bliki/AnemicDomainModel.html)
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
---
|
||||
title: Use Value Objects for Domain Concepts
|
||||
impact: HIGH
|
||||
impactDescription: eliminates primitive obsession, enforces constraints
|
||||
tags: entity, value-objects, domain, types
|
||||
---
|
||||
|
||||
## Use Value Objects for Domain Concepts
|
||||
|
||||
Replace primitive types with value objects that encapsulate validation and behavior. Value objects are immutable, compared by value, and self-validating.
|
||||
|
||||
**Incorrect (primitive obsession):**
|
||||
|
||||
```typescript
|
||||
class Customer {
|
||||
constructor(
|
||||
public id: string,
|
||||
public email: string,
|
||||
public phone: string,
|
||||
public postalCode: string
|
||||
) {}
|
||||
|
||||
changeEmail(newEmail: string): void {
|
||||
// Validation scattered or missing
|
||||
if (!newEmail.includes('@')) {
|
||||
throw new Error('Invalid email')
|
||||
}
|
||||
this.email = newEmail
|
||||
}
|
||||
}
|
||||
|
||||
// Callers can pass any string
|
||||
const customer = new Customer('123', 'not-an-email', '123', 'invalid')
|
||||
```
|
||||
|
||||
**Correct (value objects for domain concepts):**
|
||||
|
||||
```typescript
|
||||
class Email {
|
||||
private constructor(private readonly value: string) {}
|
||||
|
||||
static create(value: string): Email {
|
||||
if (!Email.isValid(value)) {
|
||||
throw new InvalidEmailError(value)
|
||||
}
|
||||
return new Email(value.toLowerCase())
|
||||
}
|
||||
|
||||
private static isValid(value: string): boolean {
|
||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
|
||||
}
|
||||
|
||||
equals(other: Email): boolean {
|
||||
return this.value === other.value
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.value
|
||||
}
|
||||
}
|
||||
|
||||
class PhoneNumber {
|
||||
private constructor(
|
||||
private readonly countryCode: string,
|
||||
private readonly number: string
|
||||
) {}
|
||||
|
||||
static create(countryCode: string, number: string): PhoneNumber {
|
||||
// Validation and normalization
|
||||
return new PhoneNumber(countryCode, number.replace(/\D/g, ''))
|
||||
}
|
||||
}
|
||||
|
||||
class Customer {
|
||||
constructor(
|
||||
public readonly id: CustomerId,
|
||||
private email: Email,
|
||||
private phone: PhoneNumber
|
||||
) {}
|
||||
|
||||
changeEmail(newEmail: Email): void {
|
||||
this.email = newEmail // Already validated
|
||||
}
|
||||
}
|
||||
|
||||
// Compile-time type safety + runtime validation
|
||||
const customer = new Customer(
|
||||
CustomerId.create('123'),
|
||||
Email.create('user@example.com'),
|
||||
PhoneNumber.create('+1', '555-1234')
|
||||
)
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Invalid values cannot exist in the system
|
||||
- Domain concepts explicitly named in code
|
||||
- Validation logic centralized and reusable
|
||||
- Type system prevents mixing up string parameters
|
||||
|
||||
Reference: [Value Object Pattern](https://martinfowler.com/bliki/ValueObject.html)
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
---
|
||||
title: Dependency Injection Containers Live at the Edge
|
||||
impact: MEDIUM
|
||||
impactDescription: prevents DI container coupling throughout codebase
|
||||
tags: frame, dependency-injection, container, composition
|
||||
---
|
||||
|
||||
## Dependency Injection Containers Live at the Edge
|
||||
|
||||
DI containers are infrastructure. Use them at composition root (main/startup) to wire dependencies, but don't let them invade domain or application code.
|
||||
|
||||
**Incorrect (DI container throughout codebase):**
|
||||
|
||||
```csharp
|
||||
// Domain depends on DI container
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
public class Order
|
||||
{
|
||||
public void Process()
|
||||
{
|
||||
// Service locator anti-pattern
|
||||
var emailService = ServiceLocator.Current.GetService<IEmailService>();
|
||||
var logger = ServiceLocator.Current.GetService<ILogger>();
|
||||
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
// Application layer decorated with DI attributes
|
||||
public class CreateOrderUseCase
|
||||
{
|
||||
[Inject]
|
||||
public IOrderRepository Orders { get; set; }
|
||||
|
||||
[Inject]
|
||||
public IPaymentGateway Payments { get; set; }
|
||||
|
||||
// Properties injected by container - hidden dependencies
|
||||
}
|
||||
|
||||
// Switching DI containers requires touching domain code
|
||||
```
|
||||
|
||||
**Correct (DI container only at composition root):**
|
||||
|
||||
```csharp
|
||||
// Domain - no DI container knowledge
|
||||
public class Order
|
||||
{
|
||||
public void Process(IEmailService email, ILogger logger)
|
||||
{
|
||||
// Dependencies passed explicitly
|
||||
}
|
||||
}
|
||||
|
||||
// Application - constructor injection, no container knowledge
|
||||
public class CreateOrderUseCase
|
||||
{
|
||||
private readonly IOrderRepository _orders;
|
||||
private readonly IPaymentGateway _payments;
|
||||
|
||||
// Plain constructor - works with any DI container or manual wiring
|
||||
public CreateOrderUseCase(
|
||||
IOrderRepository orders,
|
||||
IPaymentGateway payments)
|
||||
{
|
||||
_orders = orders;
|
||||
_payments = payments;
|
||||
}
|
||||
}
|
||||
|
||||
// Composition root - only place that knows about DI container
|
||||
// Program.cs or Startup.cs
|
||||
public class CompositionRoot
|
||||
{
|
||||
public static IServiceProvider ConfigureServices()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
|
||||
// Infrastructure
|
||||
services.AddScoped<IOrderRepository, PostgresOrderRepository>();
|
||||
services.AddScoped<IPaymentGateway, StripePaymentGateway>();
|
||||
services.AddScoped<IEmailService, SendGridEmailService>();
|
||||
|
||||
// Application
|
||||
services.AddScoped<CreateOrderUseCase>();
|
||||
services.AddScoped<GetOrdersUseCase>();
|
||||
|
||||
// Interface
|
||||
services.AddScoped<OrderController>();
|
||||
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
}
|
||||
|
||||
// For tests - manual wiring without container
|
||||
[Test]
|
||||
public void CreatesOrder()
|
||||
{
|
||||
var orders = new InMemoryOrderRepository();
|
||||
var payments = new FakePaymentGateway();
|
||||
var useCase = new CreateOrderUseCase(orders, payments);
|
||||
|
||||
useCase.Execute(command);
|
||||
|
||||
Assert.That(orders.All(), Has.Count.EqualTo(1));
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Domain and application code portable across DI containers
|
||||
- Tests don't need DI container setup
|
||||
- Dependencies explicit in constructors
|
||||
|
||||
Reference: [Composition Root Pattern](https://blog.ploeh.dk/2011/07/28/CompositionRoot/)
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
---
|
||||
title: Domain Layer Has Zero Framework Dependencies
|
||||
impact: MEDIUM
|
||||
impactDescription: enables framework migration, pure unit tests
|
||||
tags: frame, domain, purity, dependencies
|
||||
---
|
||||
|
||||
## Domain Layer Has Zero Framework Dependencies
|
||||
|
||||
The domain layer (entities and domain services) should have zero dependencies on frameworks, ORMs, or external libraries. Only language primitives and domain-specific code.
|
||||
|
||||
**Incorrect (domain depends on framework):**
|
||||
|
||||
```csharp
|
||||
// Domain entity with framework dependencies
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Domain.Entities
|
||||
{
|
||||
[Table("products")]
|
||||
public class Product
|
||||
{
|
||||
[Key]
|
||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
public int Id { get; set; }
|
||||
|
||||
[Required]
|
||||
[MaxLength(100)]
|
||||
[JsonProperty("product_name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[Column(TypeName = "decimal(18,2)")]
|
||||
public decimal Price { get; set; }
|
||||
|
||||
// EF Core navigation property
|
||||
public virtual Category Category { get; set; }
|
||||
|
||||
// Framework-specific validation
|
||||
public IEnumerable<ValidationResult> Validate(ValidationContext context)
|
||||
{
|
||||
if (Price < 0)
|
||||
yield return new ValidationResult("Price cannot be negative");
|
||||
}
|
||||
}
|
||||
}
|
||||
// Domain now depends on: EF Core, DataAnnotations, Newtonsoft.Json
|
||||
// Cannot exist without these frameworks
|
||||
```
|
||||
|
||||
**Correct (pure domain):**
|
||||
|
||||
```csharp
|
||||
// Domain entity - zero framework dependencies
|
||||
namespace Domain.Entities
|
||||
{
|
||||
public class Product
|
||||
{
|
||||
public ProductId Id { get; }
|
||||
public string Name { get; private set; }
|
||||
public Money Price { get; private set; }
|
||||
public CategoryId CategoryId { get; private set; }
|
||||
|
||||
public Product(ProductId id, string name, Money price, CategoryId categoryId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
throw new InvalidProductNameException();
|
||||
if (name.Length > 100)
|
||||
throw new ProductNameTooLongException(name.Length, 100);
|
||||
if (price.IsNegative)
|
||||
throw new NegativePriceException(price);
|
||||
|
||||
Id = id;
|
||||
Name = name;
|
||||
Price = price;
|
||||
CategoryId = categoryId;
|
||||
}
|
||||
|
||||
public void UpdatePrice(Money newPrice)
|
||||
{
|
||||
if (newPrice.IsNegative)
|
||||
throw new NegativePriceException(newPrice);
|
||||
Price = newPrice;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Infrastructure - framework dependencies isolated here
|
||||
namespace Infrastructure.Persistence
|
||||
{
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
[Table("products")]
|
||||
internal class ProductEntity
|
||||
{
|
||||
[Key] public int Id { get; set; }
|
||||
[MaxLength(100)] public string Name { get; set; }
|
||||
[Column(TypeName = "decimal(18,2)")] public decimal Price { get; set; }
|
||||
public int CategoryId { get; set; }
|
||||
}
|
||||
|
||||
internal class ProductMapper
|
||||
{
|
||||
public Product ToDomain(ProductEntity entity) => /* ... */
|
||||
public ProductEntity ToEntity(Product product) => /* ... */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**How to check domain purity:**
|
||||
```bash
|
||||
# Domain project should have no package references
|
||||
dotnet list Domain.csproj package
|
||||
# Should return: No packages found
|
||||
|
||||
# Or in package.json
|
||||
# "dependencies": {} should be empty or only contain domain libraries
|
||||
```
|
||||
|
||||
Reference: [Clean Architecture - Frameworks are Details](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
---
|
||||
title: Abstract Logging Behind Domain Interfaces
|
||||
impact: MEDIUM
|
||||
impactDescription: prevents logging framework coupling, enables structured logging
|
||||
tags: frame, logging, abstraction, cross-cutting
|
||||
---
|
||||
|
||||
## Abstract Logging Behind Domain Interfaces
|
||||
|
||||
Logging frameworks are infrastructure details. Define logging interfaces in the application layer; implement them in infrastructure. This enables switching loggers and keeps domain pure.
|
||||
|
||||
**Incorrect (logging framework in domain/application):**
|
||||
|
||||
```go
|
||||
package usecases
|
||||
|
||||
import (
|
||||
"github.com/sirupsen/logrus" // Framework dependency
|
||||
)
|
||||
|
||||
type CreateOrderUseCase struct {
|
||||
repo OrderRepository
|
||||
logger *logrus.Logger // Concrete logger
|
||||
}
|
||||
|
||||
func (uc *CreateOrderUseCase) Execute(cmd CreateOrderCommand) (*Order, error) {
|
||||
uc.logger.WithFields(logrus.Fields{
|
||||
"customer_id": cmd.CustomerID,
|
||||
"item_count": len(cmd.Items),
|
||||
}).Info("Creating order")
|
||||
|
||||
order, err := uc.repo.Create(cmd)
|
||||
if err != nil {
|
||||
uc.logger.WithError(err).Error("Failed to create order")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uc.logger.WithField("order_id", order.ID).Info("Order created")
|
||||
return order, nil
|
||||
}
|
||||
|
||||
// Switching from logrus to zap requires changing all use cases
|
||||
```
|
||||
|
||||
**Correct (logging behind interface):**
|
||||
|
||||
```go
|
||||
// application/ports/logger.go
|
||||
package ports
|
||||
|
||||
type Logger interface {
|
||||
Info(msg string, fields ...Field)
|
||||
Error(msg string, err error, fields ...Field)
|
||||
Debug(msg string, fields ...Field)
|
||||
}
|
||||
|
||||
type Field struct {
|
||||
Key string
|
||||
Value interface{}
|
||||
}
|
||||
|
||||
func F(key string, value interface{}) Field {
|
||||
return Field{Key: key, Value: value}
|
||||
}
|
||||
|
||||
// application/usecases/create_order.go
|
||||
package usecases
|
||||
|
||||
import "myapp/application/ports"
|
||||
|
||||
type CreateOrderUseCase struct {
|
||||
repo OrderRepository
|
||||
logger ports.Logger
|
||||
}
|
||||
|
||||
func (uc *CreateOrderUseCase) Execute(cmd CreateOrderCommand) (*Order, error) {
|
||||
uc.logger.Info("Creating order",
|
||||
ports.F("customer_id", cmd.CustomerID),
|
||||
ports.F("item_count", len(cmd.Items)),
|
||||
)
|
||||
|
||||
order, err := uc.repo.Create(cmd)
|
||||
if err != nil {
|
||||
uc.logger.Error("Failed to create order", err,
|
||||
ports.F("customer_id", cmd.CustomerID),
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uc.logger.Info("Order created", ports.F("order_id", order.ID))
|
||||
return order, nil
|
||||
}
|
||||
|
||||
// infrastructure/logging/logrus_logger.go
|
||||
package logging
|
||||
|
||||
import (
|
||||
"github.com/sirupsen/logrus"
|
||||
"myapp/application/ports"
|
||||
)
|
||||
|
||||
type LogrusLogger struct {
|
||||
logger *logrus.Logger
|
||||
}
|
||||
|
||||
func (l *LogrusLogger) Info(msg string, fields ...ports.Field) {
|
||||
l.logger.WithFields(toLogrusFields(fields)).Info(msg)
|
||||
}
|
||||
|
||||
func toLogrusFields(fields []ports.Field) logrus.Fields {
|
||||
result := logrus.Fields{}
|
||||
for _, f := range fields {
|
||||
result[f.Key] = f.Value
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// infrastructure/logging/zap_logger.go - Alternative implementation
|
||||
// Switch without touching use cases
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Application code doesn't import logging frameworks
|
||||
- Easy to switch logging backends
|
||||
- Test logging by asserting on mock logger calls
|
||||
|
||||
Reference: [Clean Architecture - Frameworks are Details](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
---
|
||||
title: Keep ORM Usage in Infrastructure Layer
|
||||
impact: MEDIUM
|
||||
impactDescription: isolates persistence complexity, enables ORM switching
|
||||
tags: frame, orm, infrastructure, persistence
|
||||
---
|
||||
|
||||
## Keep ORM Usage in Infrastructure Layer
|
||||
|
||||
ORM-specific code (entities, mappings, queries) belongs in the infrastructure layer. The domain and application layers should not know which ORM (or if any ORM) is being used.
|
||||
|
||||
**Incorrect (ORM in application layer):**
|
||||
|
||||
```python
|
||||
# application/usecases/get_orders.py
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
from sqlalchemy import and_, or_
|
||||
|
||||
class GetOrdersUseCase:
|
||||
def __init__(self, session: Session): # SQLAlchemy session in use case
|
||||
self.session = session
|
||||
|
||||
def execute(self, customer_id: str, filters: OrderFilters):
|
||||
# SQLAlchemy query in use case
|
||||
query = self.session.query(Order).options(
|
||||
joinedload(Order.items),
|
||||
joinedload(Order.customer)
|
||||
)
|
||||
|
||||
if filters.status:
|
||||
query = query.filter(Order.status == filters.status)
|
||||
|
||||
if filters.date_range:
|
||||
query = query.filter(and_(
|
||||
Order.created_at >= filters.date_range.start,
|
||||
Order.created_at <= filters.date_range.end
|
||||
))
|
||||
|
||||
return query.order_by(Order.created_at.desc()).all()
|
||||
|
||||
# Use case is now coupled to SQLAlchemy
|
||||
# Switching to Peewee or raw SQL requires rewriting use case
|
||||
```
|
||||
|
||||
**Correct (ORM isolated in infrastructure):**
|
||||
|
||||
```python
|
||||
# application/ports/order_repository.py
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
class OrderRepository(ABC):
|
||||
@abstractmethod
|
||||
def find_by_customer(
|
||||
self,
|
||||
customer_id: CustomerId,
|
||||
filters: OrderFilters
|
||||
) -> list[Order]:
|
||||
pass
|
||||
|
||||
# application/usecases/get_orders.py
|
||||
class GetOrdersUseCase:
|
||||
def __init__(self, orders: OrderRepository): # Interface, not Session
|
||||
self.orders = orders
|
||||
|
||||
def execute(self, customer_id: str, filters: OrderFilters) -> list[Order]:
|
||||
return self.orders.find_by_customer(
|
||||
CustomerId(customer_id),
|
||||
filters
|
||||
)
|
||||
|
||||
# infrastructure/persistence/sqlalchemy_order_repository.py
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
|
||||
class SqlAlchemyOrderRepository(OrderRepository):
|
||||
def __init__(self, session: Session):
|
||||
self.session = session
|
||||
|
||||
def find_by_customer(
|
||||
self,
|
||||
customer_id: CustomerId,
|
||||
filters: OrderFilters
|
||||
) -> list[Order]:
|
||||
query = self.session.query(OrderEntity).options(
|
||||
joinedload(OrderEntity.items)
|
||||
).filter(OrderEntity.customer_id == customer_id.value)
|
||||
|
||||
if filters.status:
|
||||
query = query.filter(OrderEntity.status == filters.status.value)
|
||||
|
||||
entities = query.order_by(OrderEntity.created_at.desc()).all()
|
||||
return [self._to_domain(e) for e in entities]
|
||||
|
||||
def _to_domain(self, entity: OrderEntity) -> Order:
|
||||
# Map ORM entity to domain entity
|
||||
pass
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Use case tests don't need database or ORM setup
|
||||
- Can switch ORMs without touching business logic
|
||||
- Complex queries encapsulated in repository
|
||||
|
||||
Reference: [Repository Pattern](https://martinfowler.com/eaaCatalog/repository.html)
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
---
|
||||
title: Web Framework Concerns Stay in Interface Layer
|
||||
impact: MEDIUM
|
||||
impactDescription: enables delivery mechanism substitution
|
||||
tags: frame, web, http, interface
|
||||
---
|
||||
|
||||
## Web Framework Concerns Stay in Interface Layer
|
||||
|
||||
HTTP-specific code (requests, responses, headers, cookies, sessions) belongs in the interface adapters layer. Use cases should be callable from any delivery mechanism.
|
||||
|
||||
**Incorrect (use case coupled to HTTP):**
|
||||
|
||||
```typescript
|
||||
// application/usecases/LoginUseCase.ts
|
||||
import { Request, Response } from 'express'
|
||||
import { sign } from 'jsonwebtoken'
|
||||
|
||||
export class LoginUseCase {
|
||||
async execute(req: Request, res: Response) {
|
||||
const { email, password } = req.body
|
||||
|
||||
const user = await this.users.findByEmail(email)
|
||||
if (!user || !user.verifyPassword(password)) {
|
||||
return res.status(401).json({ error: 'Invalid credentials' })
|
||||
}
|
||||
|
||||
// Set HTTP-only cookie
|
||||
const token = sign({ userId: user.id }, process.env.JWT_SECRET)
|
||||
res.cookie('auth_token', token, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'strict'
|
||||
})
|
||||
|
||||
return res.json({ user: { id: user.id, email: user.email } })
|
||||
}
|
||||
}
|
||||
|
||||
// Cannot call from CLI, message queue, or test without Express
|
||||
```
|
||||
|
||||
**Correct (use case independent of delivery):**
|
||||
|
||||
```typescript
|
||||
// application/usecases/LoginUseCase.ts
|
||||
export interface LoginCommand {
|
||||
email: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export interface LoginResult {
|
||||
userId: string
|
||||
email: string
|
||||
authToken: string
|
||||
}
|
||||
|
||||
export class LoginUseCase {
|
||||
constructor(
|
||||
private users: UserRepository,
|
||||
private tokenService: TokenService
|
||||
) {}
|
||||
|
||||
async execute(command: LoginCommand): Promise<LoginResult> {
|
||||
const user = await this.users.findByEmail(command.email)
|
||||
|
||||
if (!user) {
|
||||
throw new InvalidCredentialsError()
|
||||
}
|
||||
|
||||
if (!user.verifyPassword(command.password)) {
|
||||
throw new InvalidCredentialsError()
|
||||
}
|
||||
|
||||
const token = this.tokenService.generate({ userId: user.id.value })
|
||||
|
||||
return {
|
||||
userId: user.id.value,
|
||||
email: user.email.value,
|
||||
authToken: token
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// interface/http/AuthController.ts
|
||||
import { Request, Response } from 'express'
|
||||
|
||||
export class AuthController {
|
||||
constructor(private loginUseCase: LoginUseCase) {}
|
||||
|
||||
async login(req: Request, res: Response) {
|
||||
try {
|
||||
const result = await this.loginUseCase.execute({
|
||||
email: req.body.email,
|
||||
password: req.body.password
|
||||
})
|
||||
|
||||
// HTTP concerns in controller
|
||||
res.cookie('auth_token', result.authToken, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'strict'
|
||||
})
|
||||
|
||||
return res.json({
|
||||
user: { id: result.userId, email: result.email }
|
||||
})
|
||||
} catch (error) {
|
||||
if (error instanceof InvalidCredentialsError) {
|
||||
return res.status(401).json({ error: 'Invalid credentials' })
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// interface/cli/AuthCli.ts - Same use case, different delivery
|
||||
export class AuthCli {
|
||||
async login(email: string, password: string) {
|
||||
const result = await this.loginUseCase.execute({ email, password })
|
||||
console.log(`Logged in as ${result.email}`)
|
||||
fs.writeFileSync('.auth_token', result.authToken)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Same use case for HTTP, CLI, WebSocket, queue consumers
|
||||
- Easy to test without HTTP mocking
|
||||
- Web framework upgrades isolated to interface layer
|
||||
|
||||
Reference: [Clean Architecture - The Web is a Detail](https://www.oreilly.com/library/view/clean-architecture-a/9780134494272/ch31.xhtml)
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
---
|
||||
title: Verify Architectural Boundaries with Tests
|
||||
impact: LOW-MEDIUM
|
||||
impactDescription: prevents boundary erosion, enforces dependency rules
|
||||
tags: test, boundaries, architecture, enforcement
|
||||
---
|
||||
|
||||
## Verify Architectural Boundaries with Tests
|
||||
|
||||
Use automated tests to enforce that dependency rules are followed. Architecture tests catch violations before they become entrenched patterns.
|
||||
|
||||
**Incorrect (no boundary verification):**
|
||||
|
||||
```java
|
||||
// Over time, developers add shortcuts
|
||||
// domain/Order.java
|
||||
import org.springframework.stereotype.Component; // Framework in domain!
|
||||
import javax.persistence.Entity; // JPA in domain!
|
||||
|
||||
// application/CreateOrderUseCase.java
|
||||
import com.stripe.Stripe; // Direct payment vendor dependency!
|
||||
import infrastructure.email.SendGridClient; // Infrastructure in application!
|
||||
|
||||
// No tests catch these violations
|
||||
// They accumulate until refactoring becomes impossible
|
||||
```
|
||||
|
||||
**Correct (architecture tests enforce boundaries):**
|
||||
|
||||
```java
|
||||
// Using ArchUnit (Java) or similar tools
|
||||
@AnalyzeClasses(packages = "com.myapp")
|
||||
class ArchitectureTest {
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule domain_should_not_depend_on_infrastructure =
|
||||
noClasses()
|
||||
.that().resideInAPackage("..domain..")
|
||||
.should().dependOnClassesThat()
|
||||
.resideInAPackage("..infrastructure..");
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule domain_should_not_use_frameworks =
|
||||
noClasses()
|
||||
.that().resideInAPackage("..domain..")
|
||||
.should().dependOnClassesThat()
|
||||
.resideInAnyPackage(
|
||||
"org.springframework..",
|
||||
"javax.persistence..",
|
||||
"jakarta.persistence.."
|
||||
);
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule usecases_should_not_access_controllers =
|
||||
noClasses()
|
||||
.that().resideInAPackage("..application..")
|
||||
.should().dependOnClassesThat()
|
||||
.resideInAPackage("..interface..");
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule dependencies_point_inward =
|
||||
layeredArchitecture()
|
||||
.consideringAllDependencies()
|
||||
.layer("Domain").definedBy("..domain..")
|
||||
.layer("Application").definedBy("..application..")
|
||||
.layer("Infrastructure").definedBy("..infrastructure..")
|
||||
.layer("Interface").definedBy("..interface..")
|
||||
.whereLayer("Domain").mayNotAccessAnyLayer()
|
||||
.whereLayer("Application").mayOnlyAccessLayers("Domain")
|
||||
.whereLayer("Infrastructure").mayOnlyAccessLayers("Domain", "Application")
|
||||
.whereLayer("Interface").mayOnlyAccessLayers("Domain", "Application");
|
||||
}
|
||||
|
||||
// TypeScript equivalent using dependency-cruiser
|
||||
// .dependency-cruiser.js
|
||||
module.exports = {
|
||||
forbidden: [
|
||||
{
|
||||
name: 'domain-no-infra',
|
||||
from: { path: '^src/domain' },
|
||||
to: { path: '^src/infrastructure' }
|
||||
},
|
||||
{
|
||||
name: 'domain-no-frameworks',
|
||||
from: { path: '^src/domain' },
|
||||
to: { path: 'node_modules/(express|prisma|typeorm)' }
|
||||
}
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
**Run in CI:**
|
||||
```yaml
|
||||
# .github/workflows/ci.yml
|
||||
- name: Check architecture
|
||||
run: |
|
||||
./gradlew archTest # Java with ArchUnit
|
||||
npx depcruise src # TypeScript with dependency-cruiser
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Violations caught immediately in CI
|
||||
- New developers can't accidentally break boundaries
|
||||
- Architecture documentation that stays accurate
|
||||
|
||||
Reference: [ArchUnit](https://www.archunit.org/)
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
---
|
||||
title: Test Each Layer in Isolation
|
||||
impact: LOW-MEDIUM
|
||||
impactDescription: fast feedback, precise failure location
|
||||
tags: test, isolation, layers, unit
|
||||
---
|
||||
|
||||
## Test Each Layer in Isolation
|
||||
|
||||
Each architectural layer should be testable independently. Domain tests need no infrastructure. Use case tests need no web framework. This enables fast, focused testing.
|
||||
|
||||
**Incorrect (everything tested through API):**
|
||||
|
||||
```typescript
|
||||
// All tests go through HTTP - slow, imprecise failures
|
||||
describe('Order API', () => {
|
||||
let app: Express
|
||||
let db: Database
|
||||
|
||||
beforeEach(async () => {
|
||||
db = await Database.connect()
|
||||
await db.migrate()
|
||||
await db.seed()
|
||||
app = createApp(db)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await db.clean()
|
||||
await db.close()
|
||||
})
|
||||
|
||||
it('rejects order with insufficient inventory', async () => {
|
||||
// 500ms+ per test
|
||||
const response = await request(app)
|
||||
.post('/orders')
|
||||
.send({ items: [{ productId: 'p1', quantity: 1000 }] })
|
||||
|
||||
expect(response.status).toBe(400)
|
||||
expect(response.body.error).toBe('Insufficient inventory')
|
||||
})
|
||||
|
||||
// 50 more tests like this - test suite takes 5 minutes
|
||||
// When one fails, unclear if it's domain, use case, or HTTP issue
|
||||
})
|
||||
```
|
||||
|
||||
**Correct (layered testing):**
|
||||
|
||||
```typescript
|
||||
// Domain layer tests - instant, no dependencies
|
||||
describe('Order', () => {
|
||||
it('calculates total from items', () => {
|
||||
const order = new Order()
|
||||
order.addItem(new Product('p1', Money.dollars(10)), 2)
|
||||
order.addItem(new Product('p2', Money.dollars(5)), 1)
|
||||
|
||||
expect(order.total).toEqual(Money.dollars(25))
|
||||
})
|
||||
|
||||
it('prevents adding item with zero quantity', () => {
|
||||
const order = new Order()
|
||||
|
||||
expect(() => order.addItem(product, 0)).toThrow(InvalidQuantityError)
|
||||
})
|
||||
})
|
||||
|
||||
// Use case tests - fast, test doubles for ports
|
||||
describe('PlaceOrderUseCase', () => {
|
||||
it('rejects order when inventory insufficient', () => {
|
||||
const orders = new InMemoryOrderRepository()
|
||||
const inventory = new FakeInventory({ 'p1': 5 }) // Only 5 in stock
|
||||
const useCase = new PlaceOrderUseCase(orders, inventory)
|
||||
|
||||
const result = useCase.execute({
|
||||
items: [{ productId: 'p1', quantity: 10 }] // Want 10
|
||||
})
|
||||
|
||||
expect(result.isFailure).toBe(true)
|
||||
expect(result.error).toBe('INSUFFICIENT_INVENTORY')
|
||||
})
|
||||
})
|
||||
|
||||
// Infrastructure tests - verify adapters work correctly
|
||||
describe('PostgresOrderRepository', () => {
|
||||
it('persists and retrieves order', async () => {
|
||||
const repo = new PostgresOrderRepository(testDb)
|
||||
const order = createTestOrder()
|
||||
|
||||
await repo.save(order)
|
||||
const retrieved = await repo.findById(order.id)
|
||||
|
||||
expect(retrieved).toEqual(order)
|
||||
})
|
||||
})
|
||||
|
||||
// API tests - few, verify wiring only
|
||||
describe('POST /orders', () => {
|
||||
it('returns 201 when order placed successfully', async () => {
|
||||
// Most logic tested above; this verifies HTTP wiring
|
||||
const response = await request(app)
|
||||
.post('/orders')
|
||||
.send(validOrderPayload)
|
||||
|
||||
expect(response.status).toBe(201)
|
||||
expect(response.body).toHaveProperty('orderId')
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
**Test pyramid:**
|
||||
```text
|
||||
/\
|
||||
/ \ E2E: ~5 tests (slow, expensive)
|
||||
/----\
|
||||
/ \ Integration: ~20 tests (medium)
|
||||
/--------\
|
||||
/ \ Unit: ~200 tests (fast, cheap)
|
||||
/____________\
|
||||
```
|
||||
|
||||
Reference: [The Test Pyramid](https://martinfowler.com/articles/practical-test-pyramid.html)
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
---
|
||||
title: Design for Testability From the Start
|
||||
impact: LOW-MEDIUM
|
||||
impactDescription: enables comprehensive testing without test-induced damage
|
||||
tags: test, testability, design, interfaces
|
||||
---
|
||||
|
||||
## Design for Testability From the Start
|
||||
|
||||
Clean architecture makes testing easy by design. If something is hard to test, it's a sign of architectural coupling. Don't compromise architecture for testability; fix the coupling.
|
||||
|
||||
**Incorrect (hard to test, requiring workarounds):**
|
||||
|
||||
```java
|
||||
public class OrderProcessor {
|
||||
public void process(Order order) {
|
||||
// Hard to test: direct instantiation
|
||||
PaymentService payment = new PaymentService();
|
||||
|
||||
// Hard to test: static call
|
||||
if (InventoryChecker.isAvailable(order.getItems())) {
|
||||
|
||||
// Hard to test: singleton
|
||||
payment.charge(order.getTotal());
|
||||
NotificationService.getInstance().sendConfirmation(order);
|
||||
|
||||
// Hard to test: current time
|
||||
order.setProcessedAt(new Date());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test requires PowerMock, static mocking, singleton reset
|
||||
// Test is slow, brittle, and complicated
|
||||
@Test
|
||||
@PrepareForTest({InventoryChecker.class, NotificationService.class})
|
||||
public void testProcess() {
|
||||
PowerMockito.mockStatic(InventoryChecker.class);
|
||||
PowerMockito.when(InventoryChecker.isAvailable(any())).thenReturn(true);
|
||||
// ... 20 more lines of mock setup
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (testable by design):**
|
||||
|
||||
```java
|
||||
public class OrderProcessor {
|
||||
private final InventoryChecker inventory;
|
||||
private final PaymentGateway payment;
|
||||
private final NotificationPort notification;
|
||||
private final Clock clock;
|
||||
|
||||
// All dependencies injected - easy to substitute
|
||||
public OrderProcessor(
|
||||
InventoryChecker inventory,
|
||||
PaymentGateway payment,
|
||||
NotificationPort notification,
|
||||
Clock clock
|
||||
) {
|
||||
this.inventory = inventory;
|
||||
this.payment = payment;
|
||||
this.notification = notification;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
public ProcessResult process(Order order) {
|
||||
if (!inventory.isAvailable(order.getItems())) {
|
||||
return ProcessResult.unavailable();
|
||||
}
|
||||
|
||||
PaymentResult paymentResult = payment.charge(order.getTotal());
|
||||
if (!paymentResult.isSuccessful()) {
|
||||
return ProcessResult.paymentFailed(paymentResult.getError());
|
||||
}
|
||||
|
||||
order.markProcessed(clock.now());
|
||||
notification.sendConfirmation(order);
|
||||
|
||||
return ProcessResult.success(order);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void processesOrderWhenInventoryAvailable() {
|
||||
var inventory = StubInventory.withAvailability(true);
|
||||
var payment = FakePaymentGateway.alwaysSucceeds();
|
||||
var notification = new SpyNotification();
|
||||
var clock = Clock.fixed(Instant.parse("2024-01-15T10:00:00Z"), ZoneOffset.UTC);
|
||||
|
||||
var processor = new OrderProcessor(inventory, payment, notification, clock);
|
||||
var result = processor.process(order);
|
||||
|
||||
assertThat(result.isSuccessful()).isTrue();
|
||||
}
|
||||
```
|
||||
|
||||
**Testability checklist:**
|
||||
- [ ] No `new` for services (inject dependencies)
|
||||
- [ ] No static method calls for behavior (use interfaces)
|
||||
- [ ] No singletons (pass instances)
|
||||
- [ ] No direct time/random access (inject Clock, Random)
|
||||
- [ ] No hidden dependencies (everything in constructor)
|
||||
|
||||
Reference: [Growing Object-Oriented Software, Guided by Tests](http://www.growing-object-oriented-software.com/)
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
---
|
||||
title: Tests Are Part of the System Architecture
|
||||
impact: LOW-MEDIUM
|
||||
impactDescription: enables fast feedback, documents behavior
|
||||
tags: test, architecture, design, first-class
|
||||
---
|
||||
|
||||
## Tests Are Part of the System Architecture
|
||||
|
||||
Tests participate in the architecture like any other component. They follow the dependency rule, couple to stable APIs, and should be designed for maintainability.
|
||||
|
||||
**Incorrect (tests as afterthought):**
|
||||
|
||||
```python
|
||||
# tests/test_everything.py - Monolithic test file
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
class TestOrders:
|
||||
@patch('app.services.order_service.db')
|
||||
@patch('app.services.order_service.stripe')
|
||||
@patch('app.services.order_service.email_sender')
|
||||
@patch('app.services.order_service.inventory')
|
||||
def test_create_order(self, mock_inv, mock_email, mock_stripe, mock_db):
|
||||
# Testing implementation details
|
||||
mock_db.query.return_value.filter.return_value.first.return_value = Customer(id=1)
|
||||
mock_inv.check.return_value = True
|
||||
mock_stripe.PaymentIntent.create.return_value = MagicMock(id='pi_123')
|
||||
|
||||
from app.services.order_service import create_order
|
||||
result = create_order({'customer_id': 1, 'items': [...]})
|
||||
|
||||
# Asserting on internal calls, not behavior
|
||||
mock_db.query.assert_called()
|
||||
mock_stripe.PaymentIntent.create.assert_called_once()
|
||||
|
||||
# Tests coupled to implementation, break with refactoring
|
||||
# Tests slow because they patch everything
|
||||
```
|
||||
|
||||
**Correct (tests designed as architecture component):**
|
||||
|
||||
```python
|
||||
# tests/unit/domain/test_order.py - Fast, stable, test domain rules
|
||||
class TestOrder:
|
||||
def test_calculates_total_from_line_items(self):
|
||||
order = Order.create(customer_id="c1")
|
||||
order.add_item(Product("p1", Money(100)))
|
||||
order.add_item(Product("p2", Money(50)))
|
||||
|
||||
assert order.total == Money(150)
|
||||
|
||||
def test_rejects_negative_quantity(self):
|
||||
order = Order.create(customer_id="c1")
|
||||
|
||||
with pytest.raises(InvalidQuantityError):
|
||||
order.add_item(Product("p1", Money(100)), quantity=-1)
|
||||
|
||||
# tests/integration/application/test_create_order.py - Test use case
|
||||
class TestCreateOrderUseCase:
|
||||
def test_creates_order_and_reserves_inventory(self):
|
||||
# Use test doubles, not mocks of internals
|
||||
orders = InMemoryOrderRepository()
|
||||
inventory = FakeInventoryService(available={"p1": 10})
|
||||
use_case = CreateOrderUseCase(orders, inventory)
|
||||
|
||||
result = use_case.execute(CreateOrderCommand(
|
||||
customer_id="c1",
|
||||
items=[OrderItem("p1", quantity=2)]
|
||||
))
|
||||
|
||||
# Assert on observable behavior
|
||||
assert orders.find_by_id(result.order_id) is not None
|
||||
assert inventory.reserved["p1"] == 2
|
||||
|
||||
# tests/e2e/test_order_flow.py - Full flow, few tests
|
||||
class TestOrderFlow:
|
||||
def test_complete_order_journey(self, api_client, test_db):
|
||||
# Create order via API
|
||||
response = api_client.post('/orders', json={...})
|
||||
order_id = response.json['order_id']
|
||||
|
||||
# Verify order persisted
|
||||
order = test_db.query(Order).get(order_id)
|
||||
assert order.status == 'pending'
|
||||
```
|
||||
|
||||
**Test architecture mirrors system:**
|
||||
```text
|
||||
tests/
|
||||
├── unit/ # Fast, isolated
|
||||
│ ├── domain/ # Entity business rules
|
||||
│ └── application/ # Use case logic
|
||||
├── integration/ # Component interaction
|
||||
│ ├── persistence/ # Repository implementations
|
||||
│ └── external/ # Gateway implementations
|
||||
└── e2e/ # Full system
|
||||
└── api/ # HTTP endpoints
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Unit tests run in milliseconds
|
||||
- Refactoring doesn't break tests
|
||||
- Tests document intended behavior
|
||||
|
||||
Reference: [Clean Architecture - The Test Boundary](https://www.oreilly.com/library/view/clean-architecture-a/9780134494272/ch28.xhtml)
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
---
|
||||
title: Declare All Dependencies Explicitly in Constructor
|
||||
impact: HIGH
|
||||
impactDescription: enables testing, documents requirements, prevents hidden coupling
|
||||
tags: usecase, dependencies, constructor, injection
|
||||
---
|
||||
|
||||
## Declare All Dependencies Explicitly in Constructor
|
||||
|
||||
Use cases should receive all dependencies through their constructor. Hidden dependencies (service locators, singletons, static calls) make testing difficult and hide coupling.
|
||||
|
||||
**Incorrect (hidden dependencies):**
|
||||
|
||||
```java
|
||||
public class PlaceOrderUseCase {
|
||||
public OrderConfirmation execute(PlaceOrderCommand command) {
|
||||
// Hidden dependencies - impossible to test or trace
|
||||
var customer = CustomerRepository.getInstance().find(command.customerId());
|
||||
var inventory = InventoryService.getInventory();
|
||||
|
||||
for (var item : command.items()) {
|
||||
if (!inventory.isAvailable(item)) {
|
||||
throw new OutOfStockException(item);
|
||||
}
|
||||
}
|
||||
|
||||
var order = new Order(customer, command.items());
|
||||
|
||||
// More hidden dependencies
|
||||
Database.getConnection().save(order);
|
||||
EmailService.send(customer.email(), "Order placed");
|
||||
EventBus.publish(new OrderPlacedEvent(order));
|
||||
|
||||
return new OrderConfirmation(order.id());
|
||||
}
|
||||
}
|
||||
|
||||
// Testing requires mocking static methods - complex and brittle
|
||||
```
|
||||
|
||||
**Correct (explicit constructor dependencies):**
|
||||
|
||||
```java
|
||||
public class PlaceOrderUseCase {
|
||||
private final CustomerRepository customers;
|
||||
private final InventoryChecker inventory;
|
||||
private final OrderRepository orders;
|
||||
private final NotificationPort notifications;
|
||||
private final EventPublisher events;
|
||||
|
||||
public PlaceOrderUseCase(
|
||||
CustomerRepository customers,
|
||||
InventoryChecker inventory,
|
||||
OrderRepository orders,
|
||||
NotificationPort notifications,
|
||||
EventPublisher events
|
||||
) {
|
||||
this.customers = customers;
|
||||
this.inventory = inventory;
|
||||
this.orders = orders;
|
||||
this.notifications = notifications;
|
||||
this.events = events;
|
||||
}
|
||||
|
||||
public OrderConfirmation execute(PlaceOrderCommand command) {
|
||||
var customer = customers.find(command.customerId());
|
||||
|
||||
for (var item : command.items()) {
|
||||
if (!inventory.isAvailable(item)) {
|
||||
throw new OutOfStockException(item);
|
||||
}
|
||||
}
|
||||
|
||||
var order = new Order(customer, command.items());
|
||||
orders.save(order);
|
||||
|
||||
notifications.orderPlaced(customer, order);
|
||||
events.publish(new OrderPlacedEvent(order));
|
||||
|
||||
return new OrderConfirmation(order.id());
|
||||
}
|
||||
}
|
||||
|
||||
// Testing is straightforward
|
||||
@Test
|
||||
void placesOrderWhenInventoryAvailable() {
|
||||
var customers = mock(CustomerRepository.class);
|
||||
var inventory = mock(InventoryChecker.class);
|
||||
var orders = mock(OrderRepository.class);
|
||||
// ... configure mocks
|
||||
|
||||
var useCase = new PlaceOrderUseCase(customers, inventory, orders, ...);
|
||||
var result = useCase.execute(command);
|
||||
|
||||
verify(orders).save(any(Order.class));
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Dependencies visible in constructor signature
|
||||
- Tests substitute any dependency with test doubles
|
||||
- Coupling is explicit and measurable
|
||||
|
||||
Reference: [Dependency Injection Principles](https://martinfowler.com/articles/injection.html)
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
---
|
||||
title: Define Input and Output Ports for Use Cases
|
||||
impact: HIGH
|
||||
impactDescription: decouples use cases from delivery mechanism
|
||||
tags: usecase, ports, input, output, boundaries
|
||||
---
|
||||
|
||||
## Define Input and Output Ports for Use Cases
|
||||
|
||||
Use cases should define their own input (request) and output (response) data structures. These ports isolate the use case from the delivery mechanism (HTTP, CLI, queue).
|
||||
|
||||
**Incorrect (use case coupled to HTTP):**
|
||||
|
||||
```csharp
|
||||
public class RegisterUserUseCase
|
||||
{
|
||||
public IActionResult Execute(HttpRequest request) // Coupled to ASP.NET
|
||||
{
|
||||
var email = request.Form["email"];
|
||||
var password = request.Form["password"];
|
||||
|
||||
if (string.IsNullOrEmpty(email))
|
||||
return new BadRequestResult(); // HTTP-specific response
|
||||
|
||||
var user = new User(email, password);
|
||||
_repository.Save(user);
|
||||
|
||||
return new OkObjectResult(new { id = user.Id }); // JSON response
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (use case with defined ports):**
|
||||
|
||||
```csharp
|
||||
// application/ports/input/RegisterUserCommand.cs
|
||||
public record RegisterUserCommand(
|
||||
string Email,
|
||||
string Password,
|
||||
string Name
|
||||
);
|
||||
|
||||
// application/ports/output/RegisterUserResult.cs
|
||||
public record RegisterUserResult(
|
||||
string UserId,
|
||||
string Email,
|
||||
DateTime CreatedAt
|
||||
);
|
||||
|
||||
// application/usecases/RegisterUserUseCase.cs
|
||||
public class RegisterUserUseCase
|
||||
{
|
||||
private readonly IUserRepository _repository;
|
||||
private readonly IPasswordHasher _hasher;
|
||||
|
||||
public RegisterUserResult Execute(RegisterUserCommand command)
|
||||
{
|
||||
if (string.IsNullOrEmpty(command.Email))
|
||||
throw new ValidationException("Email required");
|
||||
|
||||
var hashedPassword = _hasher.Hash(command.Password);
|
||||
var user = new User(command.Email, hashedPassword, command.Name);
|
||||
_repository.Save(user);
|
||||
|
||||
return new RegisterUserResult(
|
||||
user.Id.Value,
|
||||
user.Email.Value,
|
||||
user.CreatedAt
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// interface_adapters/controllers/UserController.cs
|
||||
[ApiController]
|
||||
public class UserController : ControllerBase
|
||||
{
|
||||
[HttpPost]
|
||||
public IActionResult Register([FromBody] RegisterRequest request)
|
||||
{
|
||||
var command = new RegisterUserCommand(
|
||||
request.Email,
|
||||
request.Password,
|
||||
request.Name
|
||||
);
|
||||
|
||||
var result = _useCase.Execute(command);
|
||||
|
||||
return Ok(new { userId = result.UserId });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Same use case callable from HTTP, CLI, message queue, tests
|
||||
- Request/response format changes don't affect use case
|
||||
- Use case testable without HTTP infrastructure
|
||||
|
||||
Reference: [Clean Architecture - Input/Output Ports](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
---
|
||||
title: Use Cases Must Not Contain Presentation Logic
|
||||
impact: HIGH
|
||||
impactDescription: enables multiple UI implementations from same use case
|
||||
tags: usecase, presentation, formatting, separation
|
||||
---
|
||||
|
||||
## Use Cases Must Not Contain Presentation Logic
|
||||
|
||||
Use cases return domain data, not formatted strings, HTML, or UI-specific structures. Presentation logic belongs in the interface adapters layer.
|
||||
|
||||
**Incorrect (presentation logic in use case):**
|
||||
|
||||
```typescript
|
||||
class GetUserProfileUseCase {
|
||||
execute(userId: string): UserProfileResponse {
|
||||
const user = this.repo.findById(userId)
|
||||
const orders = this.orderRepo.findByUser(userId)
|
||||
|
||||
return {
|
||||
displayName: `${user.firstName} ${user.lastName}`, // Formatting
|
||||
memberSince: user.createdAt.toLocaleDateString('en-US', {
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
}), // Locale-specific formatting
|
||||
avatarHtml: `<img src="${user.avatarUrl}" alt="${user.firstName}"/>`, // HTML!
|
||||
orderSummary: orders.length > 0
|
||||
? `${orders.length} orders totaling $${this.sumOrders(orders).toFixed(2)}`
|
||||
: 'No orders yet', // UI text
|
||||
recentOrders: orders.slice(0, 5).map(o => ({
|
||||
...o,
|
||||
statusBadgeColor: this.getStatusColor(o.status) // UI styling
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
private getStatusColor(status: string): string {
|
||||
const colors = { pending: 'yellow', shipped: 'blue', delivered: 'green' }
|
||||
return colors[status] || 'gray'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (use case returns domain data):**
|
||||
|
||||
```typescript
|
||||
// application/usecases/GetUserProfileUseCase.ts
|
||||
class GetUserProfileUseCase {
|
||||
execute(userId: string): UserProfileResult {
|
||||
const user = this.repo.findById(userId)
|
||||
const orders = this.orderRepo.findByUser(userId)
|
||||
|
||||
return {
|
||||
user: {
|
||||
id: user.id.value,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
email: user.email.value,
|
||||
avatarUrl: user.avatarUrl,
|
||||
createdAt: user.createdAt
|
||||
},
|
||||
orders: orders.map(o => ({
|
||||
id: o.id.value,
|
||||
total: o.total.amount,
|
||||
currency: o.total.currency,
|
||||
status: o.status,
|
||||
createdAt: o.createdAt
|
||||
})),
|
||||
orderCount: orders.length,
|
||||
totalSpent: this.sumOrders(orders)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// interface_adapters/presenters/UserProfilePresenter.ts
|
||||
class UserProfilePresenter {
|
||||
present(result: UserProfileResult, locale: string): UserProfileViewModel {
|
||||
const formatter = new Intl.DateTimeFormat(locale, {
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
})
|
||||
|
||||
return {
|
||||
displayName: `${result.user.firstName} ${result.user.lastName}`,
|
||||
memberSince: formatter.format(result.user.createdAt),
|
||||
avatarUrl: result.user.avatarUrl,
|
||||
orderSummary: this.formatOrderSummary(result, locale),
|
||||
recentOrders: result.orders.slice(0, 5).map(o =>
|
||||
this.formatOrder(o, locale)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Same use case serves web, mobile, API consumers
|
||||
- Locale/format changes don't touch business logic
|
||||
- UI redesigns don't affect use case tests
|
||||
|
||||
Reference: [Clean Architecture - Presenters](https://www.oreilly.com/library/view/clean-architecture-a/9780134494272/ch23.xhtml)
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
---
|
||||
title: Use Cases Orchestrate Entities Not Implement Business Rules
|
||||
impact: HIGH
|
||||
impactDescription: prevents rule duplication, maintains single source of truth
|
||||
tags: usecase, orchestration, entities, delegation
|
||||
---
|
||||
|
||||
## Use Cases Orchestrate Entities Not Implement Business Rules
|
||||
|
||||
Use cases coordinate the flow of data to and from entities. Business rules belong in entities; use cases should not duplicate or implement them.
|
||||
|
||||
**Incorrect (business rules in use case):**
|
||||
|
||||
```go
|
||||
func (uc *ApplyDiscountUseCase) Execute(orderId string, code string) error {
|
||||
order := uc.repo.Find(orderId)
|
||||
discount := uc.discounts.Find(code)
|
||||
|
||||
// Business rules implemented in use case
|
||||
if order.Status != "pending" {
|
||||
return errors.New("cannot apply discount to processed order")
|
||||
}
|
||||
|
||||
if discount.ExpiresAt.Before(time.Now()) {
|
||||
return errors.New("discount expired")
|
||||
}
|
||||
|
||||
if order.Total.LessThan(discount.MinimumOrder) {
|
||||
return errors.New("order total below minimum")
|
||||
}
|
||||
|
||||
if discount.UsageCount >= discount.MaxUses {
|
||||
return errors.New("discount fully redeemed")
|
||||
}
|
||||
|
||||
// Calculate discount - more business rules
|
||||
var discountAmount Money
|
||||
if discount.Type == "percentage" {
|
||||
discountAmount = order.Total.MultiplyBy(discount.Value / 100)
|
||||
} else {
|
||||
discountAmount = discount.Value
|
||||
}
|
||||
|
||||
order.DiscountAmount = discountAmount
|
||||
order.Total = order.Total.Subtract(discountAmount)
|
||||
uc.repo.Save(order)
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (use case orchestrates, entities implement rules):**
|
||||
|
||||
```go
|
||||
func (uc *ApplyDiscountUseCase) Execute(orderId string, code string) error {
|
||||
order := uc.repo.Find(orderId)
|
||||
discount := uc.discounts.Find(code)
|
||||
|
||||
// Use case orchestrates the interaction
|
||||
if err := order.ApplyDiscount(discount); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uc.repo.Save(order)
|
||||
return nil
|
||||
}
|
||||
|
||||
// domain/entities/order.go
|
||||
func (o *Order) ApplyDiscount(discount *Discount) error {
|
||||
if o.status != OrderStatusPending {
|
||||
return ErrOrderNotPending
|
||||
}
|
||||
|
||||
if !discount.IsValidFor(o.total) {
|
||||
return discount.ValidationError(o.total)
|
||||
}
|
||||
|
||||
o.discount = discount
|
||||
o.discountAmount = discount.CalculateFor(o.total)
|
||||
return nil
|
||||
}
|
||||
|
||||
// domain/entities/discount.go
|
||||
func (d *Discount) IsValidFor(orderTotal Money) bool {
|
||||
return !d.IsExpired() &&
|
||||
!d.IsFullyRedeemed() &&
|
||||
orderTotal.GreaterThanOrEqual(d.minimumOrder)
|
||||
}
|
||||
|
||||
func (d *Discount) CalculateFor(total Money) Money {
|
||||
if d.discountType == PercentageDiscount {
|
||||
return total.MultiplyBy(d.value).DivideBy(100)
|
||||
}
|
||||
return d.value
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Business rules tested once in entity, not in every use case
|
||||
- Rules cannot diverge between use cases
|
||||
- Use case clearly shows workflow, not implementation details
|
||||
|
||||
Reference: [Clean Architecture - Use Cases](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
---
|
||||
title: Each Use Case Has One Reason to Change
|
||||
impact: HIGH
|
||||
impactDescription: isolates change impact, enables parallel development
|
||||
tags: usecase, single-responsibility, cohesion, srp
|
||||
---
|
||||
|
||||
## Each Use Case Has One Reason to Change
|
||||
|
||||
Each use case class should orchestrate exactly one application-specific workflow. Combining multiple use cases creates coupling that forces unrelated changes together.
|
||||
|
||||
**Incorrect (multiple use cases in one class):**
|
||||
|
||||
```python
|
||||
class OrderService:
|
||||
def __init__(self, repo, payment, shipping, notification):
|
||||
self.repo = repo
|
||||
self.payment = payment
|
||||
self.shipping = shipping
|
||||
self.notification = notification
|
||||
|
||||
def create_order(self, items, customer_id):
|
||||
# Use case 1: Create order
|
||||
order = Order.create(items, customer_id)
|
||||
self.repo.save(order)
|
||||
return order
|
||||
|
||||
def process_payment(self, order_id, payment_method):
|
||||
# Use case 2: Process payment
|
||||
order = self.repo.find(order_id)
|
||||
self.payment.charge(order.total, payment_method)
|
||||
order.mark_paid()
|
||||
self.repo.save(order)
|
||||
|
||||
def ship_order(self, order_id, address):
|
||||
# Use case 3: Ship order
|
||||
order = self.repo.find(order_id)
|
||||
tracking = self.shipping.create_shipment(order, address)
|
||||
order.mark_shipped(tracking)
|
||||
self.notification.send_shipped(order)
|
||||
self.repo.save(order)
|
||||
|
||||
# Changes to shipping logic force retest of payment logic
|
||||
```
|
||||
|
||||
**Correct (separate use case classes):**
|
||||
|
||||
```python
|
||||
class CreateOrderUseCase:
|
||||
def __init__(self, repo: OrderRepository):
|
||||
self.repo = repo
|
||||
|
||||
def execute(self, command: CreateOrderCommand) -> OrderId:
|
||||
order = Order.create(command.items, command.customer_id)
|
||||
self.repo.save(order)
|
||||
return order.id
|
||||
|
||||
|
||||
class ProcessPaymentUseCase:
|
||||
def __init__(self, repo: OrderRepository, payment: PaymentGateway):
|
||||
self.repo = repo
|
||||
self.payment = payment
|
||||
|
||||
def execute(self, command: ProcessPaymentCommand) -> None:
|
||||
order = self.repo.find(command.order_id)
|
||||
self.payment.charge(order.total, command.payment_method)
|
||||
order.mark_paid()
|
||||
self.repo.save(order)
|
||||
|
||||
|
||||
class ShipOrderUseCase:
|
||||
def __init__(
|
||||
self,
|
||||
repo: OrderRepository,
|
||||
shipping: ShippingService,
|
||||
notification: NotificationPort
|
||||
):
|
||||
self.repo = repo
|
||||
self.shipping = shipping
|
||||
self.notification = notification
|
||||
|
||||
def execute(self, command: ShipOrderCommand) -> TrackingNumber:
|
||||
order = self.repo.find(command.order_id)
|
||||
tracking = self.shipping.create_shipment(order, command.address)
|
||||
order.mark_shipped(tracking)
|
||||
self.notification.send_shipped(order)
|
||||
self.repo.save(order)
|
||||
return tracking
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Changes to shipping don't affect payment tests
|
||||
- Teams can work on different use cases in parallel
|
||||
- Each use case has minimal dependencies
|
||||
|
||||
Reference: [Clean Architecture - Use Cases](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
---
|
||||
title: Use Case Defines the Transaction Boundary
|
||||
impact: HIGH
|
||||
impactDescription: ensures data consistency, prevents partial updates
|
||||
tags: usecase, transactions, consistency, boundaries
|
||||
---
|
||||
|
||||
## Use Case Defines the Transaction Boundary
|
||||
|
||||
The use case is the natural transaction boundary. All operations within a use case should succeed or fail atomically. Infrastructure manages the transaction; use case defines the scope.
|
||||
|
||||
**Incorrect (transaction managed inside repository):**
|
||||
|
||||
```python
|
||||
class OrderRepository:
|
||||
def save(self, order):
|
||||
with self.db.transaction(): # Transaction per operation
|
||||
self.db.insert('orders', order)
|
||||
|
||||
class InventoryRepository:
|
||||
def reserve(self, items):
|
||||
with self.db.transaction(): # Separate transaction
|
||||
for item in items:
|
||||
self.db.update('inventory', item.sku, decrement=item.qty)
|
||||
|
||||
class PlaceOrderUseCase:
|
||||
def execute(self, command):
|
||||
order = Order.create(command.items)
|
||||
self.orders.save(order) # Commits
|
||||
self.inventory.reserve(command.items) # If this fails, order is orphaned!
|
||||
self.payments.charge(order.total) # If this fails, inventory is reserved but unpaid
|
||||
```
|
||||
|
||||
**Correct (use case defines transaction boundary):**
|
||||
|
||||
```python
|
||||
# application/ports/UnitOfWork.py
|
||||
class UnitOfWork(Protocol):
|
||||
def begin(self) -> None: ...
|
||||
def commit(self) -> None: ...
|
||||
def rollback(self) -> None: ...
|
||||
|
||||
# application/usecases/PlaceOrderUseCase.py
|
||||
class PlaceOrderUseCase:
|
||||
def __init__(self, uow: UnitOfWork, orders, inventory, payments):
|
||||
self.uow = uow
|
||||
self.orders = orders
|
||||
self.inventory = inventory
|
||||
self.payments = payments
|
||||
|
||||
def execute(self, command):
|
||||
self.uow.begin()
|
||||
try:
|
||||
order = Order.create(command.items)
|
||||
|
||||
self.inventory.reserve(command.items)
|
||||
self.payments.charge(order.total)
|
||||
self.orders.save(order)
|
||||
|
||||
self.uow.commit() # All or nothing
|
||||
return order.id
|
||||
|
||||
except Exception as e:
|
||||
self.uow.rollback() # Clean rollback
|
||||
raise
|
||||
|
||||
# Alternative using context manager
|
||||
class PlaceOrderUseCase:
|
||||
def execute(self, command):
|
||||
with self.uow: # Transaction spans entire use case
|
||||
order = Order.create(command.items)
|
||||
self.inventory.reserve(command.items)
|
||||
self.payments.charge(order.total)
|
||||
self.orders.save(order)
|
||||
return order.id
|
||||
```
|
||||
|
||||
**Note:** For distributed systems, consider saga patterns or eventual consistency rather than distributed transactions.
|
||||
|
||||
Reference: [Unit of Work Pattern](https://martinfowler.com/eaaCatalog/unitOfWork.html)
|
||||
@@ -0,0 +1,128 @@
|
||||
---
|
||||
name: clean-code
|
||||
description: Use when writing, reviewing, or refactoring code for maintainability and readability. Triggers on code reviews, naming discussions, function design, error handling, and test writing. Based on Robert C. Martin's Clean Code handbook with modern corrections.
|
||||
---
|
||||
|
||||
# Robert C. Martin (Uncle Bob) Clean Code Best Practices
|
||||
|
||||
Comprehensive software craftsmanship guide based on Robert C. Martin's "Clean Code: A Handbook of Agile Software Craftsmanship", updated with modern corrections where the original 2008 advice has been superseded. Contains 48 rules across 10 categories, prioritized by impact to guide code reviews, refactoring decisions, and new development. Examples are primarily in Java but principles are language-agnostic.
|
||||
|
||||
## When to Apply
|
||||
|
||||
Reference these guidelines when:
|
||||
- Writing new functions, classes, or modules
|
||||
- Naming variables, functions, classes, or files
|
||||
- Reviewing code for maintainability issues
|
||||
- Refactoring existing code to improve clarity
|
||||
- Writing or improving unit tests
|
||||
- Wrapping third-party dependencies
|
||||
|
||||
## Rule Categories by Priority
|
||||
|
||||
| Priority | Category | Impact | Prefix |
|
||||
|----------|----------|--------|--------|
|
||||
| 1 | Meaningful Names | CRITICAL | `name-` |
|
||||
| 2 | Functions | CRITICAL | `func-` |
|
||||
| 3 | Comments | HIGH | `cmt-` |
|
||||
| 4 | Formatting | HIGH | `fmt-` |
|
||||
| 5 | Error Handling | HIGH | `err-` |
|
||||
| 6 | Objects and Data Structures | MEDIUM-HIGH | `obj-` |
|
||||
| 7 | Boundaries | MEDIUM-HIGH | `bound-` |
|
||||
| 8 | Classes and Systems | MEDIUM-HIGH | `class-` |
|
||||
| 9 | Unit Tests | MEDIUM | `test-` |
|
||||
| 10 | Emergence and Simple Design | MEDIUM | `emerge-` |
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### 1. Meaningful Names (CRITICAL)
|
||||
|
||||
- [`name-intention-revealing`](references/name-intention-revealing.md) - Use names that reveal intent
|
||||
- [`name-avoid-disinformation`](references/name-avoid-disinformation.md) - Avoid misleading names
|
||||
- [`name-meaningful-distinctions`](references/name-meaningful-distinctions.md) - Make meaningful distinctions
|
||||
- [`name-pronounceable`](references/name-pronounceable.md) - Use pronounceable names
|
||||
- [`name-searchable`](references/name-searchable.md) - Use searchable names
|
||||
- [`name-avoid-encodings`](references/name-avoid-encodings.md) - Avoid encodings in names
|
||||
- [`name-class-noun`](references/name-class-noun.md) - Use noun phrases for class names
|
||||
- [`name-method-verb`](references/name-method-verb.md) - Use verb phrases for method names
|
||||
|
||||
### 2. Functions (CRITICAL)
|
||||
|
||||
- [`func-small`](references/func-small.md) - Keep functions small
|
||||
- [`func-one-thing`](references/func-one-thing.md) - Functions should do one thing
|
||||
- [`func-abstraction-level`](references/func-abstraction-level.md) - Maintain one level of abstraction
|
||||
- [`func-minimize-arguments`](references/func-minimize-arguments.md) - Minimize function arguments
|
||||
- [`func-no-side-effects`](references/func-no-side-effects.md) - Avoid side effects
|
||||
- [`func-command-query-separation`](references/func-command-query-separation.md) - Separate commands from queries
|
||||
- [`func-dry`](references/func-dry.md) - Do not repeat yourself
|
||||
|
||||
### 3. Comments (HIGH)
|
||||
|
||||
- [`cmt-express-in-code`](references/cmt-express-in-code.md) - Express yourself in code, not comments
|
||||
- [`cmt-explain-intent`](references/cmt-explain-intent.md) - Use comments to explain intent
|
||||
- [`cmt-avoid-redundant`](references/cmt-avoid-redundant.md) - Avoid redundant comments
|
||||
- [`cmt-avoid-commented-out-code`](references/cmt-avoid-commented-out-code.md) - Delete commented-out code
|
||||
- [`cmt-warning-consequences`](references/cmt-warning-consequences.md) - Use warning comments for consequences
|
||||
|
||||
### 4. Formatting (HIGH)
|
||||
|
||||
- [`fmt-vertical-formatting`](references/fmt-vertical-formatting.md) - Use vertical formatting for readability
|
||||
- [`fmt-horizontal-alignment`](references/fmt-horizontal-alignment.md) - Avoid horizontal alignment
|
||||
- [`fmt-team-rules`](references/fmt-team-rules.md) - Follow team formatting rules
|
||||
- [`fmt-indentation`](references/fmt-indentation.md) - Respect indentation rules
|
||||
|
||||
### 5. Error Handling (HIGH)
|
||||
|
||||
- [`err-use-exceptions`](references/err-use-exceptions.md) - Separate error handling from happy path
|
||||
- [`err-write-try-catch-first`](references/err-write-try-catch-first.md) - Write try-catch-finally first
|
||||
- [`err-provide-context`](references/err-provide-context.md) - Provide context with exceptions
|
||||
- [`err-define-by-caller-needs`](references/err-define-by-caller-needs.md) - Define exceptions by caller needs
|
||||
- [`err-avoid-null`](references/err-avoid-null.md) - Avoid returning and passing null
|
||||
|
||||
### 6. Objects and Data Structures (MEDIUM-HIGH)
|
||||
|
||||
- [`obj-data-abstraction`](references/obj-data-abstraction.md) - Hide data behind abstractions
|
||||
- [`obj-data-object-asymmetry`](references/obj-data-object-asymmetry.md) - Understand data/object anti-symmetry
|
||||
- [`obj-law-of-demeter`](references/obj-law-of-demeter.md) - Follow the Law of Demeter
|
||||
- [`obj-avoid-hybrids`](references/obj-avoid-hybrids.md) - Avoid hybrid data-object structures
|
||||
- [`obj-dto`](references/obj-dto.md) - Use DTOs for data transfer
|
||||
|
||||
### 7. Boundaries (MEDIUM-HIGH)
|
||||
|
||||
- [`bound-wrap-third-party`](references/bound-wrap-third-party.md) - Wrap third-party APIs
|
||||
- [`bound-learning-tests`](references/bound-learning-tests.md) - Write learning tests for third-party code
|
||||
|
||||
### 8. Classes and Systems (MEDIUM-HIGH)
|
||||
|
||||
- [`class-small`](references/class-small.md) - Keep classes small
|
||||
- [`class-cohesion`](references/class-cohesion.md) - Maintain class cohesion
|
||||
- [`class-organize-for-change`](references/class-organize-for-change.md) - Organize classes for change
|
||||
- [`class-isolate-from-change`](references/class-isolate-from-change.md) - Isolate classes from change
|
||||
- [`class-separate-concerns`](references/class-separate-concerns.md) - Separate construction from use
|
||||
|
||||
### 9. Unit Tests (MEDIUM)
|
||||
|
||||
- [`test-first-law`](references/test-first-law.md) - Follow the three laws of TDD
|
||||
- [`test-keep-clean`](references/test-keep-clean.md) - Keep tests clean
|
||||
- [`test-one-assert`](references/test-one-assert.md) - One concept per test
|
||||
- [`test-first-principles`](references/test-first-principles.md) - Follow FIRST principles
|
||||
- [`test-build-operate-check`](references/test-build-operate-check.md) - Use Build-Operate-Check pattern
|
||||
|
||||
### 10. Emergence and Simple Design (MEDIUM)
|
||||
|
||||
- [`emerge-simple-design`](references/emerge-simple-design.md) - Follow the four rules of simple design
|
||||
- [`emerge-expressiveness`](references/emerge-expressiveness.md) - Maximize expressiveness
|
||||
|
||||
## How to Use
|
||||
|
||||
Read individual reference files for detailed explanations and code examples:
|
||||
|
||||
- [Section definitions](references/_sections.md) - Category structure and impact levels
|
||||
- [Rule template](assets/templates/_template.md) - Template for adding new rules
|
||||
|
||||
## Reference Files
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| [references/_sections.md](references/_sections.md) | Category definitions and ordering |
|
||||
| [assets/templates/_template.md](assets/templates/_template.md) | Template for new rules |
|
||||
| [metadata.json](metadata.json) | Version and reference information |
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
title: Rule Title Here
|
||||
impact: MEDIUM
|
||||
impactDescription: description of impact (e.g., "eliminates confusion")
|
||||
tags: category-prefix, keyword1, keyword2
|
||||
---
|
||||
|
||||
## Rule Title Here
|
||||
|
||||
Brief explanation of the rule and why it matters. Focus on the principle and its impact on code maintainability and readability.
|
||||
|
||||
**Incorrect (description of what's wrong):**
|
||||
|
||||
```java
|
||||
// Bad code example here showing the anti-pattern
|
||||
// Comment explaining the problem
|
||||
```
|
||||
|
||||
**Correct (description of what's right):**
|
||||
|
||||
```java
|
||||
// Good code example here showing the solution
|
||||
// Comment explaining the benefit
|
||||
```
|
||||
|
||||
**When NOT to use this pattern:**
|
||||
- Exception case 1
|
||||
- Exception case 2
|
||||
|
||||
Reference: [Clean Code, Chapter N: Topic](https://www.oreilly.com/library/view/clean-code-a/9780136083238/)
|
||||
@@ -0,0 +1,56 @@
|
||||
# Sections
|
||||
|
||||
This file defines all sections, their ordering, impact levels, and descriptions.
|
||||
The section ID (in parentheses) is the filename prefix used to group rules.
|
||||
|
||||
---
|
||||
|
||||
## 1. Meaningful Names (name)
|
||||
|
||||
**Impact:** CRITICAL
|
||||
**Description:** Names are the primary documentation. Bad names cascade confusion throughout the codebase, forcing every reader to decipher intent.
|
||||
|
||||
## 2. Functions (func)
|
||||
|
||||
**Impact:** CRITICAL
|
||||
**Description:** Functions are the verbs of code. Small, focused functions enable understanding, testing, and reuse while reducing complexity.
|
||||
|
||||
## 3. Comments (cmt)
|
||||
|
||||
**Impact:** HIGH
|
||||
**Description:** Comments should explain intent, not obvious mechanics. Wrong or stale comments actively mislead readers and cost more than no comments.
|
||||
|
||||
## 4. Formatting (fmt)
|
||||
|
||||
**Impact:** HIGH
|
||||
**Description:** Consistent formatting reduces cognitive load. Code should read top-to-bottom like a newspaper article.
|
||||
|
||||
## 5. Error Handling (err)
|
||||
|
||||
**Impact:** HIGH
|
||||
**Description:** Clean error handling separates happy path from exceptional cases. Use the error mechanism idiomatic to your language (exceptions, Result types, or explicit error returns).
|
||||
|
||||
## 6. Objects and Data Structures (obj)
|
||||
|
||||
**Impact:** MEDIUM-HIGH
|
||||
**Description:** Objects hide data and expose behavior; data structures expose data and have no behavior. Mixing these creates hybrid messes.
|
||||
|
||||
## 7. Boundaries (bound)
|
||||
|
||||
**Impact:** MEDIUM-HIGH
|
||||
**Description:** Third-party code and external APIs are boundaries. Wrap them to isolate change, ease testing, and maintain control over your codebase's interfaces.
|
||||
|
||||
## 8. Classes and Systems (class)
|
||||
|
||||
**Impact:** MEDIUM-HIGH
|
||||
**Description:** Classes should be small and have a single responsibility. Systems should separate construction from use. SRP and DIP are among the highest-leverage design principles.
|
||||
|
||||
## 9. Unit Tests (test)
|
||||
|
||||
**Impact:** MEDIUM
|
||||
**Description:** Tests are first-class citizens that enable safe refactoring. Test code deserves the same care as production code.
|
||||
|
||||
## 10. Emergence and Simple Design (emerge)
|
||||
|
||||
**Impact:** MEDIUM
|
||||
**Description:** Good design emerges from following four rules in order: pass tests, reveal intent, eliminate duplication, minimize elements. Resist the urge to over-engineer.
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
title: Write Learning Tests for Third-Party Code
|
||||
impact: MEDIUM
|
||||
impactDescription: catches 90%+ of breaking changes in dependency upgrades before production
|
||||
tags: bound, boundary, learning-tests, third-party, testing
|
||||
---
|
||||
|
||||
## Write Learning Tests for Third-Party Code
|
||||
|
||||
When integrating a third-party library, write small tests that verify your understanding of how the API works. These tests serve double duty: they help you learn the API, and they act as a safety net when upgrading the library.
|
||||
|
||||
**Incorrect (learning by trial and error in production code):**
|
||||
|
||||
```java
|
||||
// Experimenting directly in application code
|
||||
public void importCustomerRecord(String json) {
|
||||
// Does Gson handle null fields? Let's find out...
|
||||
Gson gson = new Gson();
|
||||
CustomerRecord record = gson.fromJson(json, CustomerRecord.class);
|
||||
// Does this throw or return null for missing fields?
|
||||
// Who knows — we'll find out in production
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (learning tests verify assumptions):**
|
||||
|
||||
```java
|
||||
@Test
|
||||
public void gsonShouldDeserializeNullFieldsAsNull() {
|
||||
Gson gson = new Gson();
|
||||
Data data = gson.fromJson("{}", Data.class);
|
||||
|
||||
assertNull(data.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void gsonShouldHandleMalformedJson() {
|
||||
Gson gson = new Gson();
|
||||
|
||||
assertThrows(JsonSyntaxException.class,
|
||||
() -> gson.fromJson("{invalid", Data.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void gsonShouldIgnoreUnknownFields() {
|
||||
Gson gson = new Gson();
|
||||
Data data = gson.fromJson("{\"name\":\"test\",\"unknown\":true}", Data.class);
|
||||
|
||||
assertEquals("test", data.getName());
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Free education on the API's behavior
|
||||
- When you upgrade the library, these tests break first — before your production code does
|
||||
- Documents exactly which behaviors your code relies on
|
||||
- Takes minutes to write but saves hours of debugging
|
||||
|
||||
Reference: [Clean Code, Chapter 8: Boundaries](https://www.oreilly.com/library/view/clean-code-a/9780136083238/)
|
||||
@@ -0,0 +1,77 @@
|
||||
---
|
||||
title: Wrap Third-Party APIs
|
||||
impact: HIGH
|
||||
impactDescription: reduces dependency change points from N call sites to 1 wrapper
|
||||
tags: bound, boundary, wrapper, third-party, dependency
|
||||
---
|
||||
|
||||
## Wrap Third-Party APIs
|
||||
|
||||
When you use a third-party API, wrap it in a class you control. This minimizes your dependency on the third party, makes testing easier, and gives you a single place to change when the API evolves.
|
||||
|
||||
**Incorrect (direct dependency scattered across codebase):**
|
||||
|
||||
```java
|
||||
// Every caller depends on ACMEPort directly
|
||||
public class SensorReader {
|
||||
public void readSensors() {
|
||||
try {
|
||||
ACMEPort port = new ACMEPort(12);
|
||||
port.open();
|
||||
// ...
|
||||
} catch (ACMEDeviceException e) {
|
||||
logger.log("Device exception", e);
|
||||
} catch (ACMEConfigException e) {
|
||||
logger.log("Config exception", e);
|
||||
} catch (ACMECommunicationException e) {
|
||||
logger.log("Communication exception", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
// If ACME changes their API, every file that uses it must change
|
||||
```
|
||||
|
||||
**Correct (wrapped behind your own interface):**
|
||||
|
||||
```java
|
||||
public class LocalPort {
|
||||
private ACMEPort innerPort;
|
||||
|
||||
public LocalPort(int portNumber) {
|
||||
innerPort = new ACMEPort(portNumber);
|
||||
}
|
||||
|
||||
public void open() {
|
||||
try {
|
||||
innerPort.open();
|
||||
} catch (ACMEDeviceException | ACMEConfigException | ACMECommunicationException e) {
|
||||
throw new PortException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Callers depend only on your wrapper
|
||||
public class SensorReader {
|
||||
public void readSensors() {
|
||||
try {
|
||||
LocalPort port = new LocalPort(12);
|
||||
port.open();
|
||||
// ...
|
||||
} catch (PortException e) {
|
||||
logger.log("Port error", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- One place to change when the third-party API changes
|
||||
- Easier to mock in tests (mock `LocalPort`, not the vendor library)
|
||||
- Freedom to switch vendors without rewriting the entire codebase
|
||||
- Consistent exception hierarchy under your control
|
||||
|
||||
**When NOT to wrap:**
|
||||
- Stable, widely-used standard library APIs (e.g., `java.util.List`, `String`) do not need wrapping.
|
||||
- If the wrapper would be a 1:1 passthrough with no value added, it's premature. Wait until you have a concrete reason (testing, migration, or simplification).
|
||||
|
||||
Reference: [Clean Code, Chapter 8: Boundaries](https://www.oreilly.com/library/view/clean-code-a/9780136083238/)
|
||||
@@ -0,0 +1,66 @@
|
||||
---
|
||||
title: Maintain Class Cohesion
|
||||
impact: MEDIUM
|
||||
impactDescription: enables extracting focused subclasses
|
||||
tags: class, cohesion, variables, methods
|
||||
---
|
||||
|
||||
## Maintain Class Cohesion
|
||||
|
||||
Classes should have a small number of instance variables. Each method should manipulate one or more of those variables. The more variables a method manipulates, the more cohesive the method is to its class.
|
||||
|
||||
**Incorrect (low cohesion - methods use different variables):**
|
||||
|
||||
```java
|
||||
public class Utility {
|
||||
private Database db;
|
||||
private EmailService email;
|
||||
private Logger logger;
|
||||
private Cache cache;
|
||||
|
||||
public void saveUser(User user) {
|
||||
db.save(user); // Only uses db
|
||||
logger.log("Saved user"); // Only uses logger
|
||||
}
|
||||
|
||||
public void sendNotification(String message) {
|
||||
email.send(message); // Only uses email
|
||||
}
|
||||
|
||||
public Object getCached(String key) {
|
||||
return cache.get(key); // Only uses cache
|
||||
}
|
||||
}
|
||||
// Each method uses different instance variables - low cohesion
|
||||
```
|
||||
|
||||
**Correct (high cohesion - methods use shared variables):**
|
||||
|
||||
```java
|
||||
public class Stack {
|
||||
private int topOfStack = 0;
|
||||
private List<Integer> elements = new LinkedList<>();
|
||||
|
||||
public int size() {
|
||||
return topOfStack; // Uses topOfStack
|
||||
}
|
||||
|
||||
public void push(int element) {
|
||||
topOfStack++; // Uses topOfStack
|
||||
elements.add(element); // Uses elements
|
||||
}
|
||||
|
||||
public int pop() throws EmptyStackException {
|
||||
if (topOfStack == 0)
|
||||
throw new EmptyStackException();
|
||||
int element = elements.get(--topOfStack); // Uses both
|
||||
elements.remove(topOfStack); // Uses both
|
||||
return element;
|
||||
}
|
||||
}
|
||||
// Every method uses the same instance variables - high cohesion
|
||||
```
|
||||
|
||||
**When cohesion breaks down:** If a subset of methods only uses a subset of variables, extract those methods and variables into a separate class.
|
||||
|
||||
Reference: [Clean Code, Chapter 10: Classes](https://www.oreilly.com/library/view/clean-code-a/9780136083238/)
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
title: Isolate Classes from Change
|
||||
impact: MEDIUM
|
||||
impactDescription: enables testing and reduces coupling
|
||||
tags: class, dip, interfaces, coupling
|
||||
---
|
||||
|
||||
## Isolate Classes from Change
|
||||
|
||||
Depend on abstractions, not concretions. Classes should depend on interfaces rather than concrete implementations. This enables testing and reduces the impact of change.
|
||||
|
||||
**Incorrect (depends on concrete implementation):**
|
||||
|
||||
```java
|
||||
public class Portfolio {
|
||||
private NYSEStockExchange exchange;
|
||||
|
||||
public Portfolio(NYSEStockExchange exchange) {
|
||||
this.exchange = exchange;
|
||||
}
|
||||
|
||||
public Money value() {
|
||||
Money total = Money.ZERO;
|
||||
for (String symbol : holdings.keySet()) {
|
||||
// Cannot test without real stock exchange connection
|
||||
total = total.plus(exchange.currentPrice(symbol)
|
||||
.times(holdings.get(symbol)));
|
||||
}
|
||||
return total;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (depends on abstraction):**
|
||||
|
||||
```java
|
||||
public interface StockExchange {
|
||||
Money currentPrice(String symbol);
|
||||
}
|
||||
|
||||
public class Portfolio {
|
||||
private StockExchange exchange;
|
||||
|
||||
public Portfolio(StockExchange exchange) {
|
||||
this.exchange = exchange;
|
||||
}
|
||||
|
||||
public Money value() {
|
||||
Money total = Money.ZERO;
|
||||
for (String symbol : holdings.keySet()) {
|
||||
total = total.plus(exchange.currentPrice(symbol)
|
||||
.times(holdings.get(symbol)));
|
||||
}
|
||||
return total;
|
||||
}
|
||||
}
|
||||
|
||||
// Production
|
||||
Portfolio portfolio = new Portfolio(new NYSEStockExchange());
|
||||
|
||||
// Testing
|
||||
@Test
|
||||
public void portfolioValueShouldSumAllHoldings() {
|
||||
StockExchange mockExchange = mock(StockExchange.class);
|
||||
when(mockExchange.currentPrice("AAPL")).thenReturn(Money.dollars(150));
|
||||
|
||||
Portfolio portfolio = new Portfolio(mockExchange);
|
||||
portfolio.add("AAPL", 10);
|
||||
|
||||
assertEquals(Money.dollars(1500), portfolio.value());
|
||||
}
|
||||
```
|
||||
|
||||
**Dependency Inversion Principle (DIP):** High-level modules should not depend on low-level modules. Both should depend on abstractions.
|
||||
|
||||
Reference: [Clean Code, Chapter 10: Classes](https://www.oreilly.com/library/view/clean-code-a/9780136083238/)
|
||||
@@ -0,0 +1,70 @@
|
||||
---
|
||||
title: Organize Classes for Change
|
||||
impact: MEDIUM
|
||||
impactDescription: minimizes modification risk when requirements change
|
||||
tags: class, ocp, change, design
|
||||
---
|
||||
|
||||
## Organize Classes for Change
|
||||
|
||||
Classes should be organized so that change is minimally invasive. Following the Open-Closed Principle (OCP): classes should be open for extension but closed for modification.
|
||||
|
||||
**Incorrect (must modify class for each new SQL type):**
|
||||
|
||||
```java
|
||||
public class Sql {
|
||||
public Sql(String table, Column[] columns) { /* ... */ }
|
||||
|
||||
public String create() { /* ... */ }
|
||||
public String insert(Object[] fields) { /* ... */ }
|
||||
public String selectAll() { /* ... */ }
|
||||
public String findByKey(String key) { /* ... */ }
|
||||
// Must modify this class to add update, delete, etc.
|
||||
// Each change risks breaking existing functionality
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (extend without modifying):**
|
||||
|
||||
```java
|
||||
public abstract class Sql {
|
||||
protected String table;
|
||||
protected Column[] columns;
|
||||
|
||||
public Sql(String table, Column[] columns) {
|
||||
this.table = table;
|
||||
this.columns = columns;
|
||||
}
|
||||
|
||||
public abstract String generate();
|
||||
}
|
||||
|
||||
public class CreateSql extends Sql {
|
||||
public CreateSql(String table, Column[] columns) {
|
||||
super(table, columns);
|
||||
}
|
||||
|
||||
public String generate() { /* CREATE TABLE ... */ }
|
||||
}
|
||||
|
||||
public class InsertSql extends Sql {
|
||||
private Object[] fields;
|
||||
|
||||
public InsertSql(String table, Column[] columns, Object[] fields) {
|
||||
super(table, columns);
|
||||
this.fields = fields;
|
||||
}
|
||||
|
||||
public String generate() { /* INSERT INTO ... */ }
|
||||
}
|
||||
|
||||
// Adding UpdateSql doesn't require modifying existing classes
|
||||
public class UpdateSql extends Sql { /* ... */ }
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Adding new SQL types = adding new classes
|
||||
- Existing classes remain untouched
|
||||
- Each class does one thing
|
||||
|
||||
Reference: [Clean Code, Chapter 10: Classes](https://www.oreilly.com/library/view/clean-code-a/9780136083238/)
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
title: Separate Construction from Use
|
||||
impact: MEDIUM
|
||||
impactDescription: enables testing and reduces coupling
|
||||
tags: class, construction, factories, separation
|
||||
---
|
||||
|
||||
## Separate Construction from Use
|
||||
|
||||
The startup process of object construction is a separate concern from runtime logic. Applications should separate the main function (which builds objects) from the rest of the system (which uses them).
|
||||
|
||||
**Incorrect (construction mixed with use):**
|
||||
|
||||
```java
|
||||
public class OrderProcessor {
|
||||
public void processOrder(Order order) {
|
||||
// Construction buried in business logic
|
||||
EmailService emailService = new SmtpEmailService(
|
||||
"smtp.example.com", 587, "user", "password");
|
||||
InventoryService inventory = new InventoryServiceImpl(
|
||||
new PostgresConnection("localhost", 5432));
|
||||
PaymentGateway gateway = new StripeGateway(API_KEY);
|
||||
|
||||
// Business logic
|
||||
inventory.reserve(order.getItems());
|
||||
gateway.charge(order.getPayment());
|
||||
emailService.sendConfirmation(order);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (construction separated from use):**
|
||||
|
||||
```java
|
||||
// Main builds the object graph
|
||||
public class Application {
|
||||
public static void main(String[] args) {
|
||||
EmailService emailService = new SmtpEmailService(
|
||||
Config.get("smtp.host"), Config.getInt("smtp.port"));
|
||||
InventoryService inventory = new InventoryServiceImpl(
|
||||
new PostgresConnection(Config.get("db.url")));
|
||||
PaymentGateway gateway = new StripeGateway(Config.get("stripe.key"));
|
||||
|
||||
OrderProcessor processor = new OrderProcessor(
|
||||
emailService, inventory, gateway);
|
||||
|
||||
processor.start();
|
||||
}
|
||||
}
|
||||
|
||||
// Business class receives dependencies
|
||||
public class OrderProcessor {
|
||||
private final EmailService emailService;
|
||||
private final InventoryService inventory;
|
||||
private final PaymentGateway gateway;
|
||||
|
||||
public OrderProcessor(EmailService email, InventoryService inv,
|
||||
PaymentGateway pay) {
|
||||
this.emailService = email;
|
||||
this.inventory = inv;
|
||||
this.gateway = pay;
|
||||
}
|
||||
|
||||
public void processOrder(Order order) {
|
||||
inventory.reserve(order.getItems());
|
||||
gateway.charge(order.getPayment());
|
||||
emailService.sendConfirmation(order);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Alternative:** Use Dependency Injection frameworks (Spring, Guice) to manage construction.
|
||||
|
||||
Reference: [Clean Code, Chapter 11: Systems](https://www.oreilly.com/library/view/clean-code-a/9780136083238/)
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
title: Keep Classes Small
|
||||
impact: MEDIUM
|
||||
impactDescription: enables comprehension and focused testing
|
||||
tags: class, size, srp, cohesion
|
||||
---
|
||||
|
||||
## Keep Classes Small
|
||||
|
||||
Classes should be small. With functions, we measure size by counting lines. With classes, we count responsibilities. A class should have only one reason to change.
|
||||
|
||||
**Incorrect (large class with multiple responsibilities):**
|
||||
|
||||
```java
|
||||
public class SuperDashboard extends JFrame implements MetaDataUser {
|
||||
public String getCustomizerLanguagePath() { /* ... */ }
|
||||
public void setSystemConfigPath(String path) { /* ... */ }
|
||||
public String getSystemConfigDocument() { /* ... */ }
|
||||
public void setSystemConfigDocument(String doc) { /* ... */ }
|
||||
public boolean getGuruState() { /* ... */ }
|
||||
public boolean getNoviceState() { /* ... */ }
|
||||
public boolean getOpenSourceState() { /* ... */ }
|
||||
public void showObject(MetaObject object) { /* ... */ }
|
||||
public void showProgress(String s) { /* ... */ }
|
||||
public boolean isMetadataDirty() { /* ... */ }
|
||||
public void setIsMetadataDirty(boolean dirty) { /* ... */ }
|
||||
public Component getLastFocusedComponent() { /* ... */ }
|
||||
// 70+ more methods...
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (small focused classes):**
|
||||
|
||||
```java
|
||||
public class Version {
|
||||
public int getMajorVersionNumber() { /* ... */ }
|
||||
public int getMinorVersionNumber() { /* ... */ }
|
||||
public int getBuildNumber() { /* ... */ }
|
||||
}
|
||||
|
||||
public class Dashboard extends JFrame {
|
||||
private final Version version;
|
||||
private final UserPreferences preferences;
|
||||
private final ProgressIndicator progress;
|
||||
|
||||
public void show() { /* ... */ }
|
||||
}
|
||||
|
||||
public class UserPreferences {
|
||||
public Language getLanguage() { /* ... */ }
|
||||
public boolean isNovice() { /* ... */ }
|
||||
public boolean isGuru() { /* ... */ }
|
||||
}
|
||||
|
||||
public class ProgressIndicator {
|
||||
public void showProgress(String message) { /* ... */ }
|
||||
public void hideProgress() { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
**Single Responsibility Principle (SRP):** A class should have one, and only one, reason to change.
|
||||
|
||||
Reference: [Clean Code, Chapter 10: Classes](https://www.oreilly.com/library/view/clean-code-a/9780136083238/)
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
---
|
||||
title: Delete Commented-Out Code
|
||||
impact: HIGH
|
||||
impactDescription: prevents code archaeology confusion
|
||||
tags: cmt, dead-code, version-control, clutter
|
||||
---
|
||||
|
||||
## Delete Commented-Out Code
|
||||
|
||||
Commented-out code is an abomination. Others who see it will not have the courage to delete it. They think it must be there for a reason. It rots and becomes increasingly irrelevant.
|
||||
|
||||
**Incorrect (commented-out code persisting):**
|
||||
|
||||
```java
|
||||
public void processOrder(Order order) {
|
||||
validateOrder(order);
|
||||
// calculateLegacyDiscount(order);
|
||||
// if (order.getCustomer().isPremium()) {
|
||||
// applyPremiumDiscount(order);
|
||||
// }
|
||||
calculateDiscount(order);
|
||||
// sendNotification(order);
|
||||
// logOrderDetails(order);
|
||||
saveOrder(order);
|
||||
notifyWarehouse(order);
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (clean code, trust version control):**
|
||||
|
||||
```java
|
||||
public void processOrder(Order order) {
|
||||
validateOrder(order);
|
||||
calculateDiscount(order);
|
||||
saveOrder(order);
|
||||
notifyWarehouse(order);
|
||||
}
|
||||
```
|
||||
|
||||
**Why delete it?**
|
||||
- Version control remembers everything
|
||||
- Commented code creates confusion: Is it needed? Why is it there?
|
||||
- It clutters the file and makes scanning harder
|
||||
- It becomes stale and misleading over time
|
||||
|
||||
**If you really need it:** Create a git branch, or add a reference comment pointing to the commit where it was removed.
|
||||
|
||||
Reference: [Clean Code, Chapter 4: Comments](https://www.oreilly.com/library/view/clean-code-a/9780136083238/)
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
title: Avoid Redundant Comments
|
||||
impact: HIGH
|
||||
impactDescription: eliminates noise that obscures important information
|
||||
tags: cmt, redundant, noise, clutter
|
||||
---
|
||||
|
||||
## Avoid Redundant Comments
|
||||
|
||||
A comment that merely restates what the code does is worse than no comment. It adds clutter, becomes stale, and trains readers to ignore comments entirely.
|
||||
|
||||
**Incorrect (redundant comments):**
|
||||
|
||||
```java
|
||||
/**
|
||||
* The name of the customer
|
||||
*/
|
||||
private String customerName;
|
||||
|
||||
/**
|
||||
* Returns the name
|
||||
* @return the name
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
// Check if the account is closed
|
||||
if (account.isClosed()) {
|
||||
// Throw an exception
|
||||
throw new AccountClosedException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Default constructor
|
||||
*/
|
||||
public Account() {
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (comment only when necessary):**
|
||||
|
||||
```java
|
||||
private String customerName;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
if (account.isClosed()) {
|
||||
throw new AccountClosedException();
|
||||
}
|
||||
|
||||
public Account() {
|
||||
}
|
||||
|
||||
// Comments that add value:
|
||||
|
||||
// RFC 2822 date format required by legacy email parser
|
||||
private static final String DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss Z";
|
||||
|
||||
// Thread-safe: ConcurrentHashMap handles synchronization
|
||||
private Map<String, Session> activeSessions = new ConcurrentHashMap<>();
|
||||
```
|
||||
|
||||
**Rule of thumb:** If the comment just repeats the code in English, delete it.
|
||||
|
||||
Reference: [Clean Code, Chapter 4: Comments](https://www.oreilly.com/library/view/clean-code-a/9780136083238/)
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
title: Use Comments to Explain Intent
|
||||
impact: HIGH
|
||||
impactDescription: preserves decision rationale for future readers
|
||||
tags: cmt, intent, why, rationale
|
||||
---
|
||||
|
||||
## Use Comments to Explain Intent
|
||||
|
||||
Good comments explain WHY, not WHAT. The code already tells you what it does. Comments should explain the reasoning behind a decision that cannot be expressed in code.
|
||||
|
||||
**Incorrect (narrating the code):**
|
||||
|
||||
```java
|
||||
// Increment counter by 1
|
||||
counter++;
|
||||
|
||||
// Loop through all employees
|
||||
for (Employee employee : employees) {
|
||||
// Check if employee is active
|
||||
if (employee.isActive()) {
|
||||
// Add employee to list
|
||||
activeEmployees.add(employee);
|
||||
}
|
||||
}
|
||||
|
||||
// Set the name
|
||||
this.name = name;
|
||||
```
|
||||
|
||||
**Correct (explaining intent):**
|
||||
|
||||
```java
|
||||
// We use insertion sort here because the list is almost always nearly sorted,
|
||||
// and insertion sort is O(n) for nearly sorted data vs O(n log n) for quicksort
|
||||
insertionSort(nearlyOrderedList);
|
||||
|
||||
// Format matches the external API's expected date format (ISO 8601 without timezone)
|
||||
// See: https://api.vendor.com/docs/date-format
|
||||
String formattedDate = date.format(DateTimeFormatter.ISO_LOCAL_DATE);
|
||||
|
||||
// Bias toward newer sessions - users expect recent data first
|
||||
// Business requirement from Product (ticket PROD-1234)
|
||||
sessions.sort(Comparator.comparing(Session::getCreatedAt).reversed());
|
||||
```
|
||||
|
||||
**Good comment situations:**
|
||||
- Legal comments (copyright, license)
|
||||
- Explanation of intent or rationale
|
||||
- Clarification of obscure API behavior
|
||||
- Warning of consequences
|
||||
- TODO comments (temporary)
|
||||
|
||||
Reference: [Clean Code, Chapter 4: Comments](https://www.oreilly.com/library/view/clean-code-a/9780136083238/)
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
title: Express Yourself in Code, Not Comments
|
||||
impact: CRITICAL
|
||||
impactDescription: eliminates stale comment risk
|
||||
tags: cmt, self-documenting, refactoring, clarity
|
||||
---
|
||||
|
||||
## Express Yourself in Code, Not Comments
|
||||
|
||||
In many cases, creating a function that says the same thing as the comment you wanted to write is better. Code can be refactored to be self-explanatory.
|
||||
|
||||
**Incorrect (comment explains obscure code):**
|
||||
|
||||
```java
|
||||
// Check to see if the employee is eligible for full benefits
|
||||
if ((employee.flags & HOURLY_FLAG) && (employee.age > 65)) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// Add 30 days to the current date
|
||||
Date newDate = new Date(currentDate.getTime() + (30L * 24 * 60 * 60 * 1000));
|
||||
|
||||
// Returns true if the string contains only digits
|
||||
boolean result = str.matches("[0-9]+");
|
||||
```
|
||||
|
||||
**Correct (code expresses intent):**
|
||||
|
||||
```java
|
||||
if (employee.isEligibleForFullBenefits()) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// In Employee class:
|
||||
public boolean isEligibleForFullBenefits() {
|
||||
return isHourlyWorker() && age > RETIREMENT_AGE;
|
||||
}
|
||||
|
||||
Date newDate = currentDate.plusDays(30);
|
||||
|
||||
boolean result = StringUtils.isNumeric(str);
|
||||
// Or define your own:
|
||||
boolean result = containsOnlyDigits(str);
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Function names are searchable
|
||||
- Functions can be tested
|
||||
- Code documents itself
|
||||
- No risk of comment becoming stale
|
||||
|
||||
**When comments are still valuable:**
|
||||
- **Why, not what:** Comments explaining *why* a decision was made (business rules, regulatory requirements, algorithm choices) cannot be replaced by code.
|
||||
- **Warnings:** Thread-safety, performance implications, or non-obvious side effects deserve explicit comments.
|
||||
- **Legal/license comments:** Required by policy and cannot be expressed as code.
|
||||
- **Public API documentation:** Javadoc/docstrings for public APIs are expected and useful.
|
||||
|
||||
Reference: [Clean Code, Chapter 4: Comments](https://www.oreilly.com/library/view/clean-code-a/9780136083238/)
|
||||
@@ -0,0 +1,66 @@
|
||||
---
|
||||
title: Use Warning Comments for Consequences
|
||||
impact: MEDIUM
|
||||
impactDescription: prevents costly mistakes by future maintainers
|
||||
tags: cmt, warning, consequences, safety
|
||||
---
|
||||
|
||||
## Use Warning Comments for Consequences
|
||||
|
||||
Comments that warn other programmers about certain consequences are valuable. They prevent others from making mistakes that have non-obvious impacts.
|
||||
|
||||
**Incorrect (no warning about hidden dangers):**
|
||||
|
||||
```java
|
||||
public static SimpleDateFormat makeStandardDateFormat() {
|
||||
return new SimpleDateFormat("yyyy-MM-dd");
|
||||
}
|
||||
|
||||
public void runExpensiveOperation() {
|
||||
// Process all historical data
|
||||
processAllRecords();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithRealDatabase() {
|
||||
database.connect();
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (warnings prevent mistakes):**
|
||||
|
||||
```java
|
||||
// WARNING: DateTimeFormatter is thread-safe, but this legacy method returns
|
||||
// SimpleDateFormat which is NOT thread-safe. Do not cache or share the result.
|
||||
// Consider migrating callers to use DateTimeFormatter.ISO_LOCAL_DATE directly.
|
||||
public static SimpleDateFormat makeStandardDateFormat() {
|
||||
return new SimpleDateFormat("yyyy-MM-dd");
|
||||
}
|
||||
|
||||
// Preferred (Java 8+): thread-safe, no warning needed
|
||||
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||
|
||||
// WARNING: This operation takes ~30 minutes on production data
|
||||
// and consumes significant memory. Run only during maintenance windows.
|
||||
public void runExpensiveOperation() {
|
||||
processAllRecords();
|
||||
}
|
||||
|
||||
// Don't run this test unless you have several hours to kill.
|
||||
// Requires real database connection and processes 10M+ records.
|
||||
@Ignore("Long-running integration test")
|
||||
@Test
|
||||
public void testWithRealDatabase() {
|
||||
database.connect();
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**Good warnings include:**
|
||||
- Thread-safety concerns
|
||||
- Performance implications
|
||||
- External dependencies or side effects
|
||||
- Security considerations
|
||||
|
||||
Reference: [Clean Code, Chapter 4: Comments](https://www.oreilly.com/library/view/clean-code-a/9780136083238/)
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
title: Maximize Expressiveness
|
||||
impact: MEDIUM-HIGH
|
||||
impactDescription: reduces time to understand unfamiliar code by 50%+
|
||||
tags: emerge, expressiveness, readability, maintenance
|
||||
---
|
||||
|
||||
## Maximize Expressiveness
|
||||
|
||||
The majority of the cost of a software project is in long-term maintenance. Write code for the reader, not the writer. The clearer the author can make the code, the less time others will spend understanding it.
|
||||
|
||||
**Incorrect (clever but opaque):**
|
||||
|
||||
```java
|
||||
// Bit manipulation for no reason
|
||||
public boolean isWeekend(int day) {
|
||||
return ((day & 0x6) == day) && (day > 0);
|
||||
}
|
||||
|
||||
// Terse variable names in complex logic
|
||||
public int calc(int[] a, int n) {
|
||||
int r = 0;
|
||||
for (int i = 0; i < n; i++)
|
||||
r = Math.max(r, a[i] - (i > 0 ? a[i-1] : 0));
|
||||
return r;
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (clear and expressive):**
|
||||
|
||||
```java
|
||||
public boolean isWeekend(DayOfWeek day) {
|
||||
return day == DayOfWeek.SATURDAY || day == DayOfWeek.SUNDAY;
|
||||
}
|
||||
|
||||
public int findMaxDailyGain(int[] dailyPrices, int numberOfDays) {
|
||||
int maxGain = 0;
|
||||
for (int day = 1; day < numberOfDays; day++) {
|
||||
int dailyChange = dailyPrices[day] - dailyPrices[day - 1];
|
||||
maxGain = Math.max(maxGain, dailyChange);
|
||||
}
|
||||
return maxGain;
|
||||
}
|
||||
```
|
||||
|
||||
**Techniques for expressiveness:**
|
||||
- Choose good names (the single most effective technique)
|
||||
- Keep functions and classes small and focused
|
||||
- Use standard patterns and nomenclature — when you use the Strategy pattern, name the class with "Strategy" so readers recognize the intent
|
||||
- Write well-crafted unit tests — tests are documentation by example
|
||||
|
||||
Reference: [Clean Code, Chapter 12: Emergence](https://www.oreilly.com/library/view/clean-code-a/9780136083238/)
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
title: Follow the Four Rules of Simple Design
|
||||
impact: HIGH
|
||||
impactDescription: prevents over-engineering while maintaining quality
|
||||
tags: emerge, simple-design, kent-beck, refactoring
|
||||
---
|
||||
|
||||
## Follow the Four Rules of Simple Design
|
||||
|
||||
Kent Beck's four rules of Simple Design, in order of priority:
|
||||
|
||||
1. **Passes all tests** — correctness is non-negotiable
|
||||
2. **Reveals intent** — code is clear and expressive
|
||||
3. **No duplication** — every concept has a single representation
|
||||
4. **Fewest elements** — remove anything that doesn't serve the first three
|
||||
|
||||
**Incorrect (over-engineered for imaginary requirements):**
|
||||
|
||||
```java
|
||||
// Single implementation with unnecessary abstraction layers
|
||||
public interface PaymentProcessor {}
|
||||
public interface PaymentProcessorFactory {}
|
||||
public abstract class AbstractPaymentProcessor implements PaymentProcessor {}
|
||||
public class PaymentProcessorFactoryImpl implements PaymentProcessorFactory {
|
||||
public PaymentProcessor create() {
|
||||
return new DefaultPaymentProcessor();
|
||||
}
|
||||
}
|
||||
public class DefaultPaymentProcessor extends AbstractPaymentProcessor {
|
||||
public void process(Payment payment) {
|
||||
gateway.charge(payment.getAmount());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (simplest design that works):**
|
||||
|
||||
```java
|
||||
public class PaymentProcessor {
|
||||
private final PaymentGateway gateway;
|
||||
|
||||
public PaymentProcessor(PaymentGateway gateway) {
|
||||
this.gateway = gateway;
|
||||
}
|
||||
|
||||
public void process(Payment payment) {
|
||||
gateway.charge(payment.getAmount());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Rule 4 is the most overlooked:** After making code correct, expressive, and DRY, actively look for things to remove. Every class, method, and variable should justify its existence. If you have one implementation of an interface, question whether you need the interface. If a design pattern adds indirection without a current concrete benefit, remove it.
|
||||
|
||||
**When more structure is justified:**
|
||||
- When you have 2+ implementations today (not hypothetically)
|
||||
- When a framework requires it (e.g., dependency injection interfaces)
|
||||
- When tests need seams for mocking external dependencies
|
||||
|
||||
Reference: [Clean Code, Chapter 12: Emergence](https://www.oreilly.com/library/view/clean-code-a/9780136083238/)
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
title: Avoid Returning and Passing Null
|
||||
impact: HIGH
|
||||
impactDescription: eliminates NullPointerException risk
|
||||
tags: err, null, optional, null-object
|
||||
---
|
||||
|
||||
## Avoid Returning and Passing Null
|
||||
|
||||
Returning null creates work for callers and invites errors. One missing null check leads to NullPointerException. Throw exceptions, return empty collections, or use the Special Case / Null Object pattern.
|
||||
|
||||
**Incorrect (returning null):**
|
||||
|
||||
```java
|
||||
public List<Employee> getEmployees() {
|
||||
if (thereAreNoEmployees) {
|
||||
return null; // Caller must check for null
|
||||
}
|
||||
return employees;
|
||||
}
|
||||
|
||||
// Every caller must do this
|
||||
List<Employee> employees = getEmployees();
|
||||
if (employees != null) { // Easy to forget
|
||||
for (Employee e : employees) {
|
||||
pay(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Passing null
|
||||
public double calculatePay(Employee employee, Money bonus) {
|
||||
// What if bonus is null?
|
||||
}
|
||||
calculatePay(employee, null); // What does null mean?
|
||||
```
|
||||
|
||||
**Correct (avoid null entirely):**
|
||||
|
||||
```java
|
||||
// Return empty collection instead of null
|
||||
public List<Employee> getEmployees() {
|
||||
if (thereAreNoEmployees) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return employees;
|
||||
}
|
||||
|
||||
// Clean caller code
|
||||
for (Employee e : getEmployees()) {
|
||||
pay(e);
|
||||
}
|
||||
|
||||
// Use Optional for truly optional values
|
||||
public Optional<Employee> findById(Long id) {
|
||||
return Optional.ofNullable(employeeMap.get(id));
|
||||
}
|
||||
|
||||
// Use Special Case object
|
||||
public interface BillingPlan {
|
||||
Money getRate();
|
||||
}
|
||||
|
||||
public class NullBillingPlan implements BillingPlan {
|
||||
public Money getRate() {
|
||||
return Money.ZERO;
|
||||
}
|
||||
}
|
||||
|
||||
// Explicit parameters instead of null
|
||||
public double calculatePay(Employee employee, Money bonus) { /* ... */ }
|
||||
public double calculatePayWithoutBonus(Employee employee) { /* ... */ }
|
||||
```
|
||||
|
||||
**When null/nullable is acceptable:**
|
||||
- Language-level nullable types with compiler enforcement (Kotlin `?`, TypeScript `strictNullChecks`, Rust `Option<T>`) make null safe because the compiler forces handling. The problem is untracked nullability, not the concept itself.
|
||||
- Interop with APIs or frameworks that require null (e.g., JDBC, some serialization libraries).
|
||||
|
||||
Reference: [Clean Code, Chapter 7: Error Handling](https://www.oreilly.com/library/view/clean-code-a/9780136083238/)
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
title: Define Exceptions by Caller Needs
|
||||
impact: HIGH
|
||||
impactDescription: reduces catch blocks by 3-5× per call site
|
||||
tags: err, wrapper, classification, design
|
||||
---
|
||||
|
||||
## Define Exceptions by Caller Needs
|
||||
|
||||
Define exception classes based on how they are caught, not on their source or type. Often a single exception class is sufficient for a particular area of code. Wrap third-party API exceptions.
|
||||
|
||||
**Incorrect (catching many exception types):**
|
||||
|
||||
```java
|
||||
// Caller must handle many different exceptions
|
||||
try {
|
||||
port.open();
|
||||
} catch (DeviceResponseException e) {
|
||||
reportPortError(e);
|
||||
logger.log("Device response problem", e);
|
||||
} catch (ATM1212UnlockedException e) {
|
||||
reportPortError(e);
|
||||
logger.log("Unlock exception", e);
|
||||
} catch (GMXError e) {
|
||||
reportPortError(e);
|
||||
logger.log("Device response exception", e);
|
||||
} finally {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (unified exception handling):**
|
||||
|
||||
```java
|
||||
// Wrapper class translates exceptions
|
||||
public class LocalPort {
|
||||
private ACMEPort innerPort;
|
||||
|
||||
public void open() throws PortDeviceFailure {
|
||||
try {
|
||||
innerPort.open();
|
||||
} catch (DeviceResponseException e) {
|
||||
throw new PortDeviceFailure(e);
|
||||
} catch (ATM1212UnlockedException e) {
|
||||
throw new PortDeviceFailure(e);
|
||||
} catch (GMXError e) {
|
||||
throw new PortDeviceFailure(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clean caller code
|
||||
try {
|
||||
port.open();
|
||||
} catch (PortDeviceFailure e) {
|
||||
reportPortError(e);
|
||||
logger.log(e.getMessage(), e);
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits of wrapping:**
|
||||
- Minimizes dependencies on third-party APIs
|
||||
- Easier to mock for testing
|
||||
- Easier to switch implementations later
|
||||
- Single exception type per area simplifies catching
|
||||
|
||||
Reference: [Clean Code, Chapter 7: Error Handling](https://www.oreilly.com/library/view/clean-code-a/9780136083238/)
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
title: Provide Context with Exceptions
|
||||
impact: HIGH
|
||||
impactDescription: enables rapid root cause identification
|
||||
tags: err, context, messages, debugging
|
||||
---
|
||||
|
||||
## Provide Context with Exceptions
|
||||
|
||||
Each exception should provide enough context to determine the source and location of an error. Create informative error messages that describe the operation that failed and the type of failure.
|
||||
|
||||
**Incorrect (no context):**
|
||||
|
||||
```java
|
||||
throw new Exception("Error occurred");
|
||||
|
||||
throw new RuntimeException("Failed");
|
||||
|
||||
throw new DataAccessException("Query failed");
|
||||
|
||||
// Stack trace alone doesn't tell you which order, which customer
|
||||
catch (SQLException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (rich context):**
|
||||
|
||||
```java
|
||||
throw new OrderProcessingException(
|
||||
String.format("Failed to process order %s for customer %s: %s",
|
||||
orderId, customerId, "Insufficient inventory for item SKU-12345"));
|
||||
|
||||
throw new ConfigurationException(
|
||||
"Cannot load configuration from " + configPath +
|
||||
". Expected format: YAML. Check file permissions and syntax.");
|
||||
|
||||
catch (SQLException e) {
|
||||
throw new DataAccessException(
|
||||
String.format("Failed to insert order %s into table %s. " +
|
||||
"Constraint violated: %s",
|
||||
order.getId(), "orders", e.getMessage()),
|
||||
e); // Preserve original exception
|
||||
}
|
||||
```
|
||||
|
||||
**Include in exception messages:**
|
||||
- What operation was being attempted
|
||||
- What data was involved (IDs, names, quantities)
|
||||
- Why it failed (the specific error condition)
|
||||
- Preserve the original exception when wrapping
|
||||
|
||||
Reference: [Clean Code, Chapter 7: Error Handling](https://www.oreilly.com/library/view/clean-code-a/9780136083238/)
|
||||
@@ -0,0 +1,88 @@
|
||||
---
|
||||
title: Separate Error Handling from Happy Path
|
||||
impact: HIGH
|
||||
impactDescription: reduces nested conditionals by 50-80% in exception-based languages
|
||||
tags: err, exceptions, error-handling, separation, result-types
|
||||
---
|
||||
|
||||
## Separate Error Handling from Happy Path
|
||||
|
||||
Error handling is important, but when it obscures logic, it's wrong. The core insight is separation: keep the happy path visible and error handling consolidated, regardless of which mechanism your language uses.
|
||||
|
||||
**Incorrect (error handling obscures business logic):**
|
||||
|
||||
```java
|
||||
if (deletePage(page) == E_OK) {
|
||||
if (registry.deleteReference(page.name) == E_OK) {
|
||||
if (configKeys.deleteKey(page.name.makeKey()) == E_OK) {
|
||||
logger.log("page deleted");
|
||||
} else {
|
||||
logger.log("configKey not deleted");
|
||||
}
|
||||
} else {
|
||||
logger.log("deleteReference from registry failed");
|
||||
}
|
||||
} else {
|
||||
logger.log("delete failed");
|
||||
return E_ERROR;
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (Java/C#/Python — exceptions separate concerns):**
|
||||
|
||||
```java
|
||||
public void delete(Page page) {
|
||||
try {
|
||||
deletePageAndAllReferences(page);
|
||||
} catch (PageDeletionException e) {
|
||||
logger.log(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void deletePageAndAllReferences(Page page) throws PageDeletionException {
|
||||
deletePage(page);
|
||||
registry.deleteReference(page.name);
|
||||
configKeys.deleteKey(page.name.makeKey());
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (Go — explicit error returns with early return):**
|
||||
|
||||
```go
|
||||
func (s *Service) Delete(page Page) error {
|
||||
if err := s.deletePage(page); err != nil {
|
||||
return fmt.Errorf("deleting page: %w", err)
|
||||
}
|
||||
if err := s.registry.DeleteReference(page.Name); err != nil {
|
||||
return fmt.Errorf("deleting reference: %w", err)
|
||||
}
|
||||
if err := s.configKeys.DeleteKey(page.Name); err != nil {
|
||||
return fmt.Errorf("deleting config key: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (Rust — Result type with ? operator):**
|
||||
|
||||
```rust
|
||||
fn delete(&self, page: &Page) -> Result<(), DeletionError> {
|
||||
self.delete_page(page)?;
|
||||
self.registry.delete_reference(&page.name)?;
|
||||
self.config_keys.delete_key(&page.name)?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
**The principle is language-agnostic:** separate error-handling logic from business logic. The mechanism differs:
|
||||
- **Java/C#/Python:** try-catch with specific exception types
|
||||
- **Go:** explicit error returns with early return pattern
|
||||
- **Rust:** `Result<T, E>` with `?` operator
|
||||
- **TypeScript/Kotlin:** `Result` types or exceptions depending on the domain
|
||||
|
||||
**When NOT to use exceptions:**
|
||||
- In Go, Rust, or other languages designed around explicit error values — use their idiomatic patterns instead
|
||||
- For expected business outcomes (e.g., "user not found") — consider returning `Optional` or a domain-specific result type rather than throwing
|
||||
- In performance-critical hot paths where exception overhead matters
|
||||
|
||||
Reference: [Clean Code, Chapter 3: Functions](https://www.oreilly.com/library/view/clean-code-a/9780136083238/) and [Chapter 7: Error Handling](https://www.oreilly.com/library/view/clean-code-a/9780136083238/)
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
title: Write Try-Catch-Finally First
|
||||
impact: HIGH
|
||||
impactDescription: prevents missing error handling cases
|
||||
tags: err, try-catch, tdd, boundaries
|
||||
---
|
||||
|
||||
## Write Try-Catch-Finally First
|
||||
|
||||
When writing code that could throw exceptions, start with the try-catch-finally statement. This helps define what the user of the code should expect, regardless of what goes wrong in the try block.
|
||||
|
||||
**Incorrect (error handling added as afterthought):**
|
||||
|
||||
```java
|
||||
// Written without considering error cases
|
||||
public List<RecordedGrip> retrieveSection(String sectionName) {
|
||||
FileInputStream stream = new FileInputStream(sectionName);
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
|
||||
String line;
|
||||
List<RecordedGrip> grips = new ArrayList<>();
|
||||
while ((line = reader.readLine()) != null) {
|
||||
grips.add(parseGrip(line));
|
||||
}
|
||||
return grips;
|
||||
// What if file doesn't exist? What if parsing fails?
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (start with try-catch structure):**
|
||||
|
||||
```java
|
||||
// TDD approach: start with test for exception case
|
||||
@Test(expected = StorageException.class)
|
||||
public void retrieveSectionShouldThrowOnInvalidFileName() {
|
||||
sectionStore.retrieveSection("invalid-file");
|
||||
}
|
||||
|
||||
// Then write code starting with try-catch
|
||||
public List<RecordedGrip> retrieveSection(String sectionName) throws StorageException {
|
||||
try (var stream = new FileInputStream(sectionName)) {
|
||||
return readGrips(stream);
|
||||
} catch (IOException e) {
|
||||
throw new StorageException("Error reading section: " + sectionName, e);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Forces you to consider error cases first
|
||||
- Establishes clear transaction boundaries
|
||||
- The finally block ensures cleanup happens
|
||||
|
||||
Reference: [Clean Code, Chapter 7: Error Handling](https://www.oreilly.com/library/view/clean-code-a/9780136083238/)
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
title: Avoid Horizontal Alignment
|
||||
impact: MEDIUM
|
||||
impactDescription: prevents alignment maintenance burden
|
||||
tags: fmt, horizontal, alignment, whitespace
|
||||
---
|
||||
|
||||
## Avoid Horizontal Alignment
|
||||
|
||||
Horizontal alignment of variable declarations or assignments looks nice but creates maintenance problems. When one line changes length, all aligned lines need reformatting.
|
||||
|
||||
**Incorrect (horizontal alignment):**
|
||||
|
||||
```java
|
||||
private Socket socket;
|
||||
private InputStream input;
|
||||
private OutputStream output;
|
||||
private Request request;
|
||||
private Response response;
|
||||
private FitNesseContext context;
|
||||
private long requestParsingTimeLimit;
|
||||
private long requestProgress;
|
||||
|
||||
public void setResponse(Response response) { this.response = response; }
|
||||
public void setSocket(Socket socket) { this.socket = socket; }
|
||||
public void setContext(FitNesseContext context) { this.context = context; }
|
||||
```
|
||||
|
||||
**Correct (natural formatting):**
|
||||
|
||||
```java
|
||||
private Socket socket;
|
||||
private InputStream input;
|
||||
private OutputStream output;
|
||||
private Request request;
|
||||
private Response response;
|
||||
private FitNesseContext context;
|
||||
private long requestParsingTimeLimit;
|
||||
private long requestProgress;
|
||||
|
||||
public void setResponse(Response response) {
|
||||
this.response = response;
|
||||
}
|
||||
|
||||
public void setSocket(Socket socket) {
|
||||
this.socket = socket;
|
||||
}
|
||||
|
||||
public void setContext(FitNesseContext context) {
|
||||
this.context = context;
|
||||
}
|
||||
```
|
||||
|
||||
**Why avoid alignment?**
|
||||
- Adding a longer variable name requires reformatting all lines
|
||||
- Eyes are drawn to the wrong thing (the alignment, not the names)
|
||||
- Most formatters don't preserve it anyway
|
||||
|
||||
Reference: [Clean Code, Chapter 5: Formatting](https://www.oreilly.com/library/view/clean-code-a/9780136083238/)
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
title: Respect Indentation Rules
|
||||
impact: MEDIUM
|
||||
impactDescription: preserves visual hierarchy for quick scanning
|
||||
tags: fmt, indentation, hierarchy, structure
|
||||
---
|
||||
|
||||
## Respect Indentation Rules
|
||||
|
||||
Indentation makes the scope hierarchy visible. Each level of indentation represents a nested scope. Never collapse short statements onto one line to save space.
|
||||
|
||||
**Incorrect (collapsed structure):**
|
||||
|
||||
```java
|
||||
public class CommentWidget extends TextWidget {
|
||||
public CommentWidget(ParentWidget parent, String text) { super(parent, text); }
|
||||
public String render() throws Exception { return ""; }
|
||||
}
|
||||
|
||||
public void process() { for (int i = 0; i < 10; i++) { if (valid(i)) { execute(i); } } }
|
||||
|
||||
if (condition) return false; else return true;
|
||||
```
|
||||
|
||||
**Correct (proper indentation):**
|
||||
|
||||
```java
|
||||
public class CommentWidget extends TextWidget {
|
||||
|
||||
public CommentWidget(ParentWidget parent, String text) {
|
||||
super(parent, text);
|
||||
}
|
||||
|
||||
public String render() throws Exception {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
public void process() {
|
||||
for (int i = 0; i < 10; i++) {
|
||||
if (valid(i)) {
|
||||
execute(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (condition) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
**Why maintain indentation?**
|
||||
- Visual scanning relies on indentation
|
||||
- Structure is immediately apparent
|
||||
- Debugging is easier when scope is visible
|
||||
- Merges are cleaner with consistent formatting
|
||||
|
||||
Reference: [Clean Code, Chapter 5: Formatting](https://www.oreilly.com/library/view/clean-code-a/9780136083238/)
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
title: Follow Team Formatting Rules
|
||||
impact: HIGH
|
||||
impactDescription: eliminates style-related code review friction
|
||||
tags: fmt, team, consistency, standards
|
||||
---
|
||||
|
||||
## Follow Team Formatting Rules
|
||||
|
||||
A team of developers should agree upon a single formatting style. Every member should use that style. The goal is to make the software have a consistent style, as if written by one person.
|
||||
|
||||
**Incorrect (inconsistent styles mixed):**
|
||||
|
||||
```java
|
||||
// Developer A's style
|
||||
public void processOrder(Order order){
|
||||
if(order.isValid()){
|
||||
order.process();
|
||||
}
|
||||
}
|
||||
|
||||
// Developer B's style
|
||||
public void processPayment( Payment payment )
|
||||
{
|
||||
if ( payment.isValid() )
|
||||
{
|
||||
payment.process();
|
||||
}
|
||||
}
|
||||
|
||||
// Developer C's style
|
||||
public void processShipment(Shipment shipment) {
|
||||
if (shipment.isValid()) {
|
||||
shipment.process(); }}
|
||||
```
|
||||
|
||||
**Correct (consistent team style):**
|
||||
|
||||
```java
|
||||
public void processOrder(Order order) {
|
||||
if (order.isValid()) {
|
||||
order.process();
|
||||
}
|
||||
}
|
||||
|
||||
public void processPayment(Payment payment) {
|
||||
if (payment.isValid()) {
|
||||
payment.process();
|
||||
}
|
||||
}
|
||||
|
||||
public void processShipment(Shipment shipment) {
|
||||
if (shipment.isValid()) {
|
||||
shipment.process();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Implementation:**
|
||||
- Document the team's coding standards
|
||||
- Use automated formatters (Prettier, Black, google-java-format)
|
||||
- Enforce in CI/CD pipelines
|
||||
- Configure IDE to format on save
|
||||
|
||||
Your personal style preferences are less important than team consistency.
|
||||
|
||||
Reference: [Clean Code, Chapter 5: Formatting](https://www.oreilly.com/library/view/clean-code-a/9780136083238/)
|
||||
@@ -0,0 +1,72 @@
|
||||
---
|
||||
title: Use Vertical Formatting for Readability
|
||||
impact: HIGH
|
||||
impactDescription: enables scanning code like a newspaper
|
||||
tags: fmt, vertical, whitespace, organization
|
||||
---
|
||||
|
||||
## Use Vertical Formatting for Readability
|
||||
|
||||
Code should read like a newspaper article: headline at the top, details as you go down. Use vertical whitespace to separate concepts. Keep related code close together.
|
||||
|
||||
**Incorrect (no vertical organization):**
|
||||
|
||||
```java
|
||||
public class WikiPageResponder implements SecureResponder {
|
||||
private static final String CONTENT_TYPE = "text/html";
|
||||
private WikiPage page;
|
||||
private PageData pageData;
|
||||
private String pageTitle;
|
||||
private Request request;
|
||||
private PageCrawler crawler;
|
||||
public Response makeResponse(FitNesseContext context, Request request) {
|
||||
this.request = request;
|
||||
this.page = loadPage();
|
||||
if (page == null)
|
||||
return notFoundResponse(context, request);
|
||||
return makePageResponse(context);
|
||||
}
|
||||
private WikiPage loadPage() {
|
||||
String resource = request.getResource();
|
||||
return PageCrawlerImpl.getPageCrawler(context.root).getPage(resource);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (vertical formatting applied):**
|
||||
|
||||
```java
|
||||
public class WikiPageResponder implements SecureResponder {
|
||||
|
||||
private static final String CONTENT_TYPE = "text/html";
|
||||
|
||||
private WikiPage page;
|
||||
private PageData pageData;
|
||||
private String pageTitle;
|
||||
private Request request;
|
||||
private PageCrawler crawler;
|
||||
|
||||
public Response makeResponse(FitNesseContext context, Request request) {
|
||||
this.request = request;
|
||||
this.page = loadPage();
|
||||
|
||||
if (page == null)
|
||||
return notFoundResponse(context, request);
|
||||
|
||||
return makePageResponse(context);
|
||||
}
|
||||
|
||||
private WikiPage loadPage() {
|
||||
String resource = request.getResource();
|
||||
return PageCrawlerImpl.getPageCrawler(context.root).getPage(resource);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Guidelines:**
|
||||
- Blank lines between concepts (methods, logical sections)
|
||||
- Related code grouped together
|
||||
- Caller above callee (top-down reading)
|
||||
- Variable declarations close to their usage
|
||||
|
||||
Reference: [Clean Code, Chapter 5: Formatting](https://www.oreilly.com/library/view/clean-code-a/9780136083238/)
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
title: Maintain One Level of Abstraction
|
||||
impact: CRITICAL
|
||||
impactDescription: prevents jarring context switches
|
||||
tags: func, abstraction, stepdown, readability
|
||||
---
|
||||
|
||||
## Maintain One Level of Abstraction
|
||||
|
||||
Statements within a function should all be at the same level of abstraction. Mixing high-level concepts with low-level details is confusing. Code should read like a top-down narrative.
|
||||
|
||||
**Incorrect (mixed abstraction levels):**
|
||||
|
||||
```java
|
||||
public void analyzeDocument(String documentPath) {
|
||||
// High level
|
||||
Document document = loadDocument(documentPath);
|
||||
|
||||
// Low level - string manipulation details
|
||||
String text = document.getText();
|
||||
text = text.replaceAll("\\s+", " ");
|
||||
text = text.toLowerCase();
|
||||
String[] words = text.split(" ");
|
||||
|
||||
// High level again
|
||||
WordFrequency frequency = analyzeFrequency(words);
|
||||
|
||||
// Low level - file I/O details
|
||||
FileOutputStream fos = new FileOutputStream("output.txt");
|
||||
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(fos));
|
||||
for (Map.Entry<String, Integer> entry : frequency.getEntries()) {
|
||||
writer.write(entry.getKey() + ": " + entry.getValue());
|
||||
writer.newLine();
|
||||
}
|
||||
writer.close();
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (consistent abstraction level):**
|
||||
|
||||
```java
|
||||
public void analyzeDocument(String documentPath) {
|
||||
Document document = loadDocument(documentPath);
|
||||
String normalizedText = normalizeText(document);
|
||||
WordFrequency frequency = analyzeFrequency(normalizedText);
|
||||
saveResults(frequency);
|
||||
}
|
||||
|
||||
private String normalizeText(Document document) {
|
||||
String text = document.getText();
|
||||
text = collapseWhitespace(text);
|
||||
text = toLowerCase(text);
|
||||
return text;
|
||||
}
|
||||
|
||||
private void saveResults(WordFrequency frequency) {
|
||||
ResultWriter writer = new ResultWriter("output.txt");
|
||||
writer.write(frequency);
|
||||
}
|
||||
```
|
||||
|
||||
Read the code from top to bottom. Each function introduces the next level of abstraction.
|
||||
|
||||
**When mixing levels is acceptable:**
|
||||
- Short utility functions (under 10 lines) that perform one task can mix levels without confusion. The overhead of extracting one-line helpers often exceeds the readability benefit.
|
||||
- Test code frequently mixes setup (low-level) with assertions (high-level) — this is expected and follows the Arrange-Act-Assert pattern.
|
||||
|
||||
Reference: [Clean Code, Chapter 3: Functions](https://www.oreilly.com/library/view/clean-code-a/9780136083238/)
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
---
|
||||
title: Separate Commands from Queries
|
||||
impact: MEDIUM
|
||||
impactDescription: eliminates ambiguous function behavior
|
||||
tags: func, cqs, command, query, separation
|
||||
---
|
||||
|
||||
## Separate Commands from Queries
|
||||
|
||||
Functions should either do something or answer something, but not both. Either your function changes the state of an object, or it returns information about that object.
|
||||
|
||||
**Incorrect (command and query mixed):**
|
||||
|
||||
```java
|
||||
// Does this set the attribute and return success?
|
||||
// Or check if attribute exists?
|
||||
public boolean set(String attribute, String value) {
|
||||
// ... sets attribute and returns true if it was set
|
||||
}
|
||||
|
||||
// Confusing usage
|
||||
if (set("username", "bob")) {
|
||||
// Was username set, or did it already exist?
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (separated command and query):**
|
||||
|
||||
```java
|
||||
// Query - returns information
|
||||
public boolean attributeExists(String attribute) {
|
||||
return attributes.containsKey(attribute);
|
||||
}
|
||||
|
||||
// Command - changes state
|
||||
public void setAttribute(String attribute, String value) {
|
||||
attributes.put(attribute, value);
|
||||
}
|
||||
|
||||
// Clear usage
|
||||
if (!attributeExists("username")) {
|
||||
setAttribute("username", "bob");
|
||||
}
|
||||
```
|
||||
|
||||
**Alternative (return new state for immutable design):**
|
||||
|
||||
```java
|
||||
// For immutable patterns, returning new state is acceptable
|
||||
public Settings withAttribute(String attribute, String value) {
|
||||
return new Settings(this.attributes.plus(attribute, value));
|
||||
}
|
||||
```
|
||||
|
||||
The separation makes code easier to read and reason about.
|
||||
|
||||
Reference: [Clean Code, Chapter 3: Functions](https://www.oreilly.com/library/view/clean-code-a/9780136083238/)
|
||||
@@ -0,0 +1,70 @@
|
||||
---
|
||||
title: Do Not Repeat Yourself
|
||||
impact: CRITICAL
|
||||
impactDescription: prevents N× maintenance burden
|
||||
tags: func, dry, duplication, abstraction
|
||||
---
|
||||
|
||||
## Do Not Repeat Yourself
|
||||
|
||||
Duplication is the root of all evil in software. When an algorithm changes, you must change it in multiple places. When you fix a bug, you must remember to fix it everywhere.
|
||||
|
||||
**Incorrect (duplicated logic):**
|
||||
|
||||
```java
|
||||
public void processNewEmployee(Employee employee) {
|
||||
if (employee.getName() == null || employee.getName().trim().isEmpty()) {
|
||||
throw new ValidationException("Name is required");
|
||||
}
|
||||
if (employee.getEmail() == null || !employee.getEmail().contains("@")) {
|
||||
throw new ValidationException("Valid email is required");
|
||||
}
|
||||
employeeRepository.save(employee);
|
||||
emailService.sendWelcome(employee.getEmail());
|
||||
}
|
||||
|
||||
public void updateEmployee(Employee employee) {
|
||||
if (employee.getName() == null || employee.getName().trim().isEmpty()) {
|
||||
throw new ValidationException("Name is required"); // Duplicated
|
||||
}
|
||||
if (employee.getEmail() == null || !employee.getEmail().contains("@")) {
|
||||
throw new ValidationException("Valid email is required"); // Duplicated
|
||||
}
|
||||
employeeRepository.update(employee);
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (extracted common logic):**
|
||||
|
||||
```java
|
||||
public void processNewEmployee(Employee employee) {
|
||||
validateEmployee(employee);
|
||||
employeeRepository.save(employee);
|
||||
emailService.sendWelcome(employee.getEmail());
|
||||
}
|
||||
|
||||
public void updateEmployee(Employee employee) {
|
||||
validateEmployee(employee);
|
||||
employeeRepository.update(employee);
|
||||
}
|
||||
|
||||
private void validateEmployee(Employee employee) {
|
||||
requireNonEmpty(employee.getName(), "Name is required");
|
||||
requireValidEmail(employee.getEmail());
|
||||
}
|
||||
|
||||
private void requireNonEmpty(String value, String message) {
|
||||
if (value == null || value.trim().isEmpty()) {
|
||||
throw new ValidationException(message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.
|
||||
|
||||
**When NOT to apply DRY:**
|
||||
- **Coincidental duplication:** Two code blocks look similar but serve different purposes and will evolve independently. Extracting a shared function creates a false coupling that makes both harder to change later.
|
||||
- **Premature abstraction:** If the code has only been duplicated once, wait. The third occurrence reveals the true abstraction. Premature extraction often produces awkward, over-parameterized helpers.
|
||||
- **Cross-boundary duplication:** Sometimes duplication across microservices or bounded contexts is preferable to introducing a shared library that couples the services.
|
||||
|
||||
Reference: [Clean Code, Chapter 3: Functions](https://www.oreilly.com/library/view/clean-code-a/9780136083238/)
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
title: Minimize Function Arguments
|
||||
impact: HIGH
|
||||
impactDescription: reduces cognitive load and testing complexity
|
||||
tags: func, arguments, parameters, simplicity
|
||||
---
|
||||
|
||||
## Minimize Function Arguments
|
||||
|
||||
The ideal number of arguments for a function is zero (niladic). Next comes one (monadic), followed closely by two (dyadic). Three arguments (triadic) should be avoided where possible.
|
||||
|
||||
**Incorrect (too many arguments):**
|
||||
|
||||
```java
|
||||
// Flag arguments are bad - function does different things
|
||||
public void render(boolean isSuite) {}
|
||||
|
||||
// Four arguments - hard to remember order
|
||||
public Circle makeCircle(double x, double y, double radius, String color) {}
|
||||
|
||||
// Order matters and is easy to confuse
|
||||
public void assertExpectedEqualsActual(String expected, String actual) {}
|
||||
```
|
||||
|
||||
**Correct (minimized arguments):**
|
||||
|
||||
```java
|
||||
// Separate functions instead of flag
|
||||
public void renderForSingleTest() {}
|
||||
public void renderForSuite() {}
|
||||
|
||||
// Encapsulate related arguments in objects
|
||||
public Circle makeCircle(Point center, double radius, Color color) {}
|
||||
|
||||
// Even better - use a builder for many optional params
|
||||
Circle circle = Circle.builder()
|
||||
.center(new Point(0, 0))
|
||||
.radius(5.0)
|
||||
.color(Color.RED)
|
||||
.build();
|
||||
|
||||
// Self-documenting method name
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
```
|
||||
|
||||
**Why fewer arguments?**
|
||||
- Easier to understand
|
||||
- Easier to test (fewer combinations)
|
||||
- Less chance of argument order errors
|
||||
|
||||
**Acceptable monadic forms:**
|
||||
- Asking a question: `boolean fileExists(String path)`
|
||||
- Transforming input: `InputStream openFile(String path)`
|
||||
- Events: `void passwordAttemptFailed(int attempts)`
|
||||
|
||||
Reference: [Clean Code, Chapter 3: Functions](https://www.oreilly.com/library/view/clean-code-a/9780136083238/)
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
title: Avoid Side Effects
|
||||
impact: HIGH
|
||||
impactDescription: prevents hidden temporal couplings
|
||||
tags: func, side-effects, purity, predictability
|
||||
---
|
||||
|
||||
## Avoid Side Effects
|
||||
|
||||
Side effects are lies. Your function promises to do one thing, but it also does other hidden things. Avoid unexpected changes to class variables, globals, or passed arguments.
|
||||
|
||||
**Incorrect (hidden side effect):**
|
||||
|
||||
```java
|
||||
public class UserValidator {
|
||||
private Cryptographer cryptographer;
|
||||
private Session session;
|
||||
|
||||
public boolean checkPassword(String userName, String password) {
|
||||
User user = UserGateway.findByName(userName);
|
||||
if (user != null) {
|
||||
String codedPhrase = user.getPhraseEncodedByPassword();
|
||||
String phrase = cryptographer.decrypt(codedPhrase, password);
|
||||
if ("Valid Password".equals(phrase)) {
|
||||
session.initialize(); // Hidden side effect!
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Calling checkPassword twice destroys the session unexpectedly
|
||||
```
|
||||
|
||||
**Correct (explicit behavior):**
|
||||
|
||||
```java
|
||||
public class UserValidator {
|
||||
private Cryptographer cryptographer;
|
||||
|
||||
public boolean checkPassword(String userName, String password) {
|
||||
User user = UserGateway.findByName(userName);
|
||||
if (user == null) return false;
|
||||
|
||||
String codedPhrase = user.getPhraseEncodedByPassword();
|
||||
String phrase = cryptographer.decrypt(codedPhrase, password);
|
||||
return "Valid Password".equals(phrase);
|
||||
}
|
||||
}
|
||||
|
||||
// Separate function with clear name
|
||||
public class SessionManager {
|
||||
public void initializeSessionForUser(String userName, String password) {
|
||||
if (userValidator.checkPassword(userName, password)) {
|
||||
session.initialize();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If you must have a temporal coupling, make it explicit in the function name: `checkPasswordAndInitializeSession`.
|
||||
|
||||
**When side effects are expected:**
|
||||
- I/O operations (file writes, network calls, database mutations) inherently have side effects — the principle here is about *hidden* side effects, not avoiding all mutation. A function named `saveUser()` clearly signals side effects.
|
||||
- Event handlers, middleware, and lifecycle hooks are designed to produce side effects. Name them to make the effect obvious.
|
||||
|
||||
Reference: [Clean Code, Chapter 3: Functions](https://www.oreilly.com/library/view/clean-code-a/9780136083238/)
|
||||
@@ -0,0 +1,77 @@
|
||||
---
|
||||
title: Functions Should Do One Thing
|
||||
impact: CRITICAL
|
||||
impactDescription: enables understanding functions at a glance
|
||||
tags: func, single-responsibility, cohesion, focus
|
||||
---
|
||||
|
||||
## Functions Should Do One Thing
|
||||
|
||||
Functions should do one thing. They should do it well. They should do it only. If a function does multiple things, extract those things into separate functions.
|
||||
|
||||
**Incorrect (function does multiple things):**
|
||||
|
||||
```java
|
||||
public void processEmployee(Employee employee) {
|
||||
// Validate employee
|
||||
if (employee.getName() == null || employee.getName().isEmpty()) {
|
||||
throw new IllegalArgumentException("Name required");
|
||||
}
|
||||
if (employee.getSalary() < 0) {
|
||||
throw new IllegalArgumentException("Salary cannot be negative");
|
||||
}
|
||||
|
||||
// Calculate tax
|
||||
double tax = employee.getSalary() * 0.3;
|
||||
employee.setTax(tax);
|
||||
|
||||
// Save to database
|
||||
Connection conn = getConnection();
|
||||
PreparedStatement stmt = conn.prepareStatement(
|
||||
"INSERT INTO employees VALUES (?, ?, ?)");
|
||||
stmt.setString(1, employee.getName());
|
||||
stmt.setDouble(2, employee.getSalary());
|
||||
stmt.setDouble(3, tax);
|
||||
stmt.execute();
|
||||
|
||||
// Send email notification
|
||||
EmailService.send(employee.getEmail(), "Welcome!");
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (each function does one thing):**
|
||||
|
||||
```java
|
||||
public void processEmployee(Employee employee) {
|
||||
validateEmployee(employee);
|
||||
calculateTax(employee);
|
||||
saveEmployee(employee);
|
||||
sendWelcomeEmail(employee);
|
||||
}
|
||||
|
||||
private void validateEmployee(Employee employee) {
|
||||
validateName(employee);
|
||||
validateSalary(employee);
|
||||
}
|
||||
|
||||
private void calculateTax(Employee employee) {
|
||||
double tax = employee.getSalary() * TAX_RATE;
|
||||
employee.setTax(tax);
|
||||
}
|
||||
|
||||
private void saveEmployee(Employee employee) {
|
||||
employeeRepository.save(employee);
|
||||
}
|
||||
|
||||
private void sendWelcomeEmail(Employee employee) {
|
||||
emailService.sendWelcome(employee.getEmail());
|
||||
}
|
||||
```
|
||||
|
||||
**Test:** If you can extract another function from it with a name that is not merely a restatement of its implementation, the function is doing more than one thing.
|
||||
|
||||
**When NOT to split:**
|
||||
- If the extracted function's name would just restate its body (e.g., `calculateTax` containing only `salary * TAX_RATE`), the extraction adds indirection without clarity.
|
||||
- Orchestration functions that call a sequence of steps at the same abstraction level (like `processEmployee` above) are doing "one thing" — orchestrating. Don't try to extract the orchestration further.
|
||||
|
||||
Reference: [Clean Code, Chapter 3: Functions](https://www.oreilly.com/library/view/clean-code-a/9780136083238/)
|
||||
@@ -0,0 +1,71 @@
|
||||
---
|
||||
title: Keep Functions Small
|
||||
impact: CRITICAL
|
||||
impactDescription: enables comprehension at a glance
|
||||
tags: func, size, readability, comprehension
|
||||
---
|
||||
|
||||
## Keep Functions Small
|
||||
|
||||
Functions should be small. They should be smaller than that. Functions should hardly ever be 20 lines long. Each function should do one thing, do it well, and do it only.
|
||||
|
||||
**Incorrect (large function doing multiple things):**
|
||||
|
||||
```java
|
||||
public void renderPageWithSetupsAndTeardowns(PageData pageData, boolean isSuite) {
|
||||
if (isTestPage(pageData)) {
|
||||
WikiPage testPage = pageData.getWikiPage();
|
||||
StringBuffer newPageContent = new StringBuffer();
|
||||
includeSetupPages(testPage, newPageContent, isSuite);
|
||||
newPageContent.append(pageData.getContent());
|
||||
includeTeardownPages(testPage, newPageContent, isSuite);
|
||||
pageData.setContent(newPageContent.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private void includeSetupPages(WikiPage testPage, StringBuffer buffer, boolean isSuite) {
|
||||
if (isSuite) {
|
||||
WikiPage suitePage = PageCrawlerImpl.getInheritedPage(
|
||||
SuiteResponder.SUITE_SETUP_NAME, testPage);
|
||||
if (suitePage != null) {
|
||||
WikiPagePath pagePath = testPage.getPageCrawler()
|
||||
.getFullPath(suitePage);
|
||||
String pagePathName = PathParser.render(pagePath);
|
||||
buffer.append("!include -setup .")
|
||||
.append(pagePathName)
|
||||
.append("\n");
|
||||
}
|
||||
}
|
||||
// ... 20 more lines
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (small, focused functions):**
|
||||
|
||||
```java
|
||||
public void renderPageWithSetupsAndTeardowns(PageData pageData, boolean isSuite) {
|
||||
if (isTestPage(pageData))
|
||||
includeSetupsAndTeardowns(pageData, isSuite);
|
||||
}
|
||||
|
||||
private void includeSetupsAndTeardowns(PageData pageData, boolean isSuite) {
|
||||
WikiPage testPage = pageData.getWikiPage();
|
||||
String content = buildPageContent(testPage, isSuite);
|
||||
pageData.setContent(content);
|
||||
}
|
||||
|
||||
private String buildPageContent(WikiPage testPage, boolean isSuite) {
|
||||
return getSetups(testPage, isSuite) +
|
||||
testPage.getContent() +
|
||||
getTeardowns(testPage, isSuite);
|
||||
}
|
||||
```
|
||||
|
||||
Each function is 3-5 lines. Each does exactly one thing. Each is at one level of abstraction.
|
||||
|
||||
**When NOT to apply this pattern:**
|
||||
- If a 15-20 line function does one thing at one level of abstraction, it does not need further decomposition. The goal is comprehensibility, not minimizing line count.
|
||||
- Do not extract functions that are only called from one place and whose names merely restate their implementation — this adds indirection without clarity.
|
||||
- Ousterhout's "deep modules" critique: extremely small functions can create "shallow modules" that spread logic across many files, forcing readers to jump between functions to understand a single behavior. Balance depth (functionality per interface) against size.
|
||||
|
||||
Reference: [Clean Code, Chapter 3: Functions](https://www.oreilly.com/library/view/clean-code-a/9780136083238/)
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
title: Avoid Disinformation in Names
|
||||
impact: CRITICAL
|
||||
impactDescription: prevents incorrect mental models
|
||||
tags: name, disinformation, misleading, types
|
||||
---
|
||||
|
||||
## Avoid Disinformation in Names
|
||||
|
||||
Avoid names that encode false information. Do not refer to a grouping as a "List" unless it is actually a List. Avoid names that vary in small ways that are hard to distinguish.
|
||||
|
||||
**Incorrect (misleading type information):**
|
||||
|
||||
```java
|
||||
// Not actually a List, it's a Map
|
||||
private Map<String, Account> accountList;
|
||||
|
||||
// These look almost identical - easy to confuse
|
||||
private String XYZControllerForEfficientHandlingOfStrings;
|
||||
private String XYZControllerForEfficientStorageOfStrings;
|
||||
|
||||
// Lowercase L looks like 1, uppercase O looks like 0
|
||||
int a = l;
|
||||
if (O == l)
|
||||
a = O1;
|
||||
```
|
||||
|
||||
**Correct (accurate and distinguishable):**
|
||||
|
||||
```java
|
||||
// Accurately describes the data structure
|
||||
private Map<String, Account> accountsByName;
|
||||
|
||||
// Clearly distinguishable names
|
||||
private String stringProcessingController;
|
||||
private String stringStorageController;
|
||||
|
||||
// Clear variable names
|
||||
int result = leftValue;
|
||||
if (originalValue == leftValue)
|
||||
result = outputValue;
|
||||
```
|
||||
|
||||
Reference: [Clean Code, Chapter 2: Meaningful Names](https://www.oreilly.com/library/view/clean-code-a/9780136083238/)
|
||||
@@ -0,0 +1,66 @@
|
||||
---
|
||||
title: Avoid Encodings in Names
|
||||
impact: HIGH
|
||||
impactDescription: reduces mental translation burden
|
||||
tags: name, encoding, hungarian, prefixes
|
||||
---
|
||||
|
||||
## Avoid Encodings in Names
|
||||
|
||||
Encoding type or scope information into names adds an extra burden of deciphering. Modern IDEs make Hungarian Notation and member prefixes unnecessary.
|
||||
|
||||
**Incorrect (encoded type and scope):**
|
||||
|
||||
```java
|
||||
// Hungarian Notation - type encoded in name
|
||||
String strName;
|
||||
int iAge;
|
||||
boolean bIsActive;
|
||||
PhoneNumber phoneString; // Type changed but name wasn't updated!
|
||||
|
||||
// Member prefixes
|
||||
public class Part {
|
||||
private String m_dsc; // Member description
|
||||
|
||||
void setDescription(String dsc) {
|
||||
m_dsc = dsc;
|
||||
}
|
||||
}
|
||||
|
||||
// Interface prefix
|
||||
public interface IShapeFactory {}
|
||||
```
|
||||
|
||||
**Correct (no encodings):**
|
||||
|
||||
```java
|
||||
// Let the type system handle types
|
||||
String name;
|
||||
int age;
|
||||
boolean isActive;
|
||||
PhoneNumber phone;
|
||||
|
||||
// No member prefixes - IDE highlights members
|
||||
public class Part {
|
||||
private String description;
|
||||
|
||||
void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
}
|
||||
|
||||
// Drop the interface prefix — callers use ShapeFactory, not IShapeFactory
|
||||
public interface ShapeFactory {}
|
||||
|
||||
// Name implementations by what distinguishes them
|
||||
public class JsonShapeFactory implements ShapeFactory {}
|
||||
public class SvgShapeFactory implements ShapeFactory {}
|
||||
|
||||
// If only one implementation exists, consider whether you need the interface at all
|
||||
```
|
||||
|
||||
Readers learn to ignore prefixes. You end up seeing only the meaningful part of the name.
|
||||
|
||||
**Note:** `ShapeFactoryImpl` is a lesser evil than `IShapeFactory`, but `Impl` is still an encoding. When possible, name implementations by their distinguishing characteristic (protocol, storage mechanism, algorithm).
|
||||
|
||||
Reference: [Clean Code, Chapter 2: Meaningful Names](https://www.oreilly.com/library/view/clean-code-a/9780136083238/)
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
title: Use Noun Phrases for Class Names
|
||||
impact: MEDIUM-HIGH
|
||||
impactDescription: eliminates 80%+ of "what does this class do" questions
|
||||
tags: name, class, noun, identity
|
||||
---
|
||||
|
||||
## Use Noun Phrases for Class Names
|
||||
|
||||
Class names should be nouns or noun phrases. A class represents a thing or concept. Avoid verbs, manager-style names, and vague words.
|
||||
|
||||
**Incorrect (verbs, vague, or manager-style):**
|
||||
|
||||
```java
|
||||
// Verb as class name
|
||||
class ProcessPayment {}
|
||||
|
||||
// Vague manager names
|
||||
class Manager {}
|
||||
class Processor {}
|
||||
class Data {}
|
||||
class Info {}
|
||||
|
||||
// Unnecessary suffixes
|
||||
class CustomerManager {}
|
||||
class AccountProcessor {}
|
||||
class PaymentHandler {}
|
||||
```
|
||||
|
||||
**Correct (specific noun phrases):**
|
||||
|
||||
```java
|
||||
// Clear nouns representing entities
|
||||
class Payment {}
|
||||
class PaymentGateway {}
|
||||
|
||||
// Specific, descriptive nouns
|
||||
class Customer {}
|
||||
class WikiPage {}
|
||||
class Account {}
|
||||
class AddressParser {}
|
||||
|
||||
// Role-specific names when needed
|
||||
class PaymentValidator {} // Validates payments
|
||||
class AccountRepository {} // Stores accounts
|
||||
```
|
||||
|
||||
**When NOT to use this pattern:**
|
||||
|
||||
Manager-style names are acceptable when the class genuinely manages a resource lifecycle:
|
||||
- `ConnectionPool` - manages connection lifecycle
|
||||
- `ThreadPoolExecutor` - manages thread lifecycle
|
||||
|
||||
Reference: [Clean Code, Chapter 2: Meaningful Names](https://www.oreilly.com/library/view/clean-code-a/9780136083238/)
|
||||
@@ -0,0 +1,47 @@
|
||||
---
|
||||
title: Use Intention-Revealing Names
|
||||
impact: CRITICAL
|
||||
impactDescription: eliminates mental mapping overhead
|
||||
tags: name, intention, readability, clarity
|
||||
---
|
||||
|
||||
## Use Intention-Revealing Names
|
||||
|
||||
Names should reveal intent. A name should tell you why something exists, what it does, and how it is used. If a name requires a comment, it does not reveal its intent.
|
||||
|
||||
**Incorrect (requires mental decoding):**
|
||||
|
||||
```java
|
||||
int d; // elapsed time in days
|
||||
|
||||
public List<int[]> getThem() {
|
||||
List<int[]> list1 = new ArrayList<>();
|
||||
for (int[] x : theList)
|
||||
if (x[0] == 4) // What is 4? What is x[0]?
|
||||
list1.add(x);
|
||||
return list1;
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (self-documenting):**
|
||||
|
||||
```java
|
||||
int elapsedTimeInDays;
|
||||
|
||||
public List<Cell> getFlaggedCells() {
|
||||
List<Cell> flaggedCells = new ArrayList<>();
|
||||
for (Cell cell : gameBoard)
|
||||
if (cell.isFlagged())
|
||||
flaggedCells.add(cell);
|
||||
return flaggedCells;
|
||||
}
|
||||
```
|
||||
|
||||
The code now explicitly communicates its purpose: finding flagged cells in a minesweeper game. No comment needed.
|
||||
|
||||
**When short names are acceptable:**
|
||||
- Loop counters (`i`, `j`, `k`) in small, tightly-scoped loops where the index has no domain meaning.
|
||||
- Mathematical algorithms where `x`, `y`, `dx`, `dt` are conventional and understood by the target audience.
|
||||
- Lambda parameters in trivial callbacks: `items.filter(x -> x > 0)` is clearer than `items.filter(numberThatMustBePositive -> numberThatMustBePositive > 0)`.
|
||||
|
||||
Reference: [Clean Code, Chapter 2: Meaningful Names](https://www.oreilly.com/library/view/clean-code-a/9780136083238/)
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
---
|
||||
title: Make Meaningful Distinctions
|
||||
impact: CRITICAL
|
||||
impactDescription: eliminates ambiguity between similar concepts
|
||||
tags: name, distinction, noise-words, clarity
|
||||
---
|
||||
|
||||
## Make Meaningful Distinctions
|
||||
|
||||
If names must be different, then they should also mean something different. Avoid number-series naming and noise words that add no information.
|
||||
|
||||
**Incorrect (indistinguishable names):**
|
||||
|
||||
```java
|
||||
// Number series - meaningless distinction
|
||||
public static void copyChars(char[] a1, char[] a2) {
|
||||
for (int i = 0; i < a1.length; i++) {
|
||||
a2[i] = a1[i];
|
||||
}
|
||||
}
|
||||
|
||||
// Noise words - what's the difference?
|
||||
class Product {}
|
||||
class ProductInfo {}
|
||||
class ProductData {}
|
||||
|
||||
// Redundant prefixes
|
||||
String nameString;
|
||||
Customer customerObject;
|
||||
```
|
||||
|
||||
**Correct (meaningful distinctions):**
|
||||
|
||||
```java
|
||||
// Descriptive parameter names
|
||||
public static void copyChars(char[] source, char[] destination) {
|
||||
for (int i = 0; i < source.length; i++) {
|
||||
destination[i] = source[i];
|
||||
}
|
||||
}
|
||||
|
||||
// If distinctions exist, name them
|
||||
class Product {}
|
||||
class ProductDetails {} // Shows additional information
|
||||
class ProductInventory {} // Stock and availability
|
||||
|
||||
// No redundant type encoding
|
||||
String name;
|
||||
Customer customer;
|
||||
```
|
||||
|
||||
If you cannot distinguish what `ProductInfo` offers that `Product` does not, the names are noise.
|
||||
|
||||
Reference: [Clean Code, Chapter 2: Meaningful Names](https://www.oreilly.com/library/view/clean-code-a/9780136083238/)
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
title: Use Verb Phrases for Method Names
|
||||
impact: MEDIUM-HIGH
|
||||
impactDescription: reduces "what does this method do" lookups by 80%
|
||||
tags: name, method, verb, action
|
||||
---
|
||||
|
||||
## Use Verb Phrases for Method Names
|
||||
|
||||
Method names should be verbs or verb phrases. Methods do things, so their names should say what they do. Follow JavaBean conventions for accessors and mutators.
|
||||
|
||||
**Incorrect (non-verb or unclear):**
|
||||
|
||||
```java
|
||||
// Noun names for methods
|
||||
public String name() {}
|
||||
public int size() {} // Acceptable for collections
|
||||
|
||||
// Unclear action
|
||||
public void customer() {} // Does what to customer?
|
||||
|
||||
// Missing verb
|
||||
public boolean valid() {}
|
||||
```
|
||||
|
||||
**Correct (verb phrases):**
|
||||
|
||||
```java
|
||||
// Clear verb phrases
|
||||
public void postPayment() {}
|
||||
public void deletePage() {}
|
||||
public void saveCustomer() {}
|
||||
|
||||
// Accessors, mutators, predicates follow conventions
|
||||
public String getName() {}
|
||||
public void setName(String name) {}
|
||||
public boolean isPosted() {}
|
||||
public boolean hasPermission() {}
|
||||
|
||||
// Static factory methods describe what is created
|
||||
Complex fulcrumPoint = Complex.fromRealNumber(23.0);
|
||||
```
|
||||
|
||||
Use `get`, `set`, `is`, and `has` prefixes consistently for accessors, mutators, and predicates.
|
||||
|
||||
**Language-specific conventions:**
|
||||
- **Java/C#:** `getName()`, `setName()`, `isActive()` (JavaBean conventions)
|
||||
- **Python:** Use `@property` decorators — `user.name` not `user.get_name()`
|
||||
- **Kotlin/Swift:** Direct property access with `val`/`var` — `user.name` not `user.getName()`
|
||||
- **TypeScript:** Direct properties or getters — `get name(): string`
|
||||
- **Go:** `Name()` not `GetName()` (Go convention omits `Get` prefix)
|
||||
|
||||
The underlying principle (methods describe actions, names describe things) is universal. The accessor/mutator conventions are language-specific.
|
||||
|
||||
Reference: [Clean Code, Chapter 2: Meaningful Names](https://www.oreilly.com/library/view/clean-code-a/9780136083238/)
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
title: Use Pronounceable Names
|
||||
impact: HIGH
|
||||
impactDescription: enables verbal communication about code
|
||||
tags: name, pronounceable, communication, readability
|
||||
---
|
||||
|
||||
## Use Pronounceable Names
|
||||
|
||||
Use pronounceable names. If you cannot pronounce a name, you cannot discuss it without sounding foolish. Programming is a social activity; names should facilitate discussion.
|
||||
|
||||
**Incorrect (unpronounceable abbreviations):**
|
||||
|
||||
```java
|
||||
class DtaRcrd102 {
|
||||
private Date genymdhms; // generation year, month, day, hour, minute, second
|
||||
private Date modymdhms;
|
||||
private final String pszqint = "102";
|
||||
}
|
||||
|
||||
// In conversation: "Hey, look at this dee-tee-ay-arr-cee-arr-dee"
|
||||
```
|
||||
|
||||
**Correct (pronounceable names):**
|
||||
|
||||
```java
|
||||
class Customer {
|
||||
private Date generationTimestamp;
|
||||
private Date modificationTimestamp;
|
||||
private final String recordId = "102";
|
||||
}
|
||||
|
||||
// In conversation: "Hey, look at this Customer record"
|
||||
```
|
||||
|
||||
The pronounceable version enables natural conversation: "Hey, when did we modify this customer's generation timestamp?"
|
||||
|
||||
Reference: [Clean Code, Chapter 2: Meaningful Names](https://www.oreilly.com/library/view/clean-code-a/9780136083238/)
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
title: Use Searchable Names
|
||||
impact: HIGH
|
||||
impactDescription: enables effective code navigation
|
||||
tags: name, searchable, constants, refactoring
|
||||
---
|
||||
|
||||
## Use Searchable Names
|
||||
|
||||
Single-letter names and numeric constants are hard to locate across a body of text. Longer names trump shorter names, and searchable names trump convenient abbreviations.
|
||||
|
||||
**Incorrect (unsearchable):**
|
||||
|
||||
```java
|
||||
for (int j = 0; j < 34; j++) {
|
||||
s += (t[j] * 4) / 5; // What is 34? What is 4? What is 5?
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (searchable and meaningful):**
|
||||
|
||||
```java
|
||||
int realDaysPerIdealDay = 4;
|
||||
final int WORK_DAYS_PER_WEEK = 5;
|
||||
final int NUMBER_OF_TASKS = 34;
|
||||
|
||||
int sum = 0;
|
||||
for (int taskIndex = 0; taskIndex < NUMBER_OF_TASKS; taskIndex++) {
|
||||
int realTaskDays = taskEstimate[taskIndex] * realDaysPerIdealDay;
|
||||
int realTaskWeeks = realTaskDays / WORK_DAYS_PER_WEEK;
|
||||
sum += realTaskWeeks;
|
||||
}
|
||||
```
|
||||
|
||||
Now you can search for `WORK_DAYS_PER_WEEK` and find every place it is used. Try searching for `5` in a large codebase.
|
||||
|
||||
**Exception:** Single-letter names are acceptable only as local variables in short methods. The length of a name should correspond to the size of its scope.
|
||||
|
||||
Reference: [Clean Code, Chapter 2: Meaningful Names](https://www.oreilly.com/library/view/clean-code-a/9780136083238/)
|
||||
@@ -0,0 +1,69 @@
|
||||
---
|
||||
title: Avoid Hybrid Data-Object Structures
|
||||
impact: MEDIUM-HIGH
|
||||
impactDescription: prevents worst-of-both-worlds maintenance burden
|
||||
tags: obj, hybrid, structure, design
|
||||
---
|
||||
|
||||
## Avoid Hybrid Data-Object Structures
|
||||
|
||||
Hybrids are half object, half data structure. They have functions that do significant things, AND they have public variables or accessors that expose internal structure. Avoid creating these monstrosities.
|
||||
|
||||
**Incorrect (hybrid structure):**
|
||||
|
||||
```java
|
||||
// Half object, half data structure
|
||||
public class Employee {
|
||||
// Public data like a data structure
|
||||
public String name;
|
||||
public double salary;
|
||||
public List<String> skills;
|
||||
|
||||
// Behavior like an object
|
||||
public void promote() {
|
||||
this.salary *= 1.1;
|
||||
notifyHR();
|
||||
}
|
||||
|
||||
public void addSkill(String skill) {
|
||||
this.skills.add(skill);
|
||||
updateTrainingRecord();
|
||||
}
|
||||
}
|
||||
|
||||
// Client code can bypass behavior
|
||||
employee.salary = employee.salary * 1.5; // Skips notifyHR()
|
||||
employee.skills.clear(); // Skips updateTrainingRecord()
|
||||
```
|
||||
|
||||
**Correct (choose one approach):**
|
||||
|
||||
```java
|
||||
// Pure object - hide data, expose behavior
|
||||
public class Employee {
|
||||
private String name;
|
||||
private Money salary;
|
||||
private SkillSet skills;
|
||||
|
||||
public void promote(PromotionDetails details) {
|
||||
this.salary = salary.increase(details.getRaisePercentage());
|
||||
notifyHR(details);
|
||||
}
|
||||
|
||||
public void addSkill(Skill skill) {
|
||||
this.skills.add(skill);
|
||||
updateTrainingRecord(skill);
|
||||
}
|
||||
|
||||
public EmployeeReport getReport() {
|
||||
return new EmployeeReport(name, salary.getAmount(), skills.list());
|
||||
}
|
||||
}
|
||||
|
||||
// OR pure data structure - no behavior, expose data
|
||||
public record EmployeeData(String name, double salary, List<String> skills) {}
|
||||
```
|
||||
|
||||
Hybrids make it hard to add new functions AND hard to add new data structures.
|
||||
|
||||
Reference: [Clean Code, Chapter 6: Objects and Data Structures](https://www.oreilly.com/library/view/clean-code-a/9780136083238/)
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
title: Hide Data Behind Abstractions
|
||||
impact: MEDIUM-HIGH
|
||||
impactDescription: enables implementation changes without ripple effects
|
||||
tags: obj, abstraction, encapsulation, data-hiding
|
||||
---
|
||||
|
||||
## Hide Data Behind Abstractions
|
||||
|
||||
Objects hide their data behind abstractions and expose functions that operate on that data. Data structures expose their data and have no meaningful functions. Do not mix these concepts.
|
||||
|
||||
**Incorrect (exposing implementation):**
|
||||
|
||||
```java
|
||||
// Exposes internal representation - clients depend on Cartesian coordinates
|
||||
public class Point {
|
||||
public double x;
|
||||
public double y;
|
||||
}
|
||||
|
||||
// Using it forces clients to know the implementation
|
||||
double distance = Math.sqrt(point.x * point.x + point.y * point.y);
|
||||
```
|
||||
|
||||
**Correct (hiding behind abstraction — immutable):**
|
||||
|
||||
```java
|
||||
// Hides representation - could be Cartesian, polar, or something else
|
||||
public interface Point {
|
||||
double getX();
|
||||
double getY();
|
||||
double getR();
|
||||
double getTheta();
|
||||
|
||||
static Point fromCartesian(double x, double y) {
|
||||
return new CartesianPoint(x, y);
|
||||
}
|
||||
|
||||
static Point fromPolar(double r, double theta) {
|
||||
return new PolarPoint(r, theta);
|
||||
}
|
||||
}
|
||||
|
||||
// Clients work with the abstraction
|
||||
double distance = point.getR(); // Works regardless of internal representation
|
||||
Point moved = Point.fromCartesian(point.getX() + dx, point.getY() + dy);
|
||||
```
|
||||
|
||||
**Key insight:** The abstraction is not just about using getters/setters. It is about hiding the form of the data and exposing operations that work with the abstract concept. Prefer immutable objects with factory methods over mutable objects with setters — immutable designs are easier to reason about, thread-safe by default, and less prone to bugs.
|
||||
|
||||
Reference: [Clean Code, Chapter 6: Objects and Data Structures](https://www.oreilly.com/library/view/clean-code-a/9780136083238/)
|
||||
@@ -0,0 +1,72 @@
|
||||
---
|
||||
title: Understand Data/Object Anti-Symmetry
|
||||
impact: MEDIUM-HIGH
|
||||
impactDescription: prevents 2× effort when requirements change
|
||||
tags: obj, asymmetry, procedural, polymorphism
|
||||
---
|
||||
|
||||
## Understand Data/Object Anti-Symmetry
|
||||
|
||||
Objects and data structures are opposites. Objects hide data and expose behavior; data structures expose data and have no behavior. Choose based on what kind of changes you anticipate.
|
||||
|
||||
**Incorrect (wrong approach for frequent type additions):**
|
||||
|
||||
```java
|
||||
// Procedural approach - adding Triangle requires modifying EVERY function
|
||||
public class Square { public double side; }
|
||||
public class Circle { public double radius; }
|
||||
|
||||
public class Geometry {
|
||||
public double area(Object shape) {
|
||||
if (shape instanceof Square) {
|
||||
Square s = (Square) shape;
|
||||
return s.side * s.side;
|
||||
} else if (shape instanceof Circle) {
|
||||
Circle c = (Circle) shape;
|
||||
return Math.PI * c.radius * c.radius;
|
||||
}
|
||||
// Must add Triangle case here AND in perimeter() AND in draw()...
|
||||
throw new NoSuchShapeException();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (polymorphic approach for frequent type additions):**
|
||||
|
||||
```java
|
||||
// Object approach - adding Triangle only requires one new class
|
||||
public interface Shape {
|
||||
double area();
|
||||
}
|
||||
|
||||
public class Square implements Shape {
|
||||
private double side;
|
||||
|
||||
public double area() {
|
||||
return side * side;
|
||||
}
|
||||
}
|
||||
|
||||
public class Circle implements Shape {
|
||||
private double radius;
|
||||
|
||||
public double area() {
|
||||
return Math.PI * radius * radius;
|
||||
}
|
||||
}
|
||||
|
||||
// Adding Triangle is easy - just add one class
|
||||
public class Triangle implements Shape {
|
||||
private double base, height;
|
||||
|
||||
public double area() {
|
||||
return 0.5 * base * height;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Choose based on anticipated changes:**
|
||||
- Adding new functions often? Use data structures (procedural)
|
||||
- Adding new types often? Use objects (polymorphic)
|
||||
|
||||
Reference: [Clean Code, Chapter 6: Objects and Data Structures](https://www.oreilly.com/library/view/clean-code-a/9780136083238/)
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
title: Use DTOs for Data Transfer
|
||||
impact: MEDIUM
|
||||
impactDescription: prevents sensitive data exposure at boundaries
|
||||
tags: obj, dto, transfer, boundary
|
||||
---
|
||||
|
||||
## Use DTOs for Data Transfer
|
||||
|
||||
Data Transfer Objects (DTOs) are pure data structures for transferring data between layers or systems. They should have no behavior beyond accessors. Use them at system boundaries.
|
||||
|
||||
**Incorrect (domain object used for transfer):**
|
||||
|
||||
```java
|
||||
// Domain object with behavior used for API response
|
||||
public class User {
|
||||
private String name;
|
||||
private String hashedPassword;
|
||||
private CreditCard creditCard;
|
||||
|
||||
public void authenticate(String password) { /* ... */ }
|
||||
public void chargeCard(Money amount) { /* ... */ }
|
||||
|
||||
// Serialized directly - exposes sensitive data and internal structure
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (separate DTO for transfer):**
|
||||
|
||||
```java
|
||||
// Domain object with behavior (internal)
|
||||
public class User {
|
||||
private String name;
|
||||
private HashedPassword password;
|
||||
private CreditCard creditCard;
|
||||
|
||||
public void authenticate(String password) { /* ... */ }
|
||||
public void chargeCard(Money amount) { /* ... */ }
|
||||
|
||||
public UserDTO toDTO() {
|
||||
return new UserDTO(name, creditCard.getMaskedNumber());
|
||||
}
|
||||
}
|
||||
|
||||
// DTO for API response (external)
|
||||
public record UserDTO(String name, String maskedCardNumber) {}
|
||||
|
||||
// Active Record variant - DTO with persistence methods only
|
||||
public class UserRecord {
|
||||
public String name;
|
||||
public String email;
|
||||
|
||||
public void save() { /* SQL insert/update */ }
|
||||
public static UserRecord findById(Long id) { /* SQL select */ }
|
||||
}
|
||||
```
|
||||
|
||||
**Use DTOs when:**
|
||||
- Transferring data across system boundaries (API, database, message queue)
|
||||
- Hiding internal structure from external consumers
|
||||
- Transforming data format between layers
|
||||
|
||||
Reference: [Clean Code, Chapter 6: Objects and Data Structures](https://www.oreilly.com/library/view/clean-code-a/9780136083238/)
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
title: Follow the Law of Demeter
|
||||
impact: MEDIUM-HIGH
|
||||
impactDescription: prevents hidden dependencies between modules
|
||||
tags: obj, demeter, coupling, encapsulation
|
||||
---
|
||||
|
||||
## Follow the Law of Demeter
|
||||
|
||||
A method should only call methods on: its own object, objects passed as parameters, objects it creates, or its direct component objects. Avoid chained method calls on returned objects.
|
||||
|
||||
**Incorrect (train wreck - violates Law of Demeter):**
|
||||
|
||||
```java
|
||||
// Reaches through multiple objects
|
||||
String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
|
||||
|
||||
// Forces knowledge of internal structure
|
||||
customer.getAddress().getCity().getName();
|
||||
|
||||
// Chains expose implementation details
|
||||
report.getFormatter().getConfiguration().getPageSize();
|
||||
```
|
||||
|
||||
**Correct (respects object boundaries):**
|
||||
|
||||
```java
|
||||
// Option 1: Create a direct method
|
||||
String outputDir = ctxt.getOutputDirectory();
|
||||
|
||||
// Option 2: Tell, don't ask
|
||||
customer.sendBillingNotice(); // Let customer handle its own address
|
||||
|
||||
// Option 3: If data structure, use it directly (no behavior expected)
|
||||
BufferedOutputStream bos = ctxt.createScratchFileStream(classFileName);
|
||||
```
|
||||
|
||||
**Exception:** The Law of Demeter does not apply to data structures. If `Options`, `ScratchDir`, etc. are just data containers without behavior, the chaining is acceptable:
|
||||
|
||||
```java
|
||||
// Data transfer objects can be chained
|
||||
final String outputDir = ctxt.options.scratchDir.absolutePath;
|
||||
```
|
||||
|
||||
Reference: [Clean Code, Chapter 6: Objects and Data Structures](https://www.oreilly.com/library/view/clean-code-a/9780136083238/)
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
title: Use Build-Operate-Check Pattern
|
||||
impact: MEDIUM
|
||||
impactDescription: reduces test comprehension time by 50%
|
||||
tags: test, boc, arrange-act-assert, structure
|
||||
---
|
||||
|
||||
## Use Build-Operate-Check Pattern
|
||||
|
||||
Structure tests in three distinct phases: Build (arrange), Operate (act), Check (assert). This pattern makes tests easy to read and understand at a glance.
|
||||
|
||||
**Incorrect (mixed phases, unclear structure):**
|
||||
|
||||
```java
|
||||
@Test
|
||||
public void testOrder() {
|
||||
Order order = new Order();
|
||||
assertTrue(order.isEmpty());
|
||||
Product product = new Product("Widget", 9.99);
|
||||
order.add(product);
|
||||
assertEquals(1, order.getItemCount());
|
||||
assertEquals(9.99, order.getTotal(), 0.01);
|
||||
order.add(product);
|
||||
assertEquals(2, order.getItemCount());
|
||||
assertEquals(19.98, order.getTotal(), 0.01);
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (clear Build-Operate-Check):**
|
||||
|
||||
```java
|
||||
@Test
|
||||
public void addingProductShouldIncreaseItemCount() {
|
||||
// Build
|
||||
Order order = new Order();
|
||||
Product widget = new Product("Widget", 9.99);
|
||||
|
||||
// Operate
|
||||
order.add(widget);
|
||||
|
||||
// Check
|
||||
assertEquals(1, order.getItemCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addingProductShouldUpdateTotal() {
|
||||
// Build
|
||||
Order order = new Order();
|
||||
Product widget = new Product("Widget", 9.99);
|
||||
|
||||
// Operate
|
||||
order.add(widget);
|
||||
|
||||
// Check
|
||||
assertEquals(9.99, order.getTotal(), 0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addingSameProductTwiceShouldDoubleTotal() {
|
||||
// Build
|
||||
Order order = new Order();
|
||||
Product widget = new Product("Widget", 9.99);
|
||||
|
||||
// Operate
|
||||
order.add(widget);
|
||||
order.add(widget);
|
||||
|
||||
// Check
|
||||
assertEquals(19.98, order.getTotal(), 0.01);
|
||||
}
|
||||
```
|
||||
|
||||
**Alias patterns:**
|
||||
- Build-Operate-Check (BOC)
|
||||
- Arrange-Act-Assert (AAA)
|
||||
- Given-When-Then (BDD)
|
||||
|
||||
Reference: [Clean Code, Chapter 9: Unit Tests](https://www.oreilly.com/library/view/clean-code-a/9780136083238/)
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
title: Follow the Three Laws of TDD
|
||||
impact: MEDIUM
|
||||
impactDescription: achieves 90%+ test coverage naturally
|
||||
tags: test, tdd, first, laws
|
||||
---
|
||||
|
||||
## Follow the Three Laws of TDD
|
||||
|
||||
Test-Driven Development follows three rules: (1) Write a failing test before production code, (2) Write only enough test to fail, (3) Write only enough production code to pass. This cycle repeats.
|
||||
|
||||
**Incorrect (writing tests after code):**
|
||||
|
||||
```java
|
||||
// Production code written first, then tests added later (or never)
|
||||
public class Calculator {
|
||||
public int add(int a, int b) {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
public int divide(int a, int b) {
|
||||
return a / b; // Edge cases not considered
|
||||
}
|
||||
}
|
||||
|
||||
// Test written after - may not cover edge cases
|
||||
@Test
|
||||
public void testAdd() {
|
||||
assertEquals(4, calculator.add(2, 2));
|
||||
}
|
||||
// No test for divide by zero because code was written first
|
||||
```
|
||||
|
||||
**Correct (TDD cycle):**
|
||||
|
||||
```java
|
||||
// 1. Write failing test first
|
||||
@Test
|
||||
public void divideShouldReturnQuotient() {
|
||||
assertEquals(2, calculator.divide(6, 3));
|
||||
}
|
||||
|
||||
// 2. Write minimal code to pass
|
||||
public int divide(int a, int b) {
|
||||
return a / b;
|
||||
}
|
||||
|
||||
// 3. Write next failing test
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void divideShouldThrowOnDivideByZero() {
|
||||
calculator.divide(6, 0);
|
||||
}
|
||||
|
||||
// 4. Update code to pass
|
||||
public int divide(int a, int b) {
|
||||
if (b == 0) {
|
||||
throw new IllegalArgumentException("Cannot divide by zero");
|
||||
}
|
||||
return a / b;
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Tests cover requirements, not implementation
|
||||
- Edge cases discovered through test-first thinking
|
||||
- High coverage achieved naturally
|
||||
|
||||
Reference: [Clean Code, Chapter 9: Unit Tests](https://www.oreilly.com/library/view/clean-code-a/9780136083238/)
|
||||
@@ -0,0 +1,80 @@
|
||||
---
|
||||
title: Follow FIRST Principles
|
||||
impact: MEDIUM
|
||||
impactDescription: prevents test suite from becoming 10× slower over time
|
||||
tags: test, first, fast, independent, repeatable
|
||||
---
|
||||
|
||||
## Follow FIRST Principles
|
||||
|
||||
Clean tests follow FIRST: Fast, Independent, Repeatable, Self-Validating, and Timely. These properties ensure tests remain useful and maintainable.
|
||||
|
||||
**Incorrect (violating FIRST):**
|
||||
|
||||
```java
|
||||
// Slow - depends on real database
|
||||
@Test
|
||||
public void testUserQuery() {
|
||||
Connection conn = DriverManager.getConnection(PROD_DB_URL); // Slow!
|
||||
// ...
|
||||
}
|
||||
|
||||
// Not Independent - tests depend on each other
|
||||
private static User sharedUser;
|
||||
|
||||
@Test
|
||||
public void test1CreateUser() {
|
||||
sharedUser = userService.create("bob");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test2UpdateUser() {
|
||||
sharedUser.setName("alice"); // Depends on test1 running first
|
||||
}
|
||||
|
||||
// Not Repeatable - depends on external state
|
||||
@Test
|
||||
public void testPayment() {
|
||||
assertTrue(paymentGateway.charge(card, 100)); // Different result each time
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (following FIRST):**
|
||||
|
||||
```java
|
||||
// Fast - uses mocks, runs in milliseconds
|
||||
@Test
|
||||
public void queryShouldReturnMatchingUsers() {
|
||||
UserRepository repo = mock(UserRepository.class);
|
||||
when(repo.findByName("bob")).thenReturn(List.of(testUser));
|
||||
|
||||
List<User> result = userService.query("bob");
|
||||
|
||||
assertEquals(1, result.size());
|
||||
}
|
||||
|
||||
// Independent - each test sets up its own state
|
||||
@Test
|
||||
public void createShouldReturnNewUser() {
|
||||
User user = userService.create("bob");
|
||||
assertEquals("bob", user.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateShouldModifyUserName() {
|
||||
User user = userService.create("bob");
|
||||
user.setName("alice");
|
||||
assertEquals("alice", user.getName());
|
||||
}
|
||||
|
||||
// Repeatable - uses test doubles
|
||||
@Test
|
||||
public void chargeShouldReturnTrueOnSuccess() {
|
||||
PaymentGateway gateway = mock(PaymentGateway.class);
|
||||
when(gateway.charge(any(), eq(100))).thenReturn(true);
|
||||
|
||||
assertTrue(paymentService.processPayment(100));
|
||||
}
|
||||
```
|
||||
|
||||
Reference: [Clean Code, Chapter 9: Unit Tests](https://www.oreilly.com/library/view/clean-code-a/9780136083238/)
|
||||
@@ -0,0 +1,75 @@
|
||||
---
|
||||
title: Keep Tests Clean
|
||||
impact: MEDIUM
|
||||
impactDescription: maintains test maintainability over time
|
||||
tags: test, clean, maintainability, quality
|
||||
---
|
||||
|
||||
## Keep Tests Clean
|
||||
|
||||
Test code is just as important as production code. It requires thought, design, and care. If you let tests rot, they become a liability instead of an asset.
|
||||
|
||||
**Incorrect (dirty tests):**
|
||||
|
||||
```java
|
||||
@Test
|
||||
public void test1() {
|
||||
// Cryptic, no clear purpose
|
||||
S s = new S();
|
||||
s.a("x");
|
||||
s.b("y");
|
||||
assertTrue(s.c());
|
||||
s.d();
|
||||
assertEquals("xy", s.e());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEverything() {
|
||||
// Multiple assertions testing different concepts
|
||||
assertNotNull(user);
|
||||
assertEquals("Bob", user.getName());
|
||||
assertTrue(user.isActive());
|
||||
assertEquals(3, user.getOrders().size());
|
||||
assertTrue(user.getOrders().get(0).isPaid());
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (clean, readable tests):**
|
||||
|
||||
```java
|
||||
@Test
|
||||
public void concatenatingTwoStringsShouldReturnCombinedResult() {
|
||||
StringBuffer buffer = new StringBuffer();
|
||||
|
||||
buffer.append("first");
|
||||
buffer.append("second");
|
||||
|
||||
assertEquals("firstsecond", buffer.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void userShouldBeActiveAfterActivation() {
|
||||
User user = new User("Bob");
|
||||
|
||||
user.activate();
|
||||
|
||||
assertTrue(user.isActive());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void activatingUserShouldSendWelcomeEmail() {
|
||||
User user = new User("Bob");
|
||||
EmailService emailService = mock(EmailService.class);
|
||||
|
||||
user.activate(emailService);
|
||||
|
||||
verify(emailService).sendWelcome(user.getEmail());
|
||||
}
|
||||
```
|
||||
|
||||
**Qualities of clean tests:**
|
||||
- Readable: Clear setup, action, assertion
|
||||
- One concept per test
|
||||
- Descriptive names explaining what is being tested
|
||||
|
||||
Reference: [Clean Code, Chapter 9: Unit Tests](https://www.oreilly.com/library/view/clean-code-a/9780136083238/)
|
||||
Loaded 100 of 287 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user