| name | Encapsulating Complexity |
| description | Hide implementation details behind interfaces - work at domain level (what), not implementation level (how) |
| when_to_use | When designing any class or interface. When implementation details leak into public API. When storage format (JSON, SQL, files) is exposed. When working with raw data structures (dicts, rows) instead of domain objects. When client code must know HOW things work internally. When changing implementation would break client code. When database queries mixed with business logic. When switching storage type requires interface changes. When tests must know internal structure. |
| version | 1.0.0 |
| languages | all |
Encapsulating Complexity
Overview
Hide HOW things work. Expose only WHAT they do. Work at domain level (Users, Orders, Config), not implementation level (dicts, SQL rows, JSON files).
Core principle: The point of encapsulation is to create possibilities (many ways to implement) and restrict possibilities (one way to use). Implementation details hidden = free to change implementation without breaking clients.
Violating the letter of this rule is violating the spirit of information hiding.
When to Use
Apply to every class and interface:
- Designing new classes/modules
- Creating public APIs
- Reviewing code for abstraction leaks
- Refactoring to improve maintainability
Warning signs you're violating encapsulation:
- Client code knows storage format (JSON, SQL, files)
- Interface exposes database/file operations
- Returns raw dicts/rows instead of domain objects
- Client code constructs SQL queries or file paths
- Changing from JSON to YAML breaks client code
- Switching databases requires changing all callers
- Tests must know internal data structures
- Method names reveal implementation (saveToJSON, queryDatabase)
- Public fields exposing internal state
The Encapsulation Test
Ask these questions about every public method/field:
-
Does the interface expose HOW or WHAT?
- "Get user" = WHAT (good)
- "SELECT * FROM users" = HOW (bad)
-
Can I change implementation without breaking clients?
- JSON → YAML: should work
- PostgreSQL → MongoDB: should work
- If clients break: encapsulation violated
-
Do clients work at domain level or implementation level?
- Domain:
user.email, config.get_timeout()
- Implementation:
row[2], json_data['timeout_ms']
-
Do return values expose internals?
- Domain:
User object
- Implementation:
dict with database column names
If answers reveal implementation details → encapsulation violated.
Core Pattern: Hide the How
Before (Implementation Exposed)
class ConfigManager:
def __init__(self, json_path):
self.json_path = json_path
self._data = {}
def get_value(self, key):
return self._data.get(key)
def save_to_json(self):
with open(self.json_path, 'w') as f:
json.dump(self._data, f)
Client code:
config = ConfigManager("/path/to/config.json")
timeout = config.get_value("timeout_ms")
config.save_to_json()
If you switch JSON → YAML: Client code breaks. Method names wrong. Constructor signature wrong.
After (Implementation Hidden)
class Config:
def __init__(self, config_file):
self._storage = self._load(config_file)
def get_timeout(self):
return self._storage.get("timeout_ms", 5000)
def set_timeout(self, seconds):
self._storage["timeout_ms"] = seconds * 1000
def save(self):
self._persist(self._storage)
def _load(self, config_file):
pass
def _persist(self, data):
pass
Client code:
config = Config("app.config")
timeout = config.get_timeout()
config.set_timeout(10)
config.save()
Switch JSON → YAML: Client code unchanged. Just change _load() and _persist().
Domain Level vs Implementation Level
Work at the problem domain, not the solution domain:
Example 1: User Management
❌ Implementation Level:
class UserManager:
def get_user_row(self, user_id):
cur.execute("SELECT * FROM users WHERE id = %s", (user_id,))
return dict(cur.fetchone())
def update_user_column(self, user_id, column, value):
cur.execute(f"UPDATE users SET {column} = %s WHERE id = %s", ...)
Client must know:
- Database schema (column names)
- SQL concepts (rows, columns)
- Data types in database
✅ Domain Level:
class UserRepository:
def get_user(self, user_id: str) -> User:
row = self._query_user(user_id)
return User.from_storage(row)
def update_email(self, user_id: str, new_email: str) -> None:
user = self.get_user(user_id)
user.email = new_email
self._persist_user(user)
class User:
def __init__(self, id, email, name):
self.id = id
self.email = email
self.name = name
Client knows:
- Users (domain concept)
- Email (domain field)
- Nothing about database
Example 2: Configuration
❌ Implementation Level:
class ConfigManager:
def get(self, key):
return self._config.get(key)
timeout = config.get("server.timeout_milliseconds")
port = config.get("server.port")
✅ Domain Level:
class Config:
def server_timeout(self):
return self._get("server.timeout_ms", 5000) / 1000
def server_port(self):
return self._get("server.port", 8080)
timeout = config.server_timeout()
port = config.server_port()
Separation of Concerns
Don't mix persistence with domain logic:
❌ Mixed (From Baseline Test):
class ReportGenerator:
def generate(self, start_date, end_date):
conn = psycopg2.connect(...)
cursor.execute("SELECT ...")
rows = cursor.fetchall()
total = sum(row[3] for row in rows)
html = f"<html>...</html>"
with open(f"report_{start_date}.html", "w") as f:
f.write(html)
4 concerns mixed: database, calculation, formatting, file I/O
✅ Separated:
class SalesRepository:
"""Concern: Data access"""
def get_sales(self, start_date, end_date) -> List[Sale]:
pass
class SalesCalculator:
"""Concern: Business logic"""
def calculate_metrics(self, sales: List[Sale]) -> SalesMetrics:
pass
class HTMLFormatter:
"""Concern: Presentation"""
def format_report(self, metrics: SalesMetrics) -> str:
pass
class ReportGenerator:
"""Concern: Orchestration"""
def __init__(self, repo, calculator, formatter):
self._repo = repo
self._calculator = calculator
self._formatter = formatter
def generate(self, start_date, end_date, output_path):
sales = self._repo.get_sales(start_date, end_date)
metrics = self._calculator.calculate_metrics(sales)
html = self._formatter.format_report(metrics)
with open(output_path, "w") as f:
f.write(html)
Each class can change independently. Database switch doesn't touch formatting. HTML→PDF doesn't touch database.
What to Hide
Hide Storage Format
❌ Exposed:
class DataStore:
def load_from_json(self):
pass
def save_to_json(self):
pass
✅ Hidden:
class DataStore:
def load(self):
self._read_storage()
def save(self):
self._write_storage()
Hide Database Details
❌ Exposed:
def get_users(self):
cur.execute("SELECT * FROM users")
return [dict(row) for row in cur.fetchall()]
✅ Hidden:
def get_users(self) -> List[User]:
rows = self._query_all_users()
return [User.from_row(row) for row in rows]
Hide Data Structures
❌ Exposed:
class UserCache:
def __init__(self):
self.users_dict = {}
cache.users_dict[user_id] = user_data
✅ Hidden:
class UserCache:
def __init__(self):
self._storage = {}
def add_user(self, user):
self._storage[user.id] = user
def get_user(self, user_id):
return self._storage.get(user_id)
Internal dict can become Redis, Memcached, or database without breaking clients.
Hide Algorithms and Complexity
❌ Exposed:
def sort_users_by_name(users):
return quicksort(users, key=lambda u: u.name)
✅ Hidden:
def get_users_sorted_by_name(users):
return sorted(users, key=lambda u: u.name)
Interface Design Principles
1. Name by What, Not How
❌ How (implementation exposed):
save_to_json(), load_from_yaml(), write_to_file()
execute_sql_query(), get_database_connection()
parse_json_response(), build_xml_request()
✅ What (implementation hidden):
save(), load(), persist()
get_users(), find_by_email()
send_request(), get_response()
2. Accept and Return Domain Objects
❌ Raw data structures:
def create_user(self, user_dict: dict) -> dict:
✅ Domain objects:
def create_user(self, email: str, name: str) -> User:
3. Work at Single Abstraction Level
❌ Mixed levels:
def process_order(self, order):
validate_order(order)
conn.execute("INSERT INTO orders VALUES (%s, %s)", ...)
send_confirmation(order)
✅ Consistent level:
def process_order(self, order):
validate_order(order)
saved_order = self._repository.save(order)
send_confirmation(saved_order)
Common Violations from Baseline Testing
Violation 1: Exposing Storage Format
Baseline: ConfigManager exposes JSON in interface, UserManager exposes SQL.
❌ What agents naturally do:
class ConfigManager:
def __init__(self, json_path):
self.json_path = json_path
def save(self):
with open(self.json_path, 'w') as f:
json.dump(self._config, f)
✅ Hide format:
class Config:
def __init__(self, config_file):
self._loader = self._create_loader(config_file)
self._data = self._loader.load()
def save(self):
self._loader.persist(self._data)
def _create_loader(self, config_file):
if config_file.endswith('.yaml'):
return YAMLLoader(config_file)
return JSONLoader(config_file)
Violation 2: Working at Implementation Level
Baseline: UserManager returns dicts, exposes column names.
❌ What agents naturally do:
def get_user(self, user_id):
cur.execute("SELECT * FROM users WHERE id = %s", (user_id,))
return dict(cur.fetchone())
user_dict = manager.get_user(123)
email = user_dict['email']
✅ Work at domain level:
def get_user(self, user_id: str) -> User:
row = self._fetch_user_row(user_id)
return User(
id=row['id'],
email=row['email'],
name=row['name']
)
user = repo.get_user("123")
email = user.email
Violation 3: Mixed Concerns
Baseline: ReportGenerator mixes database, calculation, formatting, file I/O in one method.
❌ What agents naturally do:
def generate_report(self):
conn = psycopg2.connect(...)
rows = cursor.fetchall()
total = sum(row[3] for row in rows)
html = f"<html>...</html>"
with open("report.html", "w") as f:
f.write(html)
✅ Separate and encapsulate:
class ReportGenerator:
def __init__(self, repo, calculator, formatter, file_writer):
self._repo = repo
self._calculator = calculator
self._formatter = formatter
self._writer = file_writer
def generate(self, start_date, end_date):
sales = self._repo.get_sales(start_date, end_date)
metrics = self._calculator.calculate(sales)
content = self._formatter.format(metrics)
return self._writer.write(content)
Quick Reference
| Encapsulation Target | What to Hide | What to Expose |
|---|
| Storage format | JSON/YAML/SQL/files | load(), save() |
| Database | SQL queries, connection, schema | Domain operations (get_user()) |
| Data structures | Dict/list/tree internal structure | Domain methods |
| Algorithms | Sorting/searching implementation | High-level operation |
| File paths | Internal directory structure | Logical identifiers |
| External APIs | HTTP/gRPC/REST details | Domain operations |
| Complex state | State machine internals | Simple operations |
Techniques for Encapsulation
Technique 1: Private Implementation Methods
class UserRepository:
def find_by_email(self, email: str) -> Optional[User]:
row = self._query_by_email(email)
return User.from_row(row) if row else None
def _query_by_email(self, email: str):
pass
Technique 2: Adapter/Wrapper Pattern
class EmailService:
def __init__(self):
self._smtp_client = smtplib.SMTP(...)
self._templates = self._load_templates()
def send_welcome_email(self, user: User):
template = self._templates['welcome']
message = template.format(name=user.name)
self._smtp_client.send(user.email, message)
Technique 3: Abstract Data Types
class Font:
def set_bold(self):
self._attributes |= self.BOLD_FLAG
def is_bold(self):
return bool(self._attributes & self.BOLD_FLAG)
Client code: font.set_bold() not font.attr = font.attr | 0x02
Technique 4: Facade for Complex Subsystems
class OrderProcessor:
"""Facade hiding inventory, payment, shipping complexity."""
def __init__(self):
self._inventory = InventorySystem()
self._payment = PaymentGateway()
self._shipping = ShippingService()
def place_order(self, order: Order) -> OrderResult:
self._inventory.reserve(order.items)
self._payment.charge(order.payment_info)
self._shipping.create_shipment(order.address)
return OrderResult(success=True, order_id=order.id)
Layering and Stratification
Create layers where each hides complexity of layer below:
┌─────────────────────────────┐
│ Application Layer │ Works with: Users, Orders (domain)
├─────────────────────────────┤
│ Domain Layer │ Works with: Entities, Value Objects
├─────────────────────────────┤
│ Persistence Layer │ Works with: Rows, JSON (hidden)
├─────────────────────────────┤
│ Database/Storage │ SQL, files (fully hidden)
└─────────────────────────────┘
Each layer:
- Encapsulates complexity of layers below
- Presents abstraction to layers above
- Can be changed without affecting other layers
Benefits of Encapsulation
1. Change Implementation Freely
2. Simpler Client Code
config_data = json.load(open("config.json"))
timeout = config_data.get("server", {}).get("timeout_ms", 5000) / 1000
timeout = config.server_timeout()
3. Easier Testing
mock_repo = Mock(UserRepository)
mock_repo.get_user.return_value = User("1", "test@test.com", "Test")
4. Prevents Ripple Effects
Common Mistakes
❌ Exposing internal structure:
class ShoppingCart:
def __init__(self):
self.items = []
✅ Hide structure:
class ShoppingCart:
def __init__(self):
self._items = []
def add_item(self, item):
self._items.append(item)
def get_items(self):
return list(self._items)
❌ Returning mutable internal state:
def get_config_dict(self):
return self._config
✅ Return copies or immutable:
def get_config_dict(self):
return dict(self._config)
❌ Method names revealing implementation:
get_json_data(), save_to_database(), execute_sql(), write_file()
✅ Method names revealing purpose:
get_data(), save(), persist(), execute(), write()
Red Flags - Improve Encapsulation
Interface design:
- Method names mention implementation (JSON, SQL, HTTP, file)
- Returns raw dicts/rows/JSON instead of domain objects
- Accepts raw data instead of domain parameters
- Public fields exposing internal state
- Client code must know storage format
Implementation:
- Database queries in business logic classes
- File I/O mixed with calculations
- Multiple concerns in one class
- Working with rows/dicts instead of domain objects
- Switching implementation would break clients
All of these mean: Improve encapsulation.
Common Rationalizations
From baseline testing, agents justify poor encapsulation with:
| Excuse | Reality |
|---|
| "Client needs to know the structure" | No, client needs domain operations. Hide structure. |
| "Returning dict is simpler than creating class" | Simple to write ≠ simple to maintain. Domain objects prevent errors. |
| "Just a thin wrapper, not worth it" | Wrappers enable change. Worth it. |
| "Everything's in one place, easier to find" | Easier to find ≠ easier to change. Separation enables modification. |
| "It works, no need to abstract" | Working now ≠ maintainable later. Abstract anyway. |
| "YAGNI - we won't change database" | Can't predict future. Encapsulation is cheap insurance. |
| "Too much boilerplate" | Boilerplate prevents ripple effects. Trade-off worth it. |
Verification Checklist
Before marking class/interface design complete:
If any "no" → improve encapsulation.
Real-World Impact
From Code Complete:
- Information hiding is fundamental design heuristic
- Classes should hide their implementation behind interfaces
- Work at problem domain level, not solution domain level
- Benefits: easier to modify, reuse, and understand
From baseline testing:
- Agents naturally use private variables (
_config)
- BUT expose storage format (JSON, SQL)
- Work at implementation level (dicts, rows) not domain (objects)
- Mix concerns (database + calculation + formatting)
- Don't create domain objects - return raw data
With this skill: Hide implementation, work at domain level, separate concerns.
Integration with Other Skills
For keeping interfaces focused: See skills/coding/keeping-routines-focused - single responsibility applies to classes too
For reducing complexity: See skills/reducing-complexity - encapsulation reduces complexity by hiding details