code-ai-installer 1.4.0 → 1.5.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/.agents/skills/design_patterns_reference/SKILL.md +694 -0
- package/.agents/skills/tanstack_beast_practices/SKILL.md +3 -0
- package/AGENTS.md +1 -0
- package/agents/architect.md +55 -0
- package/locales/en/.agents/skills/design_patterns_reference/SKILL.md +694 -0
- package/locales/en/.agents/skills/tanstack_beast_practices/SKILL.md +3 -0
- package/locales/en/AGENTS.md +1 -0
- package/locales/en/agents/architect.md +56 -1
- package/package.json +1 -1
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: design_patterns_reference
|
|
3
|
+
description: Справочник паттернов проектирования с DO/DON'T примерами — SOLID, DRY/KISS/YAGNI, GoF (Strategy, Observer, Factory, Adapter, Facade, Decorator, Command, State, Template Method), архитектурные (Repository, Service Layer, DI, Event-Driven, CQRS), микросервисные (Saga, Circuit Breaker). Language-agnostic псевдокод. Используй при проектировании модулей, ревью архитектуры, или при вопросах «какой паттерн применить».
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Skill: Design Patterns Reference
|
|
7
|
+
|
|
8
|
+
DO/DON'T справочник паттернов проектирования. Language-agnostic псевдокод.
|
|
9
|
+
|
|
10
|
+
**Разделы:**
|
|
11
|
+
1. [SOLID](#1-solid)
|
|
12
|
+
2. [Fundamental Principles](#2-fundamental-principles)
|
|
13
|
+
3. [GoF Patterns](#3-gof-patterns)
|
|
14
|
+
4. [Architectural Patterns](#4-architectural-patterns)
|
|
15
|
+
5. [Microservices Patterns](#5-microservices-patterns)
|
|
16
|
+
6. [Правило выбора паттерна](#6-правило-выбора)
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 1. SOLID
|
|
21
|
+
|
|
22
|
+
### S — Single Responsibility Principle (SRP)
|
|
23
|
+
> Класс/модуль имеет ровно одну причину для изменения.
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
// ✅ DO: каждый класс — одна ответственность
|
|
27
|
+
class UserValidator
|
|
28
|
+
validate(user) → ValidationResult
|
|
29
|
+
|
|
30
|
+
class UserRepository
|
|
31
|
+
save(user) → void
|
|
32
|
+
|
|
33
|
+
class EmailNotifier
|
|
34
|
+
sendWelcome(user) → void
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
// ❌ DON'T: один класс делает всё
|
|
39
|
+
class UserManager
|
|
40
|
+
validate(user) → ...
|
|
41
|
+
saveToDb(user) → ...
|
|
42
|
+
sendEmail(user) → ...
|
|
43
|
+
generateReport() → ...
|
|
44
|
+
// Изменение email-логики ломает валидацию и БД
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### O — Open/Closed Principle (OCP)
|
|
48
|
+
> Открыт для расширения, закрыт для модификации.
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
// ✅ DO: новый тип = новый класс, без изменения существующего кода
|
|
52
|
+
interface DiscountStrategy
|
|
53
|
+
calculate(order) → number
|
|
54
|
+
|
|
55
|
+
class PercentDiscount implements DiscountStrategy
|
|
56
|
+
calculate(order) → order.total * this.percent
|
|
57
|
+
|
|
58
|
+
class FixedDiscount implements DiscountStrategy
|
|
59
|
+
calculate(order) → this.amount
|
|
60
|
+
|
|
61
|
+
class OrderService
|
|
62
|
+
applyDiscount(order, strategy: DiscountStrategy)
|
|
63
|
+
order.discount = strategy.calculate(order)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
// ❌ DON'T: каждый новый тип скидки — правка if/switch
|
|
68
|
+
class OrderService
|
|
69
|
+
applyDiscount(order, type)
|
|
70
|
+
if type == "percent" → ...
|
|
71
|
+
else if type == "fixed" → ...
|
|
72
|
+
else if type == "seasonal" → ... // каждый раз меняем этот метод
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### L — Liskov Substitution Principle (LSP)
|
|
76
|
+
> Подтипы можно подставлять вместо базового типа без поломки.
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
// ✅ DO: подтипы честно реализуют контракт
|
|
80
|
+
interface Shape
|
|
81
|
+
area() → number
|
|
82
|
+
|
|
83
|
+
class Rectangle implements Shape
|
|
84
|
+
area() → this.width * this.height
|
|
85
|
+
|
|
86
|
+
class Circle implements Shape
|
|
87
|
+
area() → π * this.radius²
|
|
88
|
+
|
|
89
|
+
// Любой Shape можно передать в calculateTotalArea(shapes[])
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
// ❌ DON'T: подтип нарушает контракт родителя
|
|
94
|
+
class Rectangle
|
|
95
|
+
setWidth(w), setHeight(h)
|
|
96
|
+
|
|
97
|
+
class Square extends Rectangle
|
|
98
|
+
setWidth(w) → this.width = w; this.height = w // сюрприз: setWidth меняет height
|
|
99
|
+
// Код, который ожидает Rectangle, ломается
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### I — Interface Segregation Principle (ISP)
|
|
103
|
+
> Много маленьких интерфейсов лучше одного толстого.
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
// ✅ DO: клиент зависит только от нужных методов
|
|
107
|
+
interface Readable
|
|
108
|
+
read() → Data
|
|
109
|
+
|
|
110
|
+
interface Writable
|
|
111
|
+
write(data) → void
|
|
112
|
+
|
|
113
|
+
interface Deletable
|
|
114
|
+
delete(id) → void
|
|
115
|
+
|
|
116
|
+
class FileStore implements Readable, Writable, Deletable
|
|
117
|
+
class ReadOnlyCache implements Readable
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
// ❌ DON'T: один толстый интерфейс — клиенты зависят от лишнего
|
|
122
|
+
interface Storage
|
|
123
|
+
read() → Data
|
|
124
|
+
write(data) → void
|
|
125
|
+
delete(id) → void
|
|
126
|
+
backup() → void
|
|
127
|
+
migrate() → void
|
|
128
|
+
|
|
129
|
+
class SimpleCache implements Storage
|
|
130
|
+
backup() → throw "Not supported" // нарушение контракта
|
|
131
|
+
migrate() → throw "Not supported"
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### D — Dependency Inversion Principle (DIP)
|
|
135
|
+
> Завись от абстракций, не от конкретных реализаций.
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
// ✅ DO: бизнес-логика зависит от интерфейса
|
|
139
|
+
interface PaymentGateway
|
|
140
|
+
charge(amount) → Result
|
|
141
|
+
|
|
142
|
+
class OrderService
|
|
143
|
+
constructor(gateway: PaymentGateway) // инъекция абстракции
|
|
144
|
+
checkout(order) → this.gateway.charge(order.total)
|
|
145
|
+
|
|
146
|
+
// Легко подменить: StripeGateway, PayPalGateway, TestGateway
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
// ❌ DON'T: прямая зависимость от конкретики
|
|
151
|
+
class OrderService
|
|
152
|
+
checkout(order)
|
|
153
|
+
stripe = new StripeClient(API_KEY) // жёстко привязан к Stripe
|
|
154
|
+
stripe.charge(order.total)
|
|
155
|
+
// Нельзя тестировать, нельзя заменить
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## 2. Fundamental Principles
|
|
161
|
+
|
|
162
|
+
### DRY — Don't Repeat Yourself
|
|
163
|
+
```
|
|
164
|
+
// ✅ DO: переиспользуемая функция
|
|
165
|
+
function formatCurrency(amount, currency)
|
|
166
|
+
return currency.symbol + amount.toFixed(2)
|
|
167
|
+
|
|
168
|
+
// Используется в Invoice, Cart, Report
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
// ❌ DON'T: копипаст одной и той же логики
|
|
173
|
+
// В Invoice: "$" + amount.toFixed(2)
|
|
174
|
+
// В Cart: "$" + amount.toFixed(2)
|
|
175
|
+
// В Report: "$" + amount.toFixed(2)
|
|
176
|
+
// Изменение формата → правка в 3+ местах
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
> ⚠️ **Осторожно:** DRY — про знание, не про код. Два одинаковых куска кода с разными причинами изменения — это НЕ дублирование.
|
|
180
|
+
|
|
181
|
+
### KISS — Keep It Simple, Stupid
|
|
182
|
+
```
|
|
183
|
+
// ✅ DO: простое и читаемое решение
|
|
184
|
+
function isAdult(age)
|
|
185
|
+
return age >= 18
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
```
|
|
189
|
+
// ❌ DON'T: переусложнение
|
|
190
|
+
function isAdult(age)
|
|
191
|
+
ageValidator = new AgeValidatorFactory.create("adult")
|
|
192
|
+
rule = ageValidator.getRuleEngine().getRule("minimum")
|
|
193
|
+
return rule.evaluate(new AgeContext(age))
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### YAGNI — You Aren't Gonna Need It
|
|
197
|
+
```
|
|
198
|
+
// ✅ DO: реализуй то, что нужно сейчас
|
|
199
|
+
class UserService
|
|
200
|
+
getUser(id) → User
|
|
201
|
+
createUser(data) → User
|
|
202
|
+
|
|
203
|
+
// ❌ DON'T: "на будущее"
|
|
204
|
+
class UserService
|
|
205
|
+
getUser(id) → User
|
|
206
|
+
createUser(data) → User
|
|
207
|
+
exportToXml() → ... // никто не просил
|
|
208
|
+
syncWithLdap() → ... // нет требования
|
|
209
|
+
generatePdfReport() → ... // "вдруг пригодится"
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Composition over Inheritance
|
|
213
|
+
```
|
|
214
|
+
// ✅ DO: композиция — гибко, тестируемо
|
|
215
|
+
class Logger
|
|
216
|
+
log(msg) → ...
|
|
217
|
+
|
|
218
|
+
class HttpClient
|
|
219
|
+
constructor(logger: Logger)
|
|
220
|
+
get(url) → this.logger.log(...); fetch(url)
|
|
221
|
+
|
|
222
|
+
class CachedHttpClient
|
|
223
|
+
constructor(client: HttpClient, cache: Cache)
|
|
224
|
+
get(url) → cache.get(url) ?? client.get(url)
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
```
|
|
228
|
+
// ❌ DON'T: глубокая иерархия наследования
|
|
229
|
+
class BaseClient → class HttpClient → class AuthHttpClient → class CachedAuthHttpClient
|
|
230
|
+
// 4 уровня наследования — хрупко, сложно менять
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Fail Fast
|
|
234
|
+
```
|
|
235
|
+
// ✅ DO: проверяй входы сразу
|
|
236
|
+
function transfer(from, to, amount)
|
|
237
|
+
if amount <= 0 → throw InvalidAmountError
|
|
238
|
+
if from.balance < amount → throw InsufficientFundsError
|
|
239
|
+
// ... основная логика только если всё валидно
|
|
240
|
+
|
|
241
|
+
// ❌ DON'T: ошибка всплывает где-то глубоко внутри
|
|
242
|
+
function transfer(from, to, amount)
|
|
243
|
+
from.balance -= amount // может стать отрицательным
|
|
244
|
+
to.balance += amount // данные уже испорчены
|
|
245
|
+
if from.balance < 0 → ... // поздно!
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## 3. GoF Patterns
|
|
251
|
+
|
|
252
|
+
### Strategy — заменяемые алгоритмы
|
|
253
|
+
```
|
|
254
|
+
// ✅ DO: алгоритм как инъектируемая стратегия
|
|
255
|
+
interface SortStrategy
|
|
256
|
+
sort(items[]) → items[]
|
|
257
|
+
|
|
258
|
+
class BubbleSort implements SortStrategy
|
|
259
|
+
class QuickSort implements SortStrategy
|
|
260
|
+
class MergeSort implements SortStrategy
|
|
261
|
+
|
|
262
|
+
class DataProcessor
|
|
263
|
+
constructor(strategy: SortStrategy)
|
|
264
|
+
process(data) → this.strategy.sort(data)
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**Когда:** несколько вариантов алгоритма, выбор в runtime.
|
|
268
|
+
|
|
269
|
+
### Observer — уведомления об изменениях
|
|
270
|
+
```
|
|
271
|
+
// ✅ DO: подписка/уведомление без жёсткой связи
|
|
272
|
+
class EventBus
|
|
273
|
+
subscribers = Map<string, Function[]>
|
|
274
|
+
subscribe(event, handler)
|
|
275
|
+
publish(event, data) → subscribers[event].forEach(h => h(data))
|
|
276
|
+
|
|
277
|
+
// Компоненты подписываются, не зная друг о друге
|
|
278
|
+
bus.subscribe("order.created", sendEmail)
|
|
279
|
+
bus.subscribe("order.created", updateInventory)
|
|
280
|
+
bus.subscribe("order.created", logAudit)
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
**Когда:** один источник события, много потребителей.
|
|
284
|
+
|
|
285
|
+
### Factory Method — создание без привязки к конкретному классу
|
|
286
|
+
```
|
|
287
|
+
// ✅ DO: фабрика выбирает реализацию
|
|
288
|
+
class NotificationFactory
|
|
289
|
+
static create(type) →
|
|
290
|
+
if type == "email" → new EmailNotification()
|
|
291
|
+
if type == "sms" → new SmsNotification()
|
|
292
|
+
if type == "push" → new PushNotification()
|
|
293
|
+
throw UnknownTypeError
|
|
294
|
+
|
|
295
|
+
// Клиент не знает про конкретные классы
|
|
296
|
+
notification = NotificationFactory.create(user.preference)
|
|
297
|
+
notification.send(message)
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
**Когда:** тип объекта определяется в runtime; клиент не должен знать конкретику.
|
|
301
|
+
|
|
302
|
+
### Adapter — совместимость несовместимого
|
|
303
|
+
```
|
|
304
|
+
// ✅ DO: адаптер оборачивает чужой интерфейс в ваш
|
|
305
|
+
interface PaymentGateway
|
|
306
|
+
charge(amount, currency) → Result
|
|
307
|
+
|
|
308
|
+
class StripeAdapter implements PaymentGateway
|
|
309
|
+
constructor(stripeClient)
|
|
310
|
+
charge(amount, currency) →
|
|
311
|
+
this.stripeClient.createPaymentIntent({
|
|
312
|
+
amount: amount * 100, // Stripe принимает в центах
|
|
313
|
+
currency: currency
|
|
314
|
+
})
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
**Когда:** интеграция с внешним API/библиотекой, интерфейс которой не совпадает с вашим.
|
|
318
|
+
|
|
319
|
+
### Facade — простой интерфейс к сложной подсистеме
|
|
320
|
+
```
|
|
321
|
+
// ✅ DO: фасад скрывает сложность
|
|
322
|
+
class OrderFacade
|
|
323
|
+
constructor(inventory, payment, shipping, notification)
|
|
324
|
+
|
|
325
|
+
placeOrder(cart, user) →
|
|
326
|
+
inventory.reserve(cart.items)
|
|
327
|
+
payment.charge(user, cart.total)
|
|
328
|
+
shipping.createShipment(user.address, cart.items)
|
|
329
|
+
notification.sendConfirmation(user, cart)
|
|
330
|
+
|
|
331
|
+
// Клиент вызывает один метод вместо четырёх подсистем
|
|
332
|
+
orderFacade.placeOrder(cart, user)
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
**Когда:** сложная подсистема из нескольких компонентов; клиенту нужен простой вход.
|
|
336
|
+
|
|
337
|
+
### Decorator — динамическое расширение поведения
|
|
338
|
+
```
|
|
339
|
+
// ✅ DO: обёртки добавляют поведение, не меняя оригинал
|
|
340
|
+
interface DataSource
|
|
341
|
+
read() → Data
|
|
342
|
+
write(data) → void
|
|
343
|
+
|
|
344
|
+
class FileDataSource implements DataSource
|
|
345
|
+
class EncryptionDecorator implements DataSource
|
|
346
|
+
constructor(source: DataSource)
|
|
347
|
+
write(data) → this.source.write(encrypt(data))
|
|
348
|
+
read() → decrypt(this.source.read())
|
|
349
|
+
|
|
350
|
+
class CompressionDecorator implements DataSource
|
|
351
|
+
constructor(source: DataSource)
|
|
352
|
+
write(data) → this.source.write(compress(data))
|
|
353
|
+
|
|
354
|
+
// Комбинируем: сжатие + шифрование + файл
|
|
355
|
+
source = new CompressionDecorator(new EncryptionDecorator(new FileDataSource()))
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
**Когда:** нужно комбинировать поведения в разных сочетаниях без взрыва подклассов.
|
|
359
|
+
|
|
360
|
+
### Command — инкапсуляция действия как объекта
|
|
361
|
+
```
|
|
362
|
+
// ✅ DO: каждое действие = объект с execute/undo
|
|
363
|
+
interface Command
|
|
364
|
+
execute() → void
|
|
365
|
+
undo() → void
|
|
366
|
+
|
|
367
|
+
class AddItemCommand implements Command
|
|
368
|
+
constructor(cart, item)
|
|
369
|
+
execute() → this.cart.add(this.item)
|
|
370
|
+
undo() → this.cart.remove(this.item)
|
|
371
|
+
|
|
372
|
+
class CommandHistory
|
|
373
|
+
stack = []
|
|
374
|
+
execute(cmd) → cmd.execute(); stack.push(cmd)
|
|
375
|
+
undo() → stack.pop().undo()
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
**Когда:** undo/redo, очередь операций, отложенное выполнение.
|
|
379
|
+
|
|
380
|
+
### State — поведение зависит от состояния
|
|
381
|
+
```
|
|
382
|
+
// ✅ DO: каждое состояние = отдельный класс
|
|
383
|
+
interface OrderState
|
|
384
|
+
next(order) → void
|
|
385
|
+
cancel(order) → void
|
|
386
|
+
|
|
387
|
+
class PendingState implements OrderState
|
|
388
|
+
next(order) → order.setState(new PaidState())
|
|
389
|
+
cancel(order) → order.setState(new CancelledState())
|
|
390
|
+
|
|
391
|
+
class PaidState implements OrderState
|
|
392
|
+
next(order) → order.setState(new ShippedState())
|
|
393
|
+
cancel(order) → throw "Cannot cancel paid order"
|
|
394
|
+
|
|
395
|
+
class Order
|
|
396
|
+
state: OrderState = new PendingState()
|
|
397
|
+
next() → this.state.next(this)
|
|
398
|
+
cancel() → this.state.cancel(this)
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
```
|
|
402
|
+
// ❌ DON'T: switch/if на строковый статус
|
|
403
|
+
class Order
|
|
404
|
+
status = "pending"
|
|
405
|
+
next()
|
|
406
|
+
if status == "pending" → status = "paid"
|
|
407
|
+
else if status == "paid" → status = "shipped"
|
|
408
|
+
// растёт бесконечно при добавлении статусов
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
**Когда:** объект с множеством состояний и разным поведением в каждом.
|
|
412
|
+
|
|
413
|
+
### Template Method — каркас алгоритма с переопределяемыми шагами
|
|
414
|
+
```
|
|
415
|
+
// ✅ DO: базовый класс определяет каркас, шаги переопределяются
|
|
416
|
+
abstract class DataImporter
|
|
417
|
+
import(source) // template method
|
|
418
|
+
data = this.read(source)
|
|
419
|
+
validated = this.validate(data)
|
|
420
|
+
transformed = this.transform(validated)
|
|
421
|
+
this.save(transformed)
|
|
422
|
+
|
|
423
|
+
abstract read(source) → RawData
|
|
424
|
+
abstract validate(data) → ValidData
|
|
425
|
+
abstract transform(data) → FinalData
|
|
426
|
+
save(data) → db.insert(data) // общая реализация
|
|
427
|
+
|
|
428
|
+
class CsvImporter extends DataImporter
|
|
429
|
+
read(source) → parseCsv(source)
|
|
430
|
+
validate(data) → validateCsvRows(data)
|
|
431
|
+
transform(data) → mapCsvToEntities(data)
|
|
432
|
+
|
|
433
|
+
class JsonImporter extends DataImporter
|
|
434
|
+
read(source) → parseJson(source)
|
|
435
|
+
validate(data) → validateJsonSchema(data)
|
|
436
|
+
transform(data) → mapJsonToEntities(data)
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
**Когда:** одинаковый каркас алгоритма, разные детали реализации.
|
|
440
|
+
|
|
441
|
+
---
|
|
442
|
+
|
|
443
|
+
## 4. Architectural Patterns
|
|
444
|
+
|
|
445
|
+
### Repository — изоляция доступа к данным
|
|
446
|
+
```
|
|
447
|
+
// ✅ DO: репозиторий инкапсулирует работу с хранилищем
|
|
448
|
+
interface UserRepository
|
|
449
|
+
findById(id) → User | null
|
|
450
|
+
findByEmail(email) → User | null
|
|
451
|
+
save(user) → User
|
|
452
|
+
delete(id) → void
|
|
453
|
+
|
|
454
|
+
class PostgresUserRepository implements UserRepository
|
|
455
|
+
findById(id) → db.query("SELECT ... WHERE id = $1", [id])
|
|
456
|
+
|
|
457
|
+
class MongoUserRepository implements UserRepository
|
|
458
|
+
findById(id) → collection.findOne({ _id: id })
|
|
459
|
+
|
|
460
|
+
// Бизнес-логика не знает про SQL/Mongo
|
|
461
|
+
class UserService
|
|
462
|
+
constructor(repo: UserRepository)
|
|
463
|
+
getUser(id) → this.repo.findById(id)
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
```
|
|
467
|
+
// ❌ DON'T: SQL в бизнес-логике
|
|
468
|
+
class UserService
|
|
469
|
+
getUser(id)
|
|
470
|
+
result = db.query("SELECT * FROM users WHERE id = $1", [id])
|
|
471
|
+
// Привязан к PostgreSQL, нельзя тестировать без БД
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
### Service Layer — бизнес-логика не в контроллерах
|
|
475
|
+
```
|
|
476
|
+
// ✅ DO: контроллер → сервис → репозиторий
|
|
477
|
+
class OrderController
|
|
478
|
+
constructor(orderService)
|
|
479
|
+
handleCreateOrder(req)
|
|
480
|
+
order = this.orderService.create(req.body)
|
|
481
|
+
return Response(201, order)
|
|
482
|
+
|
|
483
|
+
class OrderService
|
|
484
|
+
constructor(orderRepo, paymentGateway, inventory)
|
|
485
|
+
create(data)
|
|
486
|
+
this.inventory.reserve(data.items)
|
|
487
|
+
order = Order.create(data)
|
|
488
|
+
this.paymentGateway.charge(order.total)
|
|
489
|
+
this.orderRepo.save(order)
|
|
490
|
+
return order
|
|
491
|
+
|
|
492
|
+
// ❌ DON'T: бизнес-логика в контроллере
|
|
493
|
+
class OrderController
|
|
494
|
+
handleCreateOrder(req)
|
|
495
|
+
db.query("INSERT INTO orders ...") // SQL в контроллере
|
|
496
|
+
stripe.charge(...) // платёж в контроллере
|
|
497
|
+
sendEmail(...) // и email тоже
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
### Dependency Injection (DI)
|
|
501
|
+
```
|
|
502
|
+
// ✅ DO: зависимости передаются извне
|
|
503
|
+
class Application
|
|
504
|
+
start()
|
|
505
|
+
db = new PostgresConnection(config.db)
|
|
506
|
+
userRepo = new PostgresUserRepository(db)
|
|
507
|
+
emailService = new SmtpEmailService(config.smtp)
|
|
508
|
+
userService = new UserService(userRepo, emailService)
|
|
509
|
+
controller = new UserController(userService)
|
|
510
|
+
server.register(controller)
|
|
511
|
+
|
|
512
|
+
// Каждый компонент принимает зависимости через конструктор
|
|
513
|
+
// Легко тестировать: подставляем mock-репозиторий
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
```
|
|
517
|
+
// ❌ DON'T: компоненты сами создают зависимости
|
|
518
|
+
class UserService
|
|
519
|
+
repo = new PostgresUserRepository(new PostgresConnection("hardcoded"))
|
|
520
|
+
email = new SmtpEmailService("smtp://hardcoded")
|
|
521
|
+
// Нельзя протестировать без реальных PostgreSQL и SMTP
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
### Event-Driven — реакция на события
|
|
525
|
+
```
|
|
526
|
+
// ✅ DO: слабая связанность через события
|
|
527
|
+
// Сервис создаёт заказ и публикует событие
|
|
528
|
+
class OrderService
|
|
529
|
+
createOrder(data)
|
|
530
|
+
order = Order.create(data)
|
|
531
|
+
orderRepo.save(order)
|
|
532
|
+
eventBus.publish("order.created", { orderId: order.id })
|
|
533
|
+
|
|
534
|
+
// Независимые обработчики подписаны на событие
|
|
535
|
+
class InventoryHandler
|
|
536
|
+
on("order.created") → reserveItems(event.orderId)
|
|
537
|
+
|
|
538
|
+
class NotificationHandler
|
|
539
|
+
on("order.created") → sendConfirmation(event.orderId)
|
|
540
|
+
|
|
541
|
+
class AnalyticsHandler
|
|
542
|
+
on("order.created") → trackConversion(event.orderId)
|
|
543
|
+
|
|
544
|
+
// Добавляем новый обработчик → ни одна строка в OrderService не меняется
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
```
|
|
548
|
+
// ❌ DON'T: OrderService знает обо всех зависимых компонентах
|
|
549
|
+
class OrderService
|
|
550
|
+
createOrder(data)
|
|
551
|
+
order = Order.create(data)
|
|
552
|
+
orderRepo.save(order)
|
|
553
|
+
inventoryService.reserve(order) // жёсткая связь
|
|
554
|
+
notificationService.send(order) // жёсткая связь
|
|
555
|
+
analyticsService.track(order) // жёсткая связь
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
### CQRS — Command Query Responsibility Segregation
|
|
559
|
+
```
|
|
560
|
+
// ✅ DO: разные модели для записи и чтения
|
|
561
|
+
// Command side — нормализованная модель; бизнес-правила
|
|
562
|
+
class CreateOrderCommand
|
|
563
|
+
execute(data)
|
|
564
|
+
order = Order.create(data)
|
|
565
|
+
orderRepo.save(order)
|
|
566
|
+
eventBus.publish("order.created", order)
|
|
567
|
+
|
|
568
|
+
// Query side — денормализованная модель; оптимизирована под чтение
|
|
569
|
+
class OrderQueryService
|
|
570
|
+
getOrderSummary(id) →
|
|
571
|
+
readDb.query("SELECT ... FROM order_summaries WHERE id = $1", [id])
|
|
572
|
+
|
|
573
|
+
getOrdersByUser(userId, page) →
|
|
574
|
+
readDb.query("SELECT ... FROM user_orders_view WHERE user_id = $1 LIMIT ...", [userId])
|
|
575
|
+
|
|
576
|
+
// Записи обновляют read-модель через events
|
|
577
|
+
on("order.created") → updateOrderSummaryView(event)
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
**Когда:** паттерны чтения и записи сильно различаются; высокая нагрузка на чтение; нужна денормализация для быстрых запросов.
|
|
581
|
+
|
|
582
|
+
---
|
|
583
|
+
|
|
584
|
+
## 5. Microservices Patterns
|
|
585
|
+
|
|
586
|
+
### Saga — распределённые транзакции через компенсации
|
|
587
|
+
```
|
|
588
|
+
// ✅ DO: каждый шаг имеет компенсацию (откат)
|
|
589
|
+
class OrderSaga
|
|
590
|
+
steps = [
|
|
591
|
+
{ action: reserveInventory, compensate: releaseInventory },
|
|
592
|
+
{ action: chargePayment, compensate: refundPayment },
|
|
593
|
+
{ action: createShipment, compensate: cancelShipment },
|
|
594
|
+
{ action: sendConfirmation, compensate: sendCancellation },
|
|
595
|
+
]
|
|
596
|
+
|
|
597
|
+
execute(orderData)
|
|
598
|
+
completed = []
|
|
599
|
+
for step in steps
|
|
600
|
+
try
|
|
601
|
+
step.action(orderData)
|
|
602
|
+
completed.push(step)
|
|
603
|
+
catch error
|
|
604
|
+
// Откатываем все выполненные шаги в обратном порядке
|
|
605
|
+
for s in completed.reverse()
|
|
606
|
+
s.compensate(orderData)
|
|
607
|
+
throw SagaFailedError(error)
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
**Когда:** транзакция охватывает несколько сервисов; нельзя использовать распределённый 2PC.
|
|
611
|
+
|
|
612
|
+
### Circuit Breaker — защита от каскадных сбоев
|
|
613
|
+
```
|
|
614
|
+
// ✅ DO: Circuit Breaker перед вызовом внешнего сервиса
|
|
615
|
+
class CircuitBreaker
|
|
616
|
+
state = CLOSED // нормальная работа
|
|
617
|
+
failureCount = 0
|
|
618
|
+
threshold = 5
|
|
619
|
+
resetTimeout = 30s
|
|
620
|
+
|
|
621
|
+
call(fn)
|
|
622
|
+
if state == OPEN
|
|
623
|
+
if elapsed > resetTimeout → state = HALF_OPEN
|
|
624
|
+
else → throw ServiceUnavailableError // не шлём запрос
|
|
625
|
+
|
|
626
|
+
try
|
|
627
|
+
result = fn()
|
|
628
|
+
if state == HALF_OPEN → state = CLOSED; failureCount = 0
|
|
629
|
+
return result
|
|
630
|
+
catch error
|
|
631
|
+
failureCount++
|
|
632
|
+
if failureCount >= threshold → state = OPEN; startTimer()
|
|
633
|
+
throw error
|
|
634
|
+
|
|
635
|
+
// Использование
|
|
636
|
+
breaker = new CircuitBreaker()
|
|
637
|
+
userData = breaker.call(() => externalUserApi.getUser(id))
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
**Когда:** зависимость от внешнего сервиса, который может упасть; предотвращение каскадного отказа.
|
|
641
|
+
|
|
642
|
+
### Mediator — центральный координатор взаимодействий
|
|
643
|
+
```
|
|
644
|
+
// ✅ DO: компоненты общаются через медиатор, не напрямую
|
|
645
|
+
class OrderMediator
|
|
646
|
+
handlers = Map<string, Handler>
|
|
647
|
+
|
|
648
|
+
register(commandType, handler)
|
|
649
|
+
send(command)
|
|
650
|
+
handler = this.handlers.get(command.type)
|
|
651
|
+
return handler.handle(command)
|
|
652
|
+
|
|
653
|
+
// Регистрация
|
|
654
|
+
mediator.register("CreateOrder", new CreateOrderHandler(orderRepo, inventory))
|
|
655
|
+
mediator.register("CancelOrder", new CancelOrderHandler(orderRepo, payment))
|
|
656
|
+
|
|
657
|
+
// Вызов — контроллер не знает про конкретные обработчики
|
|
658
|
+
class OrderController
|
|
659
|
+
handleCreate(req) → mediator.send(new CreateOrderCommand(req.body))
|
|
660
|
+
handleCancel(req) → mediator.send(new CancelOrderCommand(req.params.id))
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
**Когда:** много взаимодействующих компонентов; нужно централизовать координацию и убрать прямые зависимости.
|
|
664
|
+
|
|
665
|
+
---
|
|
666
|
+
|
|
667
|
+
## 6. Правило выбора
|
|
668
|
+
|
|
669
|
+
| Проблема | Паттерн | Пример |
|
|
670
|
+
|----------|---------|--------|
|
|
671
|
+
| Несколько вариантов алгоритма | Strategy | Алгоритмы сортировки, расчёта скидок |
|
|
672
|
+
| Реакция на изменения | Observer / Event-Driven | Уведомления, аудит, аналитика |
|
|
673
|
+
| Создание объектов по условию | Factory Method | Нотификации: email/sms/push |
|
|
674
|
+
| Интеграция с чужим API | Adapter | Обёртка Stripe/PayPal |
|
|
675
|
+
| Сложная подсистема | Facade | Оформление заказа (inventory + payment + shipping) |
|
|
676
|
+
| Комбинации поведений | Decorator | Кэширование + шифрование + логирование |
|
|
677
|
+
| Undo/redo, очереди | Command | Текстовый редактор, batch-операции |
|
|
678
|
+
| Множество состояний объекта | State | Workflow заказа, задачи |
|
|
679
|
+
| Одинаковый каркас, разные детали | Template Method | Импорт CSV/JSON/XML |
|
|
680
|
+
| Изоляция хранилища | Repository | Работа с БД без SQL в бизнес-логике |
|
|
681
|
+
| Бизнес-логика не в контроллерах | Service Layer | REST API |
|
|
682
|
+
| Слабая связь компонентов | DI | Инъекция зависимостей через конструктор |
|
|
683
|
+
| Разные модели чтения/записи | CQRS | Dashboard + admin panel |
|
|
684
|
+
| Распределённые транзакции | Saga | Заказ: inventory → payment → shipping |
|
|
685
|
+
| Защита от каскадных сбоев | Circuit Breaker | Внешние API |
|
|
686
|
+
| Координация компонентов | Mediator | Request → Handler routing |
|
|
687
|
+
|
|
688
|
+
---
|
|
689
|
+
|
|
690
|
+
## См. также
|
|
691
|
+
- `architecture_doc` → где фиксировать выбранные паттерны
|
|
692
|
+
- `architecture_compliance_review` → проверка соответствия архитектуре
|
|
693
|
+
- `adr_log` → фиксация решений по выбору паттернов
|
|
694
|
+
- `code_review_checklist` → проверка реализации паттернов
|
|
@@ -7,6 +7,9 @@ description: TanStack (Query v5, Table v8, Virtual v3): кэширование,
|
|
|
7
7
|
|
|
8
8
|
Копипаст-паттерны для предсказуемой работы с данными, таблицами и виртуализацией.
|
|
9
9
|
|
|
10
|
+
> [!IMPORTANT]
|
|
11
|
+
> **MCP-приоритет:** Если доступен MCP-сервер `tanstack-docs`, **всегда используй его инструменты** (`tanstack_doc`, `tanstack_search_docs`, `listTanStackAddOns`, `getAddOnDetails`, `createTanStackApplication`) **первыми** — они дают актуальную документацию прямо из официальных источников TanStack. Используй паттерны этого скилла как fallback или как дополнительный контекст по архитектуре (fetcher/adapter/hook, queryKey factory, оптимистичные обновления, виртуализация), когда MCP недоступен или не содержит нужной информации.
|
|
12
|
+
|
|
10
13
|
**Разделы:**
|
|
11
14
|
1. [Query: архитектура fetcher/adapter](#1-query-архитектура)
|
|
12
15
|
2. [Query: кэширование и staleTime](#2-query-кэш)
|