claude-agent-framework 1.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 +128 -0
- package/bin/claude-framework +3 -0
- package/framework/agents/design-lead.md +240 -0
- package/framework/agents/product-owner.md +179 -0
- package/framework/agents/tech-lead.md +226 -0
- package/framework/commands/ayuda.md +127 -0
- package/framework/commands/a/303/261adir.md +98 -0
- package/framework/commands/backup.md +397 -0
- package/framework/commands/cambiar.md +110 -0
- package/framework/commands/cloud.md +457 -0
- package/framework/commands/code.md +142 -0
- package/framework/commands/debug.md +334 -0
- package/framework/commands/deploy.md +383 -0
- package/framework/commands/deshacer.md +120 -0
- package/framework/commands/estado.md +218 -0
- package/framework/commands/explica.md +227 -0
- package/framework/commands/feature.md +120 -0
- package/framework/commands/git.md +427 -0
- package/framework/commands/historial.md +202 -0
- package/framework/commands/learn.md +408 -0
- package/framework/commands/movil.md +245 -0
- package/framework/commands/nuevo.md +118 -0
- package/framework/commands/plan.md +134 -0
- package/framework/commands/prd.md +113 -0
- package/framework/commands/probar.md +148 -0
- package/framework/commands/revisar.md +208 -0
- package/framework/commands/seeds.md +230 -0
- package/framework/commands/seguridad.md +226 -0
- package/framework/commands/tasks.md +157 -0
- package/framework/skills/architecture/algorithms.md +970 -0
- package/framework/skills/architecture/clean-code.md +1080 -0
- package/framework/skills/architecture/design-patterns.md +1984 -0
- package/framework/skills/architecture/functional-programming.md +972 -0
- package/framework/skills/architecture/solid.md +991 -0
- package/framework/skills/cloud/cloud-aws.md +848 -0
- package/framework/skills/cloud/cloud-azure.md +931 -0
- package/framework/skills/cloud/cloud-gcp.md +848 -0
- package/framework/skills/cloud/message-queues.md +1229 -0
- package/framework/skills/core/accessibility.md +401 -0
- package/framework/skills/core/api.md +474 -0
- package/framework/skills/core/authentication.md +306 -0
- package/framework/skills/core/authorization.md +388 -0
- package/framework/skills/core/background-jobs.md +341 -0
- package/framework/skills/core/caching.md +473 -0
- package/framework/skills/core/code-review.md +341 -0
- package/framework/skills/core/controllers.md +290 -0
- package/framework/skills/core/cua.md +285 -0
- package/framework/skills/core/documentation.md +472 -0
- package/framework/skills/core/file-uploads.md +351 -0
- package/framework/skills/core/hotwire-native.md +296 -0
- package/framework/skills/core/hotwire.md +278 -0
- package/framework/skills/core/i18n.md +334 -0
- package/framework/skills/core/imports-exports.md +750 -0
- package/framework/skills/core/infrastructure.md +337 -0
- package/framework/skills/core/models.md +228 -0
- package/framework/skills/core/notifications.md +672 -0
- package/framework/skills/core/payments.md +581 -0
- package/framework/skills/core/performance.md +361 -0
- package/framework/skills/core/rails-scaffold.md +131 -0
- package/framework/skills/core/search.md +518 -0
- package/framework/skills/core/security.md +565 -0
- package/framework/skills/core/seeds.md +307 -0
- package/framework/skills/core/seo.md +542 -0
- package/framework/skills/core/testing.md +393 -0
- package/framework/skills/core/views.md +260 -0
- package/framework/skills/core/websockets.md +564 -0
- package/framework/skills/data/advanced-sql.md +1204 -0
- package/framework/skills/data/nosql.md +1141 -0
- package/framework/skills/devops/containers-advanced.md +1237 -0
- package/framework/skills/devops/debugging.md +834 -0
- package/framework/skills/devops/git-workflow.md +752 -0
- package/framework/skills/devops/networking.md +932 -0
- package/framework/skills/devops/shell-scripting.md +1132 -0
- package/framework/sub-agents/architecture-patterns-agent.md +1450 -0
- package/framework/sub-agents/cloud-agent.md +677 -0
- package/framework/sub-agents/data.md +504 -0
- package/framework/sub-agents/debugging-agent.md +554 -0
- package/framework/sub-agents/devops.md +483 -0
- package/framework/sub-agents/docs.md +176 -0
- package/framework/sub-agents/frontend-dev.md +349 -0
- package/framework/sub-agents/git-workflow-agent.md +697 -0
- package/framework/sub-agents/integrations.md +630 -0
- package/framework/sub-agents/native-dev.md +434 -0
- package/framework/sub-agents/qa.md +138 -0
- package/framework/sub-agents/rails-dev.md +375 -0
- package/framework/sub-agents/security.md +526 -0
- package/framework/sub-agents/ui.md +437 -0
- package/framework/sub-agents/ux.md +284 -0
- package/framework/templates/api-spec.md +500 -0
- package/framework/templates/component-spec.md +248 -0
- package/framework/templates/feature.json +13 -0
- package/framework/templates/model-spec.md +318 -0
- package/framework/templates/prd-template.md +80 -0
- package/framework/templates/task-plan.md +122 -0
- package/framework/templates/task-user-story.md +52 -0
- package/framework/templates/technical-spec.md +260 -0
- package/framework/templates/user-story.md +95 -0
- package/package.json +42 -0
- package/project-templates/CLAUDE.md +42 -0
- package/project-templates/contexts/architecture.md +25 -0
- package/project-templates/contexts/conventions.md +46 -0
- package/project-templates/contexts/design-system.md +47 -0
- package/project-templates/contexts/requirements.md +38 -0
- package/project-templates/contexts/stack.md +30 -0
- package/project-templates/history/active/models.md +11 -0
- package/project-templates/history/changelog.md +15 -0
- package/project-templates/workspace/.gitkeep +0 -0
- package/src/cli.js +52 -0
- package/src/init.js +104 -0
- package/src/status.js +75 -0
- package/src/update.js +88 -0
|
@@ -0,0 +1,991 @@
|
|
|
1
|
+
# Skill: SOLID Principles
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
Apply SOLID principles to write maintainable, extensible, and testable Ruby and Rails code.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## S - Single Responsibility Principle (SRP)
|
|
9
|
+
|
|
10
|
+
**Definition:** A class should have only one reason to change. Each class should do one thing and do it well.
|
|
11
|
+
|
|
12
|
+
### Bad Example
|
|
13
|
+
```ruby
|
|
14
|
+
# This class does too many things: data access, validation, notifications, formatting
|
|
15
|
+
class User < ApplicationRecord
|
|
16
|
+
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
17
|
+
validates :name, presence: true
|
|
18
|
+
|
|
19
|
+
def full_name
|
|
20
|
+
"#{first_name} #{last_name}"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def send_welcome_email
|
|
24
|
+
UserMailer.welcome(self).deliver_later
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def send_password_reset
|
|
28
|
+
token = generate_reset_token
|
|
29
|
+
UserMailer.password_reset(self, token).deliver_later
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def generate_monthly_report
|
|
33
|
+
orders = orders.where(created_at: 1.month.ago..Time.current)
|
|
34
|
+
total = orders.sum(:total)
|
|
35
|
+
# Generate PDF report...
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def export_to_csv
|
|
39
|
+
CSV.generate do |csv|
|
|
40
|
+
csv << %w[id name email created_at]
|
|
41
|
+
csv << [id, full_name, email, created_at]
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def calculate_loyalty_points
|
|
46
|
+
orders.sum(:total) / 10
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Good Example
|
|
52
|
+
```ruby
|
|
53
|
+
# Model handles only data and validations
|
|
54
|
+
class User < ApplicationRecord
|
|
55
|
+
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
56
|
+
validates :name, presence: true
|
|
57
|
+
|
|
58
|
+
def full_name
|
|
59
|
+
"#{first_name} #{last_name}"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Separate service for notifications
|
|
64
|
+
class UserNotificationService
|
|
65
|
+
def initialize(user)
|
|
66
|
+
@user = user
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def send_welcome_email
|
|
70
|
+
UserMailer.welcome(@user).deliver_later
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def send_password_reset
|
|
74
|
+
token = PasswordResetToken.generate_for(@user)
|
|
75
|
+
UserMailer.password_reset(@user, token).deliver_later
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Separate service for reports
|
|
80
|
+
class UserReportService
|
|
81
|
+
def initialize(user)
|
|
82
|
+
@user = user
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def monthly_report
|
|
86
|
+
orders = @user.orders.where(created_at: 1.month.ago..Time.current)
|
|
87
|
+
MonthlyReportGenerator.new(orders).generate
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Separate exporter
|
|
92
|
+
class UserCsvExporter
|
|
93
|
+
def initialize(users)
|
|
94
|
+
@users = users
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def export
|
|
98
|
+
CSV.generate do |csv|
|
|
99
|
+
csv << headers
|
|
100
|
+
@users.each { |user| csv << row(user) }
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
def headers
|
|
107
|
+
%w[id name email created_at]
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def row(user)
|
|
111
|
+
[user.id, user.full_name, user.email, user.created_at]
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Separate calculator for loyalty
|
|
116
|
+
class LoyaltyPointsCalculator
|
|
117
|
+
def initialize(user)
|
|
118
|
+
@user = user
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def calculate
|
|
122
|
+
@user.orders.sum(:total) / 10
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### How to Apply in Rails
|
|
128
|
+
- **Models:** Only data, validations, associations, scopes
|
|
129
|
+
- **Controllers:** Only HTTP handling, delegate to services
|
|
130
|
+
- **Services:** One business operation per service
|
|
131
|
+
- **Jobs:** One task per job
|
|
132
|
+
- **Mailers:** Only email composition
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## O - Open/Closed Principle (OCP)
|
|
137
|
+
|
|
138
|
+
**Definition:** Classes should be open for extension but closed for modification. Add new functionality by adding new code, not changing existing code.
|
|
139
|
+
|
|
140
|
+
### Bad Example
|
|
141
|
+
```ruby
|
|
142
|
+
class PaymentProcessor
|
|
143
|
+
def process(payment)
|
|
144
|
+
case payment.method
|
|
145
|
+
when :credit_card
|
|
146
|
+
process_credit_card(payment)
|
|
147
|
+
when :paypal
|
|
148
|
+
process_paypal(payment)
|
|
149
|
+
when :stripe
|
|
150
|
+
process_stripe(payment)
|
|
151
|
+
when :apple_pay
|
|
152
|
+
process_apple_pay(payment)
|
|
153
|
+
# Every new payment method requires modifying this class
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
private
|
|
158
|
+
|
|
159
|
+
def process_credit_card(payment)
|
|
160
|
+
# Credit card logic
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def process_paypal(payment)
|
|
164
|
+
# PayPal logic
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def process_stripe(payment)
|
|
168
|
+
# Stripe logic
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def process_apple_pay(payment)
|
|
172
|
+
# Apple Pay logic
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Good Example
|
|
178
|
+
```ruby
|
|
179
|
+
# Base class or interface
|
|
180
|
+
class PaymentMethod
|
|
181
|
+
def process(payment)
|
|
182
|
+
raise NotImplementedError, "#{self.class} must implement #process"
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Each payment method is a separate class
|
|
187
|
+
class CreditCardPayment < PaymentMethod
|
|
188
|
+
def process(payment)
|
|
189
|
+
# Credit card specific logic
|
|
190
|
+
gateway = CreditCardGateway.new
|
|
191
|
+
gateway.charge(payment.amount, payment.card_details)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
class PaypalPayment < PaymentMethod
|
|
196
|
+
def process(payment)
|
|
197
|
+
# PayPal specific logic
|
|
198
|
+
client = PaypalClient.new
|
|
199
|
+
client.execute_payment(payment.paypal_order_id)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
class StripePayment < PaymentMethod
|
|
204
|
+
def process(payment)
|
|
205
|
+
# Stripe specific logic
|
|
206
|
+
Stripe::Charge.create(
|
|
207
|
+
amount: payment.amount_in_cents,
|
|
208
|
+
source: payment.stripe_token
|
|
209
|
+
)
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Adding new payment method doesn't require changing existing code
|
|
214
|
+
class ApplePayPayment < PaymentMethod
|
|
215
|
+
def process(payment)
|
|
216
|
+
# Apple Pay specific logic
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Processor uses dependency injection
|
|
221
|
+
class PaymentProcessor
|
|
222
|
+
METHODS = {
|
|
223
|
+
credit_card: CreditCardPayment,
|
|
224
|
+
paypal: PaypalPayment,
|
|
225
|
+
stripe: StripePayment,
|
|
226
|
+
apple_pay: ApplePayPayment
|
|
227
|
+
}.freeze
|
|
228
|
+
|
|
229
|
+
def process(payment)
|
|
230
|
+
method_class = METHODS.fetch(payment.method) do
|
|
231
|
+
raise ArgumentError, "Unknown payment method: #{payment.method}"
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
method_class.new.process(payment)
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Or use a registry pattern for runtime registration
|
|
239
|
+
class PaymentMethodRegistry
|
|
240
|
+
class << self
|
|
241
|
+
def register(name, handler)
|
|
242
|
+
handlers[name] = handler
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def for(name)
|
|
246
|
+
handlers.fetch(name) { raise "Unknown payment method: #{name}" }
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
private
|
|
250
|
+
|
|
251
|
+
def handlers
|
|
252
|
+
@handlers ||= {}
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Register handlers
|
|
258
|
+
PaymentMethodRegistry.register(:credit_card, CreditCardPayment)
|
|
259
|
+
PaymentMethodRegistry.register(:paypal, PaypalPayment)
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### How to Apply in Rails
|
|
263
|
+
- Use **polymorphism** instead of conditionals
|
|
264
|
+
- Create **service objects** for each variant
|
|
265
|
+
- Use **concerns** to add behavior to models
|
|
266
|
+
- Use **decorators** to extend functionality
|
|
267
|
+
- Define **interfaces** (abstract classes) for extensibility
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## L - Liskov Substitution Principle (LSP)
|
|
272
|
+
|
|
273
|
+
**Definition:** Objects of a superclass should be replaceable with objects of its subclasses without breaking the application. Subclasses must honor the contract of the parent class.
|
|
274
|
+
|
|
275
|
+
### Bad Example
|
|
276
|
+
```ruby
|
|
277
|
+
class Bird
|
|
278
|
+
def fly
|
|
279
|
+
puts "Flying..."
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
class Sparrow < Bird
|
|
284
|
+
def fly
|
|
285
|
+
puts "Sparrow flying..."
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Violates LSP: Penguin is a bird but can't fly
|
|
290
|
+
class Penguin < Bird
|
|
291
|
+
def fly
|
|
292
|
+
raise NotImplementedError, "Penguins can't fly!"
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# This code breaks with Penguin
|
|
297
|
+
def make_bird_fly(bird)
|
|
298
|
+
bird.fly # Raises error for Penguin
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
make_bird_fly(Sparrow.new) # Works
|
|
302
|
+
make_bird_fly(Penguin.new) # Breaks!
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Good Example
|
|
306
|
+
```ruby
|
|
307
|
+
# Better hierarchy based on capabilities
|
|
308
|
+
class Bird
|
|
309
|
+
def move
|
|
310
|
+
raise NotImplementedError
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
class FlyingBird < Bird
|
|
315
|
+
def move
|
|
316
|
+
fly
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def fly
|
|
320
|
+
puts "Flying..."
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
class WalkingBird < Bird
|
|
325
|
+
def move
|
|
326
|
+
walk
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def walk
|
|
330
|
+
puts "Walking..."
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
class Sparrow < FlyingBird
|
|
335
|
+
def fly
|
|
336
|
+
puts "Sparrow flying..."
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
class Penguin < WalkingBird
|
|
341
|
+
def walk
|
|
342
|
+
puts "Penguin waddling..."
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def swim
|
|
346
|
+
puts "Penguin swimming..."
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# Now this works for all birds
|
|
351
|
+
def make_bird_move(bird)
|
|
352
|
+
bird.move
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
make_bird_move(Sparrow.new) # "Sparrow flying..."
|
|
356
|
+
make_bird_move(Penguin.new) # "Penguin waddling..."
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### Rails Example
|
|
360
|
+
```ruby
|
|
361
|
+
# Bad: Subclass changes behavior in unexpected ways
|
|
362
|
+
class Document < ApplicationRecord
|
|
363
|
+
def publish
|
|
364
|
+
update(published_at: Time.current, status: :published)
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
class DraftDocument < Document
|
|
369
|
+
def publish
|
|
370
|
+
# Violates LSP: silently does nothing instead of publishing
|
|
371
|
+
false
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
# Good: Use state machine or explicit contract
|
|
376
|
+
class Document < ApplicationRecord
|
|
377
|
+
include AASM
|
|
378
|
+
|
|
379
|
+
aasm column: :status do
|
|
380
|
+
state :draft, initial: true
|
|
381
|
+
state :published
|
|
382
|
+
|
|
383
|
+
event :publish do
|
|
384
|
+
transitions from: :draft, to: :published
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
# Or use composition over inheritance
|
|
390
|
+
class Document < ApplicationRecord
|
|
391
|
+
def publish
|
|
392
|
+
publishing_strategy.publish(self)
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def publishing_strategy
|
|
396
|
+
PublishingStrategy.for(self)
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
class PublishingStrategy
|
|
401
|
+
def self.for(document)
|
|
402
|
+
case document.document_type
|
|
403
|
+
when "standard"
|
|
404
|
+
StandardPublishing.new
|
|
405
|
+
when "review_required"
|
|
406
|
+
ReviewRequiredPublishing.new
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
class StandardPublishing
|
|
412
|
+
def publish(document)
|
|
413
|
+
document.update(published_at: Time.current, status: :published)
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
class ReviewRequiredPublishing
|
|
418
|
+
def publish(document)
|
|
419
|
+
document.update(status: :pending_review)
|
|
420
|
+
NotifyReviewersJob.perform_later(document.id)
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
### Rules for LSP
|
|
426
|
+
1. **Preconditions cannot be strengthened** in subclass
|
|
427
|
+
2. **Postconditions cannot be weakened** in subclass
|
|
428
|
+
3. **Invariants must be preserved** in subclass
|
|
429
|
+
4. **No new exceptions** that parent doesn't throw
|
|
430
|
+
5. **Return types** must be compatible (same or more specific)
|
|
431
|
+
|
|
432
|
+
---
|
|
433
|
+
|
|
434
|
+
## I - Interface Segregation Principle (ISP)
|
|
435
|
+
|
|
436
|
+
**Definition:** Clients should not be forced to depend on interfaces they don't use. Many specific interfaces are better than one general-purpose interface.
|
|
437
|
+
|
|
438
|
+
### Bad Example
|
|
439
|
+
```ruby
|
|
440
|
+
# Fat interface - forces all implementations to define everything
|
|
441
|
+
class Worker
|
|
442
|
+
def work
|
|
443
|
+
raise NotImplementedError
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def eat
|
|
447
|
+
raise NotImplementedError
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def sleep
|
|
451
|
+
raise NotImplementedError
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
def manage_team
|
|
455
|
+
raise NotImplementedError
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
def attend_meetings
|
|
459
|
+
raise NotImplementedError
|
|
460
|
+
end
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
class Developer < Worker
|
|
464
|
+
def work
|
|
465
|
+
puts "Writing code..."
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
def eat
|
|
469
|
+
puts "Eating lunch..."
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
def sleep
|
|
473
|
+
puts "Sleeping..."
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
# Forced to implement but doesn't use
|
|
477
|
+
def manage_team
|
|
478
|
+
raise "Developers don't manage teams"
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
def attend_meetings
|
|
482
|
+
puts "Attending standup..."
|
|
483
|
+
end
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
class Robot < Worker
|
|
487
|
+
def work
|
|
488
|
+
puts "Processing..."
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
# Robots don't eat or sleep - forced to implement useless methods
|
|
492
|
+
def eat
|
|
493
|
+
raise "Robots don't eat"
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
def sleep
|
|
497
|
+
raise "Robots don't sleep"
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
def manage_team
|
|
501
|
+
raise "Robots don't manage"
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
def attend_meetings
|
|
505
|
+
raise "Robots don't attend meetings"
|
|
506
|
+
end
|
|
507
|
+
end
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
### Good Example
|
|
511
|
+
```ruby
|
|
512
|
+
# Segregated interfaces using modules
|
|
513
|
+
module Workable
|
|
514
|
+
def work
|
|
515
|
+
raise NotImplementedError
|
|
516
|
+
end
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
module Eatable
|
|
520
|
+
def eat
|
|
521
|
+
raise NotImplementedError
|
|
522
|
+
end
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
module Sleepable
|
|
526
|
+
def sleep
|
|
527
|
+
raise NotImplementedError
|
|
528
|
+
end
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
module Manageable
|
|
532
|
+
def manage_team
|
|
533
|
+
raise NotImplementedError
|
|
534
|
+
end
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
module MeetingAttendable
|
|
538
|
+
def attend_meetings
|
|
539
|
+
raise NotImplementedError
|
|
540
|
+
end
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
# Classes include only what they need
|
|
544
|
+
class Developer
|
|
545
|
+
include Workable
|
|
546
|
+
include Eatable
|
|
547
|
+
include Sleepable
|
|
548
|
+
include MeetingAttendable
|
|
549
|
+
|
|
550
|
+
def work
|
|
551
|
+
puts "Writing code..."
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
def eat
|
|
555
|
+
puts "Eating lunch..."
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
def sleep
|
|
559
|
+
puts "Sleeping..."
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
def attend_meetings
|
|
563
|
+
puts "Attending standup..."
|
|
564
|
+
end
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
class Robot
|
|
568
|
+
include Workable
|
|
569
|
+
|
|
570
|
+
def work
|
|
571
|
+
puts "Processing..."
|
|
572
|
+
end
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
class Manager
|
|
576
|
+
include Workable
|
|
577
|
+
include Eatable
|
|
578
|
+
include Sleepable
|
|
579
|
+
include Manageable
|
|
580
|
+
include MeetingAttendable
|
|
581
|
+
|
|
582
|
+
def work
|
|
583
|
+
puts "Managing projects..."
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
def eat
|
|
587
|
+
puts "Business lunch..."
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
def sleep
|
|
591
|
+
puts "Sleeping..."
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
def manage_team
|
|
595
|
+
puts "Leading team..."
|
|
596
|
+
end
|
|
597
|
+
|
|
598
|
+
def attend_meetings
|
|
599
|
+
puts "Running meetings..."
|
|
600
|
+
end
|
|
601
|
+
end
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
### Rails Example
|
|
605
|
+
```ruby
|
|
606
|
+
# Bad: One giant concern with everything
|
|
607
|
+
module Reportable
|
|
608
|
+
extend ActiveSupport::Concern
|
|
609
|
+
|
|
610
|
+
def generate_pdf_report
|
|
611
|
+
# PDF generation
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
def generate_csv_report
|
|
615
|
+
# CSV generation
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
def generate_excel_report
|
|
619
|
+
# Excel generation
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
def send_report_by_email
|
|
623
|
+
# Email sending
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
def schedule_report
|
|
627
|
+
# Scheduling
|
|
628
|
+
end
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
# Good: Segregated concerns
|
|
632
|
+
module PdfExportable
|
|
633
|
+
extend ActiveSupport::Concern
|
|
634
|
+
|
|
635
|
+
def to_pdf
|
|
636
|
+
PdfGenerator.new(self).generate
|
|
637
|
+
end
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
module CsvExportable
|
|
641
|
+
extend ActiveSupport::Concern
|
|
642
|
+
|
|
643
|
+
def to_csv
|
|
644
|
+
CsvGenerator.new(self).generate
|
|
645
|
+
end
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
module ExcelExportable
|
|
649
|
+
extend ActiveSupport::Concern
|
|
650
|
+
|
|
651
|
+
def to_excel
|
|
652
|
+
ExcelGenerator.new(self).generate
|
|
653
|
+
end
|
|
654
|
+
end
|
|
655
|
+
|
|
656
|
+
module Schedulable
|
|
657
|
+
extend ActiveSupport::Concern
|
|
658
|
+
|
|
659
|
+
def schedule(at:)
|
|
660
|
+
ScheduledJob.set(wait_until: at).perform_later(self)
|
|
661
|
+
end
|
|
662
|
+
end
|
|
663
|
+
|
|
664
|
+
# Models include only what they need
|
|
665
|
+
class Invoice < ApplicationRecord
|
|
666
|
+
include PdfExportable
|
|
667
|
+
include CsvExportable
|
|
668
|
+
# Doesn't need Excel or scheduling
|
|
669
|
+
end
|
|
670
|
+
|
|
671
|
+
class Report < ApplicationRecord
|
|
672
|
+
include PdfExportable
|
|
673
|
+
include ExcelExportable
|
|
674
|
+
include Schedulable
|
|
675
|
+
# Doesn't need CSV
|
|
676
|
+
end
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
### How to Apply in Rails
|
|
680
|
+
- Create **small, focused concerns** instead of large ones
|
|
681
|
+
- Use **modules** as interfaces
|
|
682
|
+
- **Service objects** should do one thing
|
|
683
|
+
- **Policies** should be granular (CanRead, CanWrite vs CanDoEverything)
|
|
684
|
+
|
|
685
|
+
---
|
|
686
|
+
|
|
687
|
+
## D - Dependency Inversion Principle (DIP)
|
|
688
|
+
|
|
689
|
+
**Definition:** High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details.
|
|
690
|
+
|
|
691
|
+
### Bad Example
|
|
692
|
+
```ruby
|
|
693
|
+
# High-level module depends directly on low-level module
|
|
694
|
+
class OrderProcessor
|
|
695
|
+
def initialize
|
|
696
|
+
# Direct dependency on concrete implementation
|
|
697
|
+
@email_sender = SmtpEmailSender.new
|
|
698
|
+
@payment_gateway = StripeGateway.new
|
|
699
|
+
@inventory = MySqlInventory.new
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
def process(order)
|
|
703
|
+
return false unless @inventory.check_stock(order.items)
|
|
704
|
+
|
|
705
|
+
@payment_gateway.charge(order.total, order.payment_info)
|
|
706
|
+
@inventory.reduce_stock(order.items)
|
|
707
|
+
@email_sender.send_confirmation(order)
|
|
708
|
+
|
|
709
|
+
true
|
|
710
|
+
end
|
|
711
|
+
end
|
|
712
|
+
|
|
713
|
+
# Problems:
|
|
714
|
+
# - Can't easily test without real SMTP, Stripe, MySQL
|
|
715
|
+
# - Can't swap implementations
|
|
716
|
+
# - Hard to reuse in different contexts
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
### Good Example
|
|
720
|
+
```ruby
|
|
721
|
+
# Define abstractions (interfaces)
|
|
722
|
+
class EmailSender
|
|
723
|
+
def send_email(to:, subject:, body:)
|
|
724
|
+
raise NotImplementedError
|
|
725
|
+
end
|
|
726
|
+
end
|
|
727
|
+
|
|
728
|
+
class PaymentGateway
|
|
729
|
+
def charge(amount, payment_info)
|
|
730
|
+
raise NotImplementedError
|
|
731
|
+
end
|
|
732
|
+
end
|
|
733
|
+
|
|
734
|
+
class InventoryService
|
|
735
|
+
def check_stock(items)
|
|
736
|
+
raise NotImplementedError
|
|
737
|
+
end
|
|
738
|
+
|
|
739
|
+
def reduce_stock(items)
|
|
740
|
+
raise NotImplementedError
|
|
741
|
+
end
|
|
742
|
+
end
|
|
743
|
+
|
|
744
|
+
# Concrete implementations
|
|
745
|
+
class SmtpEmailSender < EmailSender
|
|
746
|
+
def send_email(to:, subject:, body:)
|
|
747
|
+
# SMTP implementation
|
|
748
|
+
end
|
|
749
|
+
end
|
|
750
|
+
|
|
751
|
+
class StripeGateway < PaymentGateway
|
|
752
|
+
def charge(amount, payment_info)
|
|
753
|
+
Stripe::Charge.create(amount: amount, source: payment_info[:token])
|
|
754
|
+
end
|
|
755
|
+
end
|
|
756
|
+
|
|
757
|
+
class DatabaseInventory < InventoryService
|
|
758
|
+
def check_stock(items)
|
|
759
|
+
items.all? { |item| Product.find(item.id).stock >= item.quantity }
|
|
760
|
+
end
|
|
761
|
+
|
|
762
|
+
def reduce_stock(items)
|
|
763
|
+
items.each { |item| Product.find(item.id).decrement!(:stock, item.quantity) }
|
|
764
|
+
end
|
|
765
|
+
end
|
|
766
|
+
|
|
767
|
+
# High-level module depends on abstractions via dependency injection
|
|
768
|
+
class OrderProcessor
|
|
769
|
+
def initialize(email_sender:, payment_gateway:, inventory:)
|
|
770
|
+
@email_sender = email_sender
|
|
771
|
+
@payment_gateway = payment_gateway
|
|
772
|
+
@inventory = inventory
|
|
773
|
+
end
|
|
774
|
+
|
|
775
|
+
def process(order)
|
|
776
|
+
return false unless @inventory.check_stock(order.items)
|
|
777
|
+
|
|
778
|
+
@payment_gateway.charge(order.total, order.payment_info)
|
|
779
|
+
@inventory.reduce_stock(order.items)
|
|
780
|
+
@email_sender.send_email(
|
|
781
|
+
to: order.user.email,
|
|
782
|
+
subject: "Order Confirmation",
|
|
783
|
+
body: "Your order has been processed"
|
|
784
|
+
)
|
|
785
|
+
|
|
786
|
+
true
|
|
787
|
+
end
|
|
788
|
+
end
|
|
789
|
+
|
|
790
|
+
# Usage in production
|
|
791
|
+
processor = OrderProcessor.new(
|
|
792
|
+
email_sender: SmtpEmailSender.new,
|
|
793
|
+
payment_gateway: StripeGateway.new,
|
|
794
|
+
inventory: DatabaseInventory.new
|
|
795
|
+
)
|
|
796
|
+
|
|
797
|
+
# Usage in tests - easy to mock
|
|
798
|
+
class MockEmailSender < EmailSender
|
|
799
|
+
attr_reader :sent_emails
|
|
800
|
+
|
|
801
|
+
def initialize
|
|
802
|
+
@sent_emails = []
|
|
803
|
+
end
|
|
804
|
+
|
|
805
|
+
def send_email(to:, subject:, body:)
|
|
806
|
+
@sent_emails << { to: to, subject: subject, body: body }
|
|
807
|
+
end
|
|
808
|
+
end
|
|
809
|
+
|
|
810
|
+
class MockPaymentGateway < PaymentGateway
|
|
811
|
+
def charge(amount, payment_info)
|
|
812
|
+
{ success: true, charge_id: "test_123" }
|
|
813
|
+
end
|
|
814
|
+
end
|
|
815
|
+
|
|
816
|
+
# Test
|
|
817
|
+
processor = OrderProcessor.new(
|
|
818
|
+
email_sender: MockEmailSender.new,
|
|
819
|
+
payment_gateway: MockPaymentGateway.new,
|
|
820
|
+
inventory: MockInventory.new
|
|
821
|
+
)
|
|
822
|
+
```
|
|
823
|
+
|
|
824
|
+
### Rails Example with Dependency Injection
|
|
825
|
+
```ruby
|
|
826
|
+
# config/initializers/dependencies.rb
|
|
827
|
+
Rails.application.config.after_initialize do
|
|
828
|
+
Rails.application.config.dependencies = {
|
|
829
|
+
email_sender: -> { SmtpEmailSender.new },
|
|
830
|
+
payment_gateway: -> { StripeGateway.new },
|
|
831
|
+
inventory: -> { DatabaseInventory.new }
|
|
832
|
+
}
|
|
833
|
+
end
|
|
834
|
+
|
|
835
|
+
# Helper to resolve dependencies
|
|
836
|
+
module DependencyResolver
|
|
837
|
+
def resolve(name)
|
|
838
|
+
Rails.application.config.dependencies[name].call
|
|
839
|
+
end
|
|
840
|
+
end
|
|
841
|
+
|
|
842
|
+
# Usage in service
|
|
843
|
+
class OrderProcessor
|
|
844
|
+
include DependencyResolver
|
|
845
|
+
|
|
846
|
+
def initialize(
|
|
847
|
+
email_sender: resolve(:email_sender),
|
|
848
|
+
payment_gateway: resolve(:payment_gateway),
|
|
849
|
+
inventory: resolve(:inventory)
|
|
850
|
+
)
|
|
851
|
+
@email_sender = email_sender
|
|
852
|
+
@payment_gateway = payment_gateway
|
|
853
|
+
@inventory = inventory
|
|
854
|
+
end
|
|
855
|
+
end
|
|
856
|
+
|
|
857
|
+
# Or using a container (dry-container gem)
|
|
858
|
+
class Container
|
|
859
|
+
extend Dry::Container::Mixin
|
|
860
|
+
|
|
861
|
+
register :email_sender do
|
|
862
|
+
SmtpEmailSender.new
|
|
863
|
+
end
|
|
864
|
+
|
|
865
|
+
register :payment_gateway do
|
|
866
|
+
StripeGateway.new
|
|
867
|
+
end
|
|
868
|
+
end
|
|
869
|
+
|
|
870
|
+
# config/environments/test.rb
|
|
871
|
+
class TestContainer < Container
|
|
872
|
+
register :email_sender do
|
|
873
|
+
MockEmailSender.new
|
|
874
|
+
end
|
|
875
|
+
|
|
876
|
+
register :payment_gateway do
|
|
877
|
+
MockPaymentGateway.new
|
|
878
|
+
end
|
|
879
|
+
end
|
|
880
|
+
```
|
|
881
|
+
|
|
882
|
+
### How to Apply in Rails
|
|
883
|
+
- **Inject dependencies** through constructor or method parameters
|
|
884
|
+
- **Use configuration** to wire up implementations
|
|
885
|
+
- **Default to production** implementations with ability to override
|
|
886
|
+
- **Create adapters** for external services
|
|
887
|
+
- **Test doubles** become trivial to use
|
|
888
|
+
|
|
889
|
+
---
|
|
890
|
+
|
|
891
|
+
## SOLID Cheat Sheet
|
|
892
|
+
|
|
893
|
+
| Principle | One-liner | Code Smell |
|
|
894
|
+
|-----------|-----------|------------|
|
|
895
|
+
| **S**ingle Responsibility | One class, one reason to change | Class has multiple unrelated methods |
|
|
896
|
+
| **O**pen/Closed | Add features by adding code, not changing it | Switch statements for types |
|
|
897
|
+
| **L**iskov Substitution | Subclasses must be substitutable | Subclass throws unexpected errors |
|
|
898
|
+
| **I**nterface Segregation | Small, focused interfaces | Fat classes/modules with unused methods |
|
|
899
|
+
| **D**ependency Inversion | Depend on abstractions, not concretions | `new` inside business logic |
|
|
900
|
+
|
|
901
|
+
---
|
|
902
|
+
|
|
903
|
+
## Applying SOLID in Rails Architecture
|
|
904
|
+
|
|
905
|
+
```
|
|
906
|
+
app/
|
|
907
|
+
├── models/ # S: Only data, validations, associations
|
|
908
|
+
├── controllers/ # S: Only HTTP handling
|
|
909
|
+
├── services/ # S: One business operation each
|
|
910
|
+
│ # O: Base service + specialized variants
|
|
911
|
+
│ # D: Inject dependencies
|
|
912
|
+
├── policies/ # I: Granular authorization
|
|
913
|
+
├── adapters/ # D: Wrap external services
|
|
914
|
+
│ ├── payment/ # O: PaymentAdapter interface + implementations
|
|
915
|
+
│ └── email/ # O: EmailAdapter interface + implementations
|
|
916
|
+
├── presenters/ # S: View-specific formatting
|
|
917
|
+
└── validators/ # S: One validation concern each
|
|
918
|
+
```
|
|
919
|
+
|
|
920
|
+
### Example Service Following All Principles
|
|
921
|
+
```ruby
|
|
922
|
+
# app/services/checkout_service.rb
|
|
923
|
+
class CheckoutService
|
|
924
|
+
# D: Dependencies injected, defaults for production
|
|
925
|
+
def initialize(
|
|
926
|
+
payment_processor: PaymentProcessor.new,
|
|
927
|
+
inventory_service: InventoryService.new,
|
|
928
|
+
notification_service: NotificationService.new
|
|
929
|
+
)
|
|
930
|
+
@payment_processor = payment_processor
|
|
931
|
+
@inventory_service = inventory_service
|
|
932
|
+
@notification_service = notification_service
|
|
933
|
+
end
|
|
934
|
+
|
|
935
|
+
# S: Single responsibility - orchestrate checkout
|
|
936
|
+
def call(cart:, user:, payment_info:)
|
|
937
|
+
# L: All services honor their contracts
|
|
938
|
+
validate_stock!(cart)
|
|
939
|
+
charge = process_payment(cart, payment_info)
|
|
940
|
+
order = create_order(cart, user, charge)
|
|
941
|
+
fulfill_order(order)
|
|
942
|
+
notify_user(user, order)
|
|
943
|
+
|
|
944
|
+
Result.success(order)
|
|
945
|
+
rescue CheckoutError => e
|
|
946
|
+
Result.failure(e.message)
|
|
947
|
+
end
|
|
948
|
+
|
|
949
|
+
private
|
|
950
|
+
|
|
951
|
+
def validate_stock!(cart)
|
|
952
|
+
cart.items.each do |item|
|
|
953
|
+
unless @inventory_service.available?(item.product_id, item.quantity)
|
|
954
|
+
raise CheckoutError, "#{item.name} is out of stock"
|
|
955
|
+
end
|
|
956
|
+
end
|
|
957
|
+
end
|
|
958
|
+
|
|
959
|
+
# O: Different payment methods handled by PaymentProcessor variants
|
|
960
|
+
def process_payment(cart, payment_info)
|
|
961
|
+
@payment_processor.charge(cart.total, payment_info)
|
|
962
|
+
end
|
|
963
|
+
|
|
964
|
+
def fulfill_order(order)
|
|
965
|
+
@inventory_service.reserve(order.items)
|
|
966
|
+
end
|
|
967
|
+
|
|
968
|
+
def notify_user(user, order)
|
|
969
|
+
@notification_service.order_confirmed(user, order)
|
|
970
|
+
end
|
|
971
|
+
end
|
|
972
|
+
|
|
973
|
+
# I: Focused interfaces
|
|
974
|
+
class PaymentProcessor
|
|
975
|
+
def charge(amount, payment_info)
|
|
976
|
+
raise NotImplementedError
|
|
977
|
+
end
|
|
978
|
+
end
|
|
979
|
+
|
|
980
|
+
class StripePaymentProcessor < PaymentProcessor
|
|
981
|
+
def charge(amount, payment_info)
|
|
982
|
+
Stripe::Charge.create(amount: amount, source: payment_info[:token])
|
|
983
|
+
end
|
|
984
|
+
end
|
|
985
|
+
|
|
986
|
+
class PaypalPaymentProcessor < PaymentProcessor
|
|
987
|
+
def charge(amount, payment_info)
|
|
988
|
+
PaypalClient.new.execute(payment_info[:order_id])
|
|
989
|
+
end
|
|
990
|
+
end
|
|
991
|
+
```
|