mcp-software-design 0.1.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/LICENSE +21 -0
- package/README.md +111 -0
- package/build/catalog.js +978 -0
- package/build/index.js +297 -0
- package/build/scaffold.js +78 -0
- package/build/smells.js +554 -0
- package/package.json +49 -0
package/build/catalog.js
ADDED
|
@@ -0,0 +1,978 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The reference data behind the server: design *principles* (SOLID, OOP,
|
|
3
|
+
* DRY, KISS, YAGNI, …) and the 23 Gang-of-Four *patterns*.
|
|
4
|
+
*
|
|
5
|
+
* This module is pure data + pure lookup/rendering helpers, kept separate
|
|
6
|
+
* from the MCP wiring (index.ts) so it can be unit-tested in isolation and
|
|
7
|
+
* reused by the scaffolder (scaffold.ts).
|
|
8
|
+
*
|
|
9
|
+
* Honesty note: principles and patterns are *judgment* tools, not lint
|
|
10
|
+
* rules. Nothing here claims a snippet "passes" or "fails" — the catalog
|
|
11
|
+
* exists so the model cites consistent, authoritative definitions instead of
|
|
12
|
+
* paraphrasing from memory.
|
|
13
|
+
*/
|
|
14
|
+
/* ------------------------------------------------------------------ *
|
|
15
|
+
* Principles
|
|
16
|
+
* ------------------------------------------------------------------ */
|
|
17
|
+
export const PRINCIPLES = [
|
|
18
|
+
{
|
|
19
|
+
slug: "single-responsibility",
|
|
20
|
+
name: "Single Responsibility Principle (SRP)",
|
|
21
|
+
category: "principle",
|
|
22
|
+
aka: ["srp"],
|
|
23
|
+
summary: "A module should have one reason to change.",
|
|
24
|
+
intent: "Each class or module should answer to exactly one actor/concern. When " +
|
|
25
|
+
"unrelated responsibilities share a class, a change requested by one " +
|
|
26
|
+
"stakeholder risks breaking another's feature. Split along the axes of " +
|
|
27
|
+
"change, not along nouns.",
|
|
28
|
+
whenToUse: [
|
|
29
|
+
"A class mixes concerns (e.g. business rules + persistence + formatting).",
|
|
30
|
+
"You keep editing the same file for unrelated reasons.",
|
|
31
|
+
"A method name needs an 'and' to describe it.",
|
|
32
|
+
],
|
|
33
|
+
tradeoffs: [
|
|
34
|
+
"Over-splitting creates anemic, scattered classes and navigation overhead.",
|
|
35
|
+
"'Responsibility' is a judgment call — define it by who requests the change.",
|
|
36
|
+
],
|
|
37
|
+
related: ["separation-of-concerns", "open-closed", "facade"],
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
slug: "open-closed",
|
|
41
|
+
name: "Open/Closed Principle (OCP)",
|
|
42
|
+
category: "principle",
|
|
43
|
+
aka: ["ocp"],
|
|
44
|
+
summary: "Open for extension, closed for modification.",
|
|
45
|
+
intent: "You should be able to add new behavior without editing existing, tested " +
|
|
46
|
+
"code — typically by depending on an abstraction and adding a new " +
|
|
47
|
+
"implementation rather than adding another branch to a growing switch.",
|
|
48
|
+
whenToUse: [
|
|
49
|
+
"A switch/if-else grows a new arm every time a variant is added.",
|
|
50
|
+
"New behavior can be expressed as a new implementation of an interface.",
|
|
51
|
+
],
|
|
52
|
+
tradeoffs: [
|
|
53
|
+
"Premature abstraction for variation that never comes is speculative (YAGNI).",
|
|
54
|
+
"Indirection makes the straight-line path harder to read.",
|
|
55
|
+
],
|
|
56
|
+
related: ["strategy", "dependency-inversion", "yagni", "factory-method"],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
slug: "liskov-substitution",
|
|
60
|
+
name: "Liskov Substitution Principle (LSP)",
|
|
61
|
+
category: "principle",
|
|
62
|
+
aka: ["lsp"],
|
|
63
|
+
summary: "Subtypes must be usable anywhere their base type is expected.",
|
|
64
|
+
intent: "A subclass must honor the base type's contract: no strengthened " +
|
|
65
|
+
"preconditions, no weakened postconditions, no surprising exceptions. If " +
|
|
66
|
+
"callers must check the concrete type, the hierarchy is broken.",
|
|
67
|
+
whenToUse: [
|
|
68
|
+
"A subclass overrides a method to throw 'not supported'.",
|
|
69
|
+
"Callers do `instanceof` checks to special-case a subtype.",
|
|
70
|
+
"The classic Square-extends-Rectangle problem.",
|
|
71
|
+
],
|
|
72
|
+
tradeoffs: [
|
|
73
|
+
"Sometimes composition beats inheritance — don't force an 'is-a' that isn't.",
|
|
74
|
+
],
|
|
75
|
+
related: ["composition-over-inheritance", "interface-segregation", "polymorphism"],
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
slug: "interface-segregation",
|
|
79
|
+
name: "Interface Segregation Principle (ISP)",
|
|
80
|
+
category: "principle",
|
|
81
|
+
aka: ["isp"],
|
|
82
|
+
summary: "Don't force clients to depend on methods they don't use.",
|
|
83
|
+
intent: "Prefer many small, role-specific interfaces over one fat interface. " +
|
|
84
|
+
"Clients then depend only on the operations they actually call, so a " +
|
|
85
|
+
"change to an unrelated method can't ripple into them.",
|
|
86
|
+
whenToUse: [
|
|
87
|
+
"Implementers are forced to stub methods they don't need.",
|
|
88
|
+
"One interface serves several unrelated client roles.",
|
|
89
|
+
],
|
|
90
|
+
tradeoffs: [
|
|
91
|
+
"Too many micro-interfaces adds ceremony; balance cohesion against granularity.",
|
|
92
|
+
],
|
|
93
|
+
related: ["single-responsibility", "dependency-inversion"],
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
slug: "dependency-inversion",
|
|
97
|
+
name: "Dependency Inversion Principle (DIP)",
|
|
98
|
+
category: "principle",
|
|
99
|
+
aka: ["dip"],
|
|
100
|
+
summary: "Depend on abstractions, not concretions.",
|
|
101
|
+
intent: "High-level policy shouldn't depend on low-level detail; both should " +
|
|
102
|
+
"depend on an abstraction. Inject collaborators through an interface so " +
|
|
103
|
+
"the detail (a specific DB, HTTP client, clock) is swappable and testable.",
|
|
104
|
+
whenToUse: [
|
|
105
|
+
"Business logic imports a concrete database/HTTP/file class directly.",
|
|
106
|
+
"You want to unit-test policy without the real dependency.",
|
|
107
|
+
],
|
|
108
|
+
tradeoffs: [
|
|
109
|
+
"An interface with exactly one impl forever is often needless indirection.",
|
|
110
|
+
"Don't confuse it with a DI framework — the principle needs no container.",
|
|
111
|
+
],
|
|
112
|
+
related: ["open-closed", "strategy", "abstract-factory", "abstraction"],
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
slug: "dry",
|
|
116
|
+
name: "DRY — Don't Repeat Yourself",
|
|
117
|
+
category: "principle",
|
|
118
|
+
aka: ["dont-repeat-yourself"],
|
|
119
|
+
summary: "Every piece of knowledge has one authoritative representation.",
|
|
120
|
+
intent: "DRY is about knowledge, not text. Two identical-looking blocks that " +
|
|
121
|
+
"encode the *same* decision should be unified; two that merely look " +
|
|
122
|
+
"alike but change for different reasons should stay apart.",
|
|
123
|
+
whenToUse: [
|
|
124
|
+
"The same business rule is copy-pasted and must be edited in lockstep.",
|
|
125
|
+
"A constant/magic value is duplicated across the codebase.",
|
|
126
|
+
],
|
|
127
|
+
tradeoffs: [
|
|
128
|
+
"Coupling unrelated code just because it looks similar is worse than duplication.",
|
|
129
|
+
"The 'rule of three' — tolerate two copies; extract on the third.",
|
|
130
|
+
],
|
|
131
|
+
related: ["single-responsibility", "kiss"],
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
slug: "kiss",
|
|
135
|
+
name: "KISS — Keep It Simple",
|
|
136
|
+
category: "principle",
|
|
137
|
+
aka: ["keep-it-simple"],
|
|
138
|
+
summary: "Prefer the simplest design that solves the problem.",
|
|
139
|
+
intent: "Complexity is a cost paid on every future read. Favor straightforward, " +
|
|
140
|
+
"boring solutions over clever ones; add structure only when a concrete " +
|
|
141
|
+
"need justifies it.",
|
|
142
|
+
whenToUse: [
|
|
143
|
+
"A design has layers/indirection with no present-day payoff.",
|
|
144
|
+
"A one-liner was replaced by a framework.",
|
|
145
|
+
],
|
|
146
|
+
tradeoffs: [
|
|
147
|
+
"'Simple' isn't 'easy' — sometimes real simplicity takes design effort.",
|
|
148
|
+
],
|
|
149
|
+
related: ["yagni", "dry"],
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
slug: "yagni",
|
|
153
|
+
name: "YAGNI — You Aren't Gonna Need It",
|
|
154
|
+
category: "principle",
|
|
155
|
+
aka: ["you-arent-gonna-need-it"],
|
|
156
|
+
summary: "Don't build for requirements you don't have yet.",
|
|
157
|
+
intent: "Speculative generality is inventory you pay to carry: extra code to " +
|
|
158
|
+
"read, test, and maintain for a future that may never arrive. Build for " +
|
|
159
|
+
"today's requirement and refactor when the real need shows up.",
|
|
160
|
+
whenToUse: [
|
|
161
|
+
"Adding config flags / hooks / abstraction 'just in case'.",
|
|
162
|
+
"Generalizing before a second concrete use case exists.",
|
|
163
|
+
],
|
|
164
|
+
tradeoffs: [
|
|
165
|
+
"Not a license to ignore obvious, cheap seams that keep options open.",
|
|
166
|
+
],
|
|
167
|
+
related: ["kiss", "open-closed"],
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
slug: "composition-over-inheritance",
|
|
171
|
+
name: "Composition Over Inheritance",
|
|
172
|
+
category: "principle",
|
|
173
|
+
aka: ["favor-composition"],
|
|
174
|
+
summary: "Assemble behavior from parts rather than inheriting it.",
|
|
175
|
+
intent: "Inheritance is tight, compile-time coupling to a base class's internals. " +
|
|
176
|
+
"Composing objects (has-a) that delegate to collaborators is more " +
|
|
177
|
+
"flexible, avoids fragile hierarchies, and lets behavior change at runtime.",
|
|
178
|
+
whenToUse: [
|
|
179
|
+
"A hierarchy is deep or exists only to share code.",
|
|
180
|
+
"You need to vary behavior along more than one axis.",
|
|
181
|
+
],
|
|
182
|
+
tradeoffs: [
|
|
183
|
+
"More small objects and wiring; inheritance is fine for true is-a + polymorphism.",
|
|
184
|
+
],
|
|
185
|
+
related: ["strategy", "decorator", "liskov-substitution"],
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
slug: "law-of-demeter",
|
|
189
|
+
name: "Law of Demeter (Principle of Least Knowledge)",
|
|
190
|
+
category: "principle",
|
|
191
|
+
aka: ["lod", "least-knowledge"],
|
|
192
|
+
summary: "Talk to friends, not strangers — avoid deep object reach-through.",
|
|
193
|
+
intent: "A method should only call methods of itself, its parameters, objects it " +
|
|
194
|
+
"creates, and its direct fields. Chains like a.getB().getC().doThing() " +
|
|
195
|
+
"couple you to a structure two objects away; ask the neighbor to do it.",
|
|
196
|
+
whenToUse: [
|
|
197
|
+
"You see train-wreck chains: obj.a().b().c().d().",
|
|
198
|
+
"A change to a distant class breaks callers that never named it.",
|
|
199
|
+
],
|
|
200
|
+
tradeoffs: [
|
|
201
|
+
"Fluent builders and pipelines are legitimate chains — target reaching, not fluency.",
|
|
202
|
+
],
|
|
203
|
+
related: ["facade", "single-responsibility"],
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
slug: "separation-of-concerns",
|
|
207
|
+
name: "Separation of Concerns",
|
|
208
|
+
category: "principle",
|
|
209
|
+
aka: ["soc"],
|
|
210
|
+
summary: "Keep distinct concerns in distinct places.",
|
|
211
|
+
intent: "Partition a system so each part addresses one concern (UI, domain, " +
|
|
212
|
+
"persistence, transport). Concerns can then evolve and be reasoned about " +
|
|
213
|
+
"independently, which is the macro-scale sibling of SRP.",
|
|
214
|
+
whenToUse: [
|
|
215
|
+
"Presentation logic is entangled with business rules or SQL.",
|
|
216
|
+
"You want layers/modules that can be tested and swapped independently.",
|
|
217
|
+
],
|
|
218
|
+
tradeoffs: [
|
|
219
|
+
"Too many layers for a tiny app is ceremony — match structure to scale.",
|
|
220
|
+
],
|
|
221
|
+
related: ["single-responsibility", "facade"],
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
slug: "encapsulation",
|
|
225
|
+
name: "Encapsulation (OOP pillar)",
|
|
226
|
+
category: "principle",
|
|
227
|
+
aka: ["information-hiding"],
|
|
228
|
+
summary: "Bundle state with behavior and hide the internals.",
|
|
229
|
+
intent: "Expose behavior, not data. Keep fields private and mutate them only " +
|
|
230
|
+
"through methods that preserve invariants, so callers depend on a stable " +
|
|
231
|
+
"contract rather than a mutable internal shape.",
|
|
232
|
+
whenToUse: [
|
|
233
|
+
"Public setters let callers put an object into an invalid state.",
|
|
234
|
+
"Invariants are enforced in many call sites instead of one owner.",
|
|
235
|
+
],
|
|
236
|
+
tradeoffs: [
|
|
237
|
+
"Plain data-transfer objects legitimately have no behavior to hide.",
|
|
238
|
+
],
|
|
239
|
+
related: ["abstraction", "single-responsibility", "law-of-demeter"],
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
slug: "abstraction",
|
|
243
|
+
name: "Abstraction (OOP pillar)",
|
|
244
|
+
category: "principle",
|
|
245
|
+
aka: [],
|
|
246
|
+
summary: "Expose the essential contract; hide the mechanism.",
|
|
247
|
+
intent: "Model a thing by what it does, not how. Callers program to an interface " +
|
|
248
|
+
"and stay insulated from implementation churn behind it.",
|
|
249
|
+
whenToUse: [
|
|
250
|
+
"Callers need one concept but several interchangeable implementations.",
|
|
251
|
+
"You want to defer or swap a mechanism (storage, transport).",
|
|
252
|
+
],
|
|
253
|
+
tradeoffs: [
|
|
254
|
+
"Leaky or premature abstractions add cost without insulation (see YAGNI).",
|
|
255
|
+
],
|
|
256
|
+
related: ["encapsulation", "dependency-inversion", "polymorphism"],
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
slug: "inheritance",
|
|
260
|
+
name: "Inheritance (OOP pillar)",
|
|
261
|
+
category: "principle",
|
|
262
|
+
aka: [],
|
|
263
|
+
summary: "Derive a specialized type from a general one (is-a).",
|
|
264
|
+
intent: "Share and specialize behavior via a base type. Powerful for genuine " +
|
|
265
|
+
"is-a relationships that also need polymorphism, but easily abused as a " +
|
|
266
|
+
"code-reuse shortcut — prefer composition when it's only about reuse.",
|
|
267
|
+
whenToUse: [
|
|
268
|
+
"A true is-a relationship where subtypes are substitutable (see LSP).",
|
|
269
|
+
"You want polymorphic dispatch over a family of types.",
|
|
270
|
+
],
|
|
271
|
+
tradeoffs: [
|
|
272
|
+
"Deep hierarchies are fragile and rigid; reuse alone doesn't justify it.",
|
|
273
|
+
],
|
|
274
|
+
related: ["polymorphism", "liskov-substitution", "composition-over-inheritance"],
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
slug: "polymorphism",
|
|
278
|
+
name: "Polymorphism (OOP pillar)",
|
|
279
|
+
category: "principle",
|
|
280
|
+
aka: [],
|
|
281
|
+
summary: "One interface, many interchangeable implementations.",
|
|
282
|
+
intent: "Callers invoke an operation on an abstraction and the right concrete " +
|
|
283
|
+
"behavior runs, chosen by type. It's the mechanism that lets OCP, " +
|
|
284
|
+
"Strategy, and dependency inversion eliminate conditionals.",
|
|
285
|
+
whenToUse: [
|
|
286
|
+
"Behavior varies by a type/kind and you'd otherwise switch on it.",
|
|
287
|
+
"You want to add variants without touching callers.",
|
|
288
|
+
],
|
|
289
|
+
tradeoffs: [
|
|
290
|
+
"Dispatch you can't see can obscure control flow; keep the set discoverable.",
|
|
291
|
+
],
|
|
292
|
+
related: ["open-closed", "strategy", "abstraction", "inheritance"],
|
|
293
|
+
},
|
|
294
|
+
];
|
|
295
|
+
/* ------------------------------------------------------------------ *
|
|
296
|
+
* GoF patterns
|
|
297
|
+
* ------------------------------------------------------------------ */
|
|
298
|
+
export const PATTERNS = [
|
|
299
|
+
/* ---------- Creational ---------- */
|
|
300
|
+
{
|
|
301
|
+
slug: "factory-method",
|
|
302
|
+
name: "Factory Method",
|
|
303
|
+
category: "creational",
|
|
304
|
+
summary: "Let subclasses decide which concrete product to instantiate.",
|
|
305
|
+
intent: "Define an interface for creating an object but defer the choice of " +
|
|
306
|
+
"concrete class to subclasses, so the creator depends only on the product " +
|
|
307
|
+
"abstraction.",
|
|
308
|
+
whenToUse: [
|
|
309
|
+
"A class can't anticipate the concrete type it must create.",
|
|
310
|
+
"You want subclasses to specify the objects the base class creates.",
|
|
311
|
+
],
|
|
312
|
+
tradeoffs: [
|
|
313
|
+
"Introduces a parallel Creator hierarchy just to vary one instantiation.",
|
|
314
|
+
],
|
|
315
|
+
related: ["abstract-factory", "template-method", "open-closed"],
|
|
316
|
+
collaboration: "Creator calls its own factoryMethod(); a ConcreteCreator overrides it to return a ConcreteProduct.",
|
|
317
|
+
participants: [
|
|
318
|
+
{ role: "Product", kind: "interface", members: ["operation()"], note: "What the factory returns." },
|
|
319
|
+
{ role: "ConcreteProduct", kind: "class", members: ["operation()"], note: "implements Product" },
|
|
320
|
+
{
|
|
321
|
+
role: "Creator",
|
|
322
|
+
kind: "abstract",
|
|
323
|
+
members: ["factoryMethod(): Product // overridden by subclasses", "someOperation() // uses factoryMethod()"],
|
|
324
|
+
},
|
|
325
|
+
{ role: "ConcreteCreator", kind: "class", members: ["factoryMethod(): Product { return new ConcreteProduct() }"], note: "extends Creator" },
|
|
326
|
+
],
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
slug: "abstract-factory",
|
|
330
|
+
name: "Abstract Factory",
|
|
331
|
+
category: "creational",
|
|
332
|
+
aka: ["kit"],
|
|
333
|
+
summary: "Create families of related objects without naming their classes.",
|
|
334
|
+
intent: "Provide an interface for creating whole families of related products so " +
|
|
335
|
+
"a client can switch the entire family (e.g. a UI theme) by swapping one " +
|
|
336
|
+
"factory.",
|
|
337
|
+
whenToUse: [
|
|
338
|
+
"Products come in interchangeable families that must stay consistent.",
|
|
339
|
+
"You want to enforce that related products are used together.",
|
|
340
|
+
],
|
|
341
|
+
tradeoffs: [
|
|
342
|
+
"Adding a new product *kind* means changing every factory interface + impl.",
|
|
343
|
+
],
|
|
344
|
+
related: ["factory-method", "singleton", "dependency-inversion"],
|
|
345
|
+
collaboration: "Client holds an AbstractFactory and asks it for products; a ConcreteFactory returns a matching family.",
|
|
346
|
+
participants: [
|
|
347
|
+
{ role: "AbstractFactory", kind: "interface", members: ["createProductA(): AbstractProductA", "createProductB(): AbstractProductB"] },
|
|
348
|
+
{ role: "ConcreteFactory1", kind: "class", members: ["createProductA(): AbstractProductA", "createProductB(): AbstractProductB"], note: "implements AbstractFactory" },
|
|
349
|
+
{ role: "AbstractProductA", kind: "interface", members: ["useA()"] },
|
|
350
|
+
{ role: "AbstractProductB", kind: "interface", members: ["useB()"] },
|
|
351
|
+
{ role: "Client", kind: "class", members: ["-factory: AbstractFactory", "run() // uses factory.createProductA()"] },
|
|
352
|
+
],
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
slug: "builder",
|
|
356
|
+
name: "Builder",
|
|
357
|
+
category: "creational",
|
|
358
|
+
summary: "Construct a complex object step by step.",
|
|
359
|
+
intent: "Separate the construction of a complex object from its representation so " +
|
|
360
|
+
"the same steps can build different results, avoiding telescoping " +
|
|
361
|
+
"constructors.",
|
|
362
|
+
whenToUse: [
|
|
363
|
+
"An object needs many optional parts / a long parameter list.",
|
|
364
|
+
"Construction must happen in stages or produce different representations.",
|
|
365
|
+
],
|
|
366
|
+
tradeoffs: [
|
|
367
|
+
"More moving parts than a plain constructor for simple objects.",
|
|
368
|
+
],
|
|
369
|
+
related: ["abstract-factory", "kiss"],
|
|
370
|
+
collaboration: "Director drives a Builder through build steps; the ConcreteBuilder accumulates state and returns the Product.",
|
|
371
|
+
participants: [
|
|
372
|
+
{ role: "Product", kind: "class", members: ["-parts: string[]"] },
|
|
373
|
+
{ role: "Builder", kind: "interface", members: ["reset()", "buildPartA()", "buildPartB()", "getResult(): Product"] },
|
|
374
|
+
{ role: "ConcreteBuilder", kind: "class", members: ["reset()", "buildPartA()", "buildPartB()", "getResult(): Product"], note: "implements Builder" },
|
|
375
|
+
{ role: "Director", kind: "class", members: ["-builder: Builder", "construct() // calls buildPartA(); buildPartB()"] },
|
|
376
|
+
],
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
slug: "prototype",
|
|
380
|
+
name: "Prototype",
|
|
381
|
+
category: "creational",
|
|
382
|
+
aka: ["clone"],
|
|
383
|
+
summary: "Create new objects by cloning an existing instance.",
|
|
384
|
+
intent: "When creation is expensive or the concrete class is decided at runtime, " +
|
|
385
|
+
"copy a pre-built prototype instead of constructing from scratch.",
|
|
386
|
+
whenToUse: [
|
|
387
|
+
"Object setup is costly and many near-identical copies are needed.",
|
|
388
|
+
"The set of concrete types is fixed at runtime, not compile time.",
|
|
389
|
+
],
|
|
390
|
+
tradeoffs: [
|
|
391
|
+
"Deep vs. shallow copy of graphs is subtle and easy to get wrong.",
|
|
392
|
+
],
|
|
393
|
+
related: ["abstract-factory", "memento"],
|
|
394
|
+
collaboration: "Client calls clone() on a Prototype to get a new, independent instance.",
|
|
395
|
+
participants: [
|
|
396
|
+
{ role: "Prototype", kind: "interface", members: ["clone(): Prototype"] },
|
|
397
|
+
{ role: "ConcretePrototype", kind: "class", members: ["-state", "clone(): Prototype // copy self"], note: "implements Prototype" },
|
|
398
|
+
],
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
slug: "singleton",
|
|
402
|
+
name: "Singleton",
|
|
403
|
+
category: "creational",
|
|
404
|
+
summary: "Ensure a class has exactly one instance with a global access point.",
|
|
405
|
+
intent: "Guarantee a single shared instance and provide a way to reach it. Use " +
|
|
406
|
+
"sparingly — it's global mutable state in disguise and complicates " +
|
|
407
|
+
"testing and concurrency.",
|
|
408
|
+
whenToUse: [
|
|
409
|
+
"Exactly one instance must coordinate access to a shared resource.",
|
|
410
|
+
],
|
|
411
|
+
tradeoffs: [
|
|
412
|
+
"Hidden global state, hard to test, thread-safety pitfalls — often an anti-pattern.",
|
|
413
|
+
"Prefer passing one instance via dependency injection instead.",
|
|
414
|
+
],
|
|
415
|
+
related: ["abstract-factory", "dependency-inversion"],
|
|
416
|
+
collaboration: "Callers use Singleton.getInstance(); the constructor is private so no other instance can exist.",
|
|
417
|
+
participants: [
|
|
418
|
+
{
|
|
419
|
+
role: "Singleton",
|
|
420
|
+
kind: "class",
|
|
421
|
+
members: ["-static instance: Singleton", "-constructor() // private", "+static getInstance(): Singleton", "+businessMethod()"],
|
|
422
|
+
},
|
|
423
|
+
],
|
|
424
|
+
},
|
|
425
|
+
/* ---------- Structural ---------- */
|
|
426
|
+
{
|
|
427
|
+
slug: "adapter",
|
|
428
|
+
name: "Adapter",
|
|
429
|
+
category: "structural",
|
|
430
|
+
aka: ["wrapper"],
|
|
431
|
+
summary: "Make an incompatible interface fit the one a client expects.",
|
|
432
|
+
intent: "Wrap an existing class so its interface matches what the client needs, " +
|
|
433
|
+
"letting otherwise-incompatible types work together without changing " +
|
|
434
|
+
"either side.",
|
|
435
|
+
whenToUse: [
|
|
436
|
+
"You want to reuse a class whose interface doesn't match your code.",
|
|
437
|
+
"Integrating a third-party/legacy API behind your own contract.",
|
|
438
|
+
],
|
|
439
|
+
tradeoffs: [
|
|
440
|
+
"Another layer to trace through; overuse hides a mismatched design.",
|
|
441
|
+
],
|
|
442
|
+
related: ["facade", "decorator", "bridge"],
|
|
443
|
+
collaboration: "Adapter implements Target and translates each call into one on the wrapped Adaptee.",
|
|
444
|
+
participants: [
|
|
445
|
+
{ role: "Target", kind: "interface", members: ["request()"], note: "What the client expects." },
|
|
446
|
+
{ role: "Adaptee", kind: "class", members: ["specificRequest()"], note: "Existing, incompatible class." },
|
|
447
|
+
{ role: "Adapter", kind: "class", members: ["-adaptee: Adaptee", "request() // -> adaptee.specificRequest()"], note: "implements Target" },
|
|
448
|
+
],
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
slug: "bridge",
|
|
452
|
+
name: "Bridge",
|
|
453
|
+
category: "structural",
|
|
454
|
+
summary: "Split an abstraction from its implementation so both vary freely.",
|
|
455
|
+
intent: "Decouple a hierarchy of abstractions from a hierarchy of implementations " +
|
|
456
|
+
"by composition, avoiding a combinatorial explosion of subclasses.",
|
|
457
|
+
whenToUse: [
|
|
458
|
+
"Behavior varies along two independent dimensions (shape × renderer).",
|
|
459
|
+
"You'd otherwise get an M×N subclass explosion.",
|
|
460
|
+
],
|
|
461
|
+
tradeoffs: [
|
|
462
|
+
"Up-front indirection; overkill when only one dimension actually varies.",
|
|
463
|
+
],
|
|
464
|
+
related: ["abstract-factory", "adapter", "composition-over-inheritance"],
|
|
465
|
+
collaboration: "Abstraction delegates the work to its Implementor; each side subclasses independently.",
|
|
466
|
+
participants: [
|
|
467
|
+
{ role: "Abstraction", kind: "class", members: ["-impl: Implementor", "operation() // delegates to impl"] },
|
|
468
|
+
{ role: "RefinedAbstraction", kind: "class", members: ["operation()"], note: "extends Abstraction" },
|
|
469
|
+
{ role: "Implementor", kind: "interface", members: ["operationImpl()"] },
|
|
470
|
+
{ role: "ConcreteImplementor", kind: "class", members: ["operationImpl()"], note: "implements Implementor" },
|
|
471
|
+
],
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
slug: "composite",
|
|
475
|
+
name: "Composite",
|
|
476
|
+
category: "structural",
|
|
477
|
+
summary: "Treat individual objects and compositions uniformly (part-whole trees).",
|
|
478
|
+
intent: "Compose objects into tree structures and let clients treat leaves and " +
|
|
479
|
+
"branches through the same interface, so recursive structures are handled " +
|
|
480
|
+
"without special-casing.",
|
|
481
|
+
whenToUse: [
|
|
482
|
+
"You have a part-whole hierarchy (files/folders, UI trees).",
|
|
483
|
+
"Clients should ignore the difference between one item and a group.",
|
|
484
|
+
],
|
|
485
|
+
tradeoffs: [
|
|
486
|
+
"A uniform interface can make leaf-invalid operations (add/remove) awkward.",
|
|
487
|
+
],
|
|
488
|
+
related: ["decorator", "iterator", "visitor"],
|
|
489
|
+
collaboration: "Composite forwards operations to its children; a Leaf just does the work.",
|
|
490
|
+
participants: [
|
|
491
|
+
{ role: "Component", kind: "interface", members: ["operation()", "add(c: Component)", "remove(c: Component)"] },
|
|
492
|
+
{ role: "Leaf", kind: "class", members: ["operation()"], note: "implements Component; no children" },
|
|
493
|
+
{ role: "Composite", kind: "class", members: ["-children: Component[]", "operation() // forwards to each child", "add(c)", "remove(c)"], note: "implements Component" },
|
|
494
|
+
],
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
slug: "decorator",
|
|
498
|
+
name: "Decorator",
|
|
499
|
+
category: "structural",
|
|
500
|
+
aka: ["wrapper"],
|
|
501
|
+
summary: "Add responsibilities to an object dynamically by wrapping it.",
|
|
502
|
+
intent: "Attach behavior to an object at runtime by wrapping it in another object " +
|
|
503
|
+
"with the same interface — a flexible alternative to subclassing for " +
|
|
504
|
+
"extension.",
|
|
505
|
+
whenToUse: [
|
|
506
|
+
"You need to add/remove responsibilities without a subclass explosion.",
|
|
507
|
+
"Behaviors should be stackable and chosen at runtime.",
|
|
508
|
+
],
|
|
509
|
+
tradeoffs: [
|
|
510
|
+
"Many small wrappers; deep stacks are hard to debug and identity-sensitive.",
|
|
511
|
+
],
|
|
512
|
+
related: ["composite", "adapter", "composition-over-inheritance", "open-closed"],
|
|
513
|
+
collaboration: "Decorator implements Component, holds a wrapped Component, and adds behavior around delegating to it.",
|
|
514
|
+
participants: [
|
|
515
|
+
{ role: "Component", kind: "interface", members: ["operation()"] },
|
|
516
|
+
{ role: "ConcreteComponent", kind: "class", members: ["operation()"], note: "implements Component" },
|
|
517
|
+
{ role: "Decorator", kind: "abstract", members: ["-wrappee: Component", "operation() // -> wrappee.operation()"], note: "implements Component" },
|
|
518
|
+
{ role: "ConcreteDecorator", kind: "class", members: ["operation() // extra behavior + super"], note: "extends Decorator" },
|
|
519
|
+
],
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
slug: "facade",
|
|
523
|
+
name: "Facade",
|
|
524
|
+
category: "structural",
|
|
525
|
+
summary: "Provide one simple entry point over a complex subsystem.",
|
|
526
|
+
intent: "Offer a unified, high-level interface that hides the wiring of a " +
|
|
527
|
+
"subsystem, giving clients an easy default path while leaving the " +
|
|
528
|
+
"internals reachable for advanced use.",
|
|
529
|
+
whenToUse: [
|
|
530
|
+
"A subsystem is complex and most clients want a simple common path.",
|
|
531
|
+
"You want to decouple clients from many internal classes.",
|
|
532
|
+
],
|
|
533
|
+
tradeoffs: [
|
|
534
|
+
"Can become a god-object if it accretes logic instead of just delegating.",
|
|
535
|
+
],
|
|
536
|
+
related: ["adapter", "mediator", "separation-of-concerns", "law-of-demeter"],
|
|
537
|
+
collaboration: "Facade delegates client requests to the appropriate subsystem classes and orchestrates them.",
|
|
538
|
+
participants: [
|
|
539
|
+
{ role: "Facade", kind: "class", members: ["-a: SubsystemA", "-b: SubsystemB", "operation() // orchestrates a + b"] },
|
|
540
|
+
{ role: "SubsystemA", kind: "class", members: ["opA()"] },
|
|
541
|
+
{ role: "SubsystemB", kind: "class", members: ["opB()"] },
|
|
542
|
+
],
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
slug: "flyweight",
|
|
546
|
+
name: "Flyweight",
|
|
547
|
+
category: "structural",
|
|
548
|
+
summary: "Share fine-grained objects to fit many of them in memory.",
|
|
549
|
+
intent: "Split state into shared intrinsic state (stored once in a flyweight) and " +
|
|
550
|
+
"context-specific extrinsic state (passed in), so huge numbers of objects " +
|
|
551
|
+
"cost little memory.",
|
|
552
|
+
whenToUse: [
|
|
553
|
+
"You need a very large number of similar objects (glyphs, tiles, particles).",
|
|
554
|
+
"Most object state can be shared and the rest passed at call time.",
|
|
555
|
+
],
|
|
556
|
+
tradeoffs: [
|
|
557
|
+
"Trades CPU (passing extrinsic state) for memory; adds real complexity.",
|
|
558
|
+
],
|
|
559
|
+
related: ["factory-method", "singleton", "composite"],
|
|
560
|
+
collaboration: "FlyweightFactory returns a shared Flyweight per key; callers pass extrinsic state into operation().",
|
|
561
|
+
participants: [
|
|
562
|
+
{ role: "Flyweight", kind: "interface", members: ["operation(extrinsicState)"] },
|
|
563
|
+
{ role: "ConcreteFlyweight", kind: "class", members: ["-intrinsicState // shared", "operation(extrinsicState)"], note: "implements Flyweight" },
|
|
564
|
+
{ role: "FlyweightFactory", kind: "class", members: ["-pool: Map<key, Flyweight>", "getFlyweight(key): Flyweight // create-if-absent"] },
|
|
565
|
+
],
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
slug: "proxy",
|
|
569
|
+
name: "Proxy",
|
|
570
|
+
category: "structural",
|
|
571
|
+
summary: "Stand in for another object to control access to it.",
|
|
572
|
+
intent: "Provide a surrogate with the same interface as the real object to add " +
|
|
573
|
+
"access control, lazy loading, caching, remoting, or logging without " +
|
|
574
|
+
"changing the real subject or its clients.",
|
|
575
|
+
whenToUse: [
|
|
576
|
+
"You need lazy init, caching, access checks, or a remote stand-in.",
|
|
577
|
+
"Cross-cutting access concerns shouldn't live in the real object.",
|
|
578
|
+
],
|
|
579
|
+
tradeoffs: [
|
|
580
|
+
"Extra indirection and possible latency; can hide surprising behavior.",
|
|
581
|
+
],
|
|
582
|
+
related: ["decorator", "adapter", "facade"],
|
|
583
|
+
collaboration: "Proxy implements Subject and forwards to the RealSubject after doing its access/lazy/caching work.",
|
|
584
|
+
participants: [
|
|
585
|
+
{ role: "Subject", kind: "interface", members: ["request()"] },
|
|
586
|
+
{ role: "RealSubject", kind: "class", members: ["request() // the real work"], note: "implements Subject" },
|
|
587
|
+
{ role: "Proxy", kind: "class", members: ["-real: RealSubject", "request() // checks/caches, then real.request()"], note: "implements Subject" },
|
|
588
|
+
],
|
|
589
|
+
},
|
|
590
|
+
/* ---------- Behavioral ---------- */
|
|
591
|
+
{
|
|
592
|
+
slug: "chain-of-responsibility",
|
|
593
|
+
name: "Chain of Responsibility",
|
|
594
|
+
category: "behavioral",
|
|
595
|
+
aka: ["cor", "chain"],
|
|
596
|
+
summary: "Pass a request along a chain until a handler deals with it.",
|
|
597
|
+
intent: "Decouple sender from receiver by giving several objects a chance to " +
|
|
598
|
+
"handle a request; each either handles it or forwards it to the next.",
|
|
599
|
+
whenToUse: [
|
|
600
|
+
"More than one object may handle a request and the handler isn't known upfront.",
|
|
601
|
+
"Middleware / event-handling / approval pipelines.",
|
|
602
|
+
],
|
|
603
|
+
tradeoffs: [
|
|
604
|
+
"No guarantee a request is handled; chains can be hard to trace.",
|
|
605
|
+
],
|
|
606
|
+
related: ["command", "composite", "decorator"],
|
|
607
|
+
collaboration: "Each Handler holds a next handler; it handles the request or delegates to next.",
|
|
608
|
+
participants: [
|
|
609
|
+
{ role: "Handler", kind: "interface", members: ["setNext(h: Handler): Handler", "handle(request)"] },
|
|
610
|
+
{ role: "BaseHandler", kind: "abstract", members: ["-next: Handler", "setNext(h)", "handle(request) // -> next?.handle(request)"], note: "implements Handler" },
|
|
611
|
+
{ role: "ConcreteHandler", kind: "class", members: ["handle(request) // if canHandle: do it; else super"], note: "extends BaseHandler" },
|
|
612
|
+
],
|
|
613
|
+
},
|
|
614
|
+
{
|
|
615
|
+
slug: "command",
|
|
616
|
+
name: "Command",
|
|
617
|
+
category: "behavioral",
|
|
618
|
+
aka: ["action", "transaction"],
|
|
619
|
+
summary: "Turn a request into a first-class object.",
|
|
620
|
+
intent: "Encapsulate a request as an object so you can parameterize, queue, log, " +
|
|
621
|
+
"and undo operations, decoupling the invoker from the receiver that does " +
|
|
622
|
+
"the work.",
|
|
623
|
+
whenToUse: [
|
|
624
|
+
"You need undo/redo, queuing, scheduling, or logging of operations.",
|
|
625
|
+
"You want to parameterize objects with an action to run later.",
|
|
626
|
+
],
|
|
627
|
+
tradeoffs: [
|
|
628
|
+
"A class per operation; simple direct calls don't need it.",
|
|
629
|
+
],
|
|
630
|
+
related: ["chain-of-responsibility", "memento", "strategy"],
|
|
631
|
+
collaboration: "Invoker triggers a Command's execute(); the Command calls the Receiver that performs the action.",
|
|
632
|
+
participants: [
|
|
633
|
+
{ role: "Command", kind: "interface", members: ["execute()", "undo() // optional: only when supporting undo/redo"] },
|
|
634
|
+
{ role: "ConcreteCommand", kind: "class", members: ["-receiver: Receiver", "execute() // -> receiver.action()", "undo()"], note: "implements Command" },
|
|
635
|
+
{ role: "Receiver", kind: "class", members: ["action() // the real work"] },
|
|
636
|
+
{ role: "Invoker", kind: "class", members: ["-command: Command", "invoke() // -> command.execute()"] },
|
|
637
|
+
],
|
|
638
|
+
},
|
|
639
|
+
{
|
|
640
|
+
slug: "interpreter",
|
|
641
|
+
name: "Interpreter",
|
|
642
|
+
category: "behavioral",
|
|
643
|
+
summary: "Represent a small language's grammar and evaluate its sentences.",
|
|
644
|
+
intent: "Given a simple, stable language, model each grammar rule as a class and " +
|
|
645
|
+
"interpret expressions by walking the resulting tree.",
|
|
646
|
+
whenToUse: [
|
|
647
|
+
"A simple, well-defined grammar (filters, rules, arithmetic) recurs.",
|
|
648
|
+
"The grammar is stable and efficiency isn't critical.",
|
|
649
|
+
],
|
|
650
|
+
tradeoffs: [
|
|
651
|
+
"A class per rule doesn't scale to complex grammars — use a real parser.",
|
|
652
|
+
],
|
|
653
|
+
related: ["composite", "visitor"],
|
|
654
|
+
collaboration: "Each Expression interprets itself against a Context; nonterminals recurse into sub-expressions.",
|
|
655
|
+
participants: [
|
|
656
|
+
{ role: "Expression", kind: "interface", members: ["interpret(context): value"] },
|
|
657
|
+
{ role: "TerminalExpression", kind: "class", members: ["interpret(context)"], note: "implements Expression" },
|
|
658
|
+
{ role: "NonterminalExpression", kind: "class", members: ["-children: Expression[]", "interpret(context) // combines children"], note: "implements Expression" },
|
|
659
|
+
{ role: "Context", kind: "class", members: ["-variables"] },
|
|
660
|
+
],
|
|
661
|
+
},
|
|
662
|
+
{
|
|
663
|
+
slug: "iterator",
|
|
664
|
+
name: "Iterator",
|
|
665
|
+
category: "behavioral",
|
|
666
|
+
aka: ["cursor"],
|
|
667
|
+
summary: "Traverse a collection without exposing its representation.",
|
|
668
|
+
intent: "Provide a uniform way to walk elements of an aggregate sequentially " +
|
|
669
|
+
"without revealing whether it's an array, tree, or list underneath.",
|
|
670
|
+
whenToUse: [
|
|
671
|
+
"You want a standard traversal API decoupled from the container's shape.",
|
|
672
|
+
"Multiple simultaneous or alternative traversals are needed.",
|
|
673
|
+
],
|
|
674
|
+
tradeoffs: [
|
|
675
|
+
"Most languages provide iterators natively — rarely hand-rolled today.",
|
|
676
|
+
],
|
|
677
|
+
related: ["composite", "factory-method"],
|
|
678
|
+
collaboration: "Aggregate creates an Iterator; the client advances it via hasNext()/next().",
|
|
679
|
+
participants: [
|
|
680
|
+
{ role: "Iterator", kind: "interface", members: ["hasNext(): boolean", "next(): T"] },
|
|
681
|
+
{ role: "Aggregate", kind: "interface", members: ["createIterator(): Iterator"] },
|
|
682
|
+
{ role: "ConcreteIterator", kind: "class", members: ["-collection", "-cursor", "hasNext()", "next()"], note: "implements Iterator" },
|
|
683
|
+
{ role: "ConcreteAggregate", kind: "class", members: ["createIterator(): Iterator"], note: "implements Aggregate" },
|
|
684
|
+
],
|
|
685
|
+
},
|
|
686
|
+
{
|
|
687
|
+
slug: "mediator",
|
|
688
|
+
name: "Mediator",
|
|
689
|
+
category: "behavioral",
|
|
690
|
+
aka: ["controller"],
|
|
691
|
+
summary: "Centralize how a set of objects interact.",
|
|
692
|
+
intent: "Replace many-to-many object references with a hub: colleagues talk to " +
|
|
693
|
+
"the mediator, not each other, reducing coupling and taming interaction " +
|
|
694
|
+
"logic.",
|
|
695
|
+
whenToUse: [
|
|
696
|
+
"Objects reference each other in a tangled web (e.g. form widgets).",
|
|
697
|
+
"Interaction logic is scattered and hard to reuse.",
|
|
698
|
+
],
|
|
699
|
+
tradeoffs: [
|
|
700
|
+
"The mediator can swell into a god-object holding all the logic.",
|
|
701
|
+
],
|
|
702
|
+
related: ["facade", "observer", "command"],
|
|
703
|
+
collaboration: "A Colleague notifies its Mediator of events; the ConcreteMediator coordinates the other colleagues.",
|
|
704
|
+
participants: [
|
|
705
|
+
{ role: "Mediator", kind: "interface", members: ["notify(sender, event)"] },
|
|
706
|
+
{ role: "ConcreteMediator", kind: "class", members: ["-colleagues", "notify(sender, event) // coordinates"], note: "implements Mediator" },
|
|
707
|
+
{ role: "Colleague", kind: "class", members: ["-mediator: Mediator", "changed() // -> mediator.notify(this, ...)"] },
|
|
708
|
+
],
|
|
709
|
+
},
|
|
710
|
+
{
|
|
711
|
+
slug: "memento",
|
|
712
|
+
name: "Memento",
|
|
713
|
+
category: "behavioral",
|
|
714
|
+
aka: ["token", "snapshot"],
|
|
715
|
+
summary: "Capture and restore an object's state without breaking encapsulation.",
|
|
716
|
+
intent: "Externalize a snapshot of an object's internal state into an opaque " +
|
|
717
|
+
"memento so it can be restored later (undo), without exposing the " +
|
|
718
|
+
"object's internals to the caretaker.",
|
|
719
|
+
whenToUse: [
|
|
720
|
+
"You need undo/rollback or checkpoints of an object's state.",
|
|
721
|
+
],
|
|
722
|
+
tradeoffs: [
|
|
723
|
+
"Snapshots can be memory-heavy; defining what to save is fiddly.",
|
|
724
|
+
],
|
|
725
|
+
related: ["command", "prototype"],
|
|
726
|
+
collaboration: "Originator writes its state into a Memento; a Caretaker stores mementos and hands one back to restore().",
|
|
727
|
+
participants: [
|
|
728
|
+
{ role: "Originator", kind: "class", members: ["-state", "save(): Memento", "restore(m: Memento)"] },
|
|
729
|
+
{ role: "Memento", kind: "class", members: ["-state // opaque to Caretaker", "getState() // Originator-only"] },
|
|
730
|
+
{ role: "Caretaker", kind: "class", members: ["-history: Memento[]", "backup()", "undo()"] },
|
|
731
|
+
],
|
|
732
|
+
},
|
|
733
|
+
{
|
|
734
|
+
slug: "observer",
|
|
735
|
+
name: "Observer",
|
|
736
|
+
category: "behavioral",
|
|
737
|
+
aka: ["publish-subscribe", "pubsub", "dependents"],
|
|
738
|
+
summary: "Notify many dependents automatically when a subject changes.",
|
|
739
|
+
intent: "Define a one-to-many dependency so that when one object changes state, " +
|
|
740
|
+
"all its observers are notified and updated — the backbone of event and " +
|
|
741
|
+
"reactive systems.",
|
|
742
|
+
whenToUse: [
|
|
743
|
+
"A change to one object requires updating an unknown number of others.",
|
|
744
|
+
"You want loose coupling between an event source and its listeners.",
|
|
745
|
+
],
|
|
746
|
+
tradeoffs: [
|
|
747
|
+
"Update storms, ordering surprises, and leaks from un-detached observers.",
|
|
748
|
+
],
|
|
749
|
+
related: ["mediator", "state", "separation-of-concerns"],
|
|
750
|
+
collaboration: "Subject keeps a list of Observers and calls update() on each when it changes.",
|
|
751
|
+
participants: [
|
|
752
|
+
{ role: "Subject", kind: "class", members: ["-observers: Observer[]", "attach(o)", "detach(o)", "notify() // for each o: o.update(this)"] },
|
|
753
|
+
{ role: "Observer", kind: "interface", members: ["update(subject)"] },
|
|
754
|
+
{ role: "ConcreteObserver", kind: "class", members: ["update(subject) // react to change"], note: "implements Observer" },
|
|
755
|
+
],
|
|
756
|
+
},
|
|
757
|
+
{
|
|
758
|
+
slug: "state",
|
|
759
|
+
name: "State",
|
|
760
|
+
category: "behavioral",
|
|
761
|
+
summary: "Let an object change its behavior when its internal state changes.",
|
|
762
|
+
intent: "Encapsulate state-specific behavior in separate state objects and " +
|
|
763
|
+
"delegate to the current one, so the object appears to change class — " +
|
|
764
|
+
"replacing sprawling state conditionals.",
|
|
765
|
+
whenToUse: [
|
|
766
|
+
"Behavior depends on state and there are many state-dependent conditionals.",
|
|
767
|
+
"State transitions form a clear machine.",
|
|
768
|
+
],
|
|
769
|
+
tradeoffs: [
|
|
770
|
+
"A class per state; overkill for two trivial states.",
|
|
771
|
+
],
|
|
772
|
+
related: ["strategy", "observer"],
|
|
773
|
+
collaboration: "Context delegates to its current State; a state can switch the Context to another state.",
|
|
774
|
+
participants: [
|
|
775
|
+
{ role: "Context", kind: "class", members: ["-state: State", "request() // -> state.handle(this)", "setState(s: State)"] },
|
|
776
|
+
{ role: "State", kind: "interface", members: ["handle(context)"] },
|
|
777
|
+
{ role: "ConcreteState", kind: "class", members: ["handle(context) // behavior + maybe context.setState(...)"], note: "implements State" },
|
|
778
|
+
],
|
|
779
|
+
},
|
|
780
|
+
{
|
|
781
|
+
slug: "strategy",
|
|
782
|
+
name: "Strategy",
|
|
783
|
+
category: "behavioral",
|
|
784
|
+
aka: ["policy"],
|
|
785
|
+
summary: "Make a family of algorithms interchangeable at runtime.",
|
|
786
|
+
intent: "Define each algorithm in its own class behind a common interface and let " +
|
|
787
|
+
"the context pick one at runtime — the go-to way to satisfy OCP for " +
|
|
788
|
+
"varying behavior.",
|
|
789
|
+
whenToUse: [
|
|
790
|
+
"You have several interchangeable ways to do one thing (sort, pricing, routing).",
|
|
791
|
+
"You'd otherwise select behavior with a conditional.",
|
|
792
|
+
],
|
|
793
|
+
tradeoffs: [
|
|
794
|
+
"Clients must know the strategies to choose; extra objects for trivial cases.",
|
|
795
|
+
],
|
|
796
|
+
related: ["state", "open-closed", "dependency-inversion", "command"],
|
|
797
|
+
collaboration: "Context holds a Strategy and calls execute() on it; swap the strategy to change behavior.",
|
|
798
|
+
participants: [
|
|
799
|
+
{ role: "Strategy", kind: "interface", members: ["execute(data): result"] },
|
|
800
|
+
{ role: "ConcreteStrategy", kind: "class", members: ["execute(data): result"], note: "implements Strategy" },
|
|
801
|
+
{ role: "Context", kind: "class", members: ["-strategy: Strategy", "setStrategy(s)", "doWork() // -> strategy.execute(...)"] },
|
|
802
|
+
],
|
|
803
|
+
},
|
|
804
|
+
{
|
|
805
|
+
slug: "template-method",
|
|
806
|
+
name: "Template Method",
|
|
807
|
+
category: "behavioral",
|
|
808
|
+
summary: "Fix an algorithm's skeleton, let subclasses fill in the steps.",
|
|
809
|
+
intent: "Define the invariant structure of an algorithm in a base method and " +
|
|
810
|
+
"defer specific steps to subclasses, so the overall flow is shared and " +
|
|
811
|
+
"only the varying steps are overridden.",
|
|
812
|
+
whenToUse: [
|
|
813
|
+
"Several algorithms share a structure but differ in a few steps.",
|
|
814
|
+
"You want to localize the common flow and forbid changing its order.",
|
|
815
|
+
],
|
|
816
|
+
tradeoffs: [
|
|
817
|
+
"Inheritance-based — less flexible than Strategy's composition.",
|
|
818
|
+
],
|
|
819
|
+
related: ["strategy", "factory-method", "inheritance"],
|
|
820
|
+
collaboration: "The base templateMethod() calls primitive steps; subclasses override the steps, not the flow.",
|
|
821
|
+
participants: [
|
|
822
|
+
{ role: "AbstractClass", kind: "abstract", members: ["templateMethod() // final: step1(); step2()", "step1() // abstract", "step2() // abstract"] },
|
|
823
|
+
{ role: "ConcreteClass", kind: "class", members: ["step1()", "step2()"], note: "extends AbstractClass" },
|
|
824
|
+
],
|
|
825
|
+
},
|
|
826
|
+
{
|
|
827
|
+
slug: "visitor",
|
|
828
|
+
name: "Visitor",
|
|
829
|
+
category: "behavioral",
|
|
830
|
+
summary: "Add operations to an object structure without changing its classes.",
|
|
831
|
+
intent: "Represent an operation to perform on the elements of an object structure. " +
|
|
832
|
+
"Visitor lets you add new operations by adding a visitor class instead of " +
|
|
833
|
+
"editing every element type.",
|
|
834
|
+
whenToUse: [
|
|
835
|
+
"A stable set of element types needs many unrelated operations.",
|
|
836
|
+
"Operations should live together, separate from the element classes.",
|
|
837
|
+
],
|
|
838
|
+
tradeoffs: [
|
|
839
|
+
"Adding a new *element* type forces changing every visitor (dual to OCP).",
|
|
840
|
+
"Double-dispatch boilerplate; breaks element encapsulation somewhat.",
|
|
841
|
+
],
|
|
842
|
+
related: ["composite", "interpreter", "iterator"],
|
|
843
|
+
collaboration: "Element.accept(visitor) calls back visitor.visitElementX(this) — double dispatch selects the operation.",
|
|
844
|
+
participants: [
|
|
845
|
+
{ role: "Visitor", kind: "interface", members: ["visitElementA(a: ElementA)", "visitElementB(b: ElementB)"] },
|
|
846
|
+
{ role: "ConcreteVisitor", kind: "class", members: ["visitElementA(a)", "visitElementB(b)"], note: "implements Visitor" },
|
|
847
|
+
{ role: "Element", kind: "interface", members: ["accept(v: Visitor)"] },
|
|
848
|
+
{ role: "ElementA", kind: "class", members: ["accept(v) // -> v.visitElementA(this)"], note: "implements Element" },
|
|
849
|
+
{ role: "ElementB", kind: "class", members: ["accept(v) // -> v.visitElementB(this)"], note: "implements Element — a second element type is what makes double dispatch worthwhile" },
|
|
850
|
+
],
|
|
851
|
+
},
|
|
852
|
+
];
|
|
853
|
+
/* ------------------------------------------------------------------ *
|
|
854
|
+
* Lookup + rendering
|
|
855
|
+
* ------------------------------------------------------------------ */
|
|
856
|
+
export const ALL = [...PRINCIPLES, ...PATTERNS];
|
|
857
|
+
/** Normalize a query for fuzzy matching: lowercase, strip non-alphanumerics. */
|
|
858
|
+
function normalize(text) {
|
|
859
|
+
return text.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
860
|
+
}
|
|
861
|
+
const CONCEPTS_BY_KEY = new Map();
|
|
862
|
+
for (const concept of ALL) {
|
|
863
|
+
CONCEPTS_BY_KEY.set(normalize(concept.slug), concept);
|
|
864
|
+
CONCEPTS_BY_KEY.set(normalize(concept.name), concept);
|
|
865
|
+
for (const alias of concept.aka ?? [])
|
|
866
|
+
CONCEPTS_BY_KEY.set(normalize(alias), concept);
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* Resolve a concept by slug, display name, alias, or a close-enough query
|
|
870
|
+
* (e.g. "SRP", "single responsibility", "factory"). Returns null if nothing
|
|
871
|
+
* matches confidently.
|
|
872
|
+
*/
|
|
873
|
+
export function findConcept(query) {
|
|
874
|
+
const normalizedQuery = normalize(query);
|
|
875
|
+
if (!normalizedQuery)
|
|
876
|
+
return null;
|
|
877
|
+
const exactMatch = CONCEPTS_BY_KEY.get(normalizedQuery);
|
|
878
|
+
if (exactMatch)
|
|
879
|
+
return exactMatch;
|
|
880
|
+
// Too short for a safe fuzzy match — an exact hit was the only chance.
|
|
881
|
+
if (normalizedQuery.length < 3)
|
|
882
|
+
return null;
|
|
883
|
+
// Score each concept by its best key overlap. Two directions:
|
|
884
|
+
// • query is a substring of a key ("factory" ⊂ "factorymethod") — strong.
|
|
885
|
+
// • key is a substring of the query ("factorymethod" ⊂ "factory method
|
|
886
|
+
// pattern") — weaker, and only for keys long enough (≥5) to be
|
|
887
|
+
// meaningful, so short aliases like "dry"/"soc" can't match unrelated
|
|
888
|
+
// long queries.
|
|
889
|
+
let best = null;
|
|
890
|
+
for (const concept of ALL) {
|
|
891
|
+
const keys = [concept.slug, concept.name, ...(concept.aka ?? [])]
|
|
892
|
+
.map(normalize)
|
|
893
|
+
.filter((key) => key.length >= 3);
|
|
894
|
+
let score = Infinity;
|
|
895
|
+
for (const key of keys) {
|
|
896
|
+
if (key.includes(normalizedQuery)) {
|
|
897
|
+
score = Math.min(score, key.length - normalizedQuery.length);
|
|
898
|
+
}
|
|
899
|
+
else if (key.length >= 5 && normalizedQuery.includes(key)) {
|
|
900
|
+
score = Math.min(score, 100 + (normalizedQuery.length - key.length));
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
if (score === Infinity)
|
|
904
|
+
continue;
|
|
905
|
+
if (!best ||
|
|
906
|
+
score < best.score ||
|
|
907
|
+
(score === best.score && concept.name.length < best.concept.name.length)) {
|
|
908
|
+
best = { concept, score };
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
return best?.concept ?? null;
|
|
912
|
+
}
|
|
913
|
+
export const CATEGORY_LABEL = {
|
|
914
|
+
principle: "Principle",
|
|
915
|
+
creational: "Creational pattern",
|
|
916
|
+
structural: "Structural pattern",
|
|
917
|
+
behavioral: "Behavioral pattern",
|
|
918
|
+
};
|
|
919
|
+
/** Render a single concept as Markdown (used by explain_concept + resources). */
|
|
920
|
+
export function conceptToMarkdown(concept) {
|
|
921
|
+
const lines = [];
|
|
922
|
+
lines.push(`## ${concept.name}`);
|
|
923
|
+
lines.push(`_${CATEGORY_LABEL[concept.category]}_ — ${concept.summary}`);
|
|
924
|
+
lines.push("");
|
|
925
|
+
lines.push(concept.intent);
|
|
926
|
+
lines.push("");
|
|
927
|
+
lines.push("**When to use**");
|
|
928
|
+
for (const useCase of concept.whenToUse)
|
|
929
|
+
lines.push(`- ${useCase}`);
|
|
930
|
+
lines.push("");
|
|
931
|
+
lines.push("**Trade-offs & cautions**");
|
|
932
|
+
for (const tradeoff of concept.tradeoffs)
|
|
933
|
+
lines.push(`- ${tradeoff}`);
|
|
934
|
+
if (concept.collaboration) {
|
|
935
|
+
lines.push("");
|
|
936
|
+
lines.push(`**How it works** — ${concept.collaboration}`);
|
|
937
|
+
}
|
|
938
|
+
if (concept.participants?.length) {
|
|
939
|
+
lines.push("");
|
|
940
|
+
lines.push("**Participants**");
|
|
941
|
+
for (const participant of concept.participants) {
|
|
942
|
+
lines.push(`- \`${participant.role}\` (${participant.kind})${participant.note ? ` — ${participant.note}` : ""}`);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
if (concept.related?.length) {
|
|
946
|
+
lines.push("");
|
|
947
|
+
lines.push(`**See also**: ${concept.related.join(", ")}`);
|
|
948
|
+
}
|
|
949
|
+
return lines.join("\n");
|
|
950
|
+
}
|
|
951
|
+
/** One-line catalog entry: "`slug` — Name: summary". */
|
|
952
|
+
export function conceptLine(concept) {
|
|
953
|
+
return `\`${concept.slug}\` — ${concept.name}: ${concept.summary}`;
|
|
954
|
+
}
|
|
955
|
+
/** Render the principles reference (design://principles). */
|
|
956
|
+
export function principlesMarkdown() {
|
|
957
|
+
return ("# Software-Design Principles\n\n" +
|
|
958
|
+
"SOLID, the OOP pillars, and the day-to-day heuristics (DRY, KISS, YAGNI, …). " +
|
|
959
|
+
"These are judgment tools, not lint rules — apply them where they earn their keep.\n\n" +
|
|
960
|
+
PRINCIPLES.map(conceptToMarkdown).join("\n\n---\n\n") +
|
|
961
|
+
"\n");
|
|
962
|
+
}
|
|
963
|
+
/** Render the GoF pattern catalog (design://patterns). */
|
|
964
|
+
export function patternsMarkdown() {
|
|
965
|
+
const categories = ["creational", "structural", "behavioral"];
|
|
966
|
+
const sections = categories.map((category) => {
|
|
967
|
+
const patternsInCategory = PATTERNS.filter((pattern) => pattern.category === category);
|
|
968
|
+
const heading = category[0].toUpperCase() + category.slice(1);
|
|
969
|
+
return (`# ${heading} patterns\n\n` +
|
|
970
|
+
patternsInCategory.map(conceptToMarkdown).join("\n\n---\n\n"));
|
|
971
|
+
});
|
|
972
|
+
return ("# Gang-of-Four Design Patterns (23)\n\n" +
|
|
973
|
+
"Named, reusable solutions to recurring design problems. A pattern is a " +
|
|
974
|
+
"vocabulary, not a goal — reach for one only when the problem it solves is " +
|
|
975
|
+
"actually present.\n\n" +
|
|
976
|
+
sections.join("\n\n") +
|
|
977
|
+
"\n");
|
|
978
|
+
}
|