practices/assertive-programming.md
# Assertive Programming
> "If It Can't Happen, Use Assertions to Ensure That It Won't"
> — David Thomas & Andrew Hunt
## Definition
**Assertive Programming** is the practice of using assertions to validate assumptions about your code's state during execution. Assertions are executable documentation that states what you believe to be true at specific points in your program. If an assertion fails, it indicates a programming error—a violation of your code's fundamental assumptions.
Think of assertions as tripwires for impossible conditions. They catch bugs early, document invariants, and serve as executable specifications of your code's contracts.
## Core Principle
**If something "can't happen," write an assertion to guarantee it won't.**
Developers often make implicit assumptions:
- "This parameter will never be null"
- "This array will always have at least one element"
- "This calculation will never produce a negative value"
Assertive programming makes these assumptions **explicit and verifiable**.
```pseudocode
// Implicit assumption: discountRate is between 0 and 1
function calculateDiscount(price, discountRate)
return price * discountRate
// Explicit assertion: discountRate must be valid
function calculateDiscountSafe(price, discountRate)
assert(discountRate >= 0 AND discountRate <= 1,
"Discount rate must be between 0 and 1")
return price * discountRate
```
## Assertions vs Error Handling
| Aspect | Assertions | Error Handling |
|--------|-----------|----------------|
| **Purpose** | Catch programmer errors | Handle runtime conditions |
| **When to use** | "This should never happen" | "This might happen" |
| **Example** | Internal invariant violation | User enters invalid input |
| **Recovery** | Program should crash/halt | Graceful recovery possible |
| **Audience** | Developers | End users |
### Examples
```pseudocode
// ASSERTION: Internal contract violation
function divide(numerator, denominator)
// Caller should have validated; this is a programming error
assert(denominator != 0, "Denominator cannot be zero")
return numerator / denominator
// ERROR HANDLING: Expected runtime condition
function getUserInput()
input = readFromUser()
if input.isEmpty()
throw ValidationError("Input cannot be empty")
return input
```
## What to Assert
### 1. Preconditions
Conditions that must be true before a routine executes.
```pseudocode
function withdrawMoney(account, amount)
assert(account != null, "Account must not be null")
assert(amount > 0, "Withdrawal amount must be positive")
assert(account.balance >= amount, "Insufficient funds")
account.balance = account.balance - amount
```
### 2. Postconditions
Conditions guaranteed to be true after a routine completes.
```pseudocode
function sortArray(array)
originalLength = array.length
// ... sorting logic ...
assert(array.length == originalLength,
"Array length changed during sort")
assert(isSorted(array), "Array is not sorted")
return array
```
### 3. Invariants
Conditions that must always hold true for a data structure.
```pseudocode
class BoundedQueue
maxSize = 100
items = []
function enqueue(item)
assert(items.length < maxSize,
"Queue invariant violated: exceeds max size")
items.add(item)
assert(items.length <= maxSize, "Post-condition failed")
function dequeue()
assert(items.length > 0, "Cannot dequeue from empty queue")
return items.removeFirst()
```
### 4. Unreachable Code Paths
Code that should never execute.
```pseudocode
function handleStatus(status)
switch status
case SUCCESS:
return processSuccess()
case ERROR:
return processError()
case PENDING:
return processPending()
default:
assert(false, "Unknown status: " + status)
```
## Leave Assertions On (The Pragmatic View)
### Traditional View
Many programming communities recommend disabling assertions in production for performance reasons.
### Pragmatic View
**Leave assertions enabled in production.**
**Why?**
1. **Bugs don't disappear in production**
- The conditions you're asserting against are bugs
- Production has unique data, load, and timing that testing can't replicate
2. **Fail fast is better than corrupt slowly**
- Immediate crash is preferable to silent data corruption
- Assertions catch bugs at the source, not after cascading failures
3. **Performance cost is negligible**
- Modern compilers optimize assertions efficiently
- Most assertions are simple boolean checks
- The cost of a bug is far higher than CPU cycles
4. **Production-only bugs exist**
- Edge cases appear under real load
- Assertions are your last line of defense
```pseudocode
// Without assertions in production:
function transfer(fromAccount, toAccount, amount)
fromAccount.balance = fromAccount.balance - amount
toAccount.balance = toAccount.balance + amount
// Bug: negative balance undetected, corrupts financial data
// With assertions enabled:
function transfer(fromAccount, toAccount, amount)
assert(amount > 0, "Transfer amount must be positive")
assert(fromAccount.balance >= amount, "Insufficient funds")
fromAccount.balance = fromAccount.balance - amount
toAccount.balance = toAccount.balance + amount
assert(fromAccount.balance >= 0, "Balance went negative!")
// Catches the bug immediately, prevents data corruption
```
## When NOT to Use Assertions
| Scenario | Use Instead |
|----------|-------------|
| Validating user input | Input validation + error handling |
| Checking external system responses | Error handling with retry logic |
| Network/IO failures | Exception handling |
| Business logic validation | Explicit conditional checks |
| Security checks | Never rely on assertions alone |
```pseudocode
// WRONG: Don't use assertions for security
function authenticateUser(username, password)
assert(password.length > 8, "Password too short")
// Attacker could disable assertions!
// RIGHT: Explicit security validation
function authenticateUser(username, password)
if password.length < 8
throw SecurityError("Password must be at least 8 characters")
```
## Best Practices
1. **Make assertion messages descriptive**
```pseudocode
// Bad
assert(x > 0)
// Good
assert(x > 0, "Product quantity must be positive, got: " + x)
```
2. **Assert the impossible, handle the unlikely**
```pseudocode
// Impossible (programming error)
assert(pointer != null, "Null pointer in initialized object")
// Unlikely but possible (runtime condition)
if fileExists(path) == false
throw FileNotFoundError("File does not exist: " + path)
```
3. **Don't put side effects in assertions**
```pseudocode
// WRONG
assert(list.remove(item) == true)
// RIGHT
wasRemoved = list.remove(item)
assert(wasRemoved == true, "Item not found in list")
```
## Summary Table
| Principle | Implementation |
|-----------|----------------|
| **Purpose** | Validate assumptions and catch programmer errors |
| **Placement** | Preconditions, postconditions, invariants, unreachable code |
| **Message quality** | Descriptive, includes context and actual values |
| **Production use** | Leave enabled (pragmatic approach) |
| **Not for** | User input, external systems, business logic, security |
| **Benefit** | Fail fast, self-documenting code, early bug detection |
**Key Takeaway**: Assertions are executable documentation of your code's contracts. They transform implicit assumptions into explicit, verifiable statements.
---
*Based on concepts from "The Pragmatic Programmer" by David Thomas and Andrew Hunt.*
practices/automation.md
# Automation
## Definition
> "Don't use manual procedures."
>
> "Civilization advances by extending the number of important operations we can perform without thinking about them."
>
> *— The Pragmatic Programmer*
Automation is the practice of using tools and scripts to perform repetitive tasks consistently and reliably. Pragmatic programmers automate everything that can be automated, freeing themselves to focus on creative problem-solving rather than routine drudgery.
## Core Principle: Don't Use Manual Procedures
Manual procedures are:
- **Error-prone**: Humans forget steps, skip validations, make typos
- **Inconsistent**: Different people execute them differently
- **Slow**: Manual work doesn't scale
- **Undocumented**: The process lives in someone's head
- **Not repeatable**: Results vary between executions
Automated procedures are:
- **Reliable**: Execute the same way every time
- **Fast**: Run at machine speed
- **Self-documenting**: The script is the documentation
- **Scalable**: Run once or a million times with equal effort
- **Auditable**: Logs prove what happened
## What to Automate
### 1. Build Process
**Manual build problems:**
```pseudocode
# Developer's mental checklist:
1. Pull latest code
2. Install dependencies (maybe?)
3. Compile (which flags again?)
4. Run some tests (which ones?)
5. Package (how exactly?)
6. Deploy to staging (where?)
```
**Automated build:**
```pseudocode
#!/usr/bin/env bash
# build.sh - Single command builds everything
function build() {
log "Updating dependencies..."
install_dependencies()
log "Running static analysis..."
run_linter() || fail "Linting failed"
log "Compiling..."
compile_source() || fail "Compilation failed"
log "Running tests..."
run_unit_tests() || fail "Tests failed"
run_integration_tests() || fail "Integration tests failed"
log "Packaging..."
create_distribution() || fail "Packaging failed"
log "Build successful!"
report_build_metrics()
}
build
```
### 2. Testing
**Automated test execution:**
```pseudocode
#!/usr/bin/env bash
# test.sh - Run all test suites
function run_all_tests() {
# Unit tests - fast feedback
run_command "test:unit" || return 1
# Integration tests - slower but comprehensive
run_command "test:integration" || return 1
# End-to-end tests - full system verification
run_command "test:e2e" || return 1
# Performance tests - regression detection
run_command "test:performance" || return 1
# Security tests - vulnerability scanning
run_command "test:security" || return 1
}
function run_command(test_suite) {
log "Running ${test_suite}..."
execute_tests(test_suite)
if failed(test_suite) then
log_failure(test_suite)
send_notification("Test suite failed: ${test_suite}")
return 1
end
log_success(test_suite)
return 0
}
```
### 3. Deployment
**Zero-downtime deployment script:**
```pseudocode
#!/usr/bin/env bash
# deploy.sh - Deploy to production safely
function deploy(environment, version) {
validate_environment(environment) || fail "Invalid environment"
validate_version(version) || fail "Invalid version"
# Pre-deployment checks
run_health_checks(environment) || fail "Environment unhealthy"
backup_database(environment) || fail "Backup failed"
# Deploy
log "Deploying ${version} to ${environment}..."
# Blue-green deployment pattern
deploy_to_standby_servers(version)
run_smoke_tests(standby_servers) || rollback()
switch_load_balancer(standby_servers)
# Post-deployment verification
run_health_checks(environment) || rollback()
run_integration_tests(environment) || rollback()
log "Deployment successful!"
notify_team("${version} deployed to ${environment}")
# Cleanup
retire_old_servers()
}
```
### 4. Database Backups
**Automated backup system:**
```pseudocode
#!/usr/bin/env bash
# backup.sh - Database backup with retention
function backup_database() {
timestamp = current_timestamp()
backup_file = "db_backup_${timestamp}.sql.gz"
log "Starting database backup..."
# Create backup
dump_database() | compress() > backup_file
verify_backup(backup_file) || fail "Backup verification failed"
# Upload to remote storage
upload_to_s3(backup_file, bucket="backups", retention_days=30)
# Keep local copy
move_to_backup_directory(backup_file)
# Cleanup old backups
delete_backups_older_than(days=7, location="local")
log "Backup completed: ${backup_file}"
send_metrics("backup.success", 1)
}
# Restore function - because backups are useless if you can't restore
function restore_database(backup_file) {
confirm_restore() || abort "Restore cancelled"
stop_application_servers()
decompress(backup_file) | restore_to_database()
verify_restore() || fail "Restore verification failed"
start_application_servers()
run_smoke_tests() || fail "Application broken after restore"
log "Restore completed successfully"
}
```
### 5. Environment Setup
**Developer onboarding script:**
```pseudocode
#!/usr/bin/env bash
# setup.sh - Get new developer productive in minutes
function setup_development_environment() {
log "Setting up development environment..."
# System dependencies
install_required_tools([
"git",
"docker",
"node",
"database_client"
])
# Project dependencies
clone_repository()
install_project_dependencies()
# Configuration
copy_template_config()
generate_local_secrets()
# Database
create_local_database()
run_migrations()
seed_test_data()
# Verification
run_build() || fail "Build failed"
run_tests() || fail "Tests failed"
log "Setup complete! Run './start.sh' to begin development."
}
```
## Cron Jobs and Scheduled Tasks
**Scheduling automated tasks:**
```pseudocode
# crontab - Schedule recurring automation
# Syntax: minute hour day month weekday command
# Every 5 minutes - Monitor system health
*/5 * * * * /scripts/health_check.sh
# Every hour - Clean temporary files
0 * * * * /scripts/cleanup_temp.sh
# Every day at 2 AM - Database backup
0 2 * * * /scripts/backup.sh
# Every Sunday at 3 AM - Full system maintenance
0 3 * * 0 /scripts/weekly_maintenance.sh
# First of every month - Generate reports
0 0 1 * * /scripts/monthly_report.sh
```
**Robust cron job script:**
```pseudocode
#!/usr/bin/env bash
# health_check.sh - Monitor and auto-heal
function health_check() {
# Lock to prevent concurrent runs
acquire_lock("health_check") || exit 0
try {
# Check critical services
for service in get_critical_services() {
if not is_healthy(service) then
log_alert("Service unhealthy: ${service}")
attempt_auto_heal(service)
if still_not_healthy(service) then
page_on_call_engineer(service)
end
end
}
# Check disk space
if disk_usage() > 80% then
cleanup_old_logs()
alert_team("Disk space cleaned: ${freed_space}")
end
# Check memory
if memory_usage() > 90% then
restart_memory_leaking_service()
alert_team("High memory usage detected and addressed")
end
} finally {
release_lock("health_check")
}
}
```
## Continuous Integration Basics
**CI pipeline configuration:**
```pseudocode
# ci_pipeline.yml - Automated CI/CD workflow
pipeline "main_branch":
triggers:
- on: push
branches: [main, develop]
- on: pull_request
stages:
- stage: "build"
steps:
- checkout_code()
- install_dependencies()
- compile()
- cache_dependencies()
- stage: "test"
parallel:
- run_unit_tests()
- run_integration_tests()
- run_linter()
- run_security_scan()
- stage: "package"
if: branch == "main"
steps:
- create_docker_image()
- push_to_registry()
- tag_release()
- stage: "deploy_staging"
if: branch == "main"
steps:
- deploy(environment="staging")
- run_smoke_tests(environment="staging")
- stage: "deploy_production"
if: branch == "main" and manual_approval
steps:
- deploy(environment="production")
- run_smoke_tests(environment="production")
- notify_team("Production deployment complete")
on_failure:
- rollback()
- alert_team("Build failed!")
on_success:
- update_status_dashboard()
```
## Infrastructure as Code
**Declarative infrastructure:**
```pseudocode
# infrastructure.tf - Define infrastructure as code
resource "web_server" {
name = "production-web"
instance_type = "large"
count = 3 # Auto-scaling group
network {
vpc = "production-vpc"
subnet = "public-subnet"
security_groups = ["web-tier"]
}
storage {
volume_size = 100
volume_type = "ssd"
backup_enabled = true
backup_retention_days = 30
}
monitoring {
enabled = true
alert_on_cpu_above = 80%
alert_on_memory_above = 85%
}
tags = {
environment = "production"
managed_by = "terraform"
cost_center = "engineering"
}
}
resource "database" {
name = "production-db"
engine = "postgres"
version = "14.5"
replication {
enabled = true
read_replicas = 2
backup_window = "02:00-03:00"
maintenance_window = "sun:03:00-sun:04:00"
}
encryption {
at_rest = true
in_transit = true
}
}
resource "load_balancer" {
name = "production-lb"
health_check {
path = "/health"
interval = 30
timeout = 5
healthy_threshold = 2
unhealthy_threshold = 3
}
routing {
distribute_to = web_server.instances
algorithm = "least_connections"
}
}
```
**Infrastructure deployment:**
```pseudocode
#!/usr/bin/env bash
# deploy_infrastructure.sh
function deploy_infrastructure() {
environment = $1
log "Deploying infrastructure for ${environment}..."
# Plan - show what will change
infrastructure_tool plan \
-var-file="${environment}.tfvars" \
-out="${environment}.plan"
# Review plan
show_plan_summary("${environment}.plan")
if environment == "production" then
require_manual_approval() || abort
end
# Apply changes
infrastructure_tool apply "${environment}.plan"
# Verify deployment
run_infrastructure_tests(environment)
log "Infrastructure deployment complete"
}
```
## Return on Investment (ROI)
### Time Savings Calculation
```pseudocode
function calculate_automation_roi(task) {
# Initial investment
time_to_automate = 4 hours
cost_to_automate = time_to_automate * developer_hourly_rate
# Ongoing savings
manual_time_per_run = task.manual_duration
automated_time_per_run = task.automated_duration
time_saved_per_run = manual_time_per_run - automated_time_per_run
runs_per_month = task.frequency
monthly_time_savings = time_saved_per_run * runs_per_month
monthly_cost_savings = monthly_time_savings * developer_hourly_rate
# Break-even analysis
break_even_months = cost_to_automate / monthly_cost_savings
# 1-year ROI
annual_savings = monthly_cost_savings * 12
roi_percentage = ((annual_savings - cost_to_automate) / cost_to_automate) * 100
return {
break_even: break_even_months,
annual_savings: annual_savings,
roi: roi_percentage
}
}
# Example: Automating daily deployment
task = {
name: "Daily deployment",
manual_duration: 45 minutes,
automated_duration: 5 minutes,
frequency: 20 times/month # Once per workday
}
roi = calculate_automation_roi(task)
# Result: Break-even in 0.3 months, 2400% annual ROI
```
### Quality Benefits
Beyond time savings, automation provides:
| Benefit | Impact |
|---------|--------|
| **Consistency** | Eliminates human error in repetitive tasks |
| **Documentation** | Scripts serve as executable documentation |
| **Knowledge Transfer** | New team members onboard faster |
| **Confidence** | Reliable processes encourage frequent releases |
| **Speed** | Fast feedback loops improve development velocity |
| **Scalability** | Handle 10x growth without 10x manual effort |
| **Auditability** | Complete logs of what happened when |
| **Reproducibility** | Recreate any environment or state on demand |
## The Automation Mindset
**When to automate:**
```pseudocode
function should_automate(task) {
# "Rule of Three" - automate after third time
if task.times_performed >= 3 then
return true
end
# High frequency tasks
if task.frequency > once_per_week then
return true
end
# Error-prone tasks
if task.error_rate > 5% then
return true
end
# Critical tasks
if task.risk == "high" and task.requires_precision then
return true
end
# Time-consuming tasks
if task.duration > 15 minutes then
return true
end
# Tasks that block others
if task.blocks_other_work then
return true
end
return false
}
```
**Automation evolution:**
```pseudocode
# Level 1: Manual process documented
"Follow these 20 steps in this wiki page..."
# Level 2: Semi-automated (script assists)
"Run this script, then manually verify, then run next script..."
# Level 3: Fully automated (one command)
"Run './deploy.sh production' and it handles everything"
# Level 4: Continuous automation (no manual trigger)
"Push to main branch, deployment happens automatically"
# Level 5: Self-healing automation
"System detects issues and fixes them without human intervention"
```
## Summary
| Aspect | Key Points |
|--------|------------|
| **Philosophy** | Automate everything that can be automated |
| **Benefits** | Consistency, speed, reliability, scalability |
| **What to Automate** | Builds, tests, deployments, backups, monitoring |
| **Tools** | Shell scripts, CI/CD pipelines, infrastructure as code |
| **ROI** | Most automation pays for itself within weeks |
| **Best Practices** | Make automation idempotent, add error handling, log everything |
| **Evolution** | Start simple, iterate, eventually achieve continuous automation |
| **Mindset** | If you do it twice, automate it the third time |
| **Documentation** | The automation script IS the documentation |
| **Quality** | Automated processes are more reliable than manual ones |
**The Pragmatic Principle**: Every manual process is a bug waiting to happen. Automate relentlessly, and your future self will thank you.
---
*Based on concepts from "The Pragmatic Programmer" by David Thomas and Andrew Hunt.*
practices/code-generators.md
# Code Generators
> "Write code that writes code."
> — David Thomas & Andrew Hunt
Code generators are programs that create source code, configuration files, or other artifacts from higher-level specifications. Instead of writing repetitive code by hand, pragmatic programmers build tools that generate it consistently and correctly.
## Types of Code Generators
### Passive Generators
**Definition**: One-time code production tools that generate output once, which developers then own and modify.
**Characteristics**:
- Run once at project setup or when adding new components
- Generated code becomes part of the codebase
- Developers customize and maintain generated output
- No ongoing synchronization needed
**Use Cases**:
- Project scaffolding and initial structure
- Boilerplate class creation
- Database migration file templates
- Test file skeletons
```pseudocode
COMMAND:
generate controller User --actions index show create
GENERATES (one time):
FILE: controllers/user_controller.ext
FUNCTION index():
// TODO: implement index logic
END
FUNCTION show(id):
// TODO: implement show logic
END
FUNCTION create(data):
// TODO: implement create logic
END
END FILE
DEVELOPER then customizes this code directly
```
### Active Generators
**Definition**: Ongoing code production tools that regenerate output whenever specifications change.
**Characteristics**:
- Run repeatedly as specs evolve
- Generated code should not be manually edited
- Changes made only to source specifications
- Maintains perfect synchronization
**Use Cases**:
- Database ORM models from schema
- API clients from OpenAPI specifications
- Language bindings from IDL files
- Documentation from code annotations
```pseudocode
SCHEMA FILE: database/schema.def
TABLE users:
FIELD id: INTEGER PRIMARY_KEY
FIELD username: STRING UNIQUE NOT_NULL
FIELD email: STRING NOT_NULL
FIELD created_at: TIMESTAMP DEFAULT_NOW
END TABLE
END SCHEMA
GENERATOR runs on schema change:
READ schema.def
FOR EACH table IN schema:
GENERATE model class with:
- Properties for each field
- Type validation
- Database mapping
- Query builders
END FOR
GENERATES: models/user.ext (DO NOT EDIT - regenerated from schema)
```
## When to Use Code Generation
| Situation | Generate? | Why |
|-----------|-----------|-----|
| Repeating same pattern 3+ times | Yes | DRY principle - eliminate repetition |
| Data structure has canonical source | Yes | Single source of truth ensures consistency |
| Multiple representations needed | Yes | Schema → models, API, docs, tests |
| Simple templating suffices | Maybe | Balance complexity vs. benefit |
| Logic is truly unique | No | Generation adds no value |
| Pattern varies significantly | No | Templates become too complex |
## Template-Based Generation
Template systems replace placeholders with actual values.
```pseudocode
TEMPLATE: crud_service.template
CLASS {{EntityName}}Service:
PRIVATE repository: {{EntityName}}Repository
FUNCTION get_all():
RETURN repository.find_all()
END
FUNCTION get_by_id(id):
result = repository.find(id)
IF result IS NULL:
THROW NotFoundException("{{EntityName}} not found")
END
RETURN result
END
FUNCTION create(data):
entity = NEW {{EntityName}}(data)
VALIDATE entity
RETURN repository.save(entity)
END
END CLASS
END TEMPLATE
GENERATOR:
INPUT: entity_name = "Product"
LOAD template FROM "crud_service.template"
replacements = {
"{{EntityName}}": entity_name
}
output = APPLY replacements TO template
WRITE output TO "services/product_service.ext"
END GENERATOR
```
## Schema-Driven Generation
More sophisticated approach using structured specifications.
```pseudocode
SCHEMA: api_spec.schema
ENTITY Product:
FIELDS:
- id: INTEGER (primary_key, auto_increment)
- name: STRING (required, max_length: 200)
- price: DECIMAL (required, min: 0)
- category_id: INTEGER (foreign_key: Category)
OPERATIONS:
- list (public)
- get (public)
- create (authenticated, role: admin)
- update (authenticated, role: admin)
- delete (authenticated, role: admin)
END ENTITY
END SCHEMA
GENERATOR SUITE:
FUNCTION generate_all(schema):
entities = PARSE schema
FOR EACH entity IN entities:
generate_model(entity)
generate_repository(entity)
generate_service(entity)
generate_controller(entity)
generate_routes(entity)
generate_validators(entity)
generate_tests(entity)
generate_api_docs(entity)
END FOR
END FUNCTION
END GENERATOR
```
## Keeping Generated Code in Sync
### Protected Regions Pattern
Allow manual customization within generated files using protected blocks.
```pseudocode
TEMPLATE with protected regions:
CLASS {{EntityName}}Service:
// GENERATED: DO NOT EDIT ABOVE THIS LINE
// CUSTOM CODE BEGIN: additional_properties
// Add custom properties here - preserved across regeneration
// CUSTOM CODE END: additional_properties
FUNCTION get_all():
RETURN repository.find_all()
END
// CUSTOM CODE BEGIN: custom_methods
// Add custom methods here - preserved across regeneration
// CUSTOM CODE END: custom_methods
END CLASS
REGENERATION LOGIC:
FUNCTION regenerate_with_preservation(template, output_file):
IF output_file EXISTS:
old_content = READ output_file
custom_blocks = EXTRACT_CUSTOM_BLOCKS(old_content)
ELSE:
custom_blocks = EMPTY
END IF
new_content = GENERATE_FROM_TEMPLATE(template)
final_content = MERGE_CUSTOM_BLOCKS(new_content, custom_blocks)
WRITE final_content TO output_file
END FUNCTION
END REGENERATION
```
### Separation Strategy
Keep generated and custom code completely separate.
```pseudocode
GENERATED FILE: models/product.generated.ext
// AUTO-GENERATED - DO NOT EDIT
CLASS ProductGenerated:
PROPERTY id: Integer
PROPERTY name: String
PROPERTY price: Decimal
FUNCTION basic_validate():
ASSERT name IS_NOT_NULL
ASSERT price >= 0
END
END CLASS
CUSTOM FILE: models/product.ext
IMPORT ProductGenerated
CLASS Product EXTENDS ProductGenerated:
// Custom business logic
FUNCTION calculate_discounted_price(discount_percent):
RETURN this.price * (1 - discount_percent / 100)
END
FUNCTION validate():
super.basic_validate()
// Additional custom validation
IF this.name.length < 3:
THROW ValidationError("Name too short")
END IF
END
END CLASS
```
## Guidelines for Effective Generators
| Guideline | Rationale |
|-----------|-----------|
| **Make generators obvious** | Clear markers prevent accidental edits |
| **Version your schemas** | Schema changes are breaking changes |
| **Test the generator** | Generators are code too |
| **Provide escape hatches** | Protected regions or extension points |
| **Document the input format** | Schemas are contracts |
| **Regenerate in CI/CD** | Catch drift between specs and code |
| **Keep it simple** | Complex generators are hard to maintain |
## Summary Table
| Aspect | Passive Generators | Active Generators |
|--------|-------------------|-------------------|
| **Frequency** | One-time | Repeated on change |
| **Output ownership** | Developer | Generator |
| **Manual editing** | Expected | Forbidden |
| **Use case** | Scaffolding, boilerplate | Schema-driven layers |
| **Sync requirement** | None | Continuous |
| **Example** | Project templates | ORM models, API clients |
## Key Principles
1. **Don't Repeat Yourself** - If you're writing similar code repeatedly, generate it
2. **Single Source of Truth** - Derive everything from one authoritative schema
3. **Passive for Kickstarts** - Use passive generation for project setup
4. **Active for Consistency** - Use active generation for schema-driven code
5. **Mark Generated Code** - Always clearly identify what's generated
6. **Test Your Generators** - Generators are production code
7. **Provide Customization** - Protected regions or inheritance for custom logic
---
*Based on concepts from "The Pragmatic Programmer" by David Thomas and Andrew Hunt.*
practices/debugging.md
# Debugging
## Definition
> "Debugging is problem solving, and problem solving requires a methodical approach coupled with a certain amount of lateral thinking."
> — *The Pragmatic Programmer*
Debugging is the systematic process of identifying, isolating, and fixing defects in code. It requires a mindset shift from blame to curiosity—treating bugs as puzzles to solve rather than evidence of failure.
---
## The Psychology of Debugging
### It's a Puzzle, Not Blame
- **Bug ≠ Personal Failure**: Bugs are inevitable in software development. They're opportunities to learn.
- **Stay Calm**: Panic and frustration cloud judgment. Approach debugging as a detective would approach a mystery.
- **Avoid Fingerpointing**: Blaming the compiler, the library, or the OS wastes time. Focus on what you can control: your code.
### Embrace the Challenge
- Debugging sharpens problem-solving skills
- Each bug reveals gaps in understanding
- Solving difficult bugs builds expertise
---
## Core Debugging Strategies
| Strategy | Description | When to Use |
|----------|-------------|-------------|
| **Binary Search** | Divide the problem space in half repeatedly | Large codebases, unclear failure point |
| **Rubber Ducking** | Explain the problem aloud to an inanimate object | Stuck on logic, need fresh perspective |
| **Explain to Someone** | Articulate the issue to a colleague (or yourself) | Complex bugs, need to organize thoughts |
| **Read the Error Message** | Carefully parse stack traces and error output | Initial triage, understanding failure mode |
| **Reproduce Reliably** | Create minimal steps to trigger the bug every time | Intermittent bugs, unclear triggers |
| **Git Bisect** | Use version control to find the breaking commit | Regression bugs, "worked yesterday" scenarios |
---
## The Scientific Method for Bugs
Debugging is science: observe, hypothesize, test, analyze.
### 1. **Observe**
Gather data without assumptions. What exactly is failing? Under what conditions?
```pseudocode
OBSERVE:
- Expected behavior: Function should return sorted list
- Actual behavior: Function returns unsorted list for inputs > 100 items
- Environment: Production server, Node.js v18
- Reproducible: Yes, 100% of the time with large datasets
```
### 2. **Hypothesize**
Form a testable theory about the root cause.
```pseudocode
HYPOTHESIS:
"The sorting algorithm fails for large arrays due to a stack overflow
in the recursive implementation."
```
### 3. **Test**
Design an experiment to validate or invalidate the hypothesis.
```pseudocode
TEST:
1. Add logging before and after sort function call
2. Test with array sizes: 50, 100, 150, 200
3. Monitor stack depth during execution
4. Check if iterative sort works instead
```
### 4. **Analyze**
Examine results. Did the hypothesis hold? Why or why not?
```pseudocode
RESULTS:
- Stack overflow occurs at ~128 items (not 100 exactly)
- Recursive depth limit exceeded
- Iterative sort completes successfully for all sizes
CONCLUSION:
Hypothesis confirmed. Replace recursive sort with iterative version.
```
### 5. **Fix and Verify**
Implement the fix and confirm it resolves the issue without side effects.
```pseudocode
FIX:
REPLACE recursive_quicksort() WITH iterative_quicksort()
VERIFY:
- Test with 50, 100, 500, 1000 items → All pass
- Run existing test suite → All green
- Performance check → 15% faster for large arrays
```
---
## Reading Stack Traces
Stack traces are breadcrumb trails. Read them bottom-to-top to understand the call chain.
### Anatomy of a Stack Trace
```pseudocode
ERROR: NullPointerException at line 42
STACK TRACE:
at processOrder(order.js:42) ← Where the error occurred
at validateOrder(validator.js:18) ← Called by this function
at handleRequest(handler.js:56) ← Called by this function
at main(app.js:10) ← Entry point
ANALYSIS:
1. Error happens in processOrder() at line 42
2. Likely cause: order object is null
3. Check validateOrder() - is it letting null through?
4. Root cause: Missing null check in validateOrder()
```
### What to Look For
- **Immediate cause**: The line where the exception was thrown
- **Call chain**: How execution reached that line
- **Patterns**: Repeated function names suggest recursion or loops
- **Library code vs. your code**: Focus on your code first
---
## Using Debuggers Effectively
### When to Use a Debugger
| Scenario | Use Debugger? | Alternative |
|----------|---------------|-------------|
| Complex state inspection | ✅ Yes | Print statements miss context |
| Stepping through loops | ✅ Yes | Understand iteration-by-iteration |
| Quick variable check | ❌ No | Faster to log |
| Race conditions | ❌ Maybe | Debugger changes timing |
| Reproducing bug | ✅ Yes | See exact state at failure |
### Debugger Techniques
```pseudocode
BREAKPOINT at suspected_function():
1. INSPECT all local variables
2. EVALUATE expressions in watch window
3. STEP OVER to see control flow
4. STEP INTO to dive into called functions
5. CONDITIONAL BREAKPOINT: stop only when counter > 100
```
---
## "Select Isn't Broken" — Trust Your Tools, Question Your Code
### The Principle
When facing a bug, assume:
1. **The OS is correct**
2. **The compiler is correct**
3. **The library is correct**
4. **Your code has the bug**
### Why This Matters
```pseudocode
BAD DEBUGGING PATH:
"This must be a bug in the database driver!"
→ Spend 3 hours reading driver source code
→ Find nothing
→ Eventually discover typo in SQL query
GOOD DEBUGGING PATH:
"Assume my code is wrong. Let me check my query."
→ Find typo in 5 minutes
```
### Exceptions to the Rule
Only blame tools when:
- You've exhausted all other possibilities
- You have reproducible proof
- Others report the same issue
- You can point to specific lines in the tool's code
---
## Reproducing Bugs Reliably
**If you can't reproduce it, you can't fix it.**
### Building a Minimal Reproduction
```pseudocode
ORIGINAL BUG REPORT:
"App crashes sometimes when users click Submit."
MINIMAL REPRODUCTION STEPS:
1. Start app with empty database
2. Create user with email containing "+"
3. Submit form with that email
4. → Crash occurs 100% of the time
ROOT CAUSE FOUND:
Email validation regex doesn't escape "+" character.
```
### Techniques for Elusive Bugs
| Bug Type | Reproduction Strategy |
|----------|----------------------|
| **Intermittent** | Add logging, run 1000x in loop |
| **Race condition** | Slow down timing with sleeps, use race detector tools |
| **Environment-specific** | Match exact environment (OS, versions, config) |
| **Heisenbug** (disappears when observed) | Use passive logging instead of debugger |
---
## Debugging Checklist
Before diving into code, run through this checklist:
```pseudocode
DEBUGGING CHECKLIST:
☐ Can I reproduce the bug reliably?
☐ Have I read the full error message and stack trace?
☐ What changed recently? (code, config, dependencies, environment)
☐ Does the bug occur in a clean environment?
☐ Have I explained the problem out loud?
☐ Am I making assumptions? (Check them!)
☐ Have I tried binary search to isolate the issue?
☐ Is this really a bug, or a misunderstanding of requirements?
```
---
## Rubber Duck Debugging
### How It Works
1. **Get a rubber duck** (or any inanimate object)
2. **Explain your code line-by-line** to the duck
3. **Articulate what each line does and why**
4. **Notice contradictions** between intent and implementation
### Why It Works
- Forces you to slow down and think deliberately
- Verbalizing reveals assumptions you didn't know you made
- Talking engages different parts of the brain than silent reading
```pseudocode
RUBBER DUCK SESSION:
"Okay duck, this function should validate email addresses.
First, I check if the input is null... wait, I'm checking AFTER
calling .trim() on it. That's why it crashes on null inputs!"
```
---
## Advanced Debugging Techniques
### Binary Search for Bugs
```pseudocode
PROBLEM: Code worked yesterday, broken today after 40 commits.
BINARY SEARCH APPROACH:
1. Jump to middle commit (commit 20)
2. Test → Still broken
3. Jump to commit 10
4. Test → Works!
5. Jump to commit 15
6. Test → Broken!
7. Jump to commit 12
8. Test → Works!
9. Conclusion: Bug introduced between commits 12-15
10. Review those 3 commits → Find the culprit
```
### Divide and Conquer
```pseudocode
PROBLEM: 500-line function produces wrong output.
DIVIDE AND CONQUER:
1. Insert logging at line 250
2. Is output correct at halfway point?
- YES → Bug is in second half
- NO → Bug is in first half
3. Repeat in relevant half (log at 125 or 375)
4. Continue until bug isolated to ~10 lines
```
---
## Common Debugging Pitfalls
| Pitfall | Why It Fails | Better Approach |
|---------|--------------|-----------------|
| **Random code changes** | No hypothesis, just guessing | Form hypothesis, test systematically |
| **Not reading error messages** | Miss critical clues | Read error messages completely, twice |
| **Assuming the bug is elsewhere** | Waste time looking in wrong place | Start with your most recent changes |
| **No logging or reproduction** | Can't verify fix works | Create reliable reproduction first |
| **Editing live production** | Risk making it worse | Reproduce locally, fix, then deploy |
---
## Summary Table: Debugging Strategies
| Strategy | Best For | Time Investment | Skill Level |
|----------|----------|-----------------|-------------|
| **Read Error Message** | All bugs | 30 seconds | Beginner |
| **Rubber Ducking** | Logic errors | 5-10 minutes | Beginner |
| **Binary Search** | Large codebases | 15-30 minutes | Intermediate |
| **Debugger** | Complex state | 10-45 minutes | Intermediate |
| **Scientific Method** | Unknown causes | 30-120 minutes | Advanced |
| **Git Bisect** | Regressions | 15-60 minutes | Intermediate |
| **Minimal Reproduction** | Intermittent bugs | 30-90 minutes | Advanced |
---
## Key Takeaways
1. **Debugging is problem-solving**, not blame assignment
2. **The tools are rarely wrong**—check your code first
3. **Reproduce reliably** before attempting to fix
4. **Use the scientific method**: observe, hypothesize, test, analyze
5. **Explain the problem out loud**—rubber ducking works
6. **Read error messages completely**—they contain clues
7. **Binary search** cuts large problem spaces quickly
8. **Stay calm and methodical**—panic makes bugs harder to find
---
*Based on concepts from "The Pragmatic Programmer" by David Thomas and Andrew Hunt.*
practices/decoupling.md
# Decoupling and the Law of Demeter
## Overview
> "Good fences make good neighbors." - The Pragmatic Programmer
Decoupling is the practice of minimizing dependencies between modules or components in a system. Well-decoupled code is easier to change, test, and reason about because modifications to one part don't ripple through the entire system.
## The Law of Demeter (LoD)
The Law of Demeter, also known as the "Principle of Least Knowledge," states that an object should only talk to its immediate friends, not to strangers. Specifically, a method should only call:
1. **Methods on itself** (its own class)
2. **Methods on objects passed as parameters**
3. **Methods on objects it creates**
4. **Methods on instance variables** (its direct dependencies)
### What NOT to Do
Don't reach through objects to access distant collaborators:
```pseudocode
// VIOLATION: Reaching through multiple objects
function getCustomerCity(order):
return order.getCustomer().getAddress().getCity()
```
This violates LoD because we're calling methods on objects we received from other calls (strangers).
### The Correct Approach
```pseudocode
// COMPLIANT: Ask the order directly
function getCustomerCity(order):
return order.getCustomerCity()
// Inside Order class:
method getCustomerCity():
return this.customer.getAddress().getCity()
```
Now the caller only talks to `order` (its immediate friend), and `order` handles the internal navigation.
## Train Wrecks
Train wrecks are long chains of method calls that couple your code to the entire chain of objects:
```pseudocode
// TRAIN WRECK: Fragile and tightly coupled
totalPrice = cart.getItems().get(0).getProduct().getPrice().getAmount()
// If any intermediate object changes, this breaks
```
### Fixing Train Wrecks
Ask for what you need, not how to get it:
```pseudocode
// BETTER: Tell, don't ask
totalPrice = cart.getFirstItemPrice()
// Or use a more intention-revealing method
totalPrice = cart.calculateFirstItemTotal()
```
## Shy Code
Shy code doesn't reveal its internals to the world. It maintains strong boundaries and communicates through well-defined interfaces.
### Example: Exposing Too Much
```pseudocode
// NOT SHY: Exposing internal structure
class Report:
property data // Public property
property formatter // Public property
method generate():
return this.formatter.format(this.data)
// Caller knows too much about internals
report = new Report()
report.formatter = new HTMLFormatter()
report.data = loadData()
output = report.generate()
```
### Example: Shy Code
```pseudocode
// SHY: Hidden internals, clear interface
class Report:
private data
private formatter
method initialize(dataSource, format):
this.data = dataSource
this.formatter = FormatterFactory.create(format)
method generate():
return this.formatter.format(this.data)
// Caller only sees what it needs
report = new Report(dataSource, "html")
output = report.generate()
```
## How to Decouple
### 1. Use Interfaces/Protocols
```pseudocode
// Define contract, not implementation
interface PaymentProcessor:
method processPayment(amount, account)
class CreditCardProcessor implements PaymentProcessor:
method processPayment(amount, account):
// Credit card specific logic
class PayPalProcessor implements PaymentProcessor:
method processPayment(amount, account):
// PayPal specific logic
// Client depends on interface, not concrete classes
class OrderService:
private processor: PaymentProcessor
method initialize(processor: PaymentProcessor):
this.processor = processor
method completeOrder(order):
this.processor.processPayment(order.total, order.account)
```
### 2. Event-Driven Architecture
```pseudocode
// Publisher doesn't know about subscribers
class OrderSystem:
private eventBus
method placeOrder(order):
// Process order
order.status = "confirmed"
// Publish event (no knowledge of listeners)
this.eventBus.publish("order.placed", order)
// Subscribers register independently
class InventoryService:
method initialize(eventBus):
eventBus.subscribe("order.placed", this.handleOrderPlaced)
method handleOrderPlaced(order):
this.reduceStock(order.items)
class EmailService:
method initialize(eventBus):
eventBus.subscribe("order.placed", this.handleOrderPlaced)
method handleOrderPlaced(order):
this.sendConfirmationEmail(order.customer)
```
### 3. Message Passing
```pseudocode
// Components communicate via messages
class CustomerRepository:
method findById(id):
query = new FindCustomerQuery(id)
return this.messageBus.send(query)
class CustomerQueryHandler:
method handle(query: FindCustomerQuery):
return this.database.fetchCustomer(query.customerId)
// No direct dependency between repository and handler
```
### 4. Dependency Injection
```pseudocode
// BAD: Hard-coded dependency
class ReportGenerator:
method generate(data):
database = new MySQLDatabase() // Tight coupling!
return database.fetch(data)
// GOOD: Injected dependency
class ReportGenerator:
private database
method initialize(database):
this.database = database // Any database implementation
method generate(data):
return this.database.fetch(data)
```
### 5. Wrapper/Adapter Pattern
```pseudocode
// Decouple from third-party libraries
interface Logger:
method log(message, level)
class Log4jAdapter implements Logger:
private log4j
method initialize():
this.log4j = new Log4jLogger()
method log(message, level):
this.log4j.write(message, level)
// Application depends on Logger interface, not Log4j
class Application:
private logger: Logger
method initialize(logger: Logger):
this.logger = logger
```
## Benefits of Decoupling
| Benefit | Description |
|---------|-------------|
| **Testability** | Mock or stub dependencies easily; test components in isolation |
| **Flexibility** | Swap implementations without changing client code |
| **Maintainability** | Changes localized to single components; reduced ripple effects |
| **Reusability** | Components work independently; can be used in different contexts |
| **Parallel Development** | Teams work on different components without blocking each other |
| **Resilience** | Failures contained; don't cascade through the system |
| **Understandability** | Each component has clear, minimal responsibilities |
## Recognizing Coupling
### Signs of Tight Coupling
```pseudocode
// Multiple dots in a call chain
result = object.getA().getB().getC().doSomething()
// Methods that only delegate to other objects
function processOrder(order):
order.getCustomer().getAccount().debit(order.getTotal())
order.getInventory().reduce(order.getItems())
// Classes that know too much about other classes' structure
function validateUser(user):
if user.profile.settings.notifications.email.enabled:
// Too much knowledge about user's internal structure
```
### Measuring Coupling
- **Afferent Coupling (Ca)**: Number of classes that depend on this class
- **Efferent Coupling (Ce)**: Number of classes this class depends on
- **Instability (I)**: Ce / (Ca + Ce) — ranges from 0 (stable) to 1 (unstable)
Aim for balanced coupling: stable interfaces with unstable implementations.
## Summary Table
| Principle | What It Means | Example |
|-----------|---------------|---------|
| **Law of Demeter** | Only talk to immediate friends | `order.getPrice()` not `order.getCustomer().getAddress().getCity()` |
| **Avoid Train Wrecks** | No long method chains | Use delegation or queries instead |
| **Shy Code** | Hide internals, expose behavior | Private data, public methods |
| **Interface-Based Design** | Depend on contracts, not implementations | `PaymentProcessor` interface, not `CreditCardProcessor` |
| **Event-Driven** | Publish events, don't call directly | Event bus instead of direct method calls |
| **Dependency Injection** | Pass dependencies in, don't create them | Constructor/method injection |
| **Message Passing** | Communicate via messages | Commands, queries, events |
| **Wrappers** | Isolate third-party code | Adapter pattern for external libraries |
## Practical Guidelines
1. **Ask, Don't Reach**: If you need data from a distant object, add a method to ask for it
2. **Tell, Don't Ask**: Instead of getting data and acting on it, tell the object what to do
3. **One Dot Rule**: Generally limit chained calls (exceptions for fluent APIs)
4. **Use Events**: For one-to-many relationships, use events rather than direct calls
5. **Inject Dependencies**: Let configuration/DI container wire collaborators
6. **Program to Interfaces**: Depend on abstractions, not concrete classes
7. **Encapsulate Collections**: Don't expose internal collections; provide methods to work with them
8. **Minimize Knowledge**: Each component should know as little as possible about others
---
*Based on concepts from "The Pragmatic Programmer" by David Thomas and Andrew Hunt.*
practices/design-by-contract.md
# Design by Contract (DbC)
## Definition
> "Nothing astonishes men so much as common sense and plain dealing." - Ralph Waldo Emerson
**Design by Contract** is a software design approach where software components collaborate based on clearly defined agreements of rights and responsibilities. Each party in a software contract has obligations and benefits, creating a formal agreement about what a routine expects and what it guarantees in return.
The contract defines:
- **What the routine expects** (preconditions)
- **What the routine guarantees** (postconditions)
- **What the routine maintains** (invariants)
## Preconditions
**Preconditions** are requirements that must be true before a routine can be called. They define the caller's obligations.
### Characteristics
- Define valid inputs and system state
- Caller's responsibility to satisfy
- Routine has no obligation to handle violations
- Should fail fast if violated
### Examples
```pseudocode
function sqrt(value)
precondition: value >= 0
...
```
```pseudocode
function withdraw(account, amount)
precondition: account.isOpen == true
precondition: account.balance >= amount
precondition: amount > 0
...
```
```pseudocode
function processOrder(order)
precondition: order != null
precondition: order.items.length > 0
precondition: order.customer.isVerified == true
...
```
## Postconditions
**Postconditions** are guarantees about the state after a routine completes successfully. They define the routine's obligations.
### Characteristics
- Define what the routine promises to deliver
- Routine's responsibility to ensure
- Must be true for all possible valid inputs
- Define the result and side effects
### Examples
```pseudocode
function deposit(account, amount)
precondition: amount > 0
postcondition: account.balance == old(account.balance) + amount
postcondition: account.lastTransaction == current_time()
```
```pseudocode
function sort(array)
postcondition: array.isSorted() == true
postcondition: array.length == old(array.length)
postcondition: array.containsAllElementsFrom(old(array)) == true
```
```pseudocode
function createUser(email, password)
precondition: email.isValid()
precondition: password.length >= 8
postcondition: result.id != null
postcondition: result.email == email
postcondition: result.passwordHash != password // password was hashed
postcondition: database.contains(result) == true
```
## Invariants
**Invariants** are conditions that must always be true for an object or system, both before and after any operation.
### Characteristics
- Define consistent state
- Must hold at construction
- Must hold before and after every public method
- May be temporarily violated during private method execution
- Restored before method exits
### Examples
```pseudocode
class BankAccount
invariant: balance >= 0
invariant: accountNumber.length == 10
invariant: transactions.length >= 0
invariant: sum(transactions.amounts) == balance
```
```pseudocode
class CircularBuffer
invariant: 0 <= readPosition < capacity
invariant: 0 <= writePosition < capacity
invariant: 0 <= size <= capacity
invariant: buffer.length == capacity
```
```pseudocode
class SortedList
invariant: forall i, j where i < j: elements[i] <= elements[j]
invariant: size == elements.length
invariant: size >= 0
```
## Benefits of Design by Contract
| Benefit | Description |
|---------|-------------|
| **Clarity** | Explicit documentation of expectations and guarantees |
| **Correctness** | Clear definition of what "correct" means for each routine |
| **Early Detection** | Bugs caught at the boundary where they occur |
| **Better Testing** | Contracts define test cases automatically |
| **Safer Refactoring** | Contracts must be maintained, preserving behavior |
| **Reduced Defensive Code** | No need to check what the contract guarantees |
| **Documentation** | Self-documenting through executable specifications |
| **Reliability** | Components can trust their collaborators |
## Implementing DbC Without Language Support
Many languages don't have built-in contract support (unlike Eiffel), but you can implement DbC principles:
### 1. Assertion-Based Contracts
```pseudocode
function withdraw(account, amount)
// Preconditions
assert(account.isOpen, "Account must be open")
assert(account.balance >= amount, "Insufficient funds")
assert(amount > 0, "Amount must be positive")
oldBalance = account.balance
// Method body
account.balance = account.balance - amount
account.lastTransaction = currentTime()
// Postconditions
assert(account.balance == oldBalance - amount, "Balance incorrectly updated")
assert(account.lastTransaction != null, "Transaction time not recorded")
// Invariant check
assert(account.balance >= 0, "Invariant violated: negative balance")
```
### 2. Guard Clauses with Explicit Errors
```pseudocode
function processPayment(order, paymentMethod)
// Preconditions as guard clauses
if order == null:
throw ContractViolation("Precondition failed: order cannot be null")
if order.total <= 0:
throw ContractViolation("Precondition failed: order total must be positive")
if not paymentMethod.isValid():
throw ContractViolation("Precondition failed: invalid payment method")
// Process payment
transaction = paymentMethod.charge(order.total)
order.status = "PAID"
// Postconditions
if transaction.status != "SUCCESS":
throw ContractViolation("Postcondition failed: payment not successful")
if order.status != "PAID":
throw ContractViolation("Postcondition failed: order status not updated")
return transaction
```
### 3. Decorator/Wrapper Pattern
```pseudocode
function withContract(preconditions, postconditions, invariants)
return function(originalFunction)
return function wrappedFunction(arguments)
// Check invariants before
for each invariant in invariants:
assert(invariant(this), "Invariant violated before call")
// Check preconditions
for each precondition in preconditions:
assert(precondition(arguments), "Precondition failed")
// Capture old state for postconditions
oldState = captureState(this)
// Execute original function
result = originalFunction.apply(this, arguments)
// Check postconditions
for each postcondition in postconditions:
assert(postcondition(result, arguments, oldState), "Postcondition failed")
// Check invariants after
for each invariant in invariants:
assert(invariant(this), "Invariant violated after call")
return result
```
### 4. Documentation Conventions
```pseudocode
/**
* Withdraws the specified amount from the account.
*
* @requires account.isOpen == true
* @requires account.balance >= amount
* @requires amount > 0
* @ensures account.balance == old(account.balance) - amount
* @ensures account.lastTransaction != null
* @invariant account.balance >= 0
*/
function withdraw(account, amount)
// Implementation
```
## Design by Contract vs. Defensive Programming
### Key Differences
| Aspect | Design by Contract | Defensive Programming |
|--------|-------------------|----------------------|
| **Philosophy** | Trust but verify at boundaries | Trust nothing |
| **Responsibility** | Caller ensures preconditions | Routine checks everything |
| **Duplicate Checks** | Minimal, at contract boundaries | Extensive, everywhere |
| **Performance** | Faster (fewer checks) | Slower (redundant checks) |
| **Failure Mode** | Fail fast at violation point | May propagate invalid state |
| **Code Clarity** | Clear responsibilities | Mixed concerns |
### When to Use Each
**Design by Contract:**
- Internal APIs and module boundaries
- When caller and callee are under your control
- Performance-critical code
- Clear ownership of components
**Defensive Programming:**
- Public APIs exposed to external users
- Input from untrusted sources
- Security-sensitive operations
- User-facing validation
### Combined Approach
```pseudocode
// Public API - Defensive
function publicWithdraw(accountId, amount)
// Defensive: validate all inputs
if accountId == null or not isValidAccountId(accountId):
return Error("Invalid account ID")
if amount <= 0:
return Error("Amount must be positive")
account = loadAccount(accountId)
if account == null:
return Error("Account not found")
// Call internal method with contract
return internalWithdraw(account, amount)
// Internal method - DbC
function internalWithdraw(account, amount)
// Contract: caller guarantees these
precondition: account != null
precondition: account.isOpen == true
precondition: account.balance >= amount
precondition: amount > 0
oldBalance = account.balance
account.balance = account.balance - amount
postcondition: account.balance == oldBalance - amount
invariant: account.balance >= 0
```
## Practical Guidelines
### 1. Start with Preconditions
Define what you need before doing anything else.
### 2. Make Contracts Explicit
Document them clearly, even if not enforceable by the language.
### 3. Check Contracts in Development
Use assertions that can be disabled in production if needed.
### 4. Fail Fast
Don't try to recover from contract violations.
### 5. Keep Contracts Simple
Complex contracts are hard to maintain and understand.
### 6. Contract Inheritance Rules
- Preconditions can only be weakened in subclasses
- Postconditions can only be strengthened in subclasses
- Invariants must be maintained by subclasses
```pseudocode
class Shape
function area()
postcondition: result >= 0
class Circle extends Shape
function area()
postcondition: result >= 0 // Must maintain parent postcondition
postcondition: result == PI * r * r // Can add stronger guarantee
```
## Summary Table
| Concept | Who Ensures | When Checked | Violation Means |
|---------|-------------|--------------|-----------------|
| **Precondition** | Caller | Before routine executes | Caller's bug |
| **Postcondition** | Routine | After routine completes | Routine's bug |
| **Invariant** | Class | Before and after all public methods | Class design bug |
| **Guard Clause** | Routine | At entry point | Input validation |
| **Assertion** | Runtime | During execution | Logic error |
## Key Principles
1. **Clear Responsibilities**: Contracts make explicit who is responsible for what
2. **No Redundant Checks**: If caller guarantees precondition, routine doesn't recheck
3. **Fail Fast**: Contract violations indicate bugs, not recoverable errors
4. **Trust Within Boundaries**: Internal code can trust contracts; external input needs validation
5. **Executable Documentation**: Contracts are verified specifications
---
*Based on concepts from "The Pragmatic Programmer" by David Thomas and Andrew Hunt.*
practices/plain-text.md
# The Power of Plain Text
## Definition
> "Keep knowledge in plain text."
> — *The Pragmatic Programmer*
Plain text refers to information stored in a format that is human-readable and self-describing. It consists of printable characters in a form that can be directly viewed and edited by people without specialized tools. Plain text emphasizes transparency and accessibility over efficiency or compactness.
## Why Plain Text Matters
### 1. Insurance Against Obsolescence
Plain text files will outlive proprietary binary formats. When applications disappear or formats become obsolete, plain text remains readable with the simplest of tools.
### 2. Leverage
Plain text enables the use of virtually every tool in the computing universe:
- Version control systems (Git, SVN)
- Text processing utilities (grep, sed, awk)
- Editors (from vi to modern IDEs)
- Scripting languages
- Unix philosophy: small tools that do one thing well
### 3. Easier Testing
Plain text configurations and data make testing straightforward:
- Manually inspect test inputs and outputs
- Create test fixtures by hand
- Compare expected vs. actual results with diff
- Debug by reading files directly
### 4. Self-Describing
Well-structured plain text carries its own meaning. A human can often understand what data represents without external documentation.
```pseudocode
# Configuration in plain text (self-describing)
database:
host: localhost
port: 5432
name: production_db
pool_size: 20
```
Compare to binary:
```pseudocode
# Binary configuration (opaque)
0x7F 0x45 0x4C 0x46 0x02 0x01 0x01 0x00
[bytes continue, meaning unclear without decoder]
```
## Drawbacks and When Binary is Appropriate
### Space and Performance
Plain text typically requires more storage space and processing time than binary formats.
**Use binary when:**
- **Performance Critical**: Real-time systems, high-frequency trading
- **Large Datasets**: Multi-gigabyte log files, scientific data
- **Network Bandwidth**: Mobile apps with limited connectivity
- **Specialized Formats**: Images, video, compiled code, databases
**Example where binary wins:**
```pseudocode
# Plain text coordinate storage
POINT: x=123.456789, y=987.654321, z=456.789012
# Binary equivalent (12 bytes vs ~50 bytes)
[4-byte float][4-byte float][4-byte float]
```
### When Plain Text Shines
- **Configuration files**: Application settings, environment configs
- **Data interchange**: API responses, exports/imports
- **Logs and debugging**: System logs, error traces
- **Documentation**: READMEs, comments, notes
- **Version control**: Anything tracked by Git
## Examples Across Domains
### 1. Configuration Files
**Application Configuration:**
```pseudocode
# app.config
log_level: DEBUG
max_connections: 100
timeout_seconds: 30
features:
- authentication
- caching
- monitoring
```
**Build Configuration:**
```pseudocode
# build.config
target: production
optimize: true
source_map: false
output_directory: ./dist
```
### 2. Data Interchange
**API Response (JSON):**
```pseudocode
{
"user": {
"id": 12345,
"name": "Alice Smith",
"email": "alice@example.com",
"roles": ["developer", "reviewer"]
},
"timestamp": "2026-02-01T10:30:00Z"
}
```
**Data Export (CSV):**
```pseudocode
id,name,department,salary
101,Bob Johnson,Engineering,95000
102,Carol White,Marketing,82000
103,David Brown,Sales,78000
```
### 3. Log Files
**Structured Logging:**
```pseudocode
[2026-02-01 10:15:32] INFO: Application started
[2026-02-01 10:15:33] DEBUG: Database connection established
[2026-02-01 10:16:45] WARN: Rate limit approaching (85% threshold)
[2026-02-01 10:18:22] ERROR: Failed to process payment
Transaction ID: txn_abc123
Error: Insufficient funds
Stack trace: payment_service.rb:145
```
**Access Logs:**
```pseudocode
192.168.1.100 - - [01/Feb/2026:10:15:32 +0000] "GET /api/users HTTP/1.1" 200 1234
192.168.1.101 - - [01/Feb/2026:10:15:33 +0000] "POST /api/orders HTTP/1.1" 201 567
```
### 4. Documentation as Code
**Markdown Documentation:**
```pseudocode
# API Documentation
## Authentication Endpoint
**POST** `/auth/login`
Request body:
- username (string, required)
- password (string, required)
Response:
- token (string): JWT authentication token
- expires_in (integer): Token lifetime in seconds
```
## Plain Text Formats
### CSV (Comma-Separated Values)
**Use for:** Tabular data, spreadsheet exports, simple datasets
```pseudocode
product_id,product_name,price,stock
SKU001,Wireless Mouse,29.99,156
SKU002,Mechanical Keyboard,89.99,43
SKU003,USB-C Hub,45.50,0
```
**Pros:**
- Universal support (Excel, databases, scripts)
- Simple structure
- Human-readable
**Cons:**
- No type information
- Escaping issues with commas/quotes
- No nested structures
### JSON (JavaScript Object Notation)
**Use for:** APIs, configuration, structured data with hierarchy
```pseudocode
{
"products": [
{
"id": "SKU001",
"name": "Wireless Mouse",
"price": 29.99,
"stock": 156,
"categories": ["electronics", "accessories"]
}
],
"metadata": {
"total": 1,
"timestamp": "2026-02-01T10:30:00Z"
}
}
```
**Pros:**
- Type support (strings, numbers, booleans, null)
- Nested structures and arrays
- Wide language support
**Cons:**
- No comments (in strict JSON)
- Verbose for large datasets
- Trailing commas not allowed
### YAML (YAML Ain't Markup Language)
**Use for:** Configuration files, CI/CD pipelines, human-edited data
```pseudocode
database:
host: localhost
port: 5432
credentials:
username: app_user
password: ${DB_PASSWORD} # Environment variable reference
features:
- name: authentication
enabled: true
- name: caching
enabled: false
ttl: 3600
```
**Pros:**
- Very human-readable
- Supports comments
- Less verbose than JSON/XML
**Cons:**
- Indentation-sensitive (can cause errors)
- More complex parsing
- Multiple ways to represent same data
### Markdown
**Use for:** Documentation, READMEs, notes, knowledge bases
```pseudocode
# Project Overview
## Features
- **Authentication**: OAuth 2.0 support
- **Caching**: Redis-backed session storage
- **Monitoring**: Prometheus metrics export
## Quick Start
1. Install dependencies: `npm install`
2. Configure environment: Copy `.env.example` to `.env`
3. Run migrations: `npm run migrate`
4. Start server: `npm start`
```
**Pros:**
- Extremely readable as plain text
- Converts to HTML for rendering
- Git-friendly (diffs work well)
**Cons:**
- Not for structured data
- Many variants (CommonMark, GFM, etc.)
### INI/Properties Files
**Use for:** Simple key-value configuration
```pseudocode
[database]
host=localhost
port=5432
name=myapp
[server]
port=8080
workers=4
timeout=30
```
**Pros:**
- Extremely simple
- Section grouping
- Wide support
**Cons:**
- Limited data types (everything is a string)
- No nesting
- No standard for arrays
## Practical Guidelines
### Choosing the Right Format
| Scenario | Recommended Format | Reason |
|----------|-------------------|---------|
| Application config | YAML or JSON | Hierarchical structure, human-editable |
| API responses | JSON | Type support, universal parsing |
| Tabular data export | CSV | Spreadsheet compatibility |
| Documentation | Markdown | Readability, version control friendly |
| System logs | Plain text (structured) | Grep-able, human-readable |
| Simple settings | INI/Properties | Minimal complexity |
| Environment variables | `.env` (key=value) | Standard for 12-factor apps |
### Best Practices
**1. Use Comments Generously**
```pseudocode
# Database connection pool settings
# WARNING: Keep pool_size below 100 to avoid overwhelming DB
pool_size: 50
```
**2. Include Metadata**
```pseudocode
# Generated: 2026-02-01 10:30:00 UTC
# Generator: data-export-v2.3.1
# Source: production database (replica)
id,name,status
1,Alice,active
```
**3. Make it Grep-Friendly**
```pseudocode
# Good: Consistent structure enables grep
[ERROR] payment_service: Transaction failed (txn_123)
[ERROR] auth_service: Invalid token (user_456)
# Bad: Inconsistent structure
Payment error in transaction 123
User 456 has invalid token - auth failure
```
**4. Version Your Formats**
```pseudocode
{
"schema_version": "2.0",
"data": {
// actual content
}
}
```
**5. Validate Plain Text**
```pseudocode
# Use schemas for validation
# JSON Schema for JSON configs
# YAML validators for YAML
# Custom validators for specialized formats
```
## Integration with Version Control
Plain text enables powerful version control workflows:
```pseudocode
# Viewing changes to configuration
git diff config/database.yml
- pool_size: 20
+ pool_size: 50
# Merging changes from multiple developers
git merge feature-branch
# Automatic merge of non-conflicting lines
```
**Binary comparison failure:**
```pseudocode
git diff config.dat
Binary files a/config.dat and b/config.dat differ
# No insight into what changed
```
## Summary
| Aspect | Plain Text | Binary |
|--------|-----------|--------|
| **Readability** | Human-readable without tools | Requires specialized decoders |
| **Tooling** | Universal (grep, sed, diff, git) | Format-specific tools only |
| **Debugging** | Direct inspection | Hex editors, debuggers |
| **Version Control** | Meaningful diffs and merges | Opaque changes |
| **Longevity** | Survives format obsolescence | Tied to application lifetime |
| **Size** | Larger (text encoding) | Compact (binary encoding) |
| **Performance** | Slower parsing | Faster parsing |
| **Testing** | Easy to create/verify fixtures | Requires binary generators |
| **Portability** | Platform-independent | May have endianness/architecture issues |
**The Pragmatic Approach:**
- Default to plain text for configuration, data interchange, and logs
- Use binary for performance-critical data, large datasets, and specialized formats
- Keep metadata and documentation in plain text even if data is binary
- Leverage plain text for maximum tool compatibility and future-proofing
---
*Based on concepts from "The Pragmatic Programmer" by David Thomas and Andrew Hunt.*
practices/refactoring.md
# Refactoring
> "Don't live with broken windows."
> — David Thomas & Andrew Hunt
## What is Refactoring?
**Refactoring** is the disciplined technique of restructuring existing code without changing its external behavior. It's about improving the internal structure, readability, and maintainability while keeping all tests passing.
Refactoring is NOT:
- Rewriting from scratch
- Adding new features
- Fixing bugs (though it often reveals them)
Refactoring IS:
- Continuous improvement
- Reducing technical debt
- Making code easier to understand
- Preparing code for new features
## When to Refactor
### Code Smells (Signals to Refactor)
| Smell | Description | Example |
|-------|-------------|---------|
| **Duplication** | Same code/logic appears multiple times | Copy-pasted validation logic |
| **Long Functions** | Function doing too many things | 200-line function with 5 responsibilities |
| **Large Classes** | Class with too many responsibilities | "God object" managing everything |
| **Long Parameter Lists** | Function taking 5+ parameters | `createUser(name, email, age, address, phone, role, status)` |
| **Divergent Change** | One class changes for multiple reasons | Class modified for DB, UI, and business logic |
| **Shotgun Surgery** | One change requires edits in many places | Changing date format touches 20 files |
| **Feature Envy** | Method uses another class's data more than its own | Method calling 10 getters on another object |
| **Data Clumps** | Same group of data appearing together | `(x, y, z)` coordinates always passed together |
| **Primitive Obsession** | Using primitives instead of small objects | Using strings for phone numbers, emails |
| **Switch Statements** | Type code with switch/case logic | `switch(type)` instead of polymorphism |
| **Comments** | Excessive comments explaining what code does | Code so complex it needs paragraph explanations |
### The Rule of Three
> "Three strikes and you refactor."
1. **First time:** Write it
2. **Second time:** Wince at duplication, but duplicate anyway
3. **Third time:** Refactor
## How to Refactor Safely
### The Golden Rule: Tests First
**Never refactor without a safety net.**
```pseudocode
// BEFORE refactoring anything
IF no_tests_exist THEN
write_characterization_tests()
END IF
VERIFY all_tests_pass()
// NOW refactor in small steps
refactor_one_thing()
run_tests()
VERIFY all_tests_pass()
refactor_next_thing()
run_tests()
VERIFY all_tests_pass()
// Repeat until done
```
### Small Steps Protocol
| DO | DON'T |
|----|-------|
| Change one thing at a time | Refactor multiple patterns simultaneously |
| Run tests after each micro-change | Wait until "done" to test |
| Commit after each successful refactor | Make huge commits with mixed changes |
| Keep builds green | Leave broken code "for later" |
| Use automated refactoring tools | Manually edit 50 files |
## Martin Fowler's Refactoring Catalog (Overview)
### Composing Methods
- **Extract Method** - Turn code fragment into its own method
- **Inline Method** - Replace method call with method body (when too simple)
- **Extract Variable** - Put expression result in a self-explanatory variable
- **Replace Temp with Query** - Extract expression into method
### Moving Features
- **Move Method** - Move method to class where it's more used
- **Move Field** - Move field to class where it's more used
- **Extract Class** - Create new class for subset of responsibilities
- **Inline Class** - Merge class into another when it does too little
### Organizing Data
- **Replace Magic Number with Named Constant**
- **Encapsulate Field** - Make field private, add accessors
- **Replace Type Code with Class** - Turn coded type into class
### Simplifying Conditionals
- **Decompose Conditional** - Extract condition and branches into methods
- **Consolidate Conditional** - Combine related conditionals
- **Replace Conditional with Polymorphism** - Use inheritance instead of switch
## Real-Time Refactoring vs. Scheduled Refactoring
### Real-Time (The Pragmatic Way)
**"Leave the code better than you found it."** — The Scout Rule
```pseudocode
// You're adding a feature
FUNCTION add_new_feature()
// See duplication or mess while working
IF code_smell_detected THEN
refactor_immediately() // Takes 5 minutes
THEN add_feature() // Now easier to add
ELSE
add_feature()
END IF
END FUNCTION
```
**Benefits:**
- No separate refactoring phase needed
- Context is fresh in your mind
- Continuous improvement
- No "refactoring sprints"
### Scheduled Refactoring (Last Resort)
Only when:
- Massive legacy codebase inherited
- Technical debt so large it blocks features
- Need dedicated time for architectural changes
**Danger:** Can become excuse to write messy code now, "fix later."
## Refactoring vs. Rewriting
| Refactoring | Rewriting |
|-------------|-----------|
| Incremental improvement | Start from scratch |
| Behavior unchanged | Often changes behavior |
| Tests pass throughout | No tests until done |
| Low risk | High risk |
| Ship while improving | Long development freeze |
### The Rewrite Trap
```pseudocode
// Tempting but dangerous
FUNCTION handle_legacy_system()
// DON'T DO THIS:
announce("We're rewriting everything!")
spend_six_months_rewriting()
discover_original_system_had_subtle_logic()
spend_three_more_months_fixing()
// DO THIS INSTEAD:
WHILE system_not_ideal DO
identify_worst_part()
write_tests_for_that_part()
refactor_incrementally()
ship_improved_version()
END WHILE
END FUNCTION
```
**When Rewrite is Justified:**
- Technology stack is obsolete and unmaintainable
- Cost of refactoring > cost of rewrite
- No tests exist and code is incomprehensible
- Business model has fundamentally changed
## Pseudocode Examples
### Example 1: Extract Method
**BEFORE:**
```pseudocode
FUNCTION process_order(order)
// Calculate total
total = 0
FOR EACH item IN order.items DO
total = total + item.price * item.quantity
END FOR
// Apply discount
IF order.customer.is_premium THEN
total = total * 0.9
END IF
// Add tax
tax = total * 0.07
total = total + tax
// Send invoice
invoice = create_invoice(order, total)
email_service.send(order.customer.email, invoice)
RETURN total
END FUNCTION
```
**AFTER:**
```pseudocode
FUNCTION process_order(order)
total = calculate_total(order)
send_invoice(order, total)
RETURN total
END FUNCTION
FUNCTION calculate_total(order)
subtotal = sum_items(order.items)
discounted = apply_discount(subtotal, order.customer)
RETURN add_tax(discounted)
END FUNCTION
FUNCTION sum_items(items)
total = 0
FOR EACH item IN items DO
total = total + item.price * item.quantity
END FOR
RETURN total
END FUNCTION
FUNCTION apply_discount(amount, customer)
IF customer.is_premium THEN
RETURN amount * 0.9
ELSE
RETURN amount
END IF
END FUNCTION
FUNCTION add_tax(amount)
RETURN amount * 1.07
END FUNCTION
```
### Example 2: Replace Type Code with Polymorphism
**BEFORE:**
```pseudocode
CLASS Employee
name
type // 1 = engineer, 2 = manager, 3 = salesperson
FUNCTION calculate_pay()
IF type == 1 THEN
RETURN base_salary + overtime * rate
ELSE IF type == 2 THEN
RETURN base_salary + bonus
ELSE IF type == 3 THEN
RETURN base_salary + commission
END IF
END FUNCTION
END CLASS
```
**AFTER:**
```pseudocode
ABSTRACT CLASS Employee
name
base_salary
ABSTRACT FUNCTION calculate_pay()
END CLASS
CLASS Engineer EXTENDS Employee
overtime_hours
hourly_rate
FUNCTION calculate_pay()
RETURN base_salary + (overtime_hours * hourly_rate)
END FUNCTION
END CLASS
CLASS Manager EXTENDS Employee
bonus
FUNCTION calculate_pay()
RETURN base_salary + bonus
END FUNCTION
END CLASS
CLASS Salesperson EXTENDS Employee
commission
FUNCTION calculate_pay()
RETURN base_salary + commission
END FUNCTION
END CLASS
```
## Summary Table
| Aspect | Key Principle |
|--------|---------------|
| **Frequency** | Continuously, as part of daily work |
| **Safety Net** | Comprehensive automated tests |
| **Step Size** | Smallest change possible, then test |
| **Triggers** | Code smells, duplication, adding features |
| **Goal** | Improve structure without changing behavior |
| **Timing** | Real-time > scheduled refactoring |
| **Alternative** | Refactor incrementally >> rewrite from scratch |
| **Commitment** | Every commit leaves code better than found |
## Key Takeaways
1. **Refactor ruthlessly, but safely** - Always have tests
2. **Small steps win** - One thing at a time, run tests
3. **Real-time refactoring** - Don't wait for "refactoring sprint"
4. **Scout rule** - Leave code better than you found it
5. **Avoid rewrites** - Incremental improvement beats Big Bang
6. **Learn the catalog** - Know common refactorings by name
7. **Code smells are signals** - Listen to them
---
*Based on concepts from "The Pragmatic Programmer" by David Thomas and Andrew Hunt.*
practices/shell-games.md
# Shell Games
> "A Pragmatic Programmer manipulates the shell the way a sculptor manipulates clay." — David Thomas & Andrew Hunt
## Definition
**Shell Games** is the practice of mastering command-line environments to automate tasks, compose tools, and create powerful workflows. Rather than being limited to GUI tools, pragmatic programmers use the shell as a flexible workbench where small, focused utilities can be combined to solve complex problems.
## Why Shell Mastery Matters
### Efficiency & Productivity
- **Speed**: Command-line operations are often faster than GUI equivalents
- **Automation**: Repetitive tasks can be scripted and reused
- **Remote Work**: Shell access works over SSH and low-bandwidth connections
- **Precision**: Exact control over operations without GUI constraints
### Flexibility & Composition
- **Combine Tools**: Chain simple utilities to create complex operations
- **Customize Workflows**: Build personalized development environments
- **Portability**: Scripts work across different machines and environments
- **Power**: Access to system features not exposed in GUIs
### Professional Development
- **Universal Skill**: Shell knowledge transfers across platforms and projects
- **DevOps Integration**: Essential for CI/CD, deployment, and infrastructure
- **Debugging**: Better understanding of how systems work
- **Self-Sufficiency**: Less dependent on specific IDE features
## Key Shell Concepts
### Pipes and Redirection
**Pipes** connect the output of one program to the input of another:
```pseudocode
# Find pattern in files and count occurrences
search_files "pattern" | count_lines
# Process data through multiple transformations
read_data | filter_lines | sort_output | remove_duplicates
# Extract specific fields and format
list_processes | select_column 2 | sort_numeric
```
**Redirection** controls where input comes from and output goes:
```pseudocode
# Send output to file (overwrite)
command > output.txt
# Append output to file
command >> log.txt
# Read input from file
command < input.txt
# Combine: read from file, write to another
transform_data < input.txt > output.txt
# Discard error messages
command 2> /dev/null
# Separate standard output and errors
command > output.txt 2> errors.txt
```
### Variables and Environment
```pseudocode
# Store values in variables
PROJECT_DIR="/path/to/project"
BUILD_TYPE="release"
# Use variables in commands
cd $PROJECT_DIR
compile --mode=$BUILD_TYPE
# Export variables for child processes
export DATABASE_URL="connection_string"
# Command substitution - use output as value
CURRENT_BRANCH=$(get_current_git_branch)
FILE_COUNT=$(count_files "*.txt")
```
### Control Structures
```pseudocode
# Conditional execution - run only if previous succeeded
compile_code && run_tests && deploy_application
# Alternative execution - run if previous failed
start_service || log_error "Service failed to start"
# Conditional blocks
if test -f "config.yml"; then
load_config "config.yml"
else
create_default_config
fi
# Loops
for file in *.txt; do
process_file "$file"
done
# Loop over command output
for user in $(list_active_users); do
send_notification "$user"
done
```
## Shell as a Workbench
### The UNIX Philosophy
The shell embodies the UNIX philosophy of small, composable tools:
1. **Do One Thing Well**: Each tool has a focused purpose
2. **Text Streams**: Universal interface for tool communication
3. **Composition**: Combine simple tools to solve complex problems
4. **Avoid Captive Interfaces**: Tools work non-interactively
### Building with Composition
```pseudocode
# Find large files modified recently
find_files --newer-than="7 days" | filter_by_size --min="100M" | sort_by_size
# Analyze log files
extract_errors logs/*.txt | count_by_type | sort_descending | take_top 10
# Code metrics
find_source_files "*.java" | count_lines_per_file | calculate_statistics
# Search and replace across files
search_files "old_api_call" | xargs replace "old_api_call" "new_api_call"
```
### One-Liners vs Scripts
**When to use one-liners:**
- Exploratory work and debugging
- Quick, one-time operations
- Interactive shell sessions
- Testing commands before scripting
**When to create scripts:**
- Repeatable processes
- Multi-step workflows
- Complex logic with error handling
- Operations that need documentation
## Common Patterns and Idioms
### File Operations
```pseudocode
# Find files by pattern
find_files --name="*.log" --path="/var/logs"
# Find files by content
search_in_files "TODO" --file-pattern="*.js" --recursive
# Batch rename files
for file in *.jpeg; do
rename "$file" "${file%.jpeg}.jpg"
done
# Archive and compress
create_archive --compress project_backup.tar.gz source_directory/
# Sync directories
synchronize source/ destination/ --archive --verbose
```
### Text Processing
```pseudocode
# Extract specific columns
cut --delimiter="," --fields=1,3 data.csv
# Search with context
search "ERROR" logs.txt --before-context=2 --after-context=2
# Replace text in-place
replace_in_file "s/old_value/new_value/g" config.txt
# Combine multiple files
combine_files *.txt > combined.txt
# Remove duplicate lines
sort_file data.txt | remove_adjacent_duplicates
```
### System Monitoring
```pseudocode
# Watch command output (refresh every 2 seconds)
watch --interval=2 "list_processes | filter_by_name 'myapp'"
# Monitor file changes
tail --follow application.log | filter_lines "ERROR"
# Disk usage analysis
check_disk_usage --human-readable | sort_by_size --reverse
# Process monitoring
list_processes --user=$USER --sort=memory | take_top 10
```
### Development Workflows
```pseudocode
# Quick project setup
mkdir new_project && cd new_project && initialize_git
# Build and test cycle
while true; do
wait_for_file_changes "src/**"
clear_screen
compile_project && run_tests
done
# Git shortcuts
git_show_status && git_add_all && git_commit --message="$1" && git_push
# Deployment pipeline
pull_latest_code && install_dependencies && run_tests && build_production && deploy_to_server
```
## Automation with Shell Scripts
### Script Structure
```pseudocode
#!/bin/shell
# Script header with description
# Purpose: Deploy application to production
# Usage: deploy.sh [version]
# Strict error handling
set_error_mode strict
set_undefined_variable_error
# Configuration
DEPLOY_DIR="/var/www/app"
BACKUP_DIR="/var/backups"
LOG_FILE="/var/log/deploy.log"
# Functions for reusability
function log_message() {
timestamp=$(current_datetime)
echo "[$timestamp] $1" >> $LOG_FILE
}
function backup_current() {
log_message "Creating backup..."
copy_directory $DEPLOY_DIR $BACKUP_DIR/backup_$(current_date)
}
function deploy_version() {
version=$1
log_message "Deploying version $version"
# Deployment logic here
}
# Main script logic
if not enough_arguments; then
echo "Usage: deploy.sh [version]"
exit 1
fi
VERSION=$1
log_message "Starting deployment of version $VERSION"
backup_current
deploy_version $VERSION
log_message "Deployment complete"
```
### Error Handling
```pseudocode
# Exit on error
set_exit_on_error
# Check command success
if not command_succeeded; then
log_error "Command failed"
exit 1
fi
# Trap errors and cleanup
on_error_or_exit {
cleanup_temporary_files
restore_previous_state
}
# Validation before proceeding
validate_preconditions || exit_with_error "Preconditions not met"
```
### Making Scripts Robust
```pseudocode
# Parameter validation
if argument_count != expected_count; then
show_usage_and_exit
fi
# Dependency checking
for required_tool in compiler test_runner deployer; do
if not command_exists $required_tool; then
echo "Error: $required_tool not found"
exit 1
fi
done
# Safe file operations
TEMP_DIR=$(create_temp_directory)
trap "remove_directory $TEMP_DIR" EXIT
# Idempotent operations (safe to run multiple times)
if not directory_exists $TARGET_DIR; then
create_directory $TARGET_DIR
fi
```
## Summary
| Aspect | Key Points |
|--------|------------|
| **Philosophy** | Use shell as flexible workbench for composing small tools |
| **Core Skills** | Pipes, redirection, scripting, command composition |
| **Benefits** | Automation, speed, precision, portability |
| **When to Use** | Repetitive tasks, file processing, system administration, build automation |
| **Best Practices** | Start with one-liners, script repetitive tasks, handle errors, validate inputs |
| **Common Patterns** | File operations, text processing, monitoring, development workflows |
| **Composition** | Chain simple tools to solve complex problems |
| **Automation** | Script frequently used workflows for consistency and speed |
| **Error Handling** | Exit on error, validate inputs, cleanup on failure |
| **Trade-offs** | Learning curve vs long-term productivity gains |
### Key Principles
1. **Master the Basics**: Learn pipes, redirection, and basic utilities thoroughly
2. **Start Simple**: Begin with one-liners before writing complex scripts
3. **Compose Tools**: Combine small utilities rather than building monoliths
4. **Automate Repetition**: If you do it twice, script it
5. **Handle Errors**: Scripts should fail gracefully and provide useful messages
6. **Document Intent**: Use clear variable names and comments
7. **Test Incrementally**: Build scripts step-by-step, testing each addition
8. **Make Portable**: Avoid platform-specific features when possible
9. **Version Control**: Track your scripts like any other code
10. **Share Knowledge**: Build a personal library of useful scripts
### Related Practices
- **Power Editing**: Shell mastery complements editor proficiency
- **Automation**: Shell is primary tool for automating development tasks
- **Debugging**: Command-line tools essential for investigation
- **Version Control**: Git and other VCS are shell-based
- **DevOps**: Infrastructure as code relies on shell scripting
---
*Based on concepts from "The Pragmatic Programmer" by David Thomas and Andrew Hunt.*
practices/testing.md
# Testing
## Definition
> "Testing is not about finding bugs. It's about clarifying your thinking and driving design. Tests are the first users of your code."
> — The Pragmatic Programmer
Testing is a fundamental practice that validates code behavior, drives better design, and serves as executable documentation. Pragmatic programmers view tests as an integral part of development, not an afterthought.
## Test Ruthlessly
**Test Early, Test Often, Test Automatically**
- Write tests as you write code, not after
- Run tests frequently during development
- Automate test execution to eliminate manual effort
- Treat failing tests as critical issues that must be fixed immediately
- Never release code that fails its tests
**TIP: A test that runs automatically is worth ten thousand test plans**
## Types of Tests
### Unit Testing
Tests individual components in isolation.
```pseudocode
// Test a single function's behavior
function test_calculateDiscount():
product = createProduct(price: 100, category: "electronics")
discount = calculateDiscount(product)
assert discount equals 10
assert product.price equals 100 // No side effects
```
**Purpose:**
- Verify individual functions/methods work correctly
- Catch bugs at the smallest scope
- Enable safe refactoring
- Document expected behavior
### Integration Testing
Tests how components work together.
```pseudocode
function test_orderProcessingPipeline():
order = createOrder(items: [item1, item2])
// Test full flow
validationResult = validateOrder(order)
paymentResult = processPayment(order)
fulfillmentResult = createShipment(order)
assert validationResult.isValid
assert paymentResult.status equals "charged"
assert fulfillmentResult.trackingNumber exists
```
**Purpose:**
- Verify components interact correctly
- Test data flow between modules
- Validate system-level behavior
- Catch interface mismatches
### Validation Testing
Tests user requirements and expectations.
```pseudocode
function test_userCanCompleteCheckout():
// Simulate user journey
user = loginUser("customer@example.com")
addItemToCart(user, product: "laptop")
proceedToCheckout(user)
enterShippingInfo(user, address: validAddress)
enterPaymentInfo(user, card: validCard)
result = confirmOrder(user)
assert result.success
assert result.confirmationEmail sent
assert user.orderHistory contains result.orderId
```
**Purpose:**
- Verify user stories and acceptance criteria
- Test from user perspective
- Validate business requirements
- Ensure system meets expectations
### Performance Testing
Tests system behavior under load.
```pseudocode
function test_apiHandlesExpectedLoad():
requests = generateRequests(count: 1000, duration: "1 minute")
startTime = now()
responses = sendConcurrentRequests(requests)
endTime = now()
assert all responses have status 200
assert average(responses.time) < 200 milliseconds
assert max(responses.time) < 1 second
assert (endTime - startTime) < 65 seconds
```
**Purpose:**
- Verify response times meet requirements
- Test system under expected load
- Identify bottlenecks and limits
- Validate scalability assumptions
## Testing Against Contract
**Design by Contract in Tests**
Every module has a contract: preconditions, postconditions, and invariants. Tests should verify these explicitly.
```pseudocode
function test_stackContract():
stack = createStack(capacity: 3)
// Test precondition: push requires non-full stack
stack.push(1)
stack.push(2)
stack.push(3)
assertThrows(() => stack.push(4), "StackOverflowError")
// Test postcondition: pop returns last pushed item
assert stack.pop() equals 3
assert stack.pop() equals 2
// Test invariant: size always reflects actual count
assert stack.size() equals 1
stack.push(5)
assert stack.size() equals 2
// Test precondition: pop requires non-empty stack
stack.pop()
stack.pop()
assertThrows(() => stack.pop(), "StackUnderflowError")
```
**Contract Testing Benefits:**
- Makes assumptions explicit
- Documents expected behavior
- Catches violations early
- Enables stronger refactoring
## Writing Testable Code
**Design for Testability**
Code that's easy to test is usually better designed.
### Avoid Tight Coupling
```pseudocode
// Hard to test - directly creates dependency
class OrderProcessor:
function processOrder(order):
database = new PostgresDatabase() // Tight coupling
result = database.save(order)
return result
// Easy to test - dependency injection
class OrderProcessor:
database: Database
function constructor(db: Database):
this.database = db
function processOrder(order):
result = this.database.save(order)
return result
// Test with mock
function test_orderProcessor():
mockDb = createMockDatabase()
processor = new OrderProcessor(mockDb)
order = createTestOrder()
processor.processOrder(order)
assert mockDb.receivedCall("save", order)
```
### Use Pure Functions
```pseudocode
// Hard to test - depends on external state
globalCounter = 0
function incrementAndGet():
globalCounter = globalCounter + 1
return globalCounter
// Easy to test - pure function
function increment(value):
return value + 1
// Test is simple and reliable
function test_increment():
assert increment(5) equals 6
assert increment(0) equals 1
assert increment(-1) equals 0
```
### Separate I/O from Logic
```pseudocode
// Hard to test - logic mixed with I/O
function processUserFile(filename):
data = readFile(filename) // I/O
total = 0
for line in data.lines:
number = parseNumber(line)
if number > 0:
total = total + number
writeFile("output.txt", total) // I/O
return total
// Easy to test - separated concerns
function calculatePositiveSum(numbers):
total = 0
for number in numbers:
if number > 0:
total = total + number
return total
function processUserFile(filename):
data = readFile(filename)
numbers = parseNumbers(data)
total = calculatePositiveSum(numbers)
writeFile("output.txt", total)
return total
// Test logic without I/O
function test_calculatePositiveSum():
assert calculatePositiveSum([1, 2, 3]) equals 6
assert calculatePositiveSum([1, -2, 3]) equals 4
assert calculatePositiveSum([-1, -2, -3]) equals 0
assert calculatePositiveSum([]) equals 0
```
## Test Coverage - What to Measure
**Coverage Metrics Should Guide, Not Rule**
- **Line Coverage**: Percentage of code lines executed by tests
- **Branch Coverage**: Percentage of decision branches tested
- **Path Coverage**: Percentage of execution paths tested
- **Mutation Coverage**: Percentage of introduced bugs caught
```pseudocode
function calculateGrade(score):
if score >= 90:
return "A"
else if score >= 80:
return "B"
else if score >= 70:
return "C"
else:
return "F"
// 50% branch coverage (only tests two branches)
function test_calculateGrade_partial():
assert calculateGrade(95) equals "A"
assert calculateGrade(60) equals "F"
// 100% branch coverage (tests all branches)
function test_calculateGrade_complete():
assert calculateGrade(95) equals "A"
assert calculateGrade(85) equals "B"
assert calculateGrade(75) equals "C"
assert calculateGrade(60) equals "F"
// Boundary testing
assert calculateGrade(90) equals "A"
assert calculateGrade(89) equals "B"
assert calculateGrade(80) equals "B"
assert calculateGrade(79) equals "C"
```
**TIP: High coverage doesn't guarantee good tests, but low coverage guarantees inadequate tests**
Focus on:
- Critical business logic
- Error handling paths
- Boundary conditions
- State transitions
- Integration points
## Tests as Documentation
**Tests Describe How Code Should Be Used**
Well-written tests serve as executable examples and living documentation.
```pseudocode
// Test documents API usage patterns
function test_shoppingCart_documentation():
// Creating and using a shopping cart
cart = createCart()
assert cart.isEmpty()
// Adding items
laptop = createProduct(id: "L1", price: 999.99)
mouse = createProduct(id: "M1", price: 29.99)
cart.addItem(laptop, quantity: 1)
cart.addItem(mouse, quantity: 2)
// Checking contents
assert cart.itemCount() equals 3
assert cart.total() equals 1059.97
// Applying discounts
cart.applyPromoCode("SAVE10")
assert cart.total() equals 953.97
// Removing items
cart.removeItem(mouse)
assert cart.itemCount() equals 1
// Clearing cart
cart.clear()
assert cart.isEmpty()
```
**Tests Answer Questions:**
- How do I create an instance?
- What parameters does this function accept?
- What does this function return?
- How do components interact?
- What happens in error cases?
## Test Organization Best Practices
### Arrange-Act-Assert Pattern
```pseudocode
function test_transferFunds():
// Arrange - set up test conditions
sourceAccount = createAccount(balance: 1000)
targetAccount = createAccount(balance: 500)
// Act - perform the operation
result = transferFunds(
from: sourceAccount,
to: targetAccount,
amount: 200
)
// Assert - verify results
assert result.success
assert sourceAccount.balance equals 800
assert targetAccount.balance equals 700
```
### One Logical Assertion Per Test
```pseudocode
// Poor - multiple unrelated assertions
function test_user_everything():
user = createUser("john@example.com")
assert user.email equals "john@example.com"
assert user.validatePassword("pass123")
assert user.cart.isEmpty()
assert user.orderHistory.count equals 0
// Better - focused tests
function test_user_createdWithCorrectEmail():
user = createUser("john@example.com")
assert user.email equals "john@example.com"
function test_user_canValidatePassword():
user = createUser("john@example.com")
user.setPassword("pass123")
assert user.validatePassword("pass123")
function test_user_startsWithEmptyCart():
user = createUser("john@example.com")
assert user.cart.isEmpty()
function test_user_startsWithNoOrderHistory():
user = createUser("john@example.com")
assert user.orderHistory.count equals 0
```
### Test Error Conditions
```pseudocode
function test_divideByZero_throwsError():
assertThrows(() => divide(10, 0), "DivisionByZeroError")
function test_invalidEmail_rejectsRegistration():
result = registerUser("not-an-email")
assert result.success equals false
assert result.error equals "Invalid email format"
function test_insufficientFunds_preventsWithdrawal():
account = createAccount(balance: 100)
result = account.withdraw(150)
assert result.success equals false
assert account.balance equals 100 // Unchanged
assert result.error contains "Insufficient funds"
```
## Summary Table
| Testing Principle | Purpose | Implementation |
|------------------|---------|----------------|
| **Test Early** | Catch bugs when cheapest to fix | Write tests during development |
| **Test Often** | Maintain confidence in changes | Run tests automatically and frequently |
| **Unit Tests** | Verify individual components | Test functions/methods in isolation |
| **Integration Tests** | Verify component interactions | Test data flow between modules |
| **Validation Tests** | Verify user requirements | Test user stories and acceptance criteria |
| **Performance Tests** | Verify non-functional requirements | Test response times and throughput |
| **Contract Testing** | Verify preconditions/postconditions | Test assumptions explicitly |
| **Testable Design** | Make testing easier | Use dependency injection, pure functions |
| **Coverage Metrics** | Guide test effort | Measure but don't obsess over percentages |
| **Tests as Docs** | Document API usage | Write clear, example-driven tests |
| **AAA Pattern** | Organize tests clearly | Arrange, Act, Assert structure |
| **Focused Tests** | Enable precise debugging | One logical assertion per test |
| **Error Testing** | Verify failure handling | Test exception paths and edge cases |
---
*Based on concepts from "The Pragmatic Programmer" by David Thomas and Andrew Hunt.*
practices/text-manipulation.md
# Text Manipulation
> "Pragmatic Programmers manipulate text the same way woodworkers shape wood."
> — David Thomas & Andrew Hunt
## Why Text Manipulation Skills Matter
Text manipulation is a foundational skill that amplifies a programmer's productivity across nearly every task. Whether you're parsing logs, transforming data formats, generating code, or cleaning input, the ability to efficiently process text streams separates pragmatic programmers from those who manually edit data line by line.
**Core Benefits:**
- **Automation Over Repetition** – One well-crafted regex or script can replace hours of manual editing
- **Universal Applicability** – Text processing applies to configuration files, logs, data formats, code generation, and more
- **Composability** – Text tools can be chained together to solve complex problems
- **Speed** – Mastery of text manipulation lets you solve problems in seconds that might take others hours
## Regular Expressions Basics
Regular expressions (regex) are pattern-matching languages for text. They're essential for searching, validating, and transforming strings.
### Core Regex Components
| Pattern | Meaning | Example Match |
|---------|---------|---------------|
| `.` | Any single character | `a`, `7`, `@` |
| `*` | Zero or more of preceding | `ab*` matches `a`, `ab`, `abbb` |
| `+` | One or more of preceding | `ab+` matches `ab`, `abbb` (not `a`) |
| `?` | Zero or one of preceding | `ab?` matches `a`, `ab` |
| `^` | Start of line | `^Error` matches lines starting with "Error" |
| `$` | End of line | `failed$` matches lines ending with "failed" |
| `[abc]` | Character class (any of a, b, c) | `[0-9]` matches any digit |
| `[^abc]` | Negated class (not a, b, or c) | `[^0-9]` matches any non-digit |
| `\d` | Digit (0-9) | Matches `3` in `abc3def` |
| `\w` | Word character (alphanumeric + _) | Matches `a`, `Z`, `5`, `_` |
| `\s` | Whitespace (space, tab, newline) | Matches spaces and tabs |
| `(group)` | Capture group | `(error\|warning)` captures error or warning |
| `\|` | Alternation (OR) | `cat\|dog` matches "cat" or "dog" |
### Common Patterns
```pseudocode
// Email validation (simplified)
pattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
// Extract IP addresses
pattern = /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/
// Match ISO date format (YYYY-MM-DD)
pattern = /^\d{4}-\d{2}-\d{2}$/
// Find function calls in code (simplified)
pattern = /\w+\s*\([^)]*\)/
```
## Text Processing Patterns
### Pattern 1: Filter Lines by Criteria
```pseudocode
// Find all error lines in a log file
filter lines in logfile where line matches /ERROR/
// Exclude comment lines from a config file
filter lines in configfile where line does not match /^\s*#/
```
### Pattern 2: Extract Specific Fields
```pseudocode
// Extract usernames from Apache access logs
for each line in access_log:
match = extract pattern /^(\S+)/ from line
print match.group(1)
// Parse CSV columns
for each line in csv_file:
fields = split line by ','
print fields[2] // third column
```
### Pattern 3: Transform Text In-Place
```pseudocode
// Replace all occurrences of "old_function" with "new_function"
for each line in source_code:
line = replace /old_function/ with "new_function" in line
print line
// Convert snake_case to camelCase
for each identifier in code:
identifier = replace /_([a-z])/ with uppercase($1) in identifier
```
### Pattern 4: Aggregate and Summarize
```pseudocode
// Count HTTP status codes in logs
counts = {}
for each line in access_log:
status = extract pattern /HTTP\/\d\.\d" (\d{3})/ from line
counts[status] += 1
for status, count in counts:
print status, count
```
## Stream Editing Concepts
Stream editors process text line by line without loading the entire file into memory.
### Key Principles
1. **Input → Transform → Output** – Read from input stream, apply transformations, write to output stream
2. **Composability** – Chain multiple stream editors together using pipes
3. **No Side Effects** – Original data remains unchanged unless explicitly redirected
4. **Efficiency** – Process gigabyte-sized files with constant memory usage
### Common Stream Editing Operations
| Operation | Description |
|-----------|-------------|
| **Search** | Filter lines matching pattern |
| **Substitute** | Replace text globally |
| **Column extraction** | Print specific field |
| **Line numbering** | Add line numbers |
| **Deduplication** | Remove duplicate lines |
| **Counting** | Count lines |
### Pipeline Composition
```pseudocode
// Count unique IP addresses hitting an endpoint
input = read access_log
filtered = filter lines matching "/api/users"
ips = extract first field from each line
sorted = sort ips
unique = deduplicate sorted
counted = count lines in unique
print counted
```
## Practical Applications
### 1. Log Parsing
```pseudocode
// Find top 10 most frequent error messages
errors = filter lines from logfile matching /ERROR/
messages = extract pattern /ERROR: (.+)$/ from errors
sorted = sort messages
counted = count unique occurrences in sorted
top10 = take first 10 from counted
print top10
```
### 2. Data Extraction
```pseudocode
// Extract email addresses from a text file
emails = extract all matches of /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/ from document
deduplicated = unique entries in emails
print each email in deduplicated
```
### 3. Code Generation
```pseudocode
// Generate database model from schema definition
for each table in schema:
print "class " + capitalize(table.name) + " {"
for each column in table.columns:
print " " + column.type + " " + column.name
print "}"
```
### 4. Data Cleaning
```pseudocode
// Standardize phone numbers
for each line in contacts:
phone = extract pattern /\d+/ from line
digits = join all matches
if length(digits) == 10:
formatted = format as "(###) ###-####"
print formatted
```
## Summary Table
| Concept | Description | When to Use |
|---------|-------------|-------------|
| **Regular Expressions** | Pattern language for matching/extracting text | Validation, search, extraction |
| **Filtering** | Select lines matching criteria | Log analysis, data cleanup |
| **Extraction** | Pull specific fields from structured text | Parsing logs, CSVs, configs |
| **Transformation** | Replace or modify text according to rules | Refactoring, normalization |
| **Aggregation** | Count, sum, or summarize data | Statistics, reporting |
| **Code Generation** | Create boilerplate from templates | Reduce manual coding |
| **Stream Editing** | Process text line-by-line without full load | Large files, pipelines |
| **Pipeline Composition** | Chain simple tools for complex tasks | Unix philosophy, modularity |
## Key Takeaways
1. **Master Regex** – Regular expressions are the Swiss Army knife of text manipulation
2. **Think in Pipelines** – Compose small, single-purpose transformations
3. **Automate Early** – If you're doing it twice, script it
4. **Test on Real Data** – Validate patterns against actual inputs
5. **Know Your Tools** – Familiarity with text processing tools pays dividends daily
---
*Based on concepts from "The Pragmatic Programmer" by David Thomas and Andrew Hunt.*
principles/domain-languages.md
# Domain Languages
## Core Principle
> "Don't program in the specification language. Instead, write a mini-language that lets you express the problem more directly." - The Pragmatic Programmer
Domain-Specific Languages (DSLs) allow you to work closer to the problem domain rather than the implementation domain. By creating specialized languages for specific tasks, you make code more declarative, maintainable, and understandable to domain experts.
## Internal vs External DSLs
### Internal DSLs
Languages built within a host programming language, leveraging its syntax and features.
**Characteristics:**
- Use host language's parser and compiler
- Look like native code but express domain concepts
- Easier to implement and maintain
- Share tooling with host language
**Example:**
```pseudocode
// Configuration DSL embedded in host language
database.configure do
set connection_pool to 10
set timeout to 30 seconds
enable query_caching
use replica "db-slave-01" for read_operations
end
```
### External DSLs
Standalone languages with custom syntax, requiring their own parsers.
**Characteristics:**
- Complete control over syntax
- Can be highly specialized
- Requires parser/interpreter development
- May need custom tooling
**Example:**
```pseudocode
// Custom configuration language
DATABASE_CONFIG {
connection_pool: 10
timeout: 30s
query_cache: enabled
read_replica: db-slave-01
}
```
## When to Use Domain Languages
### Strong Indicators
| Scenario | Why DSL Helps |
|----------|---------------|
| **Repeated patterns** | Complex logic appears in many places with slight variations |
| **Domain expert involvement** | Non-programmers need to understand or modify rules |
| **Configuration complexity** | Settings have intricate relationships and validation rules |
| **Business rule volatility** | Rules change frequently and independently of core logic |
| **Data transformation pipelines** | Multi-step processes with clear input/output contracts |
### Anti-Indicators
| Scenario | Why to Avoid |
|----------|--------------|
| **One-off tasks** | Overhead exceeds benefits for single-use code |
| **Simple mappings** | JSON or YAML configuration suffices |
| **Rapidly changing requirements** | DSL design can't stabilize |
| **Small team without expertise** | Maintenance burden too high |
## Building Domain Languages
### Mini-Languages (Executable)
Small languages designed to perform specific computational tasks.
**Pattern Matching Example:**
```pseudocode
// Email validation mini-language
RULE email_validator:
PATTERN: one_or_more(alphanumeric or [. _ -])
"@"
one_or_more(alphanumeric or [. -])
"."
between(2, 6, alphabetic)
VALIDATE:
not starts_with(".")
not ends_with(".")
not contains("..")
END RULE
```
**Workflow Definition Example:**
```pseudocode
// Order processing workflow
WORKFLOW order_fulfillment:
STEP validate_payment:
CHECK payment_method is valid
CHECK funds_available >= order_total
ON_FAIL: notify customer, halt
STEP reserve_inventory:
FOR_EACH item IN order_items:
LOCK inventory_quantity FOR item
ON_FAIL: release_locks, rollback, halt
STEP ship_order:
ASSIGN warehouse = nearest_to(customer_address)
CREATE shipping_label
NOTIFY warehouse_system
COMPLETION:
SEND confirmation_email TO customer
LOG analytics_event "order_completed"
END WORKFLOW
```
### Data Languages (Declarative)
Languages that describe structures, relationships, or configurations.
**Schema Definition Example:**
```pseudocode
// Data validation schema language
SCHEMA user_profile:
FIELD username:
TYPE: string
CONSTRAINTS:
length BETWEEN 3 AND 20
matches PATTERN "^[a-zA-Z0-9_]+$"
UNIQUE IN database
FIELD email:
TYPE: email_address
REQUIRED
UNIQUE IN database
FIELD age:
TYPE: integer
OPTIONAL
CONSTRAINTS:
MINIMUM 13
MAXIMUM 120
FIELD preferences:
TYPE: nested_object
SCHEMA:
FIELD notifications:
TYPE: boolean
DEFAULT: true
FIELD theme:
TYPE: enum["light", "dark", "auto"]
DEFAULT: "auto"
END SCHEMA
```
**Access Control Example:**
```pseudocode
// Permission definition language
PERMISSIONS:
ROLE administrator:
CAN perform ANY action ON ANY resource
ROLE editor:
CAN create, read, update ON documents
CAN read ON users
WHERE user.department EQUALS current_user.department
ROLE viewer:
CAN read ON documents
WHERE document.published EQUALS true
OR document.author EQUALS current_user
ROLE guest:
CAN read ON documents
WHERE document.visibility EQUALS "public"
END PERMISSIONS
```
## Implementation Strategies
### Lexical Analysis Approach
```pseudocode
// Simple tokenizer for DSL
FUNCTION tokenize(input_text):
tokens = empty_list
position = 0
WHILE position < length(input_text):
character = input_text[position]
IF character matches whitespace:
position = position + 1
CONTINUE
IF character matches letter:
identifier = extract_identifier(input_text, position)
tokens.add(TOKEN("IDENTIFIER", identifier))
position = position + length(identifier)
IF character matches digit:
number = extract_number(input_text, position)
tokens.add(TOKEN("NUMBER", number))
position = position + length(number)
IF character matches operator:
tokens.add(TOKEN("OPERATOR", character))
position = position + 1
RETURN tokens
END FUNCTION
```
### Parser Pattern
```pseudocode
// Recursive descent parser example
FUNCTION parse_expression(tokens):
left = parse_term(tokens)
WHILE current_token() IN ["+", "-"]:
operator = consume_token()
right = parse_term(tokens)
left = create_binary_expression(operator, left, right)
RETURN left
END FUNCTION
FUNCTION parse_term(tokens):
left = parse_factor(tokens)
WHILE current_token() IN ["*", "/"]:
operator = consume_token()
right = parse_factor(tokens)
left = create_binary_expression(operator, left, right)
RETURN left
END FUNCTION
FUNCTION parse_factor(tokens):
token = current_token()
IF token.type EQUALS "NUMBER":
consume_token()
RETURN create_number_node(token.value)
IF token.type EQUALS "IDENTIFIER":
consume_token()
RETURN create_variable_node(token.value)
IF token.value EQUALS "(":
consume_token() // consume "("
expression = parse_expression(tokens)
expect_token(")") // consume ")"
RETURN expression
RAISE syntax_error("Unexpected token")
END FUNCTION
```
## Trade-offs and Considerations
### Benefits
| Benefit | Impact |
|---------|--------|
| **Clarity** | Domain concepts expressed directly without translation layer |
| **Validation** | DSL enforces domain rules at parse time |
| **Productivity** | Experts can modify behavior without touching implementation |
| **Testability** | DSL scripts can be tested independently |
| **Evolution** | Domain logic changes without code recompilation |
### Costs
| Cost | Mitigation Strategy |
|------|---------------------|
| **Learning curve** | Comprehensive documentation, clear error messages |
| **Tooling investment** | Start with internal DSL, leverage existing tools |
| **Debugging difficulty** | Generate source maps, provide runtime introspection |
| **Maintenance burden** | Keep grammar simple, version the language spec |
| **Performance overhead** | Cache parsed results, compile to native code if needed |
### Design Guidelines
1. **Start Small**: Begin with limited scope, expand based on real needs
2. **Parse, Don't Validate**: Use type systems and structure to prevent invalid states
3. **Fail Fast**: Report errors at parse time, not runtime
4. **Optimize for Reading**: DSL will be read 10x more than written
5. **Document Extensively**: Include examples for every construct
6. **Version Carefully**: DSL changes are breaking changes for users
## Complexity Ladder
```pseudocode
// Level 1: Simple data file (not really a DSL)
config_file = {
"timeout": 30,
"retries": 3
}
// Level 2: Structured data with conventions
CONFIGURATION:
timeout: 30 seconds
retry_policy: exponential_backoff(3 attempts)
// Level 3: Mini-language with control flow
WHEN request_timeout:
RETRY with exponential_backoff
initial_delay: 1 second
max_attempts: 3
max_delay: 10 seconds
IF all_retries_exhausted:
NOTIFY operations_team
RETURN error_to_client
// Level 4: Full DSL with abstractions
POLICY request_resilience:
DEFINE retry_strategy AS exponential_backoff:
base_delay: 1s
multiplier: 2
max_attempts: 3
ON timeout_error:
APPLY retry_strategy
LOG "Request timeout, retrying"
ON max_retries_exceeded:
ESCALATE TO operations_team WITH context
RESPOND TO client WITH friendly_error_message
END POLICY
```
## Real-World Applications
### Build Systems
```pseudocode
// Declarative build DSL
BUILD target "web-app":
SOURCES: find_files("src/**/*.ts")
STEP transpile:
TOOL: typescript_compiler
OPTIONS: strict_mode, source_maps
OUTPUT: "dist/js"
STEP bundle:
TOOL: module_bundler
INPUT: transpile.output
OPTIONS: minify, tree_shake
OUTPUT: "dist/bundle.js"
STEP optimize:
TOOL: asset_optimizer
INPUT: bundle.output, find_files("assets/**")
OUTPUT: "dist/optimized"
WATCH_FILES: "src/**"
ON_CHANGE: rebuild_incrementally
END BUILD
```
### Testing DSL
```pseudocode
// Behavior specification language
SCENARIO "User registration with valid data":
GIVEN user visits registration_page
AND user is not logged_in
WHEN user enters:
username: "alice_smith"
email: "alice@example.com"
password: "SecurePass123!"
AND user clicks submit_button
THEN user should be redirected_to welcome_page
AND user should receive confirmation_email
AND database should contain user_record WITH:
username: "alice_smith"
email: "alice@example.com"
verified: false
END SCENARIO
```
## Summary: DSL Decision Matrix
| Factor | Score +1 if True | Your Score |
|--------|------------------|------------|
| Task repeats 10+ times in codebase | ✓ or ✗ | ___ |
| Non-programmers need to understand/modify | ✓ or ✗ | ___ |
| Domain has stable, well-understood concepts | ✓ or ✗ | ___ |
| Changes happen frequently | ✓ or ✗ | ___ |
| Team has language design experience | ✓ or ✗ | ___ |
| Host language is verbose for this domain | ✓ or ✗ | ___ |
| Cost of errors is high | ✓ or ✗ | ___ |
**Scoring:**
- **0-2**: Stick with general-purpose language or configuration files
- **3-4**: Consider internal DSL
- **5-7**: Strong candidate for external DSL
## Key Takeaways
1. **Start with data languages** - easier to build, lower risk
2. **Internal DSLs first** - leverage existing tooling and expertise
3. **Design for humans** - clarity trumps cleverness
4. **Parse-time validation** - catch errors before execution
5. **Document ruthlessly** - DSL without docs is a liability
6. **Version explicitly** - treat DSL as a contract with users
7. **Measure success** - does it reduce errors and increase velocity?
---
*Based on concepts from "The Pragmatic Programmer" by David Thomas and Andrew Hunt.*
principles/dry.md
# DRY - Don't Repeat Yourself
## Definition
> "Every piece of knowledge must have a single, unambiguous, authoritative representation within a system."
> — Dave Thomas & Andy Hunt
DRY is not just about code duplication—it's about knowledge duplication.
## What DRY Really Means
DRY applies to:
- **Code** - Obvious duplication
- **Documentation** - Comments that duplicate code
- **Data structures** - Redundant data storage
- **APIs** - Multiple representations of same concept
- **Developer knowledge** - Multiple developers solving same problem
## Types of Duplication
### Imposed Duplication
Duplication that seems required by the environment.
```pseudocode
// Problem: API schema duplicated in docs, code, and tests
// Solution: Generate from single source
// schema.yaml → API code
// schema.yaml → Documentation
// schema.yaml → Client SDKs
// schema.yaml → Test fixtures
```
### Inadvertent Duplication
Duplication developers don't realize they're creating.
```pseudocode
// Bad - duplicated business logic
class Order {
function total() {
return items.sum(i => i.price * i.quantity) * 1.1 // 10% tax
}
}
class Invoice {
function amount() {
return lineItems.sum(l => l.unitPrice * l.qty) * 1.1 // 10% tax
}
}
// Good - single source of truth
TAX_RATE = 1.1
class TaxCalculator {
function applyTax(amount) {
return amount * TAX_RATE
}
}
```
### Impatient Duplication
Duplication from developers taking shortcuts.
```pseudocode
// Bad - "I'll just copy this function and modify it"
function validateUser(user) {
if user.name.length < 2 { return false }
if user.email.indexOf("@") < 0 { return false }
return true
}
function validateAdmin(admin) {
if admin.name.length < 2 { return false }
if admin.email.indexOf("@") < 0 { return false } // Duplicated!
if admin.accessLevel < 5 { return false }
return true
}
// Good - compose validators
function validatePerson(person) {
return validateName(person.name) && validateEmail(person.email)
}
function validateUser(user) {
return validatePerson(user)
}
function validateAdmin(admin) {
return validatePerson(admin) && validateAccessLevel(admin.accessLevel)
}
```
### Inter-Developer Duplication
Different team members solving the same problem.
**Solutions:**
- Good communication
- Code reviews
- Shared libraries
- Documentation of existing solutions
- Regular team discussions
## Code Duplication
### The Rule of Three
1. First time: Just do it
2. Second time: Wince at duplication, but do it anyway
3. Third time: Refactor
### Extracting Duplication
```pseudocode
// Before - duplicated validation
function createUser(data) {
if data.email == null || data.email == "" {
throw Error("Email required")
}
if not data.email.match(EMAIL_REGEX) {
throw Error("Invalid email")
}
// ... create user
}
function updateUser(id, data) {
if data.email == null || data.email == "" {
throw Error("Email required")
}
if not data.email.match(EMAIL_REGEX) {
throw Error("Invalid email")
}
// ... update user
}
// After - single validation
function validateEmail(email) {
if email == null || email == "" {
throw Error("Email required")
}
if not email.match(EMAIL_REGEX) {
throw Error("Invalid email")
}
}
function createUser(data) {
validateEmail(data.email)
// ... create user
}
function updateUser(id, data) {
validateEmail(data.email)
// ... update user
}
```
## Documentation Duplication
### Bad: Comments That Duplicate Code
```pseudocode
// Bad - comment says what code says
// Add one to the counter
counter = counter + 1
// Good - comment explains why
// Compensate for zero-based index in display
counter = counter + 1
```
### Good: Generate Documentation from Code
```pseudocode
/**
* Calculate compound interest.
* @param principal - Initial amount
* @param rate - Annual interest rate (0.05 = 5%)
* @param years - Number of years
* @returns Final amount after interest
*/
function calculateInterest(principal, rate, years) {
return principal * (1 + rate) ** years
}
// Documentation generated from this single source
```
## Data Duplication
### Bad: Redundant Data
```pseudocode
// Bad - storing derived data
class Order {
items = []
itemCount = 0 // Redundant!
subtotal = 0 // Redundant!
total = 0 // Redundant!
function addItem(item) {
items.add(item)
itemCount++ // Must keep in sync
subtotal += item.price // Must keep in sync
total = subtotal * 1.1 // Must keep in sync
}
}
// Good - calculate when needed
class Order {
items = []
function addItem(item) {
items.add(item)
}
function itemCount() {
return items.length
}
function subtotal() {
return items.sum(i => i.price)
}
function total() {
return subtotal() * 1.1
}
}
```
### When Caching is OK
Caching derived data is acceptable when:
- Performance requires it
- The cache is clearly marked as cache
- There's a single mechanism to invalidate/update
```pseudocode
class Order {
items = []
_cachedTotal = null // Clearly marked as cache
function addItem(item) {
items.add(item)
_cachedTotal = null // Invalidate cache
}
function total() {
if _cachedTotal == null {
_cachedTotal = calculateTotal()
}
return _cachedTotal
}
}
```
## API/Schema Duplication
### Single Source of Truth
```yaml
# schema.yaml - THE source of truth
User:
properties:
id: integer
name: string
email: string
createdAt: datetime
```
Generate everything from this:
- Database migrations
- API endpoints
- TypeScript interfaces
- Python dataclasses
- API documentation
- Client SDKs
## When Duplication is OK
### Coincidental Similarity
Two things that look the same but represent different concepts:
```pseudocode
// These look similar but serve different purposes
class UserValidator {
function validate(user) {
return user.name.length >= 2
}
}
class ProductValidator {
function validate(product) {
return product.name.length >= 2 // Same logic, different domain
}
}
// DON'T extract - they may evolve differently
// User names might need 2 chars, product names might need 5 later
```
### Performance-Critical Code
Sometimes inline duplication is faster than function calls.
## DRY vs. Premature Abstraction
Don't create abstractions too early:
```pseudocode
// Too early - only one use case
interface DataProcessor<T, R> {
function process(input: T): R
}
class UserProcessor implements DataProcessor<RawUser, User> {
// Lots of generic code for one use case
}
// Better - wait for patterns to emerge
class UserProcessor {
function process(rawUser) {
// Simple, direct implementation
}
}
// Abstract when you see the third use case
```
## Summary
| Do | Don't |
|----|-------|
| Single source of truth | Copy-paste code |
| Generate from schemas | Maintain multiple copies |
| Calculate derived data | Store redundant data |
| Share common libraries | Solve same problem twice |
| Communicate with team | Assume others haven't solved it |
---
*Based on concepts from "The Pragmatic Programmer" by David Thomas and Andrew Hunt.*
principles/estimating.md
# Estimating
## Definition
> "Estimate to avoid surprises."
> — Dave Thomas & Andy Hunt
Good estimates help everyone plan. Bad estimates create frustration, missed deadlines, and broken trust. Learn to estimate well.
## The Purpose of Estimates
Estimates answer the question: **"How long will this take?"**
But the real question behind that is often:
- "Can we make the deadline?"
- "Should we do this project?"
- "How do we plan our resources?"
Understanding the *why* helps you give better answers.
## Units Matter
Choose units that reflect your confidence:
| Duration | Unit | Implies |
|----------|------|---------|
| 1-15 days | Days | Fairly confident |
| 3-6 weeks | Weeks | Some uncertainty |
| 2-6 months | Months | Significant uncertainty |
| More | "I need to break this down" | Too uncertain to estimate |
```pseudocode
// Bad: "It'll take 123 hours"
// Implies precision you don't have
// Good: "About 3 weeks"
// Honest uncertainty
```
## Where Estimates Come From
### 1. Ask Someone Who's Done It
```pseudocode
// Best source: Experience
you: "How long to add OAuth?"
colleague: "I did it last year. Basic setup is 2 days.
Edge cases and testing took another week.
Budget 2 weeks to be safe."
```
### 2. Break It Down
```pseudocode
// Task: Build user registration
// Break into subtasks:
// - Design database schema (0.5 days)
// - Create API endpoint (1 day)
// - Build form UI (1 day)
// - Add validation (0.5 days)
// - Write tests (1 day)
// - Handle errors (0.5 days)
// - Documentation (0.5 days)
// Total: 5 days
// Add buffer: 7 days (~1.5 weeks)
```
### 3. Build a Model
For complex estimates, identify the factors:
```pseudocode
// Estimating data migration
factors:
- Number of records: 1,000,000
- Processing time per record: 100ms
- Batch size: 1,000
- Network latency: 50ms/batch
calculation:
batches = 1,000,000 / 1,000 = 1,000 batches
processing = 1,000,000 * 0.1s = 100,000s
network = 1,000 * 0.05s = 50s
total = ~28 hours
// Add buffer for issues: 2 days
```
### 4. Iterate with Feedback
Track your estimates vs. actuals:
```
| Task | Estimated | Actual | Ratio |
|------------------|-----------|--------|-------|
| User auth | 3 days | 5 days | 1.7x |
| Payment API | 1 week | 2 weeks| 2.0x |
| Report generator | 2 weeks | 2 weeks| 1.0x |
Average ratio: 1.6x
Apply to future estimates
```
## The Estimation Process
### Step 1: Understand the Ask
```pseudocode
// Don't estimate immediately
// Ask:
// - What exactly is needed?
// - What's the scope?
// - What quality level?
// - Are there existing constraints?
// - What's driving the deadline?
```
### Step 2: Build a Model of the System
```pseudocode
// Mental model of what's involved
user_registration:
├── frontend:
│ ├── form component
│ ├── validation UI
│ └── success/error states
├── backend:
│ ├── API endpoint
│ ├── validation logic
│ ├── database operations
│ └── email verification
└── infrastructure:
├── email service setup
└── database migrations
```
### Step 3: Break into Components
```pseudocode
// Estimate each component
components = [
("Form component", "1 day"),
("API endpoint", "0.5 days"),
("Validation", "0.5 days"),
("Database", "0.5 days"),
("Email service", "1 day"),
("Testing", "1 day"),
("Buffer", "1 day")
]
total = sum(components) // 5.5 days ≈ 1 week
```
### Step 4: Give Ranges
```pseudocode
// Single number: "5 days"
// Problem: Treated as commitment
// Range: "4-8 days, most likely 5-6"
// Better: Shows uncertainty
// Confidence: "80% confident it's under 2 weeks"
// Best: Explicitly states confidence
```
## PERT Estimation
**P**rogram **E**valuation and **R**eview **T**echnique:
```pseudocode
// Three-point estimate
optimistic = 3 // Best case
mostLikely = 5 // Typical case
pessimistic = 12 // Worst case
// Weighted average
expected = (optimistic + 4*mostLikely + pessimistic) / 6
= (3 + 20 + 12) / 6
= 5.8 days
```
## Common Estimation Mistakes
### 1. Anchoring
```pseudocode
// Bad: Manager says "This should take a day, right?"
// You think: "Well... maybe... I guess?"
// Good: "Let me think about it and get back to you."
// Then estimate independently
```
### 2. Forgetting Tasks
```pseudocode
// Often forgotten:
// - Testing
// - Code review
// - Documentation
// - Deployment
// - Bug fixes
// - Meetings
// - Context switching
// - Learning curve
```
### 3. Optimism Bias
```pseudocode
// Thought: "If everything goes perfectly..."
// Reality: Everything never goes perfectly
// Add buffer:
// - Small task: +20%
// - Medium task: +50%
// - Large task: +100%
// - Unknown tech: +200%
```
### 4. Precision Theater
```pseudocode
// Bad: "It will take 17.5 hours"
// Implies false precision
// Good: "About 2-3 days"
// Honest about uncertainty
```
## Saying "I Don't Know"
It's OK to not have an estimate:
```pseudocode
// Good responses:
"I need to research this first. Give me a day."
"I've never done this. Let me do a spike."
"This is too big. Can we break it down?"
"I can estimate the first phase, but not the rest yet."
```
## When Asked for Immediate Estimates
```pseudocode
// Pressure: "Quick, how long?"
// Options:
1. "Off the top of my head, maybe X, but let me verify."
2. "I'd need to look at the code first."
3. "That sounds like a week or two, but I'm not confident."
4. "Can I get back to you in an hour?"
```
## Improving Over Time
### Keep a Log
```
| Date | Task | Estimate | Actual | Notes |
|------|------|----------|--------|-------|
| 1/15 | API | 3 days | 5 days | Auth was harder |
| 1/22 | UI | 1 week | 1 week | On track |
| 2/01 | DB | 2 days | 1 day | Overestimated |
```
### Review and Adjust
```pseudocode
// Monthly review
function reviewEstimates() {
accuracy = actual / estimated
if accuracy > 1.5 {
// Consistently underestimating
// Increase future estimates
} else if accuracy < 0.8 {
// Overestimating
// Decrease or you're sandbagging
}
}
```
## Summary
| Do | Don't |
|----|-------|
| Ask clarifying questions | Estimate immediately |
| Break tasks down | Give one big number |
| Give ranges | Imply false precision |
| Include buffer | Assume best case |
| Track actuals | Forget to learn |
| Say "I don't know" | Make up numbers |
| Use appropriate units | Say "17.5 hours" |
---
*Based on concepts from "The Pragmatic Programmer" by David Thomas and Andrew Hunt.*
principles/orthogonality.md
# Orthogonality
## Definition
> "Two or more things are orthogonal if changes in one do not affect any of the others."
> — Dave Thomas & Andy Hunt
In computing, orthogonality means independence—components that don't affect each other when changed.
## The Geometry Metaphor
In geometry, orthogonal means at right angles. Move along one axis and your position on other axes doesn't change.
```
Y
│
│ Moving along X doesn't change Y
│ Moving along Y doesn't change X
└────────── X
```
## Benefits of Orthogonality
### Increased Productivity
- Changes are localized
- Components can be combined in new ways
- Testing is easier
- Less duplication
### Reduced Risk
- Diseased sections are isolated
- System is less fragile
- Components can be replaced
- Easier to test parts in isolation
## Orthogonal Design
### Layered Architecture
```
┌─────────────────────────┐
│ Presentation Layer │ ← Changes don't affect data layer
├─────────────────────────┤
│ Business Logic │ ← Independent of UI and database
├─────────────────────────┤
│ Data Access Layer │ ← Changes don't affect business logic
├─────────────────────────┤
│ Database │
└─────────────────────────┘
```
### Non-Orthogonal Example
```pseudocode
// Bad - UI logic mixed with business logic
class OrderButton {
function onClick() {
// UI code
button.disable()
spinner.show()
// Business logic (non-orthogonal!)
order.validate()
order.calculateTotal()
order.applyDiscounts()
// Database code (non-orthogonal!)
database.save(order)
// More UI code
spinner.hide()
showSuccessMessage()
}
}
```
### Orthogonal Example
```pseudocode
// Good - separated concerns
class OrderButton {
function onClick() {
ui.showLoading()
orderService.processOrder(order)
.then(() => ui.showSuccess())
.catch(e => ui.showError(e))
}
}
class OrderService {
function processOrder(order) {
validator.validate(order)
calculator.calculateTotal(order)
repository.save(order)
}
}
class OrderRepository {
function save(order) {
database.insert("orders", order)
}
}
// Now: UI changes don't affect business logic
// Business logic changes don't affect database
```
## Testing Orthogonality
Ask these questions:
1. **If I dramatically change X, how many modules are affected?**
- Good: Only one
- Bad: Many
2. **Can I test this module in isolation?**
- Good: Yes, with simple mocks
- Bad: Need complex setup
3. **Do my modules know about each other's internals?**
- Good: No, only interfaces
- Bad: Yes, tightly coupled
## Achieving Orthogonality
### Keep Code Decoupled
```pseudocode
// Bad - objects know too much about each other
class Order {
function ship() {
customer.address.city.warehouse.stock.reduce(items)
customer.loyaltyPoints.add(total * 0.1)
emailService.templates.orderShipped.send(customer.email)
}
}
// Good - tell, don't ask
class Order {
function ship(shippingService) {
shippingService.ship(this)
}
}
class ShippingService {
function ship(order) {
warehouse.reduceStock(order.items)
loyalty.addPoints(order.customer, order.total)
notifications.sendShipped(order)
}
}
```
### Avoid Global Data
```pseudocode
// Bad - global state couples everything
global currentUser
global settings
global database
function processOrder() {
if settings.taxEnabled {
tax = currentUser.region.taxRate * total
}
database.save(order) // Which database? Can't test!
}
// Good - inject dependencies
function processOrder(order, user, settings, database) {
if settings.taxEnabled {
tax = calculateTax(user.region, total)
}
database.save(order)
}
```
### Avoid Similar Functions
If two functions look similar, they might share code that should be extracted—but be careful not to couple unrelated things.
```pseudocode
// These are similar but serve different domains
function formatUserName(user) {
return user.firstName + " " + user.lastName
}
function formatProductName(product) {
return product.brand + " " + product.model
}
// DON'T create: formatName(thing, prop1, prop2)
// They might evolve differently and should remain orthogonal
```
## Orthogonality in Teams
### Bad: Component-Based Teams
```
Team A: Database layer (all tables)
Team B: UI layer (all screens)
Team C: Business logic (all features)
Problem: One feature requires all three teams to coordinate
```
### Good: Feature-Based Teams
```
Team A: User management (UI + logic + data)
Team B: Ordering (UI + logic + data)
Team C: Inventory (UI + logic + data)
Benefit: Each team can work independently
```
## Orthogonality Checklist
When designing components, ask:
- [ ] Can I change this without affecting others?
- [ ] Can I test this in isolation?
- [ ] Does this have a single, well-defined purpose?
- [ ] Are dependencies injected, not hardcoded?
- [ ] Would a change ripple through the system?
## Real-World Examples
### Orthogonal
- **Unix pipes**: `cat file | grep pattern | sort | uniq`
- **CSS classes**: Multiple classes combine independently
- **Microservices**: Services deployed independently
- **Plugins**: Add features without changing core
### Non-Orthogonal
- **Spaghetti code**: Everything depends on everything
- **God objects**: One class that does everything
- **Tight coupling**: Can't change A without changing B
- **Feature flags everywhere**: Logic scattered across codebase
## The Helicopter Crash
The book tells a story of a helicopter crash investigation:
- Cause: A single leak in hydraulic system
- Root cause: Both control systems shared the same hydraulic lines
- Result: One failure brought down everything
**Lesson**: Keep systems truly independent. Don't let them share failure modes.
## Summary
| Orthogonal | Non-Orthogonal |
|------------|----------------|
| Layered architecture | Spaghetti code |
| Dependency injection | Global variables |
| Interface segregation | Fat interfaces |
| Single responsibility | God objects |
| Feature teams | Component teams |
| Microservices | Monolith with no boundaries |
---
*Based on concepts from "The Pragmatic Programmer" by David Thomas and Andrew Hunt.*
principles/prototypes.md
# Prototypes and Post-It Notes
## Definition
> "Prototype to learn."
> — Dave Thomas & Andy Hunt
Prototypes are experiments designed to answer questions. They're meant to be thrown away—the value is in the learning, not the code.
## Prototypes vs. Tracer Bullets
| Aspect | Prototype | Tracer Bullet |
|--------|-----------|---------------|
| Purpose | Learn and explore | Build incrementally |
| Quality | Throwaway | Production-ready |
| Fate | Discarded | Evolves into final system |
| Scope | Answer specific question | End-to-end skeleton |
| Detail | Ignores unimportant parts | Thin but complete |
## What to Prototype
### Architecture
- How will components communicate?
- Can we achieve required performance?
- Will this scale?
### New Technology
- Can we integrate this library?
- Does this service meet our needs?
- How steep is the learning curve?
### User Interface
- Is this workflow intuitive?
- What information do users need?
- How should data be presented?
### Algorithms
- Is this approach fast enough?
- Does it handle edge cases?
- Can we reduce complexity?
### External Systems
- Can we connect to this API?
- What's the data format?
- How do we handle errors?
## How to Prototype
### 1. Identify What You're Learning
```pseudocode
// Bad: "I'll prototype the user system"
// Too vague - what's the question?
// Good: "Can our ORM handle 10,000 concurrent users?"
// Clear question to answer
```
### 2. Ignore Everything Irrelevant
```pseudocode
// Prototyping: "Can we parse this file format?"
// DON'T worry about:
// - Error handling
// - Edge cases
// - Performance
// - Clean code
// - Tests
// DO focus on:
// - Can we read the format at all?
// - Do we understand the structure?
function canWeParseThis() {
// Quick and dirty
file = open("sample.dat")
data = file.read()
// Just try to parse it
result = magicParse(data)
print(result)
// Did it work? That's all we need to know.
}
```
### 3. Throw It Away
The most important step. Prototypes are:
- Not production code
- Not a head start
- Not something to "clean up later"
```pseudocode
// After prototyping:
// DELETE the prototype code
// KEEP the knowledge gained
// WRITE production code from scratch with proper architecture
```
## Prototyping Approaches
### Paper Prototypes
For UI questions, don't code at all:
```
┌──────────────────────────────────┐
│ [Logo] Search: [________] │
├──────────────────────────────────┤
│ Welcome, User! │
│ │
│ Recent Items: │
│ ┌────┐ ┌────┐ ┌────┐ │
│ │ │ │ │ │ │ │
│ └────┘ └────┘ └────┘ │
│ │
│ [Create New] [Browse All] │
└──────────────────────────────────┘
User: "I'd look for the search first"
→ Learned: Search should be more prominent
```
### Spike Solutions
Time-boxed experiments:
```pseudocode
// Spike: Can we integrate with Payment API?
// Time box: 2 hours
// Hour 1: Read docs, get credentials
// Hour 2: Make a test charge
result = paymentAPI.charge({
amount: 100,
card: testCard,
description: "Test"
})
// Outcome: Yes/No/Maybe with conditions
// Delete code, document findings
```
### Architectural Prototypes
Test system design:
```pseudocode
// Question: Can microservices communicate via message queue?
// Prototype: Simplest possible message flow
// Service A (producer)
function sendMessage() {
queue.publish("orders", { orderId: 123 })
print("Sent!")
}
// Service B (consumer)
function receiveMessage() {
queue.subscribe("orders", message => {
print("Received: " + message.orderId)
})
}
// Run both, verify messages flow
// Answer: Yes, it works. Now design properly.
```
## What to Ignore in Prototypes
| Skip | Why |
|------|-----|
| Error handling | Clutters the experiment |
| Validation | Not testing that |
| Security | Prototype isn't production |
| Performance | Unless that's the question |
| Complete functionality | Only build what answers the question |
| Documentation | It's throwaway |
| Tests | Code is deleted anyway |
## Warning: Prototype Pitfalls
### The "Clean It Up Later" Trap
```pseudocode
// Manager: "Great prototype! Ship it."
// Developer: "But it's a prototype..."
// Manager: "Just clean it up a bit."
// DON'T DO THIS!
// Prototypes have fundamental shortcuts
// "Cleaning up" is harder than rewriting
```
### Make It Clear It's a Prototype
```pseudocode
// Name it obviously
prototype_payment_spike.py
THROW_AWAY_ui_test.html
// Or use a separate branch
git checkout -b prototype/payment-api
// Document it
// ⚠️ PROTOTYPE - DO NOT SHIP
// This code answers the question: Can we integrate with X?
// It ignores: security, errors, performance
// DELETE after review
```
### Set Time Limits
```
Spike: Payment API integration
Time Box: 4 hours
Question: Can we charge cards with this API?
Hour 0: Start
Hour 4: Stop, document, delete
```
## Post-It Note Architecture
For quick design exploration:
```
┌─────────┐ ┌─────────┐ ┌─────────┐
│ User │────▶│ API │────▶│ Order │
│Interface│ │ Gateway │ │ Service │
└─────────┘ └─────────┘ └────┬────┘
│
▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Payment │◀────│ Event │◀────│Inventory│
│ Service │ │ Bus │ │ Service │
└─────────┘ └─────────┘ └─────────┘
```
Benefits:
- Fast to create and modify
- No code investment
- Easy to discuss and iterate
- Physical collaboration
## Summary
### When to Prototype
- Uncertain about feasibility
- New technology or approach
- Need stakeholder feedback early
- High-risk decisions
- Complex integration
### How to Prototype Well
1. Define the question clearly
2. Build the minimum to answer it
3. Skip everything irrelevant
4. Time-box the effort
5. Document the findings
6. **Throw away the code**
### The Golden Rule
> "If you find yourself working to 'clean up' prototype code, stop. That's a sign the prototype served its purpose. Now write production code from scratch with proper architecture."
---
*Based on concepts from "The Pragmatic Programmer" by David Thomas and Andrew Hunt.*
principles/reversibility.md
# Reversibility
## Definition
> "There are no final decisions."
> — Dave Thomas & Andy Hunt
Nothing is forever. Requirements change, technology evolves, and today's perfect solution becomes tomorrow's legacy burden. Design for change.
## The Problem with Finality
When you make an "irreversible" decision:
- You bet on predictions being correct
- You lock in technical debt
- You can't adapt to new requirements
- Migration becomes expensive or impossible
## Critical Decisions to Keep Reversible
### Database Choice
```pseudocode
// Bad - hardcoded to PostgreSQL
class UserRepository {
function findById(id) {
return pg.query("SELECT * FROM users WHERE id = $1", [id])
}
}
// Good - abstracted
interface UserRepository {
function findById(id): User
}
class PostgresUserRepository implements UserRepository {
function findById(id) {
return pg.query("SELECT * FROM users WHERE id = $1", [id])
}
}
class MongoUserRepository implements UserRepository {
function findById(id) {
return mongo.collection("users").findOne({ _id: id })
}
}
// Now switching databases is straightforward
```
### Third-Party Services
```pseudocode
// Bad - hardcoded to specific provider
function sendEmail(to, subject, body) {
sendgrid.send({
to: to,
from: "noreply@example.com",
subject: subject,
html: body
})
}
// Good - abstracted
interface EmailService {
function send(message: EmailMessage)
}
class SendGridEmailService implements EmailService {
function send(message) { /* SendGrid API */ }
}
class SESEmailService implements EmailService {
function send(message) { /* AWS SES API */ }
}
class MailgunEmailService implements EmailService {
function send(message) { /* Mailgun API */ }
}
// Switch providers by changing configuration
```
### Architecture Patterns
```pseudocode
// Bad - monolith assumptions everywhere
function processOrder(order) {
// Directly calls inventory
inventory.reserve(order.items)
// Directly calls payment
payment.charge(order.total)
// Directly calls shipping
shipping.createLabel(order)
}
// Good - message-based (can split into microservices later)
function processOrder(order) {
eventBus.publish("OrderPlaced", {
orderId: order.id,
items: order.items,
total: order.total
})
}
// Handlers can be in same process or different services
eventBus.subscribe("OrderPlaced", inventoryHandler)
eventBus.subscribe("OrderPlaced", paymentHandler)
eventBus.subscribe("OrderPlaced", shippingHandler)
```
## Strategies for Reversibility
### 1. Hide Third-Party APIs Behind Abstractions
```pseudocode
// Wrap every external service
interface PaymentGateway {
function charge(amount, card): PaymentResult
function refund(transactionId): RefundResult
}
// Implementation details hidden
class StripeGateway implements PaymentGateway { }
class BraintreeGateway implements PaymentGateway { }
class PayPalGateway implements PaymentGateway { }
```
### 2. Use Configuration, Not Code
```pseudocode
// Bad - hardcoded
DATABASE_HOST = "prod-db-1.example.com"
API_URL = "https://api.vendor.com/v2"
// Good - configurable
DATABASE_HOST = env("DATABASE_HOST")
API_URL = env("API_URL")
// Can change without redeploying
```
### 3. Design for Replacement
Ask: "How hard would it be to replace this component?"
```pseudocode
// Bad - framework deeply embedded
class User extends FrameworkActiveRecord {
// All logic tied to framework
}
// Good - domain logic separated
class User {
// Pure domain logic, no framework dependencies
}
class UserActiveRecord extends FrameworkActiveRecord {
// Framework adapter
function toUser() {
return new User(this.attributes)
}
}
```
### 4. Keep Components Small
Small components are easier to replace than large ones.
```
// Bad: One big payment module
PaymentSystem (10,000 lines)
// Good: Small, replaceable pieces
CardValidator (200 lines)
FraudChecker (300 lines)
PaymentProcessor (400 lines)
ReceiptGenerator (250 lines)
```
### 5. Write Reversible Migrations
```pseudocode
// Database migration
function up() {
addColumn("users", "phone", "varchar(20)")
}
function down() {
removeColumn("users", "phone") // Can undo!
}
// Data migration
function up() {
// Save old data before transforming
backup = database.query("SELECT * FROM users")
saveBackup(backup)
// Transform
transformData()
}
function down() {
restoreBackup() // Can undo!
}
```
## Real-World Reversibility
### Vendor Lock-In
| Area | Locked In | Reversible |
|------|-----------|------------|
| Cloud | Using AWS-specific services everywhere | Cloud-agnostic abstractions |
| Database | Stored procedures, proprietary features | Standard SQL, repository pattern |
| Framework | Business logic in framework controllers | Framework as thin adapter |
| Auth | Proprietary user format | Standard claims/tokens |
### Technology Bets
| Bet | Hedged Version |
|-----|----------------|
| "GraphQL is the future" | REST + GraphQL adapter |
| "NoSQL will scale better" | Repository pattern (swap later) |
| "Kubernetes forever" | Container-agnostic deployment |
| "React won" | Component abstraction layer |
## The Flexible Architecture
```
┌─────────────────────────────────────────────────┐
│ UI Layer │
│ (Can switch: React → Vue → Server-rendered) │
├─────────────────────────────────────────────────┤
│ API Layer │
│ (Can switch: REST → GraphQL → gRPC) │
├─────────────────────────────────────────────────┤
│ Business Logic │
│ (Pure domain code, no external dependencies) │
├─────────────────────────────────────────────────┤
│ Infrastructure Adapters │
│ (Can switch: Postgres → MySQL → DynamoDB) │
│ (Can switch: Stripe → Braintree → PayPal) │
└─────────────────────────────────────────────────┘
```
## Signs of Irreversibility
🚩 **Red flags:**
- "We're committed to X vendor"
- "Switching would require a rewrite"
- "It's embedded throughout the codebase"
- "We use proprietary features heavily"
- "Our data format only works with X"
✅ **Good signs:**
- "We could switch in a few weeks"
- "It's behind an interface"
- "We use standard formats"
- "The business logic doesn't know about infrastructure"
## Summary
> "The mistake lies in assuming that any decision is cast in stone—and in not preparing for the contingencies that might arise."
| Do | Don't |
|----|-------|
| Abstract external dependencies | Call vendor APIs directly |
| Use configuration | Hardcode values |
| Design small, replaceable parts | Build monolithic components |
| Use standard formats | Use proprietary formats |
| Write reversible migrations | Make one-way changes |
| Question "best" solutions | Assume today's choice is forever |
---
*Based on concepts from "The Pragmatic Programmer" by David Thomas and Andrew Hunt.*
principles/tracer-bullets.md
# Tracer Bullets
## Definition
> "Tracer bullets let you home in on your target by trying things and seeing how close they land."
> — Dave Thomas & Andy Hunt
In warfare, tracer bullets glow so gunners can see where their shots are going and adjust. In software, tracer bullet development means building a thin end-to-end implementation to get immediate feedback.
## The Concept
Instead of building all components in isolation and integrating later, tracer bullet development:
1. Builds a minimal skeleton through all layers
2. Gets it working end-to-end immediately
3. Adds features incrementally
4. Provides continuous feedback
## Tracer Bullets vs. Traditional
### Traditional Approach
```
Phase 1: Build complete UI [████████████░░░]
Phase 2: Build complete logic [████████████░░░]
Phase 3: Build complete database [████████████░░░]
Phase 4: Integrate (pray) [░░░░░░░░░░░░░░░] ← Big bang!
Risk: Integration problems discovered late
```
### Tracer Bullet Approach
```
Sprint 1: Thin slice through all layers [█░░░░░░░░░░░░░░]
Sprint 2: Add more features [███░░░░░░░░░░░░]
Sprint 3: Add more features [█████░░░░░░░░░░]
Sprint 4: Add more features [███████░░░░░░░░]
Benefit: Working system from day one
```
## Example: Building a Report System
### Traditional (Don't Do This)
```
Month 1: Design all report types, all formats
Month 2: Build report engine (complete)
Month 3: Build all data queries
Month 4: Build all UI templates
Month 5: Integrate and debug
Month 6: Still debugging integration issues...
```
### Tracer Bullet (Do This)
```
Week 1:
- ONE report type (Sales Summary)
- ONE output format (HTML)
- ONE data query (hardcoded SQL)
- ONE simple UI
- Working end-to-end!
Week 2:
- Add PDF output
- Still just Sales Summary
- Working end-to-end!
Week 3:
- Add second report type (Inventory)
- Working end-to-end!
Week 4:
- Add parameterized queries
- Working end-to-end!
```
## Tracer Bullet Code
```pseudocode
// Week 1: Minimal tracer bullet
// Presentation: Simple, hardcoded
class ReportController {
function showReport() {
data = reportService.getSalesSummary()
return htmlTemplate.render(data)
}
}
// Business Logic: Minimal
class ReportService {
function getSalesSummary() {
return repository.querySales()
}
}
// Data: Hardcoded query
class ReportRepository {
function querySales() {
return database.query("SELECT * FROM sales WHERE date > '2024-01-01'")
}
}
// It's not complete, but it WORKS end-to-end!
// We can demo it, get feedback, and iterate.
```
## Benefits
### 1. Users See Something Working
```
Day 1: "Here's a basic report. Is this the right direction?"
User: "Yes, but can we add totals?"
Day 2: "Here's totals. What about grouping?"
User: "Perfect! But we need it in PDF too."
Day 3: "Here's PDF export..."
```
### 2. Developers Have a Structure
New code plugs into existing skeleton:
- New report types extend existing patterns
- New formats fit existing architecture
- Team members can work in parallel
### 3. Integration is Continuous
No "integration phase" because you're always integrated.
### 4. Progress is Visible
```
✓ Basic report works
✓ PDF export works
✓ Date filtering works
□ Multiple report types
□ Scheduled generation
□ Email delivery
```
### 5. You Have Something to Demo
At any point, you have a working (if incomplete) system to show stakeholders.
## Tracer Bullets vs. Prototypes
| Tracer Bullets | Prototypes |
|----------------|------------|
| Code you keep | Code you throw away |
| Production quality | Quick and dirty |
| Lean but complete | May skip layers |
| Evolves into final system | Used to learn, then discarded |
| Real architecture | May be architectural spike |
```pseudocode
// Prototype: Quick, throwaway
function canWeDoThis() {
// Hacky code to answer a question
// Will be rewritten properly
}
// Tracer Bullet: Minimal but production-ready
function createOrder(items) {
// Simple but correct
// Will be extended, not rewritten
order = new Order(items)
repository.save(order)
return order
}
```
## When to Use Tracer Bullets
✅ **Use when:**
- Building new system with uncertain requirements
- Team needs to see progress
- You're unsure how pieces will fit together
- Stakeholders need early feedback
- You want to reduce integration risk
❌ **Don't use when:**
- Requirements are crystal clear and fixed
- You're adding to a well-established system
- The system is trivial
## Implementing Tracer Bullets
### Step 1: Identify the Layers
```
UI → API → Service → Repository → Database
```
### Step 2: Pick ONE Feature
Choose a feature that touches all layers:
- User registration (UI form → API → save to DB → return)
- View single item (DB → API → UI display)
- Simple search (UI input → API → query → results)
### Step 3: Build Thin Slice
```pseudocode
// Minimal UI
function RegistrationForm() {
return <form onSubmit={api.register}>
<input name="email" />
<button>Register</button>
</form>
}
// Minimal API
function register(request) {
email = request.body.email
user = userService.register(email)
return { id: user.id }
}
// Minimal Service
function register(email) {
user = new User(email)
return repository.save(user)
}
// Minimal Repository
function save(user) {
database.insert("users", user)
return user
}
```
### Step 4: Verify End-to-End
Actually run it:
1. Fill out form
2. Click submit
3. Check database
4. See confirmation
### Step 5: Iterate
Add features one at a time, always keeping it working.
## Summary
> "Look for the important requirements, the ones that define the system. Look for the areas where you have doubts, and where you see the biggest risks. Then prioritize your development so that these are the first areas you code."
Tracer bullets give you:
- **Immediate feedback** - Know if you're on target
- **Continuous integration** - Always working together
- **Visible progress** - Something to demo
- **A framework for growth** - Structure to build on
---
*Based on concepts from "The Pragmatic Programmer" by David Thomas and Andrew Hunt.*
SKILL.md
---
name: pragmatic-programmer
description: "Software craftsmanship principles from The Pragmatic Programmer. Use this skill when discussing best practices, debugging strategies, career development, or pragmatic approaches to software development. Auto-activates for DRY violations, debugging sessions, and development workflow improvements."
---
# The Pragmatic Programmer Reference
A comprehensive reference for pragmatic software development principles based on "The Pragmatic Programmer" by David Thomas and Andrew Hunt. This skill provides timeless advice for becoming a better developer.
## When This Skill Activates
This skill automatically activates when you:
- Discuss software development best practices
- Need debugging strategies
- Consider code duplication (DRY)
- Think about tooling and automation
- Discuss project estimation or planning
- Review development workflows
## Core Philosophy
> "Care about your craft."
> "Think about your work."
Pragmatic programmers:
- Take responsibility for their career and work
- Don't make excuses—provide options
- Are agents of change, not victims of circumstance
- Continuously learn and adapt
## Quick Reference
### Foundational Principles
| Principle | Summary |
|-----------|---------|
| [DRY - Don't Repeat Yourself](principles/dry.md) | Every piece of knowledge has a single representation |
| [Orthogonality](principles/orthogonality.md) | Keep things independent and decoupled |
| [Reversibility](principles/reversibility.md) | Make decisions reversible; avoid lock-in |
| [Tracer Bullets](principles/tracer-bullets.md) | Get feedback fast with end-to-end skeleton |
| [Prototypes](principles/prototypes.md) | Learn before committing; throw away prototypes |
| [Domain Languages](principles/domain-languages.md) | Program close to the problem domain |
| [Estimating](principles/estimating.md) | Learn to give accurate estimates |
### Practical Techniques
| Practice | Summary |
|----------|---------|
| [The Power of Plain Text](practices/plain-text.md) | Keep knowledge in accessible format |
| [Shell Games](practices/shell-games.md) | Master the command line |
| [Debugging](practices/debugging.md) | Fix the problem, not the blame |
| [Text Manipulation](practices/text-manipulation.md) | Learn text processing tools |
| [Code Generators](practices/code-generators.md) | Write code that writes code |
| [Design by Contract](practices/design-by-contract.md) | Define rights and responsibilities |
| [Assertive Programming](practices/assertive-programming.md) | If it can't happen, use assertions |
| [Decoupling](practices/decoupling.md) | Minimize dependencies between modules |
| [Refactoring](practices/refactoring.md) | Improve code continuously |
| [Testing](practices/testing.md) | Test early, test often, test automatically |
| [Automation](practices/automation.md) | Don't use manual procedures |
## The Pragmatic Tips
Key tips from the book:
1. **Care About Your Craft** - Why spend your life developing software unless you care?
2. **Think! About Your Work** - Turn off autopilot and take control
3. **Provide Options, Don't Make Excuses** - Don't say it can't be done; explain what can be done
4. **Don't Live with Broken Windows** - Fix bad designs and wrong decisions when you see them
5. **Be a Catalyst for Change** - Show people the future and help them participate
6. **Remember the Big Picture** - Don't get so focused you forget what you're doing
7. **Make Quality a Requirements Issue** - Get users involved in determining quality
8. **Invest Regularly in Your Knowledge Portfolio** - Make learning a habit
9. **Critically Analyze What You Read and Hear** - Don't be swayed by vendors or media hype
10. **It's Both What You Say and How You Say It** - Communication matters
## The Knowledge Portfolio
Treat your knowledge like a financial portfolio:
- **Invest regularly** - Learn something new routinely
- **Diversify** - Know many different technologies
- **Manage risk** - Balance safe tech with high-risk/high-reward
- **Buy low, sell high** - Learn emerging tech before it becomes mainstream
- **Review and rebalance** - Reassess periodically
### Suggestions:
- Learn a new language every year
- Read a technical book each month
- Read non-technical books too
- Take classes
- Participate in local user groups
- Experiment with different environments
- Stay current (newsletters, blogs, conferences)
## Communication
- Know what you want to say
- Know your audience
- Choose the right moment
- Choose a style
- Make it look good
- Involve your audience
- Be a listener
- Get back to people
- Documentation is part of the project, not after
## Language Translation Notes
Examples use generic pseudocode. Adapt to your language:
- **PHP**: `class`, `function`, `->`, type hints
- **JavaScript/TypeScript**: `class`, arrow functions, `.`
- **Python**: `class`, `def`, `.`, type hints
- **Java/C#**: Direct mapping with access modifiers
---
*Based on concepts from "The Pragmatic Programmer: Your Journey to Mastery" by David Thomas and Andrew Hunt.*