@schilling.mark.a/software-methodology 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/copilot-instructions.md +106 -0
- package/LICENSE +21 -0
- package/README.md +174 -0
- package/atdd-workflow/SKILL.md +117 -0
- package/atdd-workflow/references/green-phase.md +38 -0
- package/atdd-workflow/references/red-phase.md +62 -0
- package/atdd-workflow/references/refactor-phase.md +75 -0
- package/bdd-specification/SKILL.md +88 -0
- package/bdd-specification/references/example-mapping.md +105 -0
- package/bdd-specification/references/gherkin-patterns.md +214 -0
- package/cicd-pipeline/SKILL.md +64 -0
- package/cicd-pipeline/references/deployment-rollback.md +176 -0
- package/cicd-pipeline/references/environment-promotion.md +159 -0
- package/cicd-pipeline/references/pipeline-stages.md +198 -0
- package/clean-code/SKILL.md +77 -0
- package/clean-code/references/behavioral-patterns.md +329 -0
- package/clean-code/references/creational-patterns.md +197 -0
- package/clean-code/references/enterprise-patterns.md +334 -0
- package/clean-code/references/solid.md +230 -0
- package/clean-code/references/structural-patterns.md +238 -0
- package/continuous-improvement/SKILL.md +69 -0
- package/continuous-improvement/references/measurement.md +133 -0
- package/continuous-improvement/references/process-update.md +118 -0
- package/continuous-improvement/references/root-cause-analysis.md +144 -0
- package/dist/atdd-workflow.skill +0 -0
- package/dist/bdd-specification.skill +0 -0
- package/dist/cicd-pipeline.skill +0 -0
- package/dist/clean-code.skill +0 -0
- package/dist/continuous-improvement.skill +0 -0
- package/dist/green-implementation.skill +0 -0
- package/dist/product-strategy.skill +0 -0
- package/dist/story-mapping.skill +0 -0
- package/dist/ui-design-system.skill +0 -0
- package/dist/ui-design-workflow.skill +0 -0
- package/dist/ux-design.skill +0 -0
- package/dist/ux-research.skill +0 -0
- package/docs/INTEGRATION.md +229 -0
- package/docs/QUICKSTART.md +126 -0
- package/docs/SHARING.md +828 -0
- package/docs/SKILLS.md +296 -0
- package/green-implementation/SKILL.md +155 -0
- package/green-implementation/references/angular-patterns.md +239 -0
- package/green-implementation/references/common-rejections.md +180 -0
- package/green-implementation/references/playwright-patterns.md +321 -0
- package/green-implementation/references/rxjs-patterns.md +161 -0
- package/package.json +57 -0
- package/product-strategy/SKILL.md +71 -0
- package/product-strategy/references/business-model-canvas.md +199 -0
- package/product-strategy/references/canvas-alignment.md +108 -0
- package/product-strategy/references/value-proposition-canvas.md +159 -0
- package/project-templates/context.md.template +56 -0
- package/project-templates/test-strategy.md.template +87 -0
- package/story-mapping/SKILL.md +104 -0
- package/story-mapping/references/backbone.md +66 -0
- package/story-mapping/references/release-planning.md +92 -0
- package/story-mapping/references/task-template.md +78 -0
- package/story-mapping/references/walking-skeleton.md +63 -0
- package/ui-design-system/SKILL.md +48 -0
- package/ui-design-system/references/accessibility.md +134 -0
- package/ui-design-system/references/components.md +257 -0
- package/ui-design-system/references/design-tokens.md +209 -0
- package/ui-design-system/references/layout.md +136 -0
- package/ui-design-system/references/typography.md +114 -0
- package/ui-design-workflow/SKILL.md +90 -0
- package/ui-design-workflow/references/acceptance-targets.md +144 -0
- package/ui-design-workflow/references/component-selection.md +108 -0
- package/ui-design-workflow/references/scenario-to-ui.md +151 -0
- package/ui-design-workflow/references/screen-flows.md +116 -0
- package/ux-design/SKILL.md +75 -0
- package/ux-design/references/information-architecture.md +144 -0
- package/ux-design/references/interaction-patterns.md +141 -0
- package/ux-design/references/onboarding.md +159 -0
- package/ux-design/references/usability-evaluation.md +132 -0
- package/ux-research/SKILL.md +75 -0
- package/ux-research/references/journey-mapping.md +168 -0
- package/ux-research/references/mental-models.md +106 -0
- package/ux-research/references/personas.md +102 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
# Behavioral Patterns
|
|
2
|
+
|
|
3
|
+
Patterns for managing communication and responsibility between objects.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Strategy
|
|
8
|
+
|
|
9
|
+
**When to use:**
|
|
10
|
+
- A family of algorithms exists, and any one can be selected at runtime
|
|
11
|
+
- A class has a behavior that varies based on a condition, implemented as an if/else or switch
|
|
12
|
+
- You want to isolate each algorithm's implementation in its own class
|
|
13
|
+
|
|
14
|
+
**Smell that triggers it:**
|
|
15
|
+
```
|
|
16
|
+
class InvoicePrinter:
|
|
17
|
+
print(invoice):
|
|
18
|
+
if format == "pdf":
|
|
19
|
+
// 30 lines of PDF logic
|
|
20
|
+
elif format == "html":
|
|
21
|
+
// 25 lines of HTML logic
|
|
22
|
+
elif format == "csv":
|
|
23
|
+
// 20 lines of CSV logic ← new format = edit this class
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**Solution:**
|
|
27
|
+
```
|
|
28
|
+
interface PrintStrategy:
|
|
29
|
+
print(invoice): output
|
|
30
|
+
|
|
31
|
+
class PDFPrintStrategy implements PrintStrategy:
|
|
32
|
+
print(invoice): // PDF logic
|
|
33
|
+
|
|
34
|
+
class HTMLPrintStrategy implements PrintStrategy:
|
|
35
|
+
print(invoice): // HTML logic
|
|
36
|
+
|
|
37
|
+
class InvoicePrinter:
|
|
38
|
+
constructor(strategy: PrintStrategy)
|
|
39
|
+
print(invoice): return this.strategy.print(invoice)
|
|
40
|
+
|
|
41
|
+
// Swap strategy at runtime
|
|
42
|
+
printer = new InvoicePrinter(new PDFPrintStrategy())
|
|
43
|
+
printer.print(invoice)
|
|
44
|
+
|
|
45
|
+
printer = new InvoicePrinter(new HTMLPrintStrategy())
|
|
46
|
+
printer.print(invoice)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Key insight:** Strategy is the most commonly needed behavioral pattern. Whenever you see a method with branching logic that selects between algorithms, Strategy is almost certainly the answer. New algorithm = new class, zero edits to existing code.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Command
|
|
54
|
+
|
|
55
|
+
**When to use:**
|
|
56
|
+
- You need to encapsulate an action as an object
|
|
57
|
+
- You need undo/redo capability
|
|
58
|
+
- You need to queue, log, or sequence operations
|
|
59
|
+
- You want to decouple the object that performs the action from the object that triggers it
|
|
60
|
+
|
|
61
|
+
**Smell that triggers it:**
|
|
62
|
+
- A UI button or API endpoint directly calls business logic
|
|
63
|
+
- You need a history of operations (for undo, audit trail, or replay)
|
|
64
|
+
- You need to schedule actions for later execution
|
|
65
|
+
|
|
66
|
+
**Solution:**
|
|
67
|
+
```
|
|
68
|
+
interface Command:
|
|
69
|
+
execute()
|
|
70
|
+
undo()
|
|
71
|
+
|
|
72
|
+
class AddLineItemCommand implements Command:
|
|
73
|
+
constructor(invoice, lineItem)
|
|
74
|
+
execute():
|
|
75
|
+
this.invoice.addLineItem(this.lineItem)
|
|
76
|
+
undo():
|
|
77
|
+
this.invoice.removeLineItem(this.lineItem)
|
|
78
|
+
|
|
79
|
+
class ApplyDiscountCommand implements Command:
|
|
80
|
+
constructor(invoice, discount)
|
|
81
|
+
private previousDiscount: Discount
|
|
82
|
+
|
|
83
|
+
execute():
|
|
84
|
+
this.previousDiscount = this.invoice.getDiscount()
|
|
85
|
+
this.invoice.setDiscount(this.discount)
|
|
86
|
+
undo():
|
|
87
|
+
this.invoice.setDiscount(this.previousDiscount)
|
|
88
|
+
|
|
89
|
+
// Command history enables undo
|
|
90
|
+
class InvoiceEditor:
|
|
91
|
+
private history: Command[] = []
|
|
92
|
+
|
|
93
|
+
execute(command: Command):
|
|
94
|
+
command.execute()
|
|
95
|
+
this.history.push(command)
|
|
96
|
+
|
|
97
|
+
undo():
|
|
98
|
+
lastCommand = this.history.pop()
|
|
99
|
+
lastCommand.undo()
|
|
100
|
+
|
|
101
|
+
// Usage
|
|
102
|
+
editor.execute(new AddLineItemCommand(invoice, consulting))
|
|
103
|
+
editor.execute(new ApplyDiscountCommand(invoice, tenPercent))
|
|
104
|
+
editor.undo() ← removes discount
|
|
105
|
+
editor.undo() ← removes line item
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**Key insight:** Each command is a self-contained unit of work. The history stack makes undo trivial. The same pattern works for audit logging — just persist the commands instead of discarding them.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Observer
|
|
113
|
+
|
|
114
|
+
**When to use:**
|
|
115
|
+
- An object's state change should notify one or more other objects
|
|
116
|
+
- The number of interested parties is unknown or varies at runtime
|
|
117
|
+
- You want loose coupling between the thing that changed and the things that react
|
|
118
|
+
|
|
119
|
+
**Smell that triggers it:**
|
|
120
|
+
- A method that directly calls multiple other systems after doing its work
|
|
121
|
+
- Adding a new reaction to an event requires editing the source class
|
|
122
|
+
|
|
123
|
+
**Solution:**
|
|
124
|
+
```
|
|
125
|
+
interface Observer:
|
|
126
|
+
update(event: Event)
|
|
127
|
+
|
|
128
|
+
interface Observable:
|
|
129
|
+
subscribe(observer: Observer)
|
|
130
|
+
unsubscribe(observer: Observer)
|
|
131
|
+
notify(event: Event)
|
|
132
|
+
|
|
133
|
+
class Invoice implements Observable:
|
|
134
|
+
private observers: Observer[] = []
|
|
135
|
+
|
|
136
|
+
subscribe(observer): this.observers.push(observer)
|
|
137
|
+
unsubscribe(observer): this.observers = this.observers.filter(o => o !== observer)
|
|
138
|
+
|
|
139
|
+
notify(event):
|
|
140
|
+
this.observers.forEach(o => o.update(event))
|
|
141
|
+
|
|
142
|
+
markPaid(transactionId):
|
|
143
|
+
this.status = "paid"
|
|
144
|
+
this.notify(new InvoicePaidEvent(this, transactionId))
|
|
145
|
+
|
|
146
|
+
// Observers — each handles one concern
|
|
147
|
+
class EmailNotifier implements Observer:
|
|
148
|
+
update(event):
|
|
149
|
+
if event is InvoicePaidEvent:
|
|
150
|
+
sendConfirmationEmail(event.invoice.customer)
|
|
151
|
+
|
|
152
|
+
class AccountingSync implements Observer:
|
|
153
|
+
update(event):
|
|
154
|
+
if event is InvoicePaidEvent:
|
|
155
|
+
syncToAccountingSystem(event.invoice)
|
|
156
|
+
|
|
157
|
+
class AuditLogger implements Observer:
|
|
158
|
+
update(event):
|
|
159
|
+
logEvent(event)
|
|
160
|
+
|
|
161
|
+
// Wire up at startup
|
|
162
|
+
invoice.subscribe(new EmailNotifier())
|
|
163
|
+
invoice.subscribe(new AccountingSync())
|
|
164
|
+
invoice.subscribe(new AuditLogger())
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**Key insight:** `Invoice` knows nothing about email, accounting, or audit logging. New reaction = new observer class + wire it up. Zero changes to `Invoice`.
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## State
|
|
172
|
+
|
|
173
|
+
**When to use:**
|
|
174
|
+
- An object's behavior changes based on its internal state
|
|
175
|
+
- A large if/else or switch block checks state and branches behavior
|
|
176
|
+
- State transitions have rules that should be enforced
|
|
177
|
+
|
|
178
|
+
**Smell that triggers it:**
|
|
179
|
+
```
|
|
180
|
+
class Invoice:
|
|
181
|
+
pay():
|
|
182
|
+
if this.status == "draft":
|
|
183
|
+
throw Error("Cannot pay a draft")
|
|
184
|
+
if this.status == "paid":
|
|
185
|
+
throw Error("Already paid")
|
|
186
|
+
if this.status == "cancelled":
|
|
187
|
+
throw Error("Cannot pay cancelled invoice")
|
|
188
|
+
this.status = "paid" ← state logic scattered through every method
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
**Solution:**
|
|
192
|
+
```
|
|
193
|
+
interface InvoiceState:
|
|
194
|
+
pay(invoice)
|
|
195
|
+
cancel(invoice)
|
|
196
|
+
send(invoice)
|
|
197
|
+
|
|
198
|
+
class DraftState implements InvoiceState:
|
|
199
|
+
pay(invoice): throw Error("Finalize the invoice before payment")
|
|
200
|
+
cancel(invoice): invoice.setState(new CancelledState())
|
|
201
|
+
send(invoice): invoice.setState(new SentState())
|
|
202
|
+
|
|
203
|
+
class SentState implements InvoiceState:
|
|
204
|
+
pay(invoice): invoice.setState(new PaidState())
|
|
205
|
+
cancel(invoice): invoice.setState(new CancelledState())
|
|
206
|
+
send(invoice): throw Error("Already sent")
|
|
207
|
+
|
|
208
|
+
class PaidState implements InvoiceState:
|
|
209
|
+
pay(invoice): throw Error("Already paid")
|
|
210
|
+
cancel(invoice): throw Error("Cannot cancel a paid invoice")
|
|
211
|
+
send(invoice): throw Error("Already paid")
|
|
212
|
+
|
|
213
|
+
class CancelledState implements InvoiceState:
|
|
214
|
+
pay(invoice): throw Error("Cannot pay a cancelled invoice")
|
|
215
|
+
cancel(invoice): throw Error("Already cancelled")
|
|
216
|
+
send(invoice): throw Error("Cannot send a cancelled invoice")
|
|
217
|
+
|
|
218
|
+
class Invoice:
|
|
219
|
+
private state: InvoiceState = new DraftState()
|
|
220
|
+
setState(state): this.state = state
|
|
221
|
+
pay(): this.state.pay(this) ← delegates entirely to state object
|
|
222
|
+
cancel(): this.state.cancel(this)
|
|
223
|
+
send(): this.state.send(this)
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
**Key insight:** Each state is a class. Valid transitions are explicit. Invalid transitions throw immediately. Adding a new state means one new class. The `Invoice` class itself never changes.
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## Chain of Responsibility
|
|
231
|
+
|
|
232
|
+
**When to use:**
|
|
233
|
+
- A request must pass through a sequence of handlers until one handles it
|
|
234
|
+
- The set of handlers or their order may vary at runtime
|
|
235
|
+
- Each handler decides whether to handle the request or pass it along
|
|
236
|
+
|
|
237
|
+
**Smell that triggers it:**
|
|
238
|
+
- A series of if/else checks where each check either handles the case or falls through to the next
|
|
239
|
+
- Validation logic that applies multiple rules in sequence
|
|
240
|
+
|
|
241
|
+
**Solution:**
|
|
242
|
+
```
|
|
243
|
+
abstract class ValidationHandler:
|
|
244
|
+
private next: ValidationHandler
|
|
245
|
+
|
|
246
|
+
setNext(handler: ValidationHandler): ValidationHandler
|
|
247
|
+
this.next = handler
|
|
248
|
+
return handler
|
|
249
|
+
|
|
250
|
+
validate(invoice): ValidationResult
|
|
251
|
+
if this.next:
|
|
252
|
+
return this.next.validate(invoice)
|
|
253
|
+
return ValidationResult.pass()
|
|
254
|
+
|
|
255
|
+
class CustomerExistsHandler extends ValidationHandler:
|
|
256
|
+
validate(invoice):
|
|
257
|
+
if not customerExists(invoice.customerId):
|
|
258
|
+
return ValidationResult.fail("Customer not found")
|
|
259
|
+
return super.validate(invoice) ← pass to next handler
|
|
260
|
+
|
|
261
|
+
class LineItemsRequiredHandler extends ValidationHandler:
|
|
262
|
+
validate(invoice):
|
|
263
|
+
if invoice.lineItems.length == 0:
|
|
264
|
+
return ValidationResult.fail("At least one line item required")
|
|
265
|
+
return super.validate(invoice)
|
|
266
|
+
|
|
267
|
+
class MaxAmountHandler extends ValidationHandler:
|
|
268
|
+
validate(invoice):
|
|
269
|
+
if invoice.total() > maxAllowed:
|
|
270
|
+
return ValidationResult.fail("Exceeds maximum amount")
|
|
271
|
+
return super.validate(invoice)
|
|
272
|
+
|
|
273
|
+
// Wire the chain
|
|
274
|
+
customerCheck = new CustomerExistsHandler()
|
|
275
|
+
lineItemCheck = new LineItemsRequiredHandler()
|
|
276
|
+
amountCheck = new MaxAmountHandler()
|
|
277
|
+
|
|
278
|
+
customerCheck.setNext(lineItemCheck).setNext(amountCheck)
|
|
279
|
+
|
|
280
|
+
// Validate — passes through chain until failure or all pass
|
|
281
|
+
result = customerCheck.validate(invoice)
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
**Key insight:** Each handler has one responsibility. New validation rule = one new handler class, inserted into the chain. Order is controlled at wiring time.
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
## Template Method
|
|
289
|
+
|
|
290
|
+
**When to use:**
|
|
291
|
+
- Multiple classes share the same algorithm skeleton but differ in specific steps
|
|
292
|
+
- You want to enforce an algorithm's structure while letting subclasses customize individual steps
|
|
293
|
+
|
|
294
|
+
**Smell that triggers it:**
|
|
295
|
+
- Copy-pasted methods across classes that differ only in one or two steps
|
|
296
|
+
- An algorithm where the sequence of steps is fixed but the implementation of each step varies
|
|
297
|
+
|
|
298
|
+
**Solution:**
|
|
299
|
+
```
|
|
300
|
+
abstract class InvoiceExporter:
|
|
301
|
+
// Template method — defines the algorithm skeleton. Final = cannot be overridden.
|
|
302
|
+
export(invoice): final
|
|
303
|
+
data = this.extractData(invoice) ← step 1: subclass implements
|
|
304
|
+
validated = this.validate(data) ← step 2: subclass implements
|
|
305
|
+
formatted = this.format(validated) ← step 3: subclass implements
|
|
306
|
+
this.output(formatted) ← step 4: subclass implements
|
|
307
|
+
return formatted
|
|
308
|
+
|
|
309
|
+
abstract extractData(invoice): ExportData
|
|
310
|
+
abstract validate(data): ExportData
|
|
311
|
+
abstract format(data): string
|
|
312
|
+
abstract output(formatted): void
|
|
313
|
+
|
|
314
|
+
class PDFExporter extends InvoiceExporter:
|
|
315
|
+
extractData(invoice): // PDF-specific extraction
|
|
316
|
+
validate(data): // PDF-specific validation
|
|
317
|
+
format(data): // PDF rendering
|
|
318
|
+
output(formatted): // Write to PDF file
|
|
319
|
+
|
|
320
|
+
class CSVExporter extends InvoiceExporter:
|
|
321
|
+
extractData(invoice): // CSV-specific extraction
|
|
322
|
+
validate(data): // CSV-specific validation
|
|
323
|
+
format(data): // CSV formatting
|
|
324
|
+
output(formatted): // Write to CSV file
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
**Key insight:** The algorithm sequence (extract → validate → format → output) is defined once in the base class. Each exporter only implements the steps. If the sequence needs to change, it changes in one place.
|
|
328
|
+
|
|
329
|
+
**Note:** Template Method uses inheritance. If you find yourself wanting the same flexibility without inheritance, Strategy achieves the same result with composition — generally preferred in modern code.
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# Creational Patterns
|
|
2
|
+
|
|
3
|
+
Patterns for controlling how objects are created.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Factory Method
|
|
8
|
+
|
|
9
|
+
**When to use:**
|
|
10
|
+
- Object creation logic is complex or varies based on a condition
|
|
11
|
+
- You need to decouple the caller from the concrete class being created
|
|
12
|
+
- A new type of object can be added without changing the creation site
|
|
13
|
+
|
|
14
|
+
**Smell that triggers it:**
|
|
15
|
+
```
|
|
16
|
+
if type == "pdf":
|
|
17
|
+
doc = new PDFExporter()
|
|
18
|
+
elif type == "csv":
|
|
19
|
+
doc = new CSVExporter()
|
|
20
|
+
elif type == "xlsx": ← new type = edit this block
|
|
21
|
+
doc = new XLSXExporter()
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Solution:**
|
|
25
|
+
```
|
|
26
|
+
interface Exporter:
|
|
27
|
+
export(data)
|
|
28
|
+
|
|
29
|
+
class ExporterFactory:
|
|
30
|
+
static create(type: string): Exporter
|
|
31
|
+
registry = { "pdf": PDFExporter, "csv": CSVExporter, "xlsx": XLSXExporter }
|
|
32
|
+
return new registry[type]()
|
|
33
|
+
|
|
34
|
+
// Usage — caller knows nothing about concrete types
|
|
35
|
+
exporter = ExporterFactory.create(requestedFormat)
|
|
36
|
+
exporter.export(invoiceData)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**Key insight:** Adding a new format means adding a new class and registering it. The calling code never changes.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Abstract Factory
|
|
44
|
+
|
|
45
|
+
**When to use:**
|
|
46
|
+
- You need to create families of related objects that must be used together
|
|
47
|
+
- The system should be independent of how its products are created
|
|
48
|
+
- A "theme" or "variant" determines which set of objects is created
|
|
49
|
+
|
|
50
|
+
**Smell that triggers it:**
|
|
51
|
+
- Multiple factory methods that always get called together
|
|
52
|
+
- Objects that must be consistent with each other (e.g., a UI theme where button, input, and modal must all match)
|
|
53
|
+
|
|
54
|
+
**Solution:**
|
|
55
|
+
```
|
|
56
|
+
interface UIFactory:
|
|
57
|
+
createButton(): Button
|
|
58
|
+
createInput(): Input
|
|
59
|
+
createModal(): Modal
|
|
60
|
+
|
|
61
|
+
class DarkThemeFactory implements UIFactory:
|
|
62
|
+
createButton(): return new DarkButton()
|
|
63
|
+
createInput(): return new DarkInput()
|
|
64
|
+
createModal(): return new DarkModal()
|
|
65
|
+
|
|
66
|
+
class LightThemeFactory implements UIFactory:
|
|
67
|
+
createButton(): return new LightButton()
|
|
68
|
+
createInput(): return new LightInput()
|
|
69
|
+
createModal(): return new LightModal()
|
|
70
|
+
|
|
71
|
+
// App receives a factory — knows nothing about theme internals
|
|
72
|
+
class App:
|
|
73
|
+
constructor(factory: UIFactory)
|
|
74
|
+
this.button = factory.createButton()
|
|
75
|
+
this.input = factory.createInput()
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Key insight:** New theme = new factory class. The rest of the application is completely unaware.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Builder
|
|
83
|
+
|
|
84
|
+
**When to use:**
|
|
85
|
+
- An object requires many parameters, most of which are optional
|
|
86
|
+
- Construction requires a specific sequence of steps
|
|
87
|
+
- The same construction process can produce different representations
|
|
88
|
+
|
|
89
|
+
**Smell that triggers it:**
|
|
90
|
+
```
|
|
91
|
+
// Telescoping constructor — grows with every optional field
|
|
92
|
+
invoice = new Invoice(customer, date, null, null, "standard", null, true)
|
|
93
|
+
// What does the 5th null mean? Nobody knows.
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**Solution:**
|
|
97
|
+
```
|
|
98
|
+
class InvoiceBuilder:
|
|
99
|
+
private invoice = new Invoice()
|
|
100
|
+
|
|
101
|
+
setCustomer(customer): this.invoice.customer = customer; return this
|
|
102
|
+
setDueDate(date): this.invoice.dueDate = date; return this
|
|
103
|
+
setTaxRule(rule): this.invoice.taxRule = rule; return this
|
|
104
|
+
setDiscount(discount): this.invoice.discount = discount; return this
|
|
105
|
+
markAsPriority(): this.invoice.priority = true; return this
|
|
106
|
+
|
|
107
|
+
build(): Invoice
|
|
108
|
+
validate(this.invoice) ← enforce required fields here
|
|
109
|
+
return this.invoice
|
|
110
|
+
|
|
111
|
+
// Usage — reads like a specification
|
|
112
|
+
invoice = new InvoiceBuilder()
|
|
113
|
+
.setCustomer(acmeCorp)
|
|
114
|
+
.setDueDate(nextMonth)
|
|
115
|
+
.setTaxRule(standardRate)
|
|
116
|
+
.markAsPriority()
|
|
117
|
+
.build()
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**Key insight:** Each step is named. Optional fields are simply omitted. Validation happens at `build()`, not scattered across constructors.
|
|
121
|
+
|
|
122
|
+
**Test usage:** Builders are especially valuable in test fixtures. A `TestInvoiceBuilder` can set sensible defaults, and each test overrides only what it needs:
|
|
123
|
+
```
|
|
124
|
+
// Test only cares about discount behavior — everything else is default
|
|
125
|
+
invoice = TestInvoiceBuilder.defaults().setDiscount(0.15).build()
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Singleton
|
|
131
|
+
|
|
132
|
+
**When to use:**
|
|
133
|
+
- Exactly one instance must exist and be globally accessible
|
|
134
|
+
- The instance controls access to a shared resource (config, logger, registry)
|
|
135
|
+
|
|
136
|
+
**Smell that triggers it:**
|
|
137
|
+
- Multiple places trying to create the same resource independently
|
|
138
|
+
- Need for a single shared state across the application
|
|
139
|
+
|
|
140
|
+
**Solution:**
|
|
141
|
+
```
|
|
142
|
+
class Configuration:
|
|
143
|
+
private static instance: Configuration
|
|
144
|
+
private settings: Map
|
|
145
|
+
|
|
146
|
+
private constructor(): ← private — nobody else can create one
|
|
147
|
+
this.settings = loadFromFile()
|
|
148
|
+
|
|
149
|
+
static getInstance(): Configuration
|
|
150
|
+
if not this.instance:
|
|
151
|
+
this.instance = new Configuration()
|
|
152
|
+
return this.instance
|
|
153
|
+
|
|
154
|
+
get(key: string): string
|
|
155
|
+
return this.settings[key]
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
**Warning:** Singleton is the most abused pattern. Do NOT use it for:
|
|
159
|
+
- Database connections (use a connection pool instead)
|
|
160
|
+
- Services that could benefit from multiple instances
|
|
161
|
+
- Anything that makes unit testing harder
|
|
162
|
+
|
|
163
|
+
If you find yourself reaching for Singleton to solve a dependency problem, DIP (from `references/solid.md`) is almost certainly the right answer instead.
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Prototype
|
|
168
|
+
|
|
169
|
+
**When to use:**
|
|
170
|
+
- Creating new objects is expensive and you have an existing instance to clone
|
|
171
|
+
- The class hierarchy mirrors the variety of objects you want to create
|
|
172
|
+
|
|
173
|
+
**Smell that triggers it:**
|
|
174
|
+
- Object creation involves heavy initialization (network call, file read, complex calculation)
|
|
175
|
+
- You need many similar objects that differ only slightly
|
|
176
|
+
|
|
177
|
+
**Solution:**
|
|
178
|
+
```
|
|
179
|
+
interface Cloneable:
|
|
180
|
+
clone(): self
|
|
181
|
+
|
|
182
|
+
class InvoiceTemplate implements Cloneable:
|
|
183
|
+
clone():
|
|
184
|
+
copy = new InvoiceTemplate()
|
|
185
|
+
copy.taxRule = this.taxRule
|
|
186
|
+
copy.paymentTerms = this.paymentTerms
|
|
187
|
+
copy.header = this.header
|
|
188
|
+
return copy
|
|
189
|
+
|
|
190
|
+
// Usage — clone and customize, skip expensive setup
|
|
191
|
+
template = standardInvoiceTemplate
|
|
192
|
+
newInvoice = template.clone()
|
|
193
|
+
newInvoice.setCustomer(acmeCorp)
|
|
194
|
+
newInvoice.addLineItem(consultingService)
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
**Key insight:** Useful when you have template objects and need many variations quickly. Less common than Factory or Builder — apply only when creation cost is genuinely high.
|