codingbuddy-rules 2.4.2 → 3.0.2
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/.ai-rules/CHANGELOG.md +122 -0
- package/.ai-rules/agents/README.md +527 -11
- package/.ai-rules/agents/accessibility-specialist.json +0 -1
- package/.ai-rules/agents/act-mode.json +0 -1
- package/.ai-rules/agents/agent-architect.json +0 -1
- package/.ai-rules/agents/ai-ml-engineer.json +0 -1
- package/.ai-rules/agents/architecture-specialist.json +14 -2
- package/.ai-rules/agents/backend-developer.json +14 -2
- package/.ai-rules/agents/code-quality-specialist.json +0 -1
- package/.ai-rules/agents/data-engineer.json +0 -1
- package/.ai-rules/agents/devops-engineer.json +24 -2
- package/.ai-rules/agents/documentation-specialist.json +0 -1
- package/.ai-rules/agents/eval-mode.json +0 -1
- package/.ai-rules/agents/event-architecture-specialist.json +719 -0
- package/.ai-rules/agents/frontend-developer.json +14 -2
- package/.ai-rules/agents/i18n-specialist.json +0 -1
- package/.ai-rules/agents/integration-specialist.json +11 -1
- package/.ai-rules/agents/migration-specialist.json +676 -0
- package/.ai-rules/agents/mobile-developer.json +0 -1
- package/.ai-rules/agents/observability-specialist.json +747 -0
- package/.ai-rules/agents/performance-specialist.json +24 -2
- package/.ai-rules/agents/plan-mode.json +0 -1
- package/.ai-rules/agents/platform-engineer.json +0 -1
- package/.ai-rules/agents/security-specialist.json +27 -16
- package/.ai-rules/agents/seo-specialist.json +0 -1
- package/.ai-rules/agents/solution-architect.json +0 -1
- package/.ai-rules/agents/technical-planner.json +0 -1
- package/.ai-rules/agents/test-strategy-specialist.json +14 -2
- package/.ai-rules/agents/ui-ux-designer.json +0 -1
- package/.ai-rules/rules/core.md +25 -0
- package/.ai-rules/skills/README.md +35 -0
- package/.ai-rules/skills/database-migration/SKILL.md +531 -0
- package/.ai-rules/skills/database-migration/expand-contract-patterns.md +314 -0
- package/.ai-rules/skills/database-migration/large-scale-migration.md +414 -0
- package/.ai-rules/skills/database-migration/rollback-strategies.md +359 -0
- package/.ai-rules/skills/database-migration/validation-procedures.md +428 -0
- package/.ai-rules/skills/dependency-management/SKILL.md +381 -0
- package/.ai-rules/skills/dependency-management/license-compliance.md +282 -0
- package/.ai-rules/skills/dependency-management/lock-file-management.md +437 -0
- package/.ai-rules/skills/dependency-management/major-upgrade-guide.md +292 -0
- package/.ai-rules/skills/dependency-management/security-vulnerability-response.md +230 -0
- package/.ai-rules/skills/incident-response/SKILL.md +373 -0
- package/.ai-rules/skills/incident-response/communication-templates.md +322 -0
- package/.ai-rules/skills/incident-response/escalation-matrix.md +347 -0
- package/.ai-rules/skills/incident-response/postmortem-template.md +351 -0
- package/.ai-rules/skills/incident-response/severity-classification.md +256 -0
- package/.ai-rules/skills/performance-optimization/CREATION-LOG.md +87 -0
- package/.ai-rules/skills/performance-optimization/SKILL.md +76 -0
- package/.ai-rules/skills/performance-optimization/documentation-template.md +70 -0
- package/.ai-rules/skills/pr-review/SKILL.md +768 -0
- package/.ai-rules/skills/refactoring/SKILL.md +192 -0
- package/.ai-rules/skills/refactoring/refactoring-catalog.md +1377 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1377 @@
|
|
|
1
|
+
# Refactoring Catalog
|
|
2
|
+
|
|
3
|
+
A comprehensive reference of refactoring patterns organized by category.
|
|
4
|
+
Based on Martin Fowler's refactoring catalog.
|
|
5
|
+
|
|
6
|
+
## Table of Contents
|
|
7
|
+
|
|
8
|
+
1. **[Method-Level Refactorings](#1-method-level-refactorings)**
|
|
9
|
+
- [Extract Method](#extract-method) | [Inline Method](#inline-method) | [Rename Method/Function](#rename-methodfunction)
|
|
10
|
+
- [Add Parameter](#add-parameter) | [Remove Parameter](#remove-parameter) | [Replace Parameter with Method Call](#replace-parameter-with-method-call)
|
|
11
|
+
|
|
12
|
+
2. **[Duplication Elimination](#2-duplication-elimination)**
|
|
13
|
+
- [Extract Method for Duplicate Code](#extract-method-for-duplicate-code) | [Pull Up Method](#pull-up-method)
|
|
14
|
+
- [Push Down Method](#push-down-method) | [Substitute Algorithm](#substitute-algorithm)
|
|
15
|
+
|
|
16
|
+
3. **[Moving Features](#3-moving-features)**
|
|
17
|
+
- [Move Method](#move-method) | [Move Field](#move-field) | [Extract Class](#extract-class)
|
|
18
|
+
- [Inline Class](#inline-class) | [Hide Delegate](#hide-delegate)
|
|
19
|
+
|
|
20
|
+
4. **[Data Organization](#4-data-organization)**
|
|
21
|
+
- [Replace Primitive with Object](#replace-primitive-with-object) | [Replace Magic Number with Constant](#replace-magic-number-with-constant)
|
|
22
|
+
- [Encapsulate Field](#encapsulate-field) | [Replace Type Code with Class](#replace-type-code-with-class)
|
|
23
|
+
|
|
24
|
+
5. **[Conditional Simplification](#5-conditional-simplification)**
|
|
25
|
+
- [Decompose Conditional](#decompose-conditional) | [Consolidate Conditional Expression](#consolidate-conditional-expression)
|
|
26
|
+
- [Replace Conditional with Polymorphism](#replace-conditional-with-polymorphism) | [Introduce Null Object](#introduce-null-object)
|
|
27
|
+
- [Replace Nested Conditional with Guard Clauses](#replace-nested-conditional-with-guard-clauses)
|
|
28
|
+
|
|
29
|
+
6. **[API Simplification](#6-api-simplification)**
|
|
30
|
+
- [Separate Query from Modifier](#separate-query-from-modifier) | [Parameterize Method](#parameterize-method)
|
|
31
|
+
- [Replace Parameter with Query](#replace-parameter-with-query) | [Introduce Parameter Object](#introduce-parameter-object)
|
|
32
|
+
|
|
33
|
+
7. **[React/Next.js Specific Refactorings](#7-reactnextjs-specific-refactorings)**
|
|
34
|
+
- [Extract Custom Hook](#extract-custom-hook) | [Lift State Up](#lift-state-up) | [Replace Local State with URL State](#replace-local-state-with-url-state)
|
|
35
|
+
- [Convert to Server Component](#convert-to-server-component) | [Extract Server Action](#extract-server-action)
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 1. Method-Level Refactorings
|
|
40
|
+
|
|
41
|
+
### Extract Method
|
|
42
|
+
|
|
43
|
+
**Code Smell:** Long method, duplicated code fragment
|
|
44
|
+
|
|
45
|
+
**Before:**
|
|
46
|
+
```typescript
|
|
47
|
+
function printInvoice(invoice: Invoice) {
|
|
48
|
+
console.log("=== Invoice ===");
|
|
49
|
+
console.log(`Customer: ${invoice.customer.name}`);
|
|
50
|
+
|
|
51
|
+
// Calculate total
|
|
52
|
+
let total = 0;
|
|
53
|
+
for (const item of invoice.items) {
|
|
54
|
+
total += item.price * item.quantity;
|
|
55
|
+
}
|
|
56
|
+
if (invoice.discount) {
|
|
57
|
+
total = total * (1 - invoice.discount);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
console.log(`Total: $${total.toFixed(2)}`);
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**After:**
|
|
65
|
+
```typescript
|
|
66
|
+
function printInvoice(invoice: Invoice) {
|
|
67
|
+
console.log("=== Invoice ===");
|
|
68
|
+
console.log(`Customer: ${invoice.customer.name}`);
|
|
69
|
+
console.log(`Total: $${calculateTotal(invoice).toFixed(2)}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function calculateTotal(invoice: Invoice): number {
|
|
73
|
+
let total = 0;
|
|
74
|
+
for (const item of invoice.items) {
|
|
75
|
+
total += item.price * item.quantity;
|
|
76
|
+
}
|
|
77
|
+
if (invoice.discount) {
|
|
78
|
+
total = total * (1 - invoice.discount);
|
|
79
|
+
}
|
|
80
|
+
return total;
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**When to apply:**
|
|
85
|
+
- Method is too long (>20 lines)
|
|
86
|
+
- Code fragment appears in multiple places
|
|
87
|
+
- Code needs a comment to explain what it does
|
|
88
|
+
- You want to test the extracted logic independently
|
|
89
|
+
|
|
90
|
+
**IDE support:** Most IDEs have "Extract Method/Function" refactoring (Ctrl+Alt+M in JetBrains, Ctrl+Shift+R in VS Code with extension)
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
### Inline Method
|
|
95
|
+
|
|
96
|
+
**Code Smell:** Method body is as clear as its name, excessive delegation
|
|
97
|
+
|
|
98
|
+
**Before:**
|
|
99
|
+
```typescript
|
|
100
|
+
function getRating(driver: Driver): number {
|
|
101
|
+
return moreThanFiveLateDeliveries(driver) ? 2 : 1;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function moreThanFiveLateDeliveries(driver: Driver): boolean {
|
|
105
|
+
return driver.numberOfLateDeliveries > 5;
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**After:**
|
|
110
|
+
```typescript
|
|
111
|
+
function getRating(driver: Driver): number {
|
|
112
|
+
return driver.numberOfLateDeliveries > 5 ? 2 : 1;
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**When to apply:**
|
|
117
|
+
- Method body is as obvious as its name
|
|
118
|
+
- Too many simple delegating methods
|
|
119
|
+
- Method is only called once
|
|
120
|
+
- Preparing for further refactoring
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
### Rename Method/Function
|
|
125
|
+
|
|
126
|
+
**Code Smell:** Name doesn't reveal intent
|
|
127
|
+
|
|
128
|
+
**Before:**
|
|
129
|
+
```typescript
|
|
130
|
+
function calc(a: number, b: number): number {
|
|
131
|
+
return a * b * 0.1;
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**After:**
|
|
136
|
+
```typescript
|
|
137
|
+
function calculateTax(price: number, quantity: number): number {
|
|
138
|
+
return price * quantity * 0.1;
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**When to apply:**
|
|
143
|
+
- Name is abbreviated or unclear
|
|
144
|
+
- Name doesn't match what method does
|
|
145
|
+
- Name uses implementation details instead of intent
|
|
146
|
+
|
|
147
|
+
**IDE support:** All major IDEs support rename refactoring (F2 in VS Code, Shift+F6 in JetBrains)
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
### Add Parameter
|
|
152
|
+
|
|
153
|
+
**Code Smell:** Method needs more information to do its job
|
|
154
|
+
|
|
155
|
+
**Before:**
|
|
156
|
+
```typescript
|
|
157
|
+
function getContact(customer: Customer): Contact {
|
|
158
|
+
return customer.primaryContact;
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**After:**
|
|
163
|
+
```typescript
|
|
164
|
+
function getContact(customer: Customer, type: ContactType): Contact {
|
|
165
|
+
return type === 'primary'
|
|
166
|
+
? customer.primaryContact
|
|
167
|
+
: customer.secondaryContact;
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**When to apply:**
|
|
172
|
+
- Method needs additional data
|
|
173
|
+
- Behavior should vary based on caller's context
|
|
174
|
+
- Multiple similar methods could be unified
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
### Remove Parameter
|
|
179
|
+
|
|
180
|
+
**Code Smell:** Parameter is no longer used
|
|
181
|
+
|
|
182
|
+
**Before:**
|
|
183
|
+
```typescript
|
|
184
|
+
function calculatePrice(
|
|
185
|
+
quantity: number,
|
|
186
|
+
pricePerUnit: number,
|
|
187
|
+
customer: Customer // Never used
|
|
188
|
+
): number {
|
|
189
|
+
return quantity * pricePerUnit;
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**After:**
|
|
194
|
+
```typescript
|
|
195
|
+
function calculatePrice(quantity: number, pricePerUnit: number): number {
|
|
196
|
+
return quantity * pricePerUnit;
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**When to apply:**
|
|
201
|
+
- Parameter is never used in method body
|
|
202
|
+
- Parameter was for removed feature
|
|
203
|
+
- Parameter is always same value
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
### Replace Parameter with Method Call
|
|
208
|
+
|
|
209
|
+
**Code Smell:** Parameter can be obtained by calling another method
|
|
210
|
+
|
|
211
|
+
**Before:**
|
|
212
|
+
```typescript
|
|
213
|
+
const basePrice = quantity * itemPrice;
|
|
214
|
+
const discount = getDiscount();
|
|
215
|
+
const finalPrice = calculateFinalPrice(basePrice, discount);
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
**After:**
|
|
219
|
+
```typescript
|
|
220
|
+
const basePrice = quantity * itemPrice;
|
|
221
|
+
const finalPrice = calculateFinalPrice(basePrice);
|
|
222
|
+
|
|
223
|
+
function calculateFinalPrice(basePrice: number): number {
|
|
224
|
+
const discount = getDiscount();
|
|
225
|
+
return basePrice * (1 - discount);
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
**When to apply:**
|
|
230
|
+
- Parameter can be obtained from another method the receiver can call
|
|
231
|
+
- Simplifies calling code
|
|
232
|
+
- Parameter is always derived the same way
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## 2. Duplication Elimination
|
|
237
|
+
|
|
238
|
+
### Extract Method for Duplicate Code
|
|
239
|
+
|
|
240
|
+
**Code Smell:** Same code fragment in multiple places
|
|
241
|
+
|
|
242
|
+
**Before:**
|
|
243
|
+
```typescript
|
|
244
|
+
function validateUser(user: User) {
|
|
245
|
+
if (!user.email || !user.email.includes('@')) {
|
|
246
|
+
throw new Error('Invalid email');
|
|
247
|
+
}
|
|
248
|
+
// ... more validation
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function validateAdmin(admin: Admin) {
|
|
252
|
+
if (!admin.email || !admin.email.includes('@')) {
|
|
253
|
+
throw new Error('Invalid email');
|
|
254
|
+
}
|
|
255
|
+
// ... more validation
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
**After:**
|
|
260
|
+
```typescript
|
|
261
|
+
function validateEmail(email: string): void {
|
|
262
|
+
if (!email || !email.includes('@')) {
|
|
263
|
+
throw new Error('Invalid email');
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function validateUser(user: User) {
|
|
268
|
+
validateEmail(user.email);
|
|
269
|
+
// ... more validation
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function validateAdmin(admin: Admin) {
|
|
273
|
+
validateEmail(admin.email);
|
|
274
|
+
// ... more validation
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
### Pull Up Method
|
|
281
|
+
|
|
282
|
+
**Code Smell:** Identical method in multiple subclasses
|
|
283
|
+
|
|
284
|
+
**Before:**
|
|
285
|
+
```typescript
|
|
286
|
+
class Employee {
|
|
287
|
+
// ...
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
class Engineer extends Employee {
|
|
291
|
+
getName(): string {
|
|
292
|
+
return this.name;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
class Salesperson extends Employee {
|
|
297
|
+
getName(): string {
|
|
298
|
+
return this.name;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
**After:**
|
|
304
|
+
```typescript
|
|
305
|
+
class Employee {
|
|
306
|
+
getName(): string {
|
|
307
|
+
return this.name;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
class Engineer extends Employee {}
|
|
312
|
+
class Salesperson extends Employee {}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
**When to apply:**
|
|
316
|
+
- Same method in multiple subclasses
|
|
317
|
+
- Methods have identical behavior
|
|
318
|
+
- All subclasses need this method
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
### Push Down Method
|
|
323
|
+
|
|
324
|
+
**Code Smell:** Method only used by some subclasses
|
|
325
|
+
|
|
326
|
+
**Before:**
|
|
327
|
+
```typescript
|
|
328
|
+
class Employee {
|
|
329
|
+
getQuota(): number {
|
|
330
|
+
return this.quota; // Only relevant for Salesperson
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
class Engineer extends Employee {}
|
|
335
|
+
class Salesperson extends Employee {}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
**After:**
|
|
339
|
+
```typescript
|
|
340
|
+
class Employee {}
|
|
341
|
+
|
|
342
|
+
class Engineer extends Employee {}
|
|
343
|
+
|
|
344
|
+
class Salesperson extends Employee {
|
|
345
|
+
getQuota(): number {
|
|
346
|
+
return this.quota;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
353
|
+
### Substitute Algorithm
|
|
354
|
+
|
|
355
|
+
**Code Smell:** Algorithm can be replaced with clearer one
|
|
356
|
+
|
|
357
|
+
**Before:**
|
|
358
|
+
```typescript
|
|
359
|
+
function findPerson(people: string[]): string {
|
|
360
|
+
for (let i = 0; i < people.length; i++) {
|
|
361
|
+
if (people[i] === 'Don') return 'Don';
|
|
362
|
+
if (people[i] === 'John') return 'John';
|
|
363
|
+
if (people[i] === 'Kent') return 'Kent';
|
|
364
|
+
}
|
|
365
|
+
return '';
|
|
366
|
+
}
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
**After:**
|
|
370
|
+
```typescript
|
|
371
|
+
function findPerson(people: string[]): string {
|
|
372
|
+
const candidates = ['Don', 'John', 'Kent'];
|
|
373
|
+
return people.find(p => candidates.includes(p)) ?? '';
|
|
374
|
+
}
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
---
|
|
378
|
+
|
|
379
|
+
## 3. Moving Features
|
|
380
|
+
|
|
381
|
+
### Move Method
|
|
382
|
+
|
|
383
|
+
**Code Smell:** Feature envy - method uses more features of another class
|
|
384
|
+
|
|
385
|
+
**Before:**
|
|
386
|
+
```typescript
|
|
387
|
+
class Account {
|
|
388
|
+
type: AccountType;
|
|
389
|
+
daysOverdrawn: number;
|
|
390
|
+
|
|
391
|
+
overdraftCharge(): number {
|
|
392
|
+
if (this.type.isPremium) {
|
|
393
|
+
const baseCharge = 10;
|
|
394
|
+
if (this.daysOverdrawn <= 7) {
|
|
395
|
+
return baseCharge;
|
|
396
|
+
}
|
|
397
|
+
return baseCharge + (this.daysOverdrawn - 7) * 0.85;
|
|
398
|
+
}
|
|
399
|
+
return this.daysOverdrawn * 1.75;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
**After:**
|
|
405
|
+
```typescript
|
|
406
|
+
class Account {
|
|
407
|
+
type: AccountType;
|
|
408
|
+
daysOverdrawn: number;
|
|
409
|
+
|
|
410
|
+
overdraftCharge(): number {
|
|
411
|
+
return this.type.overdraftCharge(this.daysOverdrawn);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
class AccountType {
|
|
416
|
+
isPremium: boolean;
|
|
417
|
+
|
|
418
|
+
overdraftCharge(daysOverdrawn: number): number {
|
|
419
|
+
if (this.isPremium) {
|
|
420
|
+
const baseCharge = 10;
|
|
421
|
+
if (daysOverdrawn <= 7) return baseCharge;
|
|
422
|
+
return baseCharge + (daysOverdrawn - 7) * 0.85;
|
|
423
|
+
}
|
|
424
|
+
return daysOverdrawn * 1.75;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
---
|
|
430
|
+
|
|
431
|
+
### Move Field
|
|
432
|
+
|
|
433
|
+
**Code Smell:** Field is used more by another class
|
|
434
|
+
|
|
435
|
+
**Before:**
|
|
436
|
+
```typescript
|
|
437
|
+
class Customer {
|
|
438
|
+
discountRate: number;
|
|
439
|
+
// ... customer fields
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
class Order {
|
|
443
|
+
customer: Customer;
|
|
444
|
+
|
|
445
|
+
getDiscount(): number {
|
|
446
|
+
return this.basePrice * this.customer.discountRate;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
**After:**
|
|
452
|
+
```typescript
|
|
453
|
+
class Customer {
|
|
454
|
+
// discountRate moved to Order or a new DiscountPolicy class
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
class Order {
|
|
458
|
+
customer: Customer;
|
|
459
|
+
discountRate: number;
|
|
460
|
+
|
|
461
|
+
getDiscount(): number {
|
|
462
|
+
return this.basePrice * this.discountRate;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
---
|
|
468
|
+
|
|
469
|
+
### Extract Class
|
|
470
|
+
|
|
471
|
+
**Code Smell:** Class does too much (Large Class)
|
|
472
|
+
|
|
473
|
+
**Before:**
|
|
474
|
+
```typescript
|
|
475
|
+
class Person {
|
|
476
|
+
name: string;
|
|
477
|
+
officeAreaCode: string;
|
|
478
|
+
officeNumber: string;
|
|
479
|
+
|
|
480
|
+
getOfficePhone(): string {
|
|
481
|
+
return `(${this.officeAreaCode}) ${this.officeNumber}`;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
**After:**
|
|
487
|
+
```typescript
|
|
488
|
+
class Person {
|
|
489
|
+
name: string;
|
|
490
|
+
officePhone: TelephoneNumber;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
class TelephoneNumber {
|
|
494
|
+
areaCode: string;
|
|
495
|
+
number: string;
|
|
496
|
+
|
|
497
|
+
toString(): string {
|
|
498
|
+
return `(${this.areaCode}) ${this.number}`;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
**When to apply:**
|
|
504
|
+
- Class has too many responsibilities
|
|
505
|
+
- Subset of data/methods form a logical group
|
|
506
|
+
- Class is hard to understand
|
|
507
|
+
|
|
508
|
+
---
|
|
509
|
+
|
|
510
|
+
### Inline Class
|
|
511
|
+
|
|
512
|
+
**Code Smell:** Class does too little
|
|
513
|
+
|
|
514
|
+
**Before:**
|
|
515
|
+
```typescript
|
|
516
|
+
class Person {
|
|
517
|
+
name: string;
|
|
518
|
+
officePhone: TelephoneNumber;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
class TelephoneNumber {
|
|
522
|
+
number: string;
|
|
523
|
+
// Only one field, no real behavior
|
|
524
|
+
}
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
**After:**
|
|
528
|
+
```typescript
|
|
529
|
+
class Person {
|
|
530
|
+
name: string;
|
|
531
|
+
officePhoneNumber: string;
|
|
532
|
+
}
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
---
|
|
536
|
+
|
|
537
|
+
### Hide Delegate
|
|
538
|
+
|
|
539
|
+
**Code Smell:** Client knows too much about class structure
|
|
540
|
+
|
|
541
|
+
**Before:**
|
|
542
|
+
```typescript
|
|
543
|
+
// Client code
|
|
544
|
+
const manager = person.department.manager;
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
**After:**
|
|
548
|
+
```typescript
|
|
549
|
+
class Person {
|
|
550
|
+
department: Department;
|
|
551
|
+
|
|
552
|
+
getManager(): Person {
|
|
553
|
+
return this.department.manager;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Client code
|
|
558
|
+
const manager = person.getManager();
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
---
|
|
562
|
+
|
|
563
|
+
## 4. Data Organization
|
|
564
|
+
|
|
565
|
+
### Replace Primitive with Object
|
|
566
|
+
|
|
567
|
+
**Code Smell:** Primitive obsession
|
|
568
|
+
|
|
569
|
+
**Before:**
|
|
570
|
+
```typescript
|
|
571
|
+
class Order {
|
|
572
|
+
priority: string; // "high", "rush", "normal"
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Usage
|
|
576
|
+
if (order.priority === 'high' || order.priority === 'rush') {
|
|
577
|
+
// ...
|
|
578
|
+
}
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
**After:**
|
|
582
|
+
```typescript
|
|
583
|
+
class Priority {
|
|
584
|
+
private value: string;
|
|
585
|
+
|
|
586
|
+
constructor(value: string) {
|
|
587
|
+
if (!['low', 'normal', 'high', 'rush'].includes(value)) {
|
|
588
|
+
throw new Error('Invalid priority');
|
|
589
|
+
}
|
|
590
|
+
this.value = value;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
higherThan(other: Priority): boolean {
|
|
594
|
+
return this.index() > other.index();
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
private index(): number {
|
|
598
|
+
return ['low', 'normal', 'high', 'rush'].indexOf(this.value);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
class Order {
|
|
603
|
+
priority: Priority;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Usage
|
|
607
|
+
if (order.priority.higherThan(new Priority('normal'))) {
|
|
608
|
+
// ...
|
|
609
|
+
}
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
---
|
|
613
|
+
|
|
614
|
+
### Replace Magic Number with Constant
|
|
615
|
+
|
|
616
|
+
**Code Smell:** Unexplained literal numbers
|
|
617
|
+
|
|
618
|
+
**Before:**
|
|
619
|
+
```typescript
|
|
620
|
+
function potentialEnergy(mass: number, height: number): number {
|
|
621
|
+
return mass * 9.81 * height;
|
|
622
|
+
}
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
**After:**
|
|
626
|
+
```typescript
|
|
627
|
+
const GRAVITATIONAL_CONSTANT = 9.81;
|
|
628
|
+
|
|
629
|
+
function potentialEnergy(mass: number, height: number): number {
|
|
630
|
+
return mass * GRAVITATIONAL_CONSTANT * height;
|
|
631
|
+
}
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
---
|
|
635
|
+
|
|
636
|
+
### Encapsulate Field
|
|
637
|
+
|
|
638
|
+
**Code Smell:** Public field
|
|
639
|
+
|
|
640
|
+
**Before:**
|
|
641
|
+
```typescript
|
|
642
|
+
class Person {
|
|
643
|
+
name: string;
|
|
644
|
+
}
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
**After:**
|
|
648
|
+
```typescript
|
|
649
|
+
class Person {
|
|
650
|
+
private _name: string;
|
|
651
|
+
|
|
652
|
+
get name(): string {
|
|
653
|
+
return this._name;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
set name(value: string) {
|
|
657
|
+
this._name = value;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
---
|
|
663
|
+
|
|
664
|
+
### Replace Type Code with Class
|
|
665
|
+
|
|
666
|
+
**Code Smell:** Type code that affects behavior
|
|
667
|
+
|
|
668
|
+
**Before:**
|
|
669
|
+
```typescript
|
|
670
|
+
class Employee {
|
|
671
|
+
type: number; // 0 = engineer, 1 = salesperson, 2 = manager
|
|
672
|
+
|
|
673
|
+
getBonus(): number {
|
|
674
|
+
switch (this.type) {
|
|
675
|
+
case 0: return 0;
|
|
676
|
+
case 1: return this.sales * 0.1;
|
|
677
|
+
case 2: return 1000;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
**After:**
|
|
684
|
+
```typescript
|
|
685
|
+
abstract class EmployeeType {
|
|
686
|
+
abstract getBonus(employee: Employee): number;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
class Engineer extends EmployeeType {
|
|
690
|
+
getBonus(): number { return 0; }
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
class Salesperson extends EmployeeType {
|
|
694
|
+
getBonus(employee: Employee): number {
|
|
695
|
+
return employee.sales * 0.1;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
class Manager extends EmployeeType {
|
|
700
|
+
getBonus(): number { return 1000; }
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
class Employee {
|
|
704
|
+
type: EmployeeType;
|
|
705
|
+
|
|
706
|
+
getBonus(): number {
|
|
707
|
+
return this.type.getBonus(this);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
---
|
|
713
|
+
|
|
714
|
+
## 5. Conditional Simplification
|
|
715
|
+
|
|
716
|
+
### Decompose Conditional
|
|
717
|
+
|
|
718
|
+
**Code Smell:** Complex conditional logic
|
|
719
|
+
|
|
720
|
+
**Before:**
|
|
721
|
+
```typescript
|
|
722
|
+
function calculateCharge(date: Date, quantity: number): number {
|
|
723
|
+
if (date < SUMMER_START || date > SUMMER_END) {
|
|
724
|
+
return quantity * winterRate + winterServiceCharge;
|
|
725
|
+
} else {
|
|
726
|
+
return quantity * summerRate;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
**After:**
|
|
732
|
+
```typescript
|
|
733
|
+
function calculateCharge(date: Date, quantity: number): number {
|
|
734
|
+
if (isSummer(date)) {
|
|
735
|
+
return summerCharge(quantity);
|
|
736
|
+
} else {
|
|
737
|
+
return winterCharge(quantity);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function isSummer(date: Date): boolean {
|
|
742
|
+
return date >= SUMMER_START && date <= SUMMER_END;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function summerCharge(quantity: number): number {
|
|
746
|
+
return quantity * summerRate;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function winterCharge(quantity: number): number {
|
|
750
|
+
return quantity * winterRate + winterServiceCharge;
|
|
751
|
+
}
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
---
|
|
755
|
+
|
|
756
|
+
### Consolidate Conditional Expression
|
|
757
|
+
|
|
758
|
+
**Code Smell:** Multiple conditionals with same result
|
|
759
|
+
|
|
760
|
+
**Before:**
|
|
761
|
+
```typescript
|
|
762
|
+
function disabilityAmount(employee: Employee): number {
|
|
763
|
+
if (employee.seniority < 2) return 0;
|
|
764
|
+
if (employee.monthsDisabled > 12) return 0;
|
|
765
|
+
if (employee.isPartTime) return 0;
|
|
766
|
+
// Calculate disability
|
|
767
|
+
return /* ... */;
|
|
768
|
+
}
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
**After:**
|
|
772
|
+
```typescript
|
|
773
|
+
function disabilityAmount(employee: Employee): number {
|
|
774
|
+
if (isNotEligibleForDisability(employee)) return 0;
|
|
775
|
+
// Calculate disability
|
|
776
|
+
return /* ... */;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function isNotEligibleForDisability(employee: Employee): boolean {
|
|
780
|
+
return employee.seniority < 2
|
|
781
|
+
|| employee.monthsDisabled > 12
|
|
782
|
+
|| employee.isPartTime;
|
|
783
|
+
}
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
---
|
|
787
|
+
|
|
788
|
+
### Replace Conditional with Polymorphism
|
|
789
|
+
|
|
790
|
+
**Code Smell:** Switch on type code
|
|
791
|
+
|
|
792
|
+
**Before:**
|
|
793
|
+
```typescript
|
|
794
|
+
function plumage(bird: Bird): string {
|
|
795
|
+
switch (bird.type) {
|
|
796
|
+
case 'EuropeanSwallow':
|
|
797
|
+
return 'average';
|
|
798
|
+
case 'AfricanSwallow':
|
|
799
|
+
return bird.numberOfCoconuts > 2 ? 'tired' : 'average';
|
|
800
|
+
case 'NorwegianBlueParrot':
|
|
801
|
+
return bird.voltage > 100 ? 'scorched' : 'beautiful';
|
|
802
|
+
default:
|
|
803
|
+
return 'unknown';
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
**After:**
|
|
809
|
+
```typescript
|
|
810
|
+
abstract class Bird {
|
|
811
|
+
abstract plumage(): string;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
class EuropeanSwallow extends Bird {
|
|
815
|
+
plumage(): string { return 'average'; }
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
class AfricanSwallow extends Bird {
|
|
819
|
+
numberOfCoconuts: number;
|
|
820
|
+
plumage(): string {
|
|
821
|
+
return this.numberOfCoconuts > 2 ? 'tired' : 'average';
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
class NorwegianBlueParrot extends Bird {
|
|
826
|
+
voltage: number;
|
|
827
|
+
plumage(): string {
|
|
828
|
+
return this.voltage > 100 ? 'scorched' : 'beautiful';
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
```
|
|
832
|
+
|
|
833
|
+
---
|
|
834
|
+
|
|
835
|
+
### Introduce Null Object
|
|
836
|
+
|
|
837
|
+
**Code Smell:** Repeated null checks
|
|
838
|
+
|
|
839
|
+
**Before:**
|
|
840
|
+
```typescript
|
|
841
|
+
function getPaymentPlan(customer: Customer | null): string {
|
|
842
|
+
if (customer === null) return 'basic';
|
|
843
|
+
return customer.plan;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function getName(customer: Customer | null): string {
|
|
847
|
+
if (customer === null) return 'occupant';
|
|
848
|
+
return customer.name;
|
|
849
|
+
}
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
**After:**
|
|
853
|
+
```typescript
|
|
854
|
+
class NullCustomer implements Customer {
|
|
855
|
+
get plan(): string { return 'basic'; }
|
|
856
|
+
get name(): string { return 'occupant'; }
|
|
857
|
+
isNull(): boolean { return true; }
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// Replace null with NullCustomer at source
|
|
861
|
+
function getPaymentPlan(customer: Customer): string {
|
|
862
|
+
return customer.plan;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function getName(customer: Customer): string {
|
|
866
|
+
return customer.name;
|
|
867
|
+
}
|
|
868
|
+
```
|
|
869
|
+
|
|
870
|
+
---
|
|
871
|
+
|
|
872
|
+
### Replace Nested Conditional with Guard Clauses
|
|
873
|
+
|
|
874
|
+
**Code Smell:** Deeply nested conditionals
|
|
875
|
+
|
|
876
|
+
**Before:**
|
|
877
|
+
```typescript
|
|
878
|
+
function getPayAmount(employee: Employee): number {
|
|
879
|
+
let result: number;
|
|
880
|
+
if (employee.isDead) {
|
|
881
|
+
result = deadAmount();
|
|
882
|
+
} else {
|
|
883
|
+
if (employee.isSeparated) {
|
|
884
|
+
result = separatedAmount();
|
|
885
|
+
} else {
|
|
886
|
+
if (employee.isRetired) {
|
|
887
|
+
result = retiredAmount();
|
|
888
|
+
} else {
|
|
889
|
+
result = normalPayAmount();
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
return result;
|
|
894
|
+
}
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
**After:**
|
|
898
|
+
```typescript
|
|
899
|
+
function getPayAmount(employee: Employee): number {
|
|
900
|
+
if (employee.isDead) return deadAmount();
|
|
901
|
+
if (employee.isSeparated) return separatedAmount();
|
|
902
|
+
if (employee.isRetired) return retiredAmount();
|
|
903
|
+
return normalPayAmount();
|
|
904
|
+
}
|
|
905
|
+
```
|
|
906
|
+
|
|
907
|
+
---
|
|
908
|
+
|
|
909
|
+
## 6. API Simplification
|
|
910
|
+
|
|
911
|
+
### Separate Query from Modifier
|
|
912
|
+
|
|
913
|
+
**Code Smell:** Method both returns value and changes state
|
|
914
|
+
|
|
915
|
+
**Before:**
|
|
916
|
+
```typescript
|
|
917
|
+
function getTotalOutstandingAndSetReadyForSummaries(): number {
|
|
918
|
+
this.readyForSummaries = true;
|
|
919
|
+
return this.invoices.reduce((sum, inv) => sum + inv.amount, 0);
|
|
920
|
+
}
|
|
921
|
+
```
|
|
922
|
+
|
|
923
|
+
**After:**
|
|
924
|
+
```typescript
|
|
925
|
+
function getTotalOutstanding(): number {
|
|
926
|
+
return this.invoices.reduce((sum, inv) => sum + inv.amount, 0);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
function setReadyForSummaries(): void {
|
|
930
|
+
this.readyForSummaries = true;
|
|
931
|
+
}
|
|
932
|
+
```
|
|
933
|
+
|
|
934
|
+
---
|
|
935
|
+
|
|
936
|
+
### Parameterize Method
|
|
937
|
+
|
|
938
|
+
**Code Smell:** Multiple similar methods
|
|
939
|
+
|
|
940
|
+
**Before:**
|
|
941
|
+
```typescript
|
|
942
|
+
function tenPercentRaise(person: Person): void {
|
|
943
|
+
person.salary = person.salary * 1.1;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function fivePercentRaise(person: Person): void {
|
|
947
|
+
person.salary = person.salary * 1.05;
|
|
948
|
+
}
|
|
949
|
+
```
|
|
950
|
+
|
|
951
|
+
**After:**
|
|
952
|
+
```typescript
|
|
953
|
+
function raise(person: Person, factor: number): void {
|
|
954
|
+
person.salary = person.salary * (1 + factor);
|
|
955
|
+
}
|
|
956
|
+
```
|
|
957
|
+
|
|
958
|
+
---
|
|
959
|
+
|
|
960
|
+
### Replace Parameter with Query
|
|
961
|
+
|
|
962
|
+
**Code Smell:** Parameter can be computed
|
|
963
|
+
|
|
964
|
+
**Before:**
|
|
965
|
+
```typescript
|
|
966
|
+
function finalPrice(basePrice: number, discountLevel: number): number {
|
|
967
|
+
const discount = discountLevel === 2 ? 0.1 : 0.05;
|
|
968
|
+
return basePrice * (1 - discount);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// Caller
|
|
972
|
+
const level = customer.discountLevel;
|
|
973
|
+
const price = finalPrice(basePrice, level);
|
|
974
|
+
```
|
|
975
|
+
|
|
976
|
+
**After:**
|
|
977
|
+
```typescript
|
|
978
|
+
function finalPrice(basePrice: number): number {
|
|
979
|
+
const discount = this.discountLevel === 2 ? 0.1 : 0.05;
|
|
980
|
+
return basePrice * (1 - discount);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// Caller
|
|
984
|
+
const price = finalPrice(basePrice);
|
|
985
|
+
```
|
|
986
|
+
|
|
987
|
+
---
|
|
988
|
+
|
|
989
|
+
### Introduce Parameter Object
|
|
990
|
+
|
|
991
|
+
**Code Smell:** Groups of parameters that travel together
|
|
992
|
+
|
|
993
|
+
**Before:**
|
|
994
|
+
```typescript
|
|
995
|
+
function amountInvoiced(startDate: Date, endDate: Date): number { /* ... */ }
|
|
996
|
+
function amountReceived(startDate: Date, endDate: Date): number { /* ... */ }
|
|
997
|
+
function amountOverdue(startDate: Date, endDate: Date): number { /* ... */ }
|
|
998
|
+
```
|
|
999
|
+
|
|
1000
|
+
**After:**
|
|
1001
|
+
```typescript
|
|
1002
|
+
class DateRange {
|
|
1003
|
+
constructor(
|
|
1004
|
+
readonly start: Date,
|
|
1005
|
+
readonly end: Date
|
|
1006
|
+
) {}
|
|
1007
|
+
|
|
1008
|
+
contains(date: Date): boolean {
|
|
1009
|
+
return date >= this.start && date <= this.end;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
function amountInvoiced(range: DateRange): number { /* ... */ }
|
|
1014
|
+
function amountReceived(range: DateRange): number { /* ... */ }
|
|
1015
|
+
function amountOverdue(range: DateRange): number { /* ... */ }
|
|
1016
|
+
```
|
|
1017
|
+
|
|
1018
|
+
---
|
|
1019
|
+
|
|
1020
|
+
## 7. React/Next.js Specific Refactorings
|
|
1021
|
+
|
|
1022
|
+
### Extract Custom Hook
|
|
1023
|
+
|
|
1024
|
+
**Code Smell:** Stateful logic duplicated across components, complex useState/useEffect clusters
|
|
1025
|
+
|
|
1026
|
+
**Before:**
|
|
1027
|
+
```tsx
|
|
1028
|
+
function UserProfile() {
|
|
1029
|
+
const [user, setUser] = useState<User | null>(null);
|
|
1030
|
+
const [loading, setLoading] = useState(true);
|
|
1031
|
+
const [error, setError] = useState<Error | null>(null);
|
|
1032
|
+
|
|
1033
|
+
useEffect(() => {
|
|
1034
|
+
setLoading(true);
|
|
1035
|
+
fetchUser()
|
|
1036
|
+
.then(setUser)
|
|
1037
|
+
.catch(setError)
|
|
1038
|
+
.finally(() => setLoading(false));
|
|
1039
|
+
}, []);
|
|
1040
|
+
|
|
1041
|
+
if (loading) return <Spinner />;
|
|
1042
|
+
if (error) return <ErrorMessage error={error} />;
|
|
1043
|
+
return <ProfileCard user={user} />;
|
|
1044
|
+
}
|
|
1045
|
+
```
|
|
1046
|
+
|
|
1047
|
+
**After:**
|
|
1048
|
+
```tsx
|
|
1049
|
+
// hooks/useUser.ts
|
|
1050
|
+
function useUser() {
|
|
1051
|
+
const [user, setUser] = useState<User | null>(null);
|
|
1052
|
+
const [loading, setLoading] = useState(true);
|
|
1053
|
+
const [error, setError] = useState<Error | null>(null);
|
|
1054
|
+
|
|
1055
|
+
useEffect(() => {
|
|
1056
|
+
setLoading(true);
|
|
1057
|
+
fetchUser()
|
|
1058
|
+
.then(setUser)
|
|
1059
|
+
.catch(setError)
|
|
1060
|
+
.finally(() => setLoading(false));
|
|
1061
|
+
}, []);
|
|
1062
|
+
|
|
1063
|
+
return { user, loading, error };
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
// components/UserProfile.tsx
|
|
1067
|
+
function UserProfile() {
|
|
1068
|
+
const { user, loading, error } = useUser();
|
|
1069
|
+
|
|
1070
|
+
if (loading) return <Spinner />;
|
|
1071
|
+
if (error) return <ErrorMessage error={error} />;
|
|
1072
|
+
return <ProfileCard user={user} />;
|
|
1073
|
+
}
|
|
1074
|
+
```
|
|
1075
|
+
|
|
1076
|
+
**When to apply:**
|
|
1077
|
+
- Same stateful logic appears in 2+ components
|
|
1078
|
+
- Component has 3+ useState calls with related logic
|
|
1079
|
+
- useEffect logic could be reused elsewhere
|
|
1080
|
+
- Testing stateful logic independently from UI
|
|
1081
|
+
|
|
1082
|
+
---
|
|
1083
|
+
|
|
1084
|
+
### Lift State Up
|
|
1085
|
+
|
|
1086
|
+
**Code Smell:** Sibling components need to share state, prop drilling through intermediaries
|
|
1087
|
+
|
|
1088
|
+
**Before:**
|
|
1089
|
+
```tsx
|
|
1090
|
+
function ProductList() {
|
|
1091
|
+
const [selectedId, setSelectedId] = useState<string | null>(null);
|
|
1092
|
+
return (
|
|
1093
|
+
<div>
|
|
1094
|
+
{products.map(p => (
|
|
1095
|
+
<ProductCard
|
|
1096
|
+
key={p.id}
|
|
1097
|
+
product={p}
|
|
1098
|
+
selected={p.id === selectedId}
|
|
1099
|
+
onSelect={() => setSelectedId(p.id)}
|
|
1100
|
+
/>
|
|
1101
|
+
))}
|
|
1102
|
+
</div>
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
function ProductDetails() {
|
|
1107
|
+
// ❌ Can't access selectedId - needs to duplicate state or prop drill
|
|
1108
|
+
const [selectedId, setSelectedId] = useState<string | null>(null);
|
|
1109
|
+
// ...
|
|
1110
|
+
}
|
|
1111
|
+
```
|
|
1112
|
+
|
|
1113
|
+
**After:**
|
|
1114
|
+
```tsx
|
|
1115
|
+
function ProductPage() {
|
|
1116
|
+
const [selectedId, setSelectedId] = useState<string | null>(null);
|
|
1117
|
+
|
|
1118
|
+
return (
|
|
1119
|
+
<div className="grid grid-cols-2">
|
|
1120
|
+
<ProductList
|
|
1121
|
+
selectedId={selectedId}
|
|
1122
|
+
onSelect={setSelectedId}
|
|
1123
|
+
/>
|
|
1124
|
+
<ProductDetails productId={selectedId} />
|
|
1125
|
+
</div>
|
|
1126
|
+
);
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
function ProductList({ selectedId, onSelect }: ProductListProps) {
|
|
1130
|
+
return (
|
|
1131
|
+
<div>
|
|
1132
|
+
{products.map(p => (
|
|
1133
|
+
<ProductCard
|
|
1134
|
+
key={p.id}
|
|
1135
|
+
product={p}
|
|
1136
|
+
selected={p.id === selectedId}
|
|
1137
|
+
onSelect={() => onSelect(p.id)}
|
|
1138
|
+
/>
|
|
1139
|
+
))}
|
|
1140
|
+
</div>
|
|
1141
|
+
);
|
|
1142
|
+
}
|
|
1143
|
+
```
|
|
1144
|
+
|
|
1145
|
+
**When to apply:**
|
|
1146
|
+
- Sibling components need access to same state
|
|
1147
|
+
- State changes in one component should reflect in another
|
|
1148
|
+
- Prop drilling becomes excessive (consider context for deep trees)
|
|
1149
|
+
|
|
1150
|
+
---
|
|
1151
|
+
|
|
1152
|
+
### Replace Local State with URL State
|
|
1153
|
+
|
|
1154
|
+
**Code Smell:** Filter/search/pagination state lost on refresh, state not shareable via URL
|
|
1155
|
+
|
|
1156
|
+
**Before:**
|
|
1157
|
+
```tsx
|
|
1158
|
+
function ProductsPage() {
|
|
1159
|
+
const [search, setSearch] = useState('');
|
|
1160
|
+
const [category, setCategory] = useState('all');
|
|
1161
|
+
const [page, setPage] = useState(1);
|
|
1162
|
+
|
|
1163
|
+
// ❌ State lost on refresh, can't share URL
|
|
1164
|
+
return (
|
|
1165
|
+
<div>
|
|
1166
|
+
<SearchInput value={search} onChange={setSearch} />
|
|
1167
|
+
<CategoryFilter value={category} onChange={setCategory} />
|
|
1168
|
+
<ProductGrid search={search} category={category} page={page} />
|
|
1169
|
+
<Pagination page={page} onChange={setPage} />
|
|
1170
|
+
</div>
|
|
1171
|
+
);
|
|
1172
|
+
}
|
|
1173
|
+
```
|
|
1174
|
+
|
|
1175
|
+
**After:**
|
|
1176
|
+
```tsx
|
|
1177
|
+
// Next.js App Router
|
|
1178
|
+
function ProductsPage({ searchParams }: { searchParams: SearchParams }) {
|
|
1179
|
+
const search = searchParams.q ?? '';
|
|
1180
|
+
const category = searchParams.category ?? 'all';
|
|
1181
|
+
const page = Number(searchParams.page) || 1;
|
|
1182
|
+
|
|
1183
|
+
return (
|
|
1184
|
+
<div>
|
|
1185
|
+
<SearchInput defaultValue={search} />
|
|
1186
|
+
<CategoryFilter value={category} />
|
|
1187
|
+
<ProductGrid search={search} category={category} page={page} />
|
|
1188
|
+
<Pagination page={page} />
|
|
1189
|
+
</div>
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
// SearchInput uses router.push or Link to update URL
|
|
1194
|
+
function SearchInput({ defaultValue }: { defaultValue: string }) {
|
|
1195
|
+
const router = useRouter();
|
|
1196
|
+
const pathname = usePathname();
|
|
1197
|
+
|
|
1198
|
+
const handleSearch = (value: string) => {
|
|
1199
|
+
const params = new URLSearchParams(window.location.search);
|
|
1200
|
+
params.set('q', value);
|
|
1201
|
+
params.set('page', '1'); // Reset to first page
|
|
1202
|
+
router.push(`${pathname}?${params.toString()}`);
|
|
1203
|
+
};
|
|
1204
|
+
|
|
1205
|
+
return <Input defaultValue={defaultValue} onChange={handleSearch} />;
|
|
1206
|
+
}
|
|
1207
|
+
```
|
|
1208
|
+
|
|
1209
|
+
**When to apply:**
|
|
1210
|
+
- Users expect to bookmark or share filtered views
|
|
1211
|
+
- State should persist across browser refresh
|
|
1212
|
+
- Back/forward navigation should restore previous state
|
|
1213
|
+
- SEO benefits from indexable filter combinations
|
|
1214
|
+
|
|
1215
|
+
---
|
|
1216
|
+
|
|
1217
|
+
### Convert to Server Component
|
|
1218
|
+
|
|
1219
|
+
**Code Smell:** Client component only fetches and displays data, no interactivity
|
|
1220
|
+
|
|
1221
|
+
**Before:**
|
|
1222
|
+
```tsx
|
|
1223
|
+
'use client';
|
|
1224
|
+
|
|
1225
|
+
import { useEffect, useState } from 'react';
|
|
1226
|
+
|
|
1227
|
+
export function UserList() {
|
|
1228
|
+
const [users, setUsers] = useState<User[]>([]);
|
|
1229
|
+
const [loading, setLoading] = useState(true);
|
|
1230
|
+
|
|
1231
|
+
useEffect(() => {
|
|
1232
|
+
fetch('/api/users')
|
|
1233
|
+
.then(res => res.json())
|
|
1234
|
+
.then(setUsers)
|
|
1235
|
+
.finally(() => setLoading(false));
|
|
1236
|
+
}, []);
|
|
1237
|
+
|
|
1238
|
+
if (loading) return <Skeleton />;
|
|
1239
|
+
|
|
1240
|
+
return (
|
|
1241
|
+
<ul>
|
|
1242
|
+
{users.map(user => (
|
|
1243
|
+
<li key={user.id}>{user.name}</li>
|
|
1244
|
+
))}
|
|
1245
|
+
</ul>
|
|
1246
|
+
);
|
|
1247
|
+
}
|
|
1248
|
+
```
|
|
1249
|
+
|
|
1250
|
+
**After:**
|
|
1251
|
+
```tsx
|
|
1252
|
+
// Server Component (default in App Router)
|
|
1253
|
+
import { getUsers } from '@/entities/user/apis';
|
|
1254
|
+
|
|
1255
|
+
export async function UserList() {
|
|
1256
|
+
const users = await getUsers();
|
|
1257
|
+
|
|
1258
|
+
return (
|
|
1259
|
+
<ul>
|
|
1260
|
+
{users.map(user => (
|
|
1261
|
+
<li key={user.id}>{user.name}</li>
|
|
1262
|
+
))}
|
|
1263
|
+
</ul>
|
|
1264
|
+
);
|
|
1265
|
+
}
|
|
1266
|
+
```
|
|
1267
|
+
|
|
1268
|
+
**When to apply:**
|
|
1269
|
+
- Component only displays data (no useState, useEffect for interactivity)
|
|
1270
|
+
- Data fetching can happen on server
|
|
1271
|
+
- Reducing client JavaScript bundle size
|
|
1272
|
+
- Avoiding loading states when possible
|
|
1273
|
+
|
|
1274
|
+
**Benefits:**
|
|
1275
|
+
- Zero client JavaScript for this component
|
|
1276
|
+
- No loading state needed (data fetched before render)
|
|
1277
|
+
- Better SEO (content in initial HTML)
|
|
1278
|
+
- Reduced network waterfall
|
|
1279
|
+
|
|
1280
|
+
---
|
|
1281
|
+
|
|
1282
|
+
### Extract Server Action
|
|
1283
|
+
|
|
1284
|
+
**Code Smell:** Form submission logic mixed with UI, API routes for simple mutations
|
|
1285
|
+
|
|
1286
|
+
**Before:**
|
|
1287
|
+
```tsx
|
|
1288
|
+
'use client';
|
|
1289
|
+
|
|
1290
|
+
export function ContactForm() {
|
|
1291
|
+
const [pending, setPending] = useState(false);
|
|
1292
|
+
|
|
1293
|
+
const handleSubmit = async (e: FormEvent) => {
|
|
1294
|
+
e.preventDefault();
|
|
1295
|
+
setPending(true);
|
|
1296
|
+
|
|
1297
|
+
const formData = new FormData(e.target as HTMLFormElement);
|
|
1298
|
+
await fetch('/api/contact', {
|
|
1299
|
+
method: 'POST',
|
|
1300
|
+
body: JSON.stringify(Object.fromEntries(formData)),
|
|
1301
|
+
});
|
|
1302
|
+
|
|
1303
|
+
setPending(false);
|
|
1304
|
+
};
|
|
1305
|
+
|
|
1306
|
+
return (
|
|
1307
|
+
<form onSubmit={handleSubmit}>
|
|
1308
|
+
<input name="email" type="email" required />
|
|
1309
|
+
<textarea name="message" required />
|
|
1310
|
+
<button disabled={pending}>
|
|
1311
|
+
{pending ? 'Sending...' : 'Send'}
|
|
1312
|
+
</button>
|
|
1313
|
+
</form>
|
|
1314
|
+
);
|
|
1315
|
+
}
|
|
1316
|
+
```
|
|
1317
|
+
|
|
1318
|
+
**After:**
|
|
1319
|
+
```tsx
|
|
1320
|
+
// actions/contact.ts
|
|
1321
|
+
'use server';
|
|
1322
|
+
|
|
1323
|
+
import { z } from 'zod';
|
|
1324
|
+
|
|
1325
|
+
const schema = z.object({
|
|
1326
|
+
email: z.string().email(),
|
|
1327
|
+
message: z.string().min(10),
|
|
1328
|
+
});
|
|
1329
|
+
|
|
1330
|
+
export async function submitContact(formData: FormData) {
|
|
1331
|
+
const data = schema.parse({
|
|
1332
|
+
email: formData.get('email'),
|
|
1333
|
+
message: formData.get('message'),
|
|
1334
|
+
});
|
|
1335
|
+
|
|
1336
|
+
await db.contact.create({ data });
|
|
1337
|
+
revalidatePath('/contact');
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
// components/ContactForm.tsx
|
|
1341
|
+
import { submitContact } from '@/actions/contact';
|
|
1342
|
+
|
|
1343
|
+
export function ContactForm() {
|
|
1344
|
+
return (
|
|
1345
|
+
<form action={submitContact}>
|
|
1346
|
+
<input name="email" type="email" required />
|
|
1347
|
+
<textarea name="message" required />
|
|
1348
|
+
<SubmitButton />
|
|
1349
|
+
</form>
|
|
1350
|
+
);
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
function SubmitButton() {
|
|
1354
|
+
const { pending } = useFormStatus();
|
|
1355
|
+
return (
|
|
1356
|
+
<button disabled={pending}>
|
|
1357
|
+
{pending ? 'Sending...' : 'Send'}
|
|
1358
|
+
</button>
|
|
1359
|
+
);
|
|
1360
|
+
}
|
|
1361
|
+
```
|
|
1362
|
+
|
|
1363
|
+
**When to apply:**
|
|
1364
|
+
- Form submission doesn't need client-side validation beyond HTML5
|
|
1365
|
+
- Action is a simple database mutation
|
|
1366
|
+
- Progressive enhancement desired (works without JS)
|
|
1367
|
+
- Reducing client-side state management
|
|
1368
|
+
|
|
1369
|
+
---
|
|
1370
|
+
|
|
1371
|
+
## Related Resources
|
|
1372
|
+
|
|
1373
|
+
- Martin Fowler's Refactoring Catalog: https://refactoring.com/catalog/
|
|
1374
|
+
- "Refactoring" by Martin Fowler (2nd Edition)
|
|
1375
|
+
- "Clean Code" by Robert C. Martin
|
|
1376
|
+
- React Documentation: https://react.dev/learn/sharing-state-between-components
|
|
1377
|
+
- Next.js App Router: https://nextjs.org/docs/app
|