Class interfaces present one cohesive abstraction - don't mix domain logic with serialization, persistence, or unrelated concerns
A class interface should present ONE cohesive abstraction. All methods should work toward a consistent purpose at a consistent level.
Core principle: Each class implements one Abstract Data Type (ADT). If you can't identify what ADT the class implements, it has poor abstraction.
Goal: Anyone using the class should see a clear, consistent set of related operations, not a miscellaneous grab-bag.
Apply when designing any class:
Warning signs of poor abstraction:
Baseline violation:
class Employee:
def calculate_annual_salary(self): # ā
Domain operation
return self.salary * 12
def update_department(self, dept): # ā
Domain operation
self.department = dept
def to_json(self): # ā Serialization detail
return json.dumps({...})
def get_details(self): # ā
Domain operation
return {...}
Problem: Employee is a domain concept. JSON is a serialization format. Mixing these means:
ā Separate concerns:
class Employee:
"""Domain: Employee business logic only."""
def __init__(self, name, employee_id, department, salary):
self.name = name
self.employee_id = employee_id
self.department = department
self.salary = salary
def calculate_annual_salary(self): # Domain
return self.salary * 12
def update_department(self, dept): # Domain
self.department = dept
# Separate serializer
class EmployeeSerializer:
"""Concern: Serialization formats."""
@staticmethod
def to_json(employee: Employee) -> str:
return json.dumps({
'name': employee.name,
'id': employee.employee_id,
...
})
@staticmethod
def to_xml(employee: Employee) -> str:
# Can add XML without touching Employee
pass
Now: Employee knows nothing about formats. Add CSV/XML/Protobuf without changing Employee.
Baseline violation:
class Program:
"""Initialize application components."""
def _init_database(self): # Database concern
pass
def _setup_web_server(self): # Web concern
pass
def _start_background_jobs(self): # Jobs concern
pass
def _init_command_stack(self): # Command concern
pass
def _init_report_formatter(self): # Reports concern
pass
Problem: These are unrelated functions grouped because they happen at startup (temporal cohesion). The class has no consistent abstraction - it's a miscellaneous collection.
Code Complete specifically calls this out as poor abstraction.
ā Each subsystem initializes itself:
class DatabaseSystem:
"""Abstraction: Database operations."""
def initialize(self):
# Database-specific initialization
pass
class WebServer:
"""Abstraction: Web serving."""
def start(self):
# Web server initialization
pass
class BackgroundJobManager:
"""Abstraction: Job processing."""
def start(self):
# Job system initialization
pass
# Coordinator stays high-level
class Application:
def __init__(self):
self.database = DatabaseSystem()
self.web_server = WebServer()
self.jobs = BackgroundJobManager()
def start(self):
# High-level orchestration
self.database.initialize()
self.web_server.start()
self.jobs.start()
Now: Each class has consistent abstraction. Program no longer a grab-bag.
Baseline violation:
class DataProcessor:
"""Mixes data access with statistics."""
def process_dataset(self, dataset_id):
# Loads from PostgreSQL (persistence concern)
values = self._load_dataset(dataset_id)
# Calculates statistics (business logic concern)
mean = statistics.mean(values)
# Two concerns in one class
Problem: Class does two things - data access AND statistics. If you switch from PostgreSQL to MongoDB, you must modify this class. If you change statistical algorithm, you modify same class.
ā Separate concerns:
class DatasetRepository:
"""Abstraction: Dataset storage/retrieval."""
def get_dataset_values(self, dataset_id: str) -> list[float]:
# PostgreSQL details hidden here
# Can switch to MongoDB without affecting calculator
pass
class StatisticsCalculator:
"""Abstraction: Statistical computations."""
def calculate_metrics(self, values: list[float]) -> dict:
# Pure calculation, no database knowledge
mean = statistics.mean(values)
median = statistics.median(values)
# Returns statistics only
pass
class DataProcessor:
"""Abstraction: Orchestration."""
def __init__(self, repository, calculator):
self._repository = repository
self._calculator = calculator
def process_dataset(self, dataset_id: str) -> dict:
# High-level only - delegates to focused abstractions
values = self._repository.get_dataset_values(dataset_id)
return self._calculator.calculate_metrics(values)
Now: Each class has single, consistent abstraction. Database changes don't affect calculator. Algorithm changes don't affect repository.
For any class, ask:
What abstraction does this class represent?
Are all methods at same abstraction level?
Do methods belong together?
If answers reveal inconsistency ā poor abstraction.
Unrelated things in one class:
class Utilities:
def validate_email(self): # Validation
def format_currency(self): # Formatting
def connect_database(self): # Database
No relationship. Avoid this.
Things done at same time:
class Startup:
def init_database(self): # Done at startup
def init_webserver(self): # Done at startup
def init_logging(self): # Done at startup
Related by WHEN not WHAT. Weak abstraction.
One clear purpose:
class EmployeeCalculations:
def calculate_annual_salary(self, employee):
def calculate_tax_withholding(self, employee):
def calculate_benefits_cost(self, employee):
All methods work toward one purpose. Best abstraction.
Always aim for functional cohesion.
| Violation | Example | Fix |
|---|---|---|
| Domain + Format mixed | Employee.to_json() |
Separate Serializer class |
| Business + Persistence mixed | OrderProcessor with SQL |
Separate Repository from logic |
| High + Low level mixed | Orchestration with SQL queries | Extract low-level to private/separate |
| Grab-bag class | Utilities, Program, Helpers |
Split by actual purpose |
| Temporal cohesion | Startup class with unrelated inits | Each subsystem owns initialization |
class Employee:
"""Abstraction: Employee domain model."""
# All methods are employee operations
def calculate_annual_salary(self):
def update_department(self, dept):
def get_compensation_summary(self):
def is_eligible_for_bonus(self):
Consistent: All domain operations about employees.
class Employee:
"""Mixed abstraction - unclear purpose."""
def calculate_annual_salary(self): # Domain
def to_json(self): # Serialization
def save_to_database(self): # Persistence
def send_welcome_email(self): # Notification
Inconsistent: Domain + serialization + persistence + notifications.
Split when:
Example split:
# Before: One class, mixed abstractions
class UserService:
def create_user(self, email, name):
# Validation
if not self._is_valid_email(email):
raise ValueError()
# Create domain object
user = User(email, name)
# Persist to database
self._db.execute("INSERT INTO users ...")
# Send welcome email
self._smtp.send(email, "Welcome!")
return user
# After: Focused classes
class UserValidator:
"""Abstraction: Validation."""
def validate_registration(self, email, name):
pass
class UserRepository:
"""Abstraction: Persistence."""
def save_user(self, user):
pass
class UserNotifier:
"""Abstraction: Notifications."""
def send_welcome_email(self, user):
pass
class UserService:
"""Abstraction: Orchestration."""
def create_user(self, email, name):
self._validator.validate_registration(email, name)
user = User(email, name)
self._repository.save_user(user)
self._notifier.send_welcome_email(user)
return user
Class level:
Manager, Handler, Processor, Utility)Method level:
to_json)All of these mean: Improve abstraction consistency.
From baseline testing:
| Excuse | Reality |
|---|---|
| "Keeps everything in one place" | One place ā good organization. Split by purpose, not location. |
| "It's just a coordinator" | Coordinators coordinate related things. Unrelated = grab-bag. |
| "Easier than multiple classes" | Easier to write ā easier to maintain. Abstraction quality matters. |
| "Production-ready code" | Working ā well-abstracted. Can be both. |
| "All used during startup" | Temporal relationship is weak. Use functional relationships. |
| "Serialization is part of the object" | No - serialization is a separate concern. External responsibility. |
For each class, verify:
If any "no" ā split class or clarify abstraction.
From Code Complete:
From baseline testing:
With this skill: Separate concerns, maintain abstraction consistency, functional cohesion.
For single responsibility: See skills/coding/keeping-routines-focused - same principle applies to classes (one clear purpose)
For encapsulation: See skills/encapsulating-complexity - hiding implementation details supports abstraction consistency
For complexity: See skills/reducing-complexity - consistent abstractions reduce mental load