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.
Files changed (53) hide show
  1. package/.ai-rules/CHANGELOG.md +122 -0
  2. package/.ai-rules/agents/README.md +527 -11
  3. package/.ai-rules/agents/accessibility-specialist.json +0 -1
  4. package/.ai-rules/agents/act-mode.json +0 -1
  5. package/.ai-rules/agents/agent-architect.json +0 -1
  6. package/.ai-rules/agents/ai-ml-engineer.json +0 -1
  7. package/.ai-rules/agents/architecture-specialist.json +14 -2
  8. package/.ai-rules/agents/backend-developer.json +14 -2
  9. package/.ai-rules/agents/code-quality-specialist.json +0 -1
  10. package/.ai-rules/agents/data-engineer.json +0 -1
  11. package/.ai-rules/agents/devops-engineer.json +24 -2
  12. package/.ai-rules/agents/documentation-specialist.json +0 -1
  13. package/.ai-rules/agents/eval-mode.json +0 -1
  14. package/.ai-rules/agents/event-architecture-specialist.json +719 -0
  15. package/.ai-rules/agents/frontend-developer.json +14 -2
  16. package/.ai-rules/agents/i18n-specialist.json +0 -1
  17. package/.ai-rules/agents/integration-specialist.json +11 -1
  18. package/.ai-rules/agents/migration-specialist.json +676 -0
  19. package/.ai-rules/agents/mobile-developer.json +0 -1
  20. package/.ai-rules/agents/observability-specialist.json +747 -0
  21. package/.ai-rules/agents/performance-specialist.json +24 -2
  22. package/.ai-rules/agents/plan-mode.json +0 -1
  23. package/.ai-rules/agents/platform-engineer.json +0 -1
  24. package/.ai-rules/agents/security-specialist.json +27 -16
  25. package/.ai-rules/agents/seo-specialist.json +0 -1
  26. package/.ai-rules/agents/solution-architect.json +0 -1
  27. package/.ai-rules/agents/technical-planner.json +0 -1
  28. package/.ai-rules/agents/test-strategy-specialist.json +14 -2
  29. package/.ai-rules/agents/ui-ux-designer.json +0 -1
  30. package/.ai-rules/rules/core.md +25 -0
  31. package/.ai-rules/skills/README.md +35 -0
  32. package/.ai-rules/skills/database-migration/SKILL.md +531 -0
  33. package/.ai-rules/skills/database-migration/expand-contract-patterns.md +314 -0
  34. package/.ai-rules/skills/database-migration/large-scale-migration.md +414 -0
  35. package/.ai-rules/skills/database-migration/rollback-strategies.md +359 -0
  36. package/.ai-rules/skills/database-migration/validation-procedures.md +428 -0
  37. package/.ai-rules/skills/dependency-management/SKILL.md +381 -0
  38. package/.ai-rules/skills/dependency-management/license-compliance.md +282 -0
  39. package/.ai-rules/skills/dependency-management/lock-file-management.md +437 -0
  40. package/.ai-rules/skills/dependency-management/major-upgrade-guide.md +292 -0
  41. package/.ai-rules/skills/dependency-management/security-vulnerability-response.md +230 -0
  42. package/.ai-rules/skills/incident-response/SKILL.md +373 -0
  43. package/.ai-rules/skills/incident-response/communication-templates.md +322 -0
  44. package/.ai-rules/skills/incident-response/escalation-matrix.md +347 -0
  45. package/.ai-rules/skills/incident-response/postmortem-template.md +351 -0
  46. package/.ai-rules/skills/incident-response/severity-classification.md +256 -0
  47. package/.ai-rules/skills/performance-optimization/CREATION-LOG.md +87 -0
  48. package/.ai-rules/skills/performance-optimization/SKILL.md +76 -0
  49. package/.ai-rules/skills/performance-optimization/documentation-template.md +70 -0
  50. package/.ai-rules/skills/pr-review/SKILL.md +768 -0
  51. package/.ai-rules/skills/refactoring/SKILL.md +192 -0
  52. package/.ai-rules/skills/refactoring/refactoring-catalog.md +1377 -0
  53. 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