11---
22name : design-patterns
3- description : Reference skill for GoF design patterns, SOLID, Object Calisthenics, Python Zen, and other SE principles — with smell triggers and Python before/after examples
4- version : " 1 .0"
3+ description : GoF design pattern catalogue — smell triggers and Python before/after examples
4+ version : " 2 .0"
55author : software-engineer
66audience : software-engineer
77workflow : feature-lifecycle
88---
99
1010# Design Patterns Reference
1111
12- Load this skill when:
13- - Running the architecture smell check in Step 2 and a smell is detected
14- - Refactoring in Step 3 and a pattern smell appears in the self-declaration
12+ Load this skill when the refactor skill's smell table points to a GoF pattern and you need the Python before/after example.
13+
14+ Sources: Gamma, Helm, Johnson, Vlissides. * Design Patterns: Elements of Reusable Object-Oriented Software * . Addison-Wesley, 1995. See ` docs/scientific-research/oop-design.md ` entry 34.
1515
1616---
1717
1818## How to Use This Skill
1919
20- 1 . ** Identify the smell** from the checklist in your self-declaration or architecture check
20+ 1 . ** Identify the smell** from the refactor skill's lookup table
21212 . ** Find the smell category** below (Creational / Structural / Behavioral)
22223 . ** Read the trigger and the before/after example**
23- 4 . ** Apply the pattern** and update the Architecture section (Step 2) or the refactored code (Step 3)
23+ 4 . ** Apply the pattern** — update the stub files (Step 2) or the refactored code (Step 3)
2424
2525---
2626
@@ -178,7 +178,7 @@ def apply_discount(order: Order, strategy: DiscountStrategy) -> Money:
178178#### Smell: Feature Envy
179179** Signal** : A method in class A uses data from class B more than its own data. The method "envies" class B.
180180
181- ** Pattern** : Move Method to the envied class (not a GoF pattern — a Fowler refactoring that often precedes Strategy or Command)
181+ ** Pattern** : Move Method to the envied class (Fowler refactoring that often precedes Strategy or Command)
182182
183183``` python
184184# BEFORE — OrderPrinter knows too much about Order internals
@@ -309,9 +309,9 @@ class Order:
309309class Order :
310310 def confirm (self ) -> None :
311311 self .status = " confirmed"
312- EmailService().send_confirmation(self ) # direct coupling
313- InventoryService().reserve(self ) # direct coupling
314- AnalyticsService().record_conversion(self ) # direct coupling
312+ EmailService().send_confirmation(self ) # direct coupling
313+ InventoryService().reserve(self ) # direct coupling
314+ AnalyticsService().record_conversion(self ) # direct coupling
315315```
316316
317317``` python
@@ -380,182 +380,6 @@ class JsonImporter(Importer):
380380
381381---
382382
383- ## SOLID — Python Examples
384-
385- ### S — Single Responsibility
386- One class, one reason to change.
387-
388- ``` python
389- # WRONG — Report handles both data and formatting
390- class Report :
391- def generate (self ) -> dict : ...
392- def to_pdf (self ) -> bytes : ... # separate concern
393- def to_csv (self ) -> str : ... # separate concern
394-
395- # RIGHT — split concerns
396- class Report :
397- def generate (self ) -> ReportData: ...
398-
399- class PdfRenderer :
400- def render (self , data : ReportData) -> bytes : ...
401- ```
402-
403- ### O — Open/Closed
404- Open for extension, closed for modification.
405-
406- ``` python
407- # WRONG — must edit this function to add a new format
408- def export (data : ReportData, fmt : str ) -> bytes :
409- if fmt == " pdf" : ...
410- elif fmt == " csv" : ...
411-
412- # RIGHT — new formats extend without touching existing code
413- class Exporter (Protocol ):
414- def export (self , data : ReportData) -> bytes : ...
415-
416- class PdfExporter :
417- def export (self , data : ReportData) -> bytes : ...
418- ```
419-
420- ### L — Liskov Substitution
421- Subtypes must be fully substitutable for their base type.
422-
423- ``` python
424- # WRONG — ReadOnlyFile violates the contract of File
425- class File :
426- def write (self , content : str ) -> None : ...
427-
428- class ReadOnlyFile (File ):
429- def write (self , content : str ) -> None :
430- raise PermissionError # narrows the contract — LSP violation
431-
432- # RIGHT — separate interfaces for readable and writable
433- class ReadableFile (Protocol ):
434- def read (self ) -> str : ...
435-
436- class WritableFile (Protocol ):
437- def write (self , content : str ) -> None : ...
438- ```
439-
440- ### I — Interface Segregation
441- No implementor should be forced to implement methods it doesn't use.
442-
443- ``` python
444- # WRONG — Printer is forced to implement scan() and fax()
445- class Machine (Protocol ):
446- def print (self , doc : Document) -> None : ...
447- def scan (self , doc : Document) -> None : ...
448- def fax (self , doc : Document) -> None : ...
449-
450- # RIGHT — each capability is its own Protocol
451- class Printer (Protocol ):
452- def print (self , doc : Document) -> None : ...
453-
454- class Scanner (Protocol ):
455- def scan (self , doc : Document) -> None : ...
456- ```
457-
458- ### D — Dependency Inversion
459- Domain depends on abstractions (Protocols), not on concrete I/O or frameworks.
460-
461- ``` python
462- # WRONG — domain imports infrastructure directly
463- from app.db import PostgresConnection
464-
465- class OrderRepository :
466- def __init__ (self ) -> None :
467- self .db = PostgresConnection() # domain imports infra
468-
469- # RIGHT — domain defines the Protocol; infra implements it
470- class OrderRepository (Protocol ):
471- def find (self , order_id : OrderId) -> Order: ...
472- def save (self , order : Order) -> None : ...
473-
474- class PostgresOrderRepository : # in adapters/
475- def find (self , order_id : OrderId) -> Order: ...
476- def save (self , order : Order) -> None : ...
477- ```
478-
479- ---
480-
481- ## Object Calisthenics — Python Rules
482-
483- Jeff Bay's 9 rules for object-oriented discipline. Each has a Python signal.
484-
485- | Rule | Constraint | Python Signal of Violation |
486- | ---| ---| ---|
487- | ** OC-1** | One indent level per method | ` for ` inside ` if ` inside a method body |
488- | ** OC-2** | No ` else ` after ` return ` | ` if cond: return x \n else: return y ` |
489- | ** OC-3** | Wrap all primitives that have domain meaning | ` def process(user_id: int) ` instead of ` def process(user_id: UserId) ` |
490- | ** OC-4** | Wrap all collections that have domain meaning | ` list[Order] ` passed around instead of ` OrderCollection ` |
491- | ** OC-5** | One dot per line | ` obj.repo.find(id).name ` |
492- | ** OC-6** | No abbreviations | ` usr ` , ` mgr ` , ` cfg ` , ` val ` , ` tmp ` |
493- | ** OC-7** | Keep classes small (≤50 lines) and methods short (≤20 lines) | Any method requiring scrolling |
494- | ** OC-8** | No class with more than 2 instance variables | ` __init__ ` with 3+ ` self.x = ` assignments |
495- | ** OC-9** | No getters/setters | ` def get_name(self) ` / ` def set_name(self, v) ` |
496-
497- ---
498-
499- ## Python Zen — Mapped to Code Practices
500-
501- The relevant items from PEP 20 (` import this ` ) with concrete code implications:
502-
503- | Zen Item | Code Practice |
504- | ---| ---|
505- | Beautiful is better than ugly | Name things clearly; prefer named types over bare primitives |
506- | Explicit is better than implicit | Explicit return types; explicit Protocol dependencies; no magic |
507- | Simple is better than complex | KISS — one function, one job; prefer a plain function over a class |
508- | Complex is better than complicated | A well-designed abstraction is acceptable; an accidental tangle is not |
509- | Flat is better than nested | OC-1 — one indent level; early returns |
510- | Sparse is better than dense | One statement per line; no semicolons; no lambda chains |
511- | Readability counts | OC-6 — no abbreviations; docstrings on every public function |
512- | Special cases aren't special enough to break the rules | Do not add ` if isinstance ` branches to avoid refactoring |
513- | Errors should never pass silently | No bare ` except: ` ; no ` except Exception: pass ` |
514- | In the face of ambiguity, refuse the temptation to guess | Raise on invalid input; never silently return a default |
515- | There should be one obvious way to do it | DRY — every shared concept in exactly one place |
516- | If the implementation is hard to explain, it's a bad idea | KISS — if you can't describe the function in one sentence, split it |
517-
518- ---
519-
520- ## Other Principles
521-
522- ### Law of Demeter (Tell, Don't Ask)
523- A method should only call methods on:
524- - ` self `
525- - Objects passed as parameters
526- - Objects it creates
527- - Direct component objects (` self.x ` )
528-
529- ** Violation signal** : ` a.b.c() ` — two dots. Assign ` b = a.b ` and call ` b.c() ` , or better: ask ` a ` to do what you need (` a.do_thing() ` ).
530-
531- ### Command-Query Separation (CQS)
532- A method either ** changes state** (command) or ** returns a value** (query) — never both.
533-
534- ``` python
535- # WRONG — pop() both returns and mutates
536- value = stack.pop()
537-
538- # RIGHT (CQS strict)
539- value = stack.peek() # query — no mutation
540- stack.remove_top() # command — no return value
541- ```
542-
543- Note: Python's standard library violates CQS in places (` list.pop() ` , ` dict.update() ` ). Apply CQS to your domain objects; do not fight the stdlib.
544-
545- ### Tell, Don't Ask
546- Instead of querying an object's state and acting on it externally, tell the object to do the work itself.
547-
548- ``` python
549- # WRONG — ask state, decide externally
550- if order.status == OrderStatus.PENDING :
551- order.status = OrderStatus.CONFIRMED
552-
553- # RIGHT — tell the object
554- order.confirm() # Order decides if the transition is valid
555- ```
556-
557- ---
558-
559383## Quick Smell → Pattern Lookup
560384
561385| Smell | Pattern |
@@ -569,4 +393,3 @@ order.confirm() # Order decides if the transition is valid
569393| Class directly calls B, C, D on state change | Observer |
570394| Two functions share the same skeleton, differ in one step | Template Method |
571395| Subsystem is complex and callers need a simple entry point | Facade |
572- | Object needs logging/caching without changing its class | Decorator / Proxy |
0 commit comments