@rmrdeveloper/sideroom-pi 7.0.1 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +162 -125
- package/assets/artifacts/GUIDELINES_TEMPLATE.md +193 -413
- package/extensions/ask/model.ts +60 -15
- package/extensions/guidelines/guard.ts +136 -0
- package/extensions/guidelines/index.ts +9 -0
- package/extensions/guidelines/prompt.ts +5 -4
- package/package.json +1 -1
- package/skills/sideroom-domain-modeling/SKILL.md +81 -0
- package/skills/sideroom-domain-scaffold/SKILL.md +70 -0
- package/skills/sideroom-grill/SKILL.md +61 -0
- package/skills/sideroom-guidelines/SKILL.md +12 -8
- package/skills/sideroom-guidelines/references/languages/go.md +369 -19
- package/skills/sideroom-guidelines/references/languages/java.md +249 -16
- package/skills/sideroom-guidelines/references/languages/php-laravel.md +242 -20
- package/skills/sideroom-guidelines/references/languages/python.md +312 -12
- package/skills/sideroom-guidelines/references/languages/rust.md +244 -12
- package/skills/sideroom-guidelines/references/languages/typescript.md +248 -29
|
@@ -1,27 +1,377 @@
|
|
|
1
|
-
# Go
|
|
1
|
+
# Go Coding Guidelines
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Rules every Go change must follow.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
## Index
|
|
6
|
+
|
|
7
|
+
| # | Rule | Tier |
|
|
8
|
+
| --- | ------ | ------ |
|
|
9
|
+
| 1 | [Braced conditionals](#1-braced-conditionals) | mechanical |
|
|
10
|
+
| 2 | [No magic strings](#2-no-magic-strings) | mechanical |
|
|
11
|
+
| 3 | [Guard clauses](#3-guard-clauses) | structural |
|
|
12
|
+
| 4 | [Fail fast](#4-fail-fast) | structural |
|
|
13
|
+
| 5 | [Explicit error handling](#5-explicit-error-handling) | structural |
|
|
14
|
+
| 6 | [Clear names](#6-clear-names) | structural |
|
|
15
|
+
| 7 | [Command Query Separation](#7-command-query-separation-cqs) | structural |
|
|
16
|
+
| 8 | [Null/undefined handling](#8-nullundefined-handling) | structural |
|
|
17
|
+
| 9 | [Immutability by default](#9-immutability-by-default) | structural |
|
|
18
|
+
| 10 | [Validate once](#10-validate-once) | structural |
|
|
19
|
+
| 11 | [Single Responsibility](#11-single-responsibility-srp) | architectural |
|
|
20
|
+
| 12 | [DRY (real duplication only)](#12-dry-real-duplication-only) | architectural |
|
|
21
|
+
| 13 | [KISS](#13-kiss) | architectural |
|
|
22
|
+
| 14 | [YAGNI](#14-yagni) | architectural |
|
|
23
|
+
| 15 | [Law of Demeter](#15-law-of-demeter) | architectural |
|
|
24
|
+
| 16 | [Composition over inheritance](#16-composition-over-inheritance) | architectural |
|
|
25
|
+
| 17 | [Dependency direction](#17-dependency-direction) | architectural |
|
|
26
|
+
| 18 | [Testability](#18-testability-as-a-design-constraint) | architectural |
|
|
27
|
+
| 19 | [Comments](#19-comments) | architectural |
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
### 1. Braced conditionals
|
|
32
|
+
|
|
33
|
+
RULE: Go requires braces for every `if`/`else` body. Keep even a one-line guard as a full, formatted block rather than compressing it onto one line.
|
|
34
|
+
|
|
35
|
+
```go
|
|
36
|
+
// bad
|
|
37
|
+
if user == nil {
|
|
38
|
+
return 0
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// good
|
|
42
|
+
if user == nil {
|
|
43
|
+
return 0
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 2. No magic strings
|
|
48
|
+
|
|
49
|
+
RULE: Name domain/protocol literals once (const, enum, shared contract) and reuse the name. One-off strings with no reuse and no domain meaning (a single log label) may stay inline.
|
|
8
50
|
|
|
9
51
|
```go
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if err == nil {
|
|
14
|
-
return user, nil
|
|
15
|
-
}
|
|
16
|
-
return nil, nil
|
|
52
|
+
// bad
|
|
53
|
+
if order.Status == "pending_payment" {
|
|
54
|
+
charge(order)
|
|
17
55
|
}
|
|
18
56
|
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
return user, nil
|
|
57
|
+
// good
|
|
58
|
+
const PendingPayment OrderStatus = "pending_payment"
|
|
59
|
+
|
|
60
|
+
if order.Status == PendingPayment {
|
|
61
|
+
charge(order)
|
|
26
62
|
}
|
|
27
63
|
```
|
|
64
|
+
|
|
65
|
+
### 3. Guard clauses
|
|
66
|
+
|
|
67
|
+
RULE: Validate and exit early. Keep the happy path last, at the shallowest indent. No pyramid nesting.
|
|
68
|
+
|
|
69
|
+
```go
|
|
70
|
+
// bad
|
|
71
|
+
if user != nil {
|
|
72
|
+
if user.Active {
|
|
73
|
+
return 0.2
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return 0
|
|
77
|
+
|
|
78
|
+
// good
|
|
79
|
+
if user == nil || !user.Active || !user.HasSubscription {
|
|
80
|
+
return 0
|
|
81
|
+
}
|
|
82
|
+
return 0.2
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### 4. Fail fast
|
|
86
|
+
|
|
87
|
+
RULE: Invalid input, impossible state, or a broken dependency returns an error immediately with a clear message. No fallback chains or silent defaults.
|
|
88
|
+
WHY: This is for invariants that should never happen if upstream contracts hold — not license to re-check business rules a boundary already validated (see rule 10).
|
|
89
|
+
|
|
90
|
+
```go
|
|
91
|
+
// bad
|
|
92
|
+
zone, ok := zones[order.ZoneID]
|
|
93
|
+
if !ok {
|
|
94
|
+
zone = zones["default"]
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// good
|
|
98
|
+
zone, ok := zones[order.ZoneID]
|
|
99
|
+
if !ok {
|
|
100
|
+
return Zone{}, fmt.Errorf("unknown zone ID: %s", order.ZoneID)
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### 5. Explicit error handling
|
|
105
|
+
|
|
106
|
+
RULE: Never ignore an error. Return it with context, or let it propagate.
|
|
107
|
+
|
|
108
|
+
```go
|
|
109
|
+
// bad
|
|
110
|
+
user, _ := api.GetUser(id)
|
|
111
|
+
|
|
112
|
+
// good
|
|
113
|
+
user, err := api.GetUser(id)
|
|
114
|
+
if err != nil {
|
|
115
|
+
return User{}, fmt.Errorf("load user %s: %w", id, err)
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### 6. Clear names
|
|
120
|
+
|
|
121
|
+
RULE: Names reveal role or domain meaning. No `data`, `info`, `temp`, `result`, `obj`, `val`, `x`. A long honest name beats a short vague one.
|
|
122
|
+
|
|
123
|
+
```go
|
|
124
|
+
// bad
|
|
125
|
+
func process(data []Subscription) []Subscription {
|
|
126
|
+
return filter(data, func(x Subscription) bool {
|
|
127
|
+
return x.Days > 0
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// good
|
|
132
|
+
func activeSubscriptions(subscriptions []Subscription) []Subscription {
|
|
133
|
+
return filter(subscriptions, func(subscription Subscription) bool {
|
|
134
|
+
return subscription.RemainingDays > 0
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### 7. Command Query Separation (CQS)
|
|
140
|
+
|
|
141
|
+
RULE: A function either does something (side effect) or returns something (query) — never both. A "getter" must never mutate.
|
|
142
|
+
|
|
143
|
+
```go
|
|
144
|
+
// bad
|
|
145
|
+
func nextID(counter *Counter) int {
|
|
146
|
+
counter.Value++
|
|
147
|
+
return counter.Value
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// good
|
|
151
|
+
func peekNextID(counter Counter) int {
|
|
152
|
+
return counter.Value + 1
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
func incrementCounter(counter *Counter) {
|
|
156
|
+
counter.Value++
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### 8. Null/undefined handling
|
|
161
|
+
|
|
162
|
+
RULE: Give `nil` one meaning for a type and never overload it for several business states. Use explicit values or the `(value, ok)` idiom for distinct states.
|
|
163
|
+
|
|
164
|
+
```go
|
|
165
|
+
// bad
|
|
166
|
+
func discount(user *User) *int {
|
|
167
|
+
if user == nil || user.Plan == nil || user.Plan.Discount == 0 {
|
|
168
|
+
return nil
|
|
169
|
+
}
|
|
170
|
+
return &user.Plan.Discount
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// good
|
|
174
|
+
func discount(user *User) int {
|
|
175
|
+
if user == nil || user.Plan == nil {
|
|
176
|
+
return 0
|
|
177
|
+
}
|
|
178
|
+
return user.Plan.Discount
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### 9. Immutability by default
|
|
183
|
+
|
|
184
|
+
RULE: Return new values instead of mutating parameters. Never hide a side effect inside what looks like a pure transform.
|
|
185
|
+
|
|
186
|
+
```go
|
|
187
|
+
// bad
|
|
188
|
+
func addItem(items []Item, item Item) []Item {
|
|
189
|
+
return append(items, item)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// good
|
|
193
|
+
func addItem(items []Item, item Item) []Item {
|
|
194
|
+
updated := append([]Item(nil), items...)
|
|
195
|
+
return append(updated, item)
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### 10. Validate once
|
|
200
|
+
|
|
201
|
+
RULE: Validate at the boundary that owns the input (decoder, schema, DTO). Downstream code trusts that contract — no re-checking the same required/type/range rule in a helper, use case, or adapter further in.
|
|
202
|
+
|
|
203
|
+
```go
|
|
204
|
+
// bad
|
|
205
|
+
func update(profile ValidatedProfile) error {
|
|
206
|
+
if profile.Email == "" {
|
|
207
|
+
return errors.New("email required")
|
|
208
|
+
}
|
|
209
|
+
return repo.Update(profile)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// good
|
|
213
|
+
func update(profile ValidatedProfile) error {
|
|
214
|
+
return repo.Update(profile)
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### 11. Single Responsibility (SRP)
|
|
219
|
+
|
|
220
|
+
RULE: A function, class, or module has one reason to change. If it validates AND transforms AND persists AND notifies, split it.
|
|
221
|
+
|
|
222
|
+
```go
|
|
223
|
+
// bad
|
|
224
|
+
func save(input Input) error {
|
|
225
|
+
validate(input)
|
|
226
|
+
user := normalize(input)
|
|
227
|
+
repo.Save(user)
|
|
228
|
+
return mailer.Welcome(user)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// good
|
|
232
|
+
func create(input ValidatedInput) (User, error) {
|
|
233
|
+
user := normalize(input)
|
|
234
|
+
return user, repo.Save(user)
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### 12. DRY (real duplication only)
|
|
239
|
+
|
|
240
|
+
RULE: Extract when at least 2 of 3 hold: (a) the logic appears 3+ times, (b) a future rule change would need to touch all occurrences at once, (c) the copies represent the same domain concept. Do not extract on the first or second occurrence, and do not extract look-alike code that belongs to different concepts (a user's email validation vs a vendor's) — they will diverge.
|
|
241
|
+
|
|
242
|
+
```go
|
|
243
|
+
// bad — extracted after one use
|
|
244
|
+
func formatName(value string) string {
|
|
245
|
+
return strings.ToUpper(strings.TrimSpace(value))
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// good — third occurrence confirms one domain concept
|
|
249
|
+
func normalizeTicketStatus(status string) TicketStatus {
|
|
250
|
+
return ParseTicketStatus(strings.TrimSpace(status))
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### 13. KISS
|
|
255
|
+
|
|
256
|
+
RULE: Ship the simplest solution for the current, real requirement. No configurability, hooks, or layers added "just in case."
|
|
257
|
+
|
|
258
|
+
```go
|
|
259
|
+
// bad
|
|
260
|
+
func formatPrice(value float64, options FormatOptions) string {
|
|
261
|
+
return formatter.Format(value, options)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// good
|
|
265
|
+
func formatPrice(value float64) string {
|
|
266
|
+
return fmt.Sprintf("$%.2f", value)
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### 14. YAGNI
|
|
271
|
+
|
|
272
|
+
RULE: Don't add fields, params, or branches for a use case that hasn't arrived. (Differs from KISS: KISS simplifies the chosen solution; YAGNI stops you building the unrequested one.)
|
|
273
|
+
|
|
274
|
+
```go
|
|
275
|
+
// bad
|
|
276
|
+
func createInvoice(order Order, recurring, multiCurrency bool) Invoice {
|
|
277
|
+
return build(order, recurring, multiCurrency)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// good
|
|
281
|
+
func createInvoice(order Order) Invoice {
|
|
282
|
+
return Invoice{
|
|
283
|
+
Total: order.Total,
|
|
284
|
+
Items: order.Items,
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### 15. Law of Demeter
|
|
290
|
+
|
|
291
|
+
RULE: Talk only to immediate collaborators. Never reach through an object to grab something several levels deep inside its internal structure.
|
|
292
|
+
|
|
293
|
+
```go
|
|
294
|
+
// bad
|
|
295
|
+
func cityName(user User) string {
|
|
296
|
+
return user.Address.City.Name
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// good
|
|
300
|
+
func cityName(user User) string {
|
|
301
|
+
return user.CityName()
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### 16. Composition over inheritance
|
|
306
|
+
|
|
307
|
+
RULE: Compose small, focused behaviors instead of building inheritance chains that force unrelated methods onto every subtype.
|
|
308
|
+
|
|
309
|
+
```go
|
|
310
|
+
// bad
|
|
311
|
+
type Animal interface {
|
|
312
|
+
Sound() string
|
|
313
|
+
Fly()
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// good
|
|
317
|
+
type Dog struct {
|
|
318
|
+
Sound SoundBehavior
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
type Bird struct {
|
|
322
|
+
Sound SoundBehavior
|
|
323
|
+
Flight FlightBehavior
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### 17. Dependency direction
|
|
328
|
+
|
|
329
|
+
RULE: Inner layers (domain/business logic) never import outer layers (framework, DB, HTTP). Outer layers depend inward, never the reverse.
|
|
330
|
+
|
|
331
|
+
```go
|
|
332
|
+
// bad
|
|
333
|
+
func total(id string) Money {
|
|
334
|
+
return mysql.NewInvoiceRepository().Get(id).Total()
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// good
|
|
338
|
+
func total(invoice Invoice) Money {
|
|
339
|
+
return invoice.Total()
|
|
340
|
+
}
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### 18. Testability as a design constraint
|
|
344
|
+
|
|
345
|
+
RULE: Inject dependencies (clock, network, random) instead of hardcoding them. If a function is hard to test, redesign it — don't skip the test.
|
|
346
|
+
|
|
347
|
+
```go
|
|
348
|
+
// bad
|
|
349
|
+
func expired(subscription Subscription) bool {
|
|
350
|
+
return subscription.ExpiresAt.Before(time.Now())
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// good
|
|
354
|
+
func expired(subscription Subscription, now time.Time) bool {
|
|
355
|
+
return subscription.ExpiresAt.Before(now)
|
|
356
|
+
}
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### 19. Comments
|
|
360
|
+
|
|
361
|
+
RULE: Comment only non-obvious intent — a trade-off, a constraint, a hazard names can't carry. Delete narration, restated logic, and stale TODOs. Test: if deleting the comment leaves the code equally clear, delete it.
|
|
362
|
+
|
|
363
|
+
```go
|
|
364
|
+
// bad
|
|
365
|
+
// Increment the counter.
|
|
366
|
+
counter++
|
|
367
|
+
|
|
368
|
+
// good
|
|
369
|
+
// Poll because the provider cannot guarantee single webhook delivery.
|
|
370
|
+
ticker := time.NewTicker(5 * time.Second)
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
**When in doubt:** fail fast, keep it flat, keep it small.
|
|
376
|
+
|
|
377
|
+
**Enforcement note:** rules 1–2 are mechanical and should be backed by Go formatting or linting. Rules 3–10 are structural checks. Rules 11–19 require review judgment.
|
|
@@ -1,24 +1,257 @@
|
|
|
1
|
-
# Java
|
|
1
|
+
# Java Coding Guidelines
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Rules every Java change must follow.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
## Index
|
|
6
|
+
|
|
7
|
+
| # | Rule | Tier |
|
|
8
|
+
| --- | ------ | ------ |
|
|
9
|
+
| 1 | [Braced conditionals](#1-braced-conditionals) | mechanical |
|
|
10
|
+
| 2 | [No magic strings](#2-no-magic-strings) | mechanical |
|
|
11
|
+
| 3 | [Guard clauses](#3-guard-clauses) | structural |
|
|
12
|
+
| 4 | [Fail fast](#4-fail-fast) | structural |
|
|
13
|
+
| 5 | [Explicit error handling](#5-explicit-error-handling) | structural |
|
|
14
|
+
| 6 | [Clear names](#6-clear-names) | structural |
|
|
15
|
+
| 7 | [Command Query Separation](#7-command-query-separation-cqs) | structural |
|
|
16
|
+
| 8 | [Null/undefined handling](#8-nullundefined-handling) | structural |
|
|
17
|
+
| 9 | [Immutability by default](#9-immutability-by-default) | structural |
|
|
18
|
+
| 10 | [Validate once](#10-validate-once) | structural |
|
|
19
|
+
| 11 | [Single Responsibility](#11-single-responsibility-srp) | architectural |
|
|
20
|
+
| 12 | [DRY (real duplication only)](#12-dry-real-duplication-only) | architectural |
|
|
21
|
+
| 13 | [KISS](#13-kiss) | architectural |
|
|
22
|
+
| 14 | [YAGNI](#14-yagni) | architectural |
|
|
23
|
+
| 15 | [Law of Demeter](#15-law-of-demeter) | architectural |
|
|
24
|
+
| 16 | [Composition over inheritance](#16-composition-over-inheritance) | architectural |
|
|
25
|
+
| 17 | [Dependency direction](#17-dependency-direction) | architectural |
|
|
26
|
+
| 18 | [Testability](#18-testability-as-a-design-constraint) | architectural |
|
|
27
|
+
| 19 | [Comments](#19-comments) | architectural |
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
### 1. Braced conditionals
|
|
32
|
+
|
|
33
|
+
RULE: Every `if`/`else` body uses braces, even a one-line guard. Never bind a statement to a conditional by indentation alone.
|
|
8
34
|
|
|
9
35
|
```java
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
36
|
+
// bad
|
|
37
|
+
if (user == null) return 0;
|
|
38
|
+
// good
|
|
39
|
+
if (user == null) {
|
|
40
|
+
return 0;
|
|
14
41
|
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 2. No magic strings
|
|
45
|
+
|
|
46
|
+
RULE: Name domain/protocol literals once (const, enum, shared contract) and reuse the name. One-off strings with no reuse and no domain meaning (a single log label) may stay inline.
|
|
47
|
+
|
|
48
|
+
```java
|
|
49
|
+
// bad
|
|
50
|
+
if (order.status().equals("pending_payment")) { charge(order); }
|
|
51
|
+
// good
|
|
52
|
+
if (order.status() == OrderStatus.PENDING_PAYMENT) { charge(order); }
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 3. Guard clauses
|
|
56
|
+
|
|
57
|
+
RULE: Validate and exit early. Keep the happy path last, at the shallowest indent. No pyramid nesting.
|
|
58
|
+
|
|
59
|
+
```java
|
|
60
|
+
// bad
|
|
61
|
+
if (user != null) { if (user.isActive()) { return user.hasSubscription() ? .2 : 0; } }
|
|
62
|
+
return 0;
|
|
63
|
+
// good
|
|
64
|
+
if (user == null || !user.isActive() || !user.hasSubscription()) { return 0; }
|
|
65
|
+
return .2;
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### 4. Fail fast
|
|
69
|
+
|
|
70
|
+
RULE: Invalid input, impossible state, or a broken dependency throws immediately with a clear message. No fallback chains or silent defaults.
|
|
71
|
+
WHY: This is for invariants that should never happen if upstream contracts hold — not license to re-check business rules a boundary already validated (see rule 10).
|
|
72
|
+
|
|
73
|
+
```java
|
|
74
|
+
// bad
|
|
75
|
+
Zone zone = zones.getOrDefault(order.zoneId(), Zone.DEFAULT);
|
|
76
|
+
// good
|
|
77
|
+
Zone zone = Optional.ofNullable(zones.get(order.zoneId()))
|
|
78
|
+
.orElseThrow(() -> new IllegalArgumentException("Unknown zoneId: " + order.zoneId()));
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### 5. Explicit error handling
|
|
82
|
+
|
|
83
|
+
RULE: Never swallow an error in an empty or generic `catch`. Re-throw with context, or let it propagate.
|
|
84
|
+
|
|
85
|
+
```java
|
|
86
|
+
// bad
|
|
87
|
+
try { return api.getUser(id); } catch (Exception error) { return null; }
|
|
88
|
+
// good
|
|
89
|
+
try { return api.getUser(id); }
|
|
90
|
+
catch (ApiException error) { throw new UserLoadException("Failed to load user " + id, error); }
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### 6. Clear names
|
|
94
|
+
|
|
95
|
+
RULE: Names reveal role or domain meaning. No `data`, `info`, `temp`, `result`, `obj`, `val`, `x`. A long honest name beats a short vague one.
|
|
15
96
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return zone.baseCost();
|
|
97
|
+
```java
|
|
98
|
+
// bad
|
|
99
|
+
List<Subscription> process(List<Subscription> data) { return data.stream().filter(x -> x.days() > 0).toList(); }
|
|
100
|
+
// good
|
|
101
|
+
List<Subscription> activeSubscriptions(List<Subscription> subscriptions) {
|
|
102
|
+
return subscriptions.stream().filter(subscription -> subscription.remainingDays() > 0).toList();
|
|
23
103
|
}
|
|
24
104
|
```
|
|
105
|
+
|
|
106
|
+
### 7. Command Query Separation (CQS)
|
|
107
|
+
|
|
108
|
+
RULE: A function either does something (side effect) or returns something (query) — never both. A "getter" must never mutate.
|
|
109
|
+
|
|
110
|
+
```java
|
|
111
|
+
// bad
|
|
112
|
+
int getNextId(Counter counter) { counter.increment(); return counter.value(); }
|
|
113
|
+
// good
|
|
114
|
+
int peekNextId(Counter counter) { return counter.value() + 1; }
|
|
115
|
+
void incrementCounter(Counter counter) { counter.increment(); }
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### 8. Null/undefined handling
|
|
119
|
+
|
|
120
|
+
RULE: Pick one convention for `null` and never mix meanings. Never overload null to mean several business states — give each state its own explicit value, using `Optional` only for genuine absence.
|
|
121
|
+
|
|
122
|
+
```java
|
|
123
|
+
// bad
|
|
124
|
+
Integer discount(User user) { return user == null || user.plan() == null || user.plan().discount() == 0 ? null : user.plan().discount(); }
|
|
125
|
+
// good
|
|
126
|
+
int discount(User user) { return user == null || user.plan() == null ? 0 : user.plan().discount(); }
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### 9. Immutability by default
|
|
130
|
+
|
|
131
|
+
RULE: Return new values instead of mutating parameters. Never hide a side effect inside what looks like a pure transform.
|
|
132
|
+
|
|
133
|
+
```java
|
|
134
|
+
// bad
|
|
135
|
+
Cart addItem(Cart cart, Item item) { cart.items().add(item); return cart; }
|
|
136
|
+
// good
|
|
137
|
+
Cart addItem(Cart cart, Item item) { return cart.withItems(Stream.concat(cart.items().stream(), Stream.of(item)).toList()); }
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### 10. Validate once
|
|
141
|
+
|
|
142
|
+
RULE: Validate at the boundary that owns the input (schema, request DTO). Downstream code trusts that contract — no re-checking the same required/type/range rule in a helper, use case, or adapter further in.
|
|
143
|
+
|
|
144
|
+
```java
|
|
145
|
+
// bad
|
|
146
|
+
void update(ValidatedProfile profile) { if (profile.email().isBlank()) { throw new IllegalArgumentException(); } repository.update(profile); }
|
|
147
|
+
// good
|
|
148
|
+
void update(ValidatedProfile profile) { repository.update(profile); }
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### 11. Single Responsibility (SRP)
|
|
152
|
+
|
|
153
|
+
RULE: A function, class, or module has one reason to change. If it validates AND transforms AND persists AND notifies, split it.
|
|
154
|
+
|
|
155
|
+
```java
|
|
156
|
+
// bad
|
|
157
|
+
void save(UserInput input) { validate(input); User user = normalize(input); repository.save(user); mailer.welcome(user); }
|
|
158
|
+
// good
|
|
159
|
+
User create(ValidatedUser input) { User user = normalizer.normalize(input); repository.save(user); return user; }
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### 12. DRY (real duplication only)
|
|
163
|
+
|
|
164
|
+
RULE: Extract when at least 2 of 3 hold: (a) the logic appears 3+ times, (b) a future rule change would need to touch all occurrences at once, (c) the copies represent the same domain concept. Do not extract on the first or second occurrence, and do not extract look-alike code that belongs to different concepts (a user's email validation vs a vendor's) — they will diverge.
|
|
165
|
+
|
|
166
|
+
```java
|
|
167
|
+
// bad — extracted after one real use
|
|
168
|
+
String formatName(String value) { return value.trim().toUpperCase(); }
|
|
169
|
+
// good — same domain concept, third occurrence confirms the pattern
|
|
170
|
+
TicketStatus normalizeTicketStatus(String status) { return TicketStatus.parse(status.trim()); }
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### 13. KISS
|
|
174
|
+
|
|
175
|
+
RULE: Ship the simplest solution for the current, real requirement. No configurability, hooks, or layers added "just in case."
|
|
176
|
+
|
|
177
|
+
```java
|
|
178
|
+
// bad
|
|
179
|
+
String formatPrice(BigDecimal value, Currency currency, Locale locale, boolean symbol) { return configurableFormatter.format(value); }
|
|
180
|
+
// good
|
|
181
|
+
String formatPrice(BigDecimal value) { return "$" + value.setScale(2); }
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### 14. YAGNI
|
|
185
|
+
|
|
186
|
+
RULE: Don't add fields, params, or branches for a use case that hasn't arrived. (Differs from KISS: KISS simplifies the chosen solution; YAGNI stops you building the unrequested one.)
|
|
187
|
+
|
|
188
|
+
```java
|
|
189
|
+
// bad
|
|
190
|
+
Invoice createInvoice(Order order, boolean recurring, boolean multiCurrency) { return builder.build(order, recurring, multiCurrency); }
|
|
191
|
+
// good
|
|
192
|
+
Invoice createInvoice(Order order) { return new Invoice(order.total(), order.items()); }
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### 15. Law of Demeter
|
|
196
|
+
|
|
197
|
+
RULE: Talk only to immediate collaborators. Never reach through an object to grab something several levels deep inside its internal structure.
|
|
198
|
+
|
|
199
|
+
```java
|
|
200
|
+
// bad
|
|
201
|
+
String cityName(User user) { return user.address().city().name(); }
|
|
202
|
+
// good
|
|
203
|
+
String cityName(User user) { return user.cityName(); }
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### 16. Composition over inheritance
|
|
207
|
+
|
|
208
|
+
RULE: Compose small, focused behaviors instead of building inheritance chains that force unrelated methods onto every subclass.
|
|
209
|
+
|
|
210
|
+
```java
|
|
211
|
+
// bad
|
|
212
|
+
abstract class Animal { abstract String sound(); abstract void fly(); }
|
|
213
|
+
final class Dog extends Animal { public void fly() { throw new UnsupportedOperationException(); } }
|
|
214
|
+
// good
|
|
215
|
+
final class Dog { private final SoundBehavior sound = new Bark(); }
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### 17. Dependency direction
|
|
219
|
+
|
|
220
|
+
RULE: Inner layers (domain/business logic) never import outer layers (framework, DB, HTTP). Outer layers depend inward, never the reverse.
|
|
221
|
+
|
|
222
|
+
```java
|
|
223
|
+
// bad
|
|
224
|
+
Money total(UUID id) { return new MysqlInvoiceRepository().get(id).total(); }
|
|
225
|
+
// good
|
|
226
|
+
Money total(Invoice invoice) { return invoice.total(); }
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### 18. Testability as a design constraint
|
|
230
|
+
|
|
231
|
+
RULE: Inject dependencies (clock, network, random) instead of hardcoding them. If a function is hard to test, redesign it — don't skip the test.
|
|
232
|
+
|
|
233
|
+
```java
|
|
234
|
+
// bad
|
|
235
|
+
boolean expired(Subscription subscription) { return subscription.expiresAt().isBefore(Instant.now()); }
|
|
236
|
+
// good
|
|
237
|
+
boolean expired(Subscription subscription, Clock clock) { return subscription.expiresAt().isBefore(clock.instant()); }
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### 19. Comments
|
|
241
|
+
|
|
242
|
+
RULE: Comment only non-obvious intent — a trade-off, a constraint, a hazard names can't carry. Delete narration, restated logic, and stale TODOs. Test: if deleting the comment leaves the code equally clear, delete it.
|
|
243
|
+
|
|
244
|
+
```java
|
|
245
|
+
// bad
|
|
246
|
+
// Increment the counter.
|
|
247
|
+
counter.increment();
|
|
248
|
+
// good
|
|
249
|
+
// Poll because the provider cannot guarantee single webhook delivery.
|
|
250
|
+
scheduler.scheduleAtFixedRate(this::checkPaymentStatus, 0, 5, SECONDS);
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
**When in doubt:** fail fast, keep it flat, keep it small.
|
|
256
|
+
|
|
257
|
+
**Enforcement note:** rules 1–2 are mechanical and should be backed by Java formatting or linting. Rules 3–10 are structural checks. Rules 11–19 require review judgment.
|