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,1984 @@
|
|
|
1
|
+
# Skill: Design Patterns (GoF)
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
Implement Gang of Four design patterns in Ruby and Rails to solve common software design problems with proven, reusable solutions.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Creational Patterns
|
|
9
|
+
|
|
10
|
+
### Factory Method
|
|
11
|
+
**Problem:** Need to create objects without specifying their exact class.
|
|
12
|
+
**When to use:** When a class cannot anticipate the type of objects it needs to create.
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
# Base creator
|
|
16
|
+
class NotificationFactory
|
|
17
|
+
def self.create(type, recipient, message)
|
|
18
|
+
case type
|
|
19
|
+
when :email
|
|
20
|
+
EmailNotification.new(recipient, message)
|
|
21
|
+
when :sms
|
|
22
|
+
SmsNotification.new(recipient, message)
|
|
23
|
+
when :push
|
|
24
|
+
PushNotification.new(recipient, message)
|
|
25
|
+
else
|
|
26
|
+
raise ArgumentError, "Unknown notification type: #{type}"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Product classes
|
|
32
|
+
class EmailNotification
|
|
33
|
+
def initialize(recipient, message)
|
|
34
|
+
@recipient = recipient
|
|
35
|
+
@message = message
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def deliver
|
|
39
|
+
# Send email
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
class SmsNotification
|
|
44
|
+
def initialize(recipient, message)
|
|
45
|
+
@recipient = recipient
|
|
46
|
+
@message = message
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def deliver
|
|
50
|
+
# Send SMS
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Usage
|
|
55
|
+
notification = NotificationFactory.create(:email, user.email, "Welcome!")
|
|
56
|
+
notification.deliver
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Rails Example:**
|
|
60
|
+
```ruby
|
|
61
|
+
# app/services/payment_processor_factory.rb
|
|
62
|
+
class PaymentProcessorFactory
|
|
63
|
+
PROCESSORS = {
|
|
64
|
+
stripe: StripeProcessor,
|
|
65
|
+
paypal: PaypalProcessor,
|
|
66
|
+
braintree: BraintreeProcessor
|
|
67
|
+
}.freeze
|
|
68
|
+
|
|
69
|
+
def self.for(gateway)
|
|
70
|
+
PROCESSORS.fetch(gateway) do
|
|
71
|
+
raise ArgumentError, "Unknown payment gateway: #{gateway}"
|
|
72
|
+
end.new
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Usage in controller
|
|
77
|
+
class PaymentsController < ApplicationController
|
|
78
|
+
def create
|
|
79
|
+
processor = PaymentProcessorFactory.for(params[:gateway].to_sym)
|
|
80
|
+
result = processor.charge(current_user, amount)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
### Abstract Factory
|
|
88
|
+
**Problem:** Need to create families of related objects without specifying their concrete classes.
|
|
89
|
+
**When to use:** When your system needs to work with multiple families of products.
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
# Abstract factory
|
|
93
|
+
class UIFactory
|
|
94
|
+
def create_button
|
|
95
|
+
raise NotImplementedError
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def create_input
|
|
99
|
+
raise NotImplementedError
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Concrete factories
|
|
104
|
+
class MaterialUIFactory < UIFactory
|
|
105
|
+
def create_button
|
|
106
|
+
MaterialButton.new
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def create_input
|
|
110
|
+
MaterialInput.new
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
class BootstrapUIFactory < UIFactory
|
|
115
|
+
def create_button
|
|
116
|
+
BootstrapButton.new
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def create_input
|
|
120
|
+
BootstrapInput.new
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Usage
|
|
125
|
+
factory = Rails.application.config.ui_framework == :material ?
|
|
126
|
+
MaterialUIFactory.new :
|
|
127
|
+
BootstrapUIFactory.new
|
|
128
|
+
|
|
129
|
+
button = factory.create_button
|
|
130
|
+
input = factory.create_input
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Rails Example:**
|
|
134
|
+
```ruby
|
|
135
|
+
# app/services/export_factory.rb
|
|
136
|
+
class ExportFactory
|
|
137
|
+
def self.for(format)
|
|
138
|
+
case format
|
|
139
|
+
when :pdf
|
|
140
|
+
PdfExportFactory.new
|
|
141
|
+
when :excel
|
|
142
|
+
ExcelExportFactory.new
|
|
143
|
+
when :csv
|
|
144
|
+
CsvExportFactory.new
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
class PdfExportFactory
|
|
150
|
+
def create_document
|
|
151
|
+
PdfDocument.new
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def create_formatter
|
|
155
|
+
PdfFormatter.new
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def create_writer
|
|
159
|
+
PdfWriter.new
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Usage
|
|
164
|
+
factory = ExportFactory.for(:pdf)
|
|
165
|
+
doc = factory.create_document
|
|
166
|
+
formatter = factory.create_formatter
|
|
167
|
+
writer = factory.create_writer
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
### Builder
|
|
173
|
+
**Problem:** Need to construct complex objects step by step.
|
|
174
|
+
**When to use:** When an object requires many steps to be created or has many optional parameters.
|
|
175
|
+
|
|
176
|
+
```ruby
|
|
177
|
+
class QueryBuilder
|
|
178
|
+
def initialize
|
|
179
|
+
@select = []
|
|
180
|
+
@where = []
|
|
181
|
+
@order = nil
|
|
182
|
+
@limit = nil
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def select(*columns)
|
|
186
|
+
@select.concat(columns)
|
|
187
|
+
self
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def where(condition)
|
|
191
|
+
@where << condition
|
|
192
|
+
self
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def order(column, direction = :asc)
|
|
196
|
+
@order = { column: column, direction: direction }
|
|
197
|
+
self
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def limit(count)
|
|
201
|
+
@limit = count
|
|
202
|
+
self
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def build
|
|
206
|
+
query = "SELECT #{@select.join(', ')} FROM users"
|
|
207
|
+
query += " WHERE #{@where.join(' AND ')}" if @where.any?
|
|
208
|
+
query += " ORDER BY #{@order[:column]} #{@order[:direction]}" if @order
|
|
209
|
+
query += " LIMIT #{@limit}" if @limit
|
|
210
|
+
query
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Usage with method chaining
|
|
215
|
+
query = QueryBuilder.new
|
|
216
|
+
.select(:id, :name, :email)
|
|
217
|
+
.where("active = true")
|
|
218
|
+
.where("created_at > '2024-01-01'")
|
|
219
|
+
.order(:created_at, :desc)
|
|
220
|
+
.limit(10)
|
|
221
|
+
.build
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
**Rails Example:**
|
|
225
|
+
```ruby
|
|
226
|
+
# app/builders/user_builder.rb
|
|
227
|
+
class UserBuilder
|
|
228
|
+
def initialize
|
|
229
|
+
@user = User.new
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def with_email(email)
|
|
233
|
+
@user.email = email
|
|
234
|
+
self
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def with_name(first, last)
|
|
238
|
+
@user.first_name = first
|
|
239
|
+
@user.last_name = last
|
|
240
|
+
self
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def with_role(role)
|
|
244
|
+
@user.role = role
|
|
245
|
+
self
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def with_profile(attributes = {})
|
|
249
|
+
@user.build_profile(attributes)
|
|
250
|
+
self
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def with_preferences(prefs)
|
|
254
|
+
@user.preferences = prefs
|
|
255
|
+
self
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def build
|
|
259
|
+
@user
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def create!
|
|
263
|
+
@user.save!
|
|
264
|
+
@user
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Usage
|
|
269
|
+
user = UserBuilder.new
|
|
270
|
+
.with_email("john@example.com")
|
|
271
|
+
.with_name("John", "Doe")
|
|
272
|
+
.with_role(:admin)
|
|
273
|
+
.with_profile(bio: "Developer")
|
|
274
|
+
.create!
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
### Singleton
|
|
280
|
+
**Problem:** Ensure a class has only one instance with global access.
|
|
281
|
+
**When to use:** When exactly one instance is needed (configurations, connection pools, caches).
|
|
282
|
+
|
|
283
|
+
```ruby
|
|
284
|
+
# Ruby's built-in Singleton module
|
|
285
|
+
require 'singleton'
|
|
286
|
+
|
|
287
|
+
class Configuration
|
|
288
|
+
include Singleton
|
|
289
|
+
|
|
290
|
+
attr_accessor :api_key, :debug_mode, :timeout
|
|
291
|
+
|
|
292
|
+
def initialize
|
|
293
|
+
@api_key = nil
|
|
294
|
+
@debug_mode = false
|
|
295
|
+
@timeout = 30
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Usage
|
|
300
|
+
config = Configuration.instance
|
|
301
|
+
config.api_key = "secret123"
|
|
302
|
+
config.debug_mode = true
|
|
303
|
+
|
|
304
|
+
# Anywhere else in the app
|
|
305
|
+
Configuration.instance.api_key # => "secret123"
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
**Rails Example:**
|
|
309
|
+
```ruby
|
|
310
|
+
# Rails already provides singletons via configuration
|
|
311
|
+
# config/application.rb
|
|
312
|
+
module MyApp
|
|
313
|
+
class Application < Rails::Application
|
|
314
|
+
config.my_setting = "value"
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Access anywhere
|
|
319
|
+
Rails.application.config.my_setting
|
|
320
|
+
|
|
321
|
+
# Custom singleton service
|
|
322
|
+
class FeatureFlags
|
|
323
|
+
include Singleton
|
|
324
|
+
|
|
325
|
+
def initialize
|
|
326
|
+
@flags = {}
|
|
327
|
+
load_flags
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def enabled?(feature)
|
|
331
|
+
@flags[feature.to_sym] == true
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def enable(feature)
|
|
335
|
+
@flags[feature.to_sym] = true
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
private
|
|
339
|
+
|
|
340
|
+
def load_flags
|
|
341
|
+
@flags = Rails.cache.fetch("feature_flags", expires_in: 1.hour) do
|
|
342
|
+
Feature.pluck(:name, :enabled).to_h.symbolize_keys
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# Usage
|
|
348
|
+
FeatureFlags.instance.enabled?(:new_checkout)
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
353
|
+
### Prototype
|
|
354
|
+
**Problem:** Create new objects by copying existing ones.
|
|
355
|
+
**When to use:** When creating an object is expensive and you can clone existing ones.
|
|
356
|
+
|
|
357
|
+
```ruby
|
|
358
|
+
class Document
|
|
359
|
+
attr_accessor :title, :content, :styles, :metadata
|
|
360
|
+
|
|
361
|
+
def initialize
|
|
362
|
+
@styles = {}
|
|
363
|
+
@metadata = {}
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def clone
|
|
367
|
+
copy = super
|
|
368
|
+
copy.styles = @styles.dup
|
|
369
|
+
copy.metadata = @metadata.dup
|
|
370
|
+
copy
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# Usage
|
|
375
|
+
template = Document.new
|
|
376
|
+
template.title = "Template"
|
|
377
|
+
template.styles = { font: "Arial", size: 12 }
|
|
378
|
+
template.metadata = { author: "System", version: 1 }
|
|
379
|
+
|
|
380
|
+
# Create copies
|
|
381
|
+
doc1 = template.clone
|
|
382
|
+
doc1.title = "Report Q1"
|
|
383
|
+
|
|
384
|
+
doc2 = template.clone
|
|
385
|
+
doc2.title = "Report Q2"
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
**Rails Example:**
|
|
389
|
+
```ruby
|
|
390
|
+
# Using ActiveRecord's dup
|
|
391
|
+
class Order < ApplicationRecord
|
|
392
|
+
has_many :order_items
|
|
393
|
+
|
|
394
|
+
def duplicate
|
|
395
|
+
new_order = dup
|
|
396
|
+
new_order.status = :draft
|
|
397
|
+
new_order.created_at = nil
|
|
398
|
+
|
|
399
|
+
order_items.each do |item|
|
|
400
|
+
new_order.order_items << item.dup
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
new_order
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# Usage
|
|
408
|
+
original_order = Order.find(1)
|
|
409
|
+
new_order = original_order.duplicate
|
|
410
|
+
new_order.save!
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
415
|
+
## Structural Patterns
|
|
416
|
+
|
|
417
|
+
### Adapter
|
|
418
|
+
**Problem:** Make incompatible interfaces work together.
|
|
419
|
+
**When to use:** When you need to use an existing class with an incompatible interface.
|
|
420
|
+
|
|
421
|
+
```ruby
|
|
422
|
+
# Target interface expected by client
|
|
423
|
+
class PaymentGateway
|
|
424
|
+
def charge(amount, card)
|
|
425
|
+
raise NotImplementedError
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
# Adaptee - legacy system with different interface
|
|
430
|
+
class LegacyPaymentSystem
|
|
431
|
+
def process_payment(cents, card_number, expiry, cvv)
|
|
432
|
+
# Legacy implementation
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
# Adapter
|
|
437
|
+
class LegacyPaymentAdapter < PaymentGateway
|
|
438
|
+
def initialize
|
|
439
|
+
@legacy = LegacyPaymentSystem.new
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
def charge(amount, card)
|
|
443
|
+
cents = (amount * 100).to_i
|
|
444
|
+
@legacy.process_payment(
|
|
445
|
+
cents,
|
|
446
|
+
card[:number],
|
|
447
|
+
card[:expiry],
|
|
448
|
+
card[:cvv]
|
|
449
|
+
)
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
# Usage
|
|
454
|
+
gateway = LegacyPaymentAdapter.new
|
|
455
|
+
gateway.charge(99.99, { number: "4111...", expiry: "12/25", cvv: "123" })
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
**Rails Example:**
|
|
459
|
+
```ruby
|
|
460
|
+
# app/adapters/external_user_adapter.rb
|
|
461
|
+
class ExternalUserAdapter
|
|
462
|
+
def initialize(external_data)
|
|
463
|
+
@data = external_data
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
def to_user_params
|
|
467
|
+
{
|
|
468
|
+
email: @data["email_address"],
|
|
469
|
+
first_name: @data["given_name"],
|
|
470
|
+
last_name: @data["family_name"],
|
|
471
|
+
phone: format_phone(@data["phone_number"]),
|
|
472
|
+
external_id: @data["id"]
|
|
473
|
+
}
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
private
|
|
477
|
+
|
|
478
|
+
def format_phone(phone)
|
|
479
|
+
phone&.gsub(/\D/, "")
|
|
480
|
+
end
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
# Usage
|
|
484
|
+
external_data = ExternalApi.fetch_user(123)
|
|
485
|
+
adapter = ExternalUserAdapter.new(external_data)
|
|
486
|
+
User.create!(adapter.to_user_params)
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
---
|
|
490
|
+
|
|
491
|
+
### Bridge
|
|
492
|
+
**Problem:** Separate abstraction from implementation so both can vary independently.
|
|
493
|
+
**When to use:** When you want to avoid a permanent binding between abstraction and implementation.
|
|
494
|
+
|
|
495
|
+
```ruby
|
|
496
|
+
# Implementation interface
|
|
497
|
+
class Renderer
|
|
498
|
+
def render_title(text)
|
|
499
|
+
raise NotImplementedError
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
def render_body(text)
|
|
503
|
+
raise NotImplementedError
|
|
504
|
+
end
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
# Concrete implementations
|
|
508
|
+
class HtmlRenderer < Renderer
|
|
509
|
+
def render_title(text)
|
|
510
|
+
"<h1>#{text}</h1>"
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
def render_body(text)
|
|
514
|
+
"<p>#{text}</p>"
|
|
515
|
+
end
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
class MarkdownRenderer < Renderer
|
|
519
|
+
def render_title(text)
|
|
520
|
+
"# #{text}\n"
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
def render_body(text)
|
|
524
|
+
"#{text}\n"
|
|
525
|
+
end
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
# Abstraction
|
|
529
|
+
class Document
|
|
530
|
+
def initialize(renderer)
|
|
531
|
+
@renderer = renderer
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
def render
|
|
535
|
+
raise NotImplementedError
|
|
536
|
+
end
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
# Refined abstraction
|
|
540
|
+
class Article < Document
|
|
541
|
+
attr_accessor :title, :body
|
|
542
|
+
|
|
543
|
+
def render
|
|
544
|
+
@renderer.render_title(@title) + @renderer.render_body(@body)
|
|
545
|
+
end
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
# Usage
|
|
549
|
+
html_article = Article.new(HtmlRenderer.new)
|
|
550
|
+
html_article.title = "Hello"
|
|
551
|
+
html_article.body = "World"
|
|
552
|
+
html_article.render # => "<h1>Hello</h1><p>World</p>"
|
|
553
|
+
|
|
554
|
+
md_article = Article.new(MarkdownRenderer.new)
|
|
555
|
+
md_article.title = "Hello"
|
|
556
|
+
md_article.body = "World"
|
|
557
|
+
md_article.render # => "# Hello\nWorld\n"
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
---
|
|
561
|
+
|
|
562
|
+
### Composite
|
|
563
|
+
**Problem:** Compose objects into tree structures and treat individual objects and compositions uniformly.
|
|
564
|
+
**When to use:** When you need to represent part-whole hierarchies.
|
|
565
|
+
|
|
566
|
+
```ruby
|
|
567
|
+
# Component
|
|
568
|
+
class MenuComponent
|
|
569
|
+
def add(component)
|
|
570
|
+
raise NotImplementedError
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
def render
|
|
574
|
+
raise NotImplementedError
|
|
575
|
+
end
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
# Leaf
|
|
579
|
+
class MenuItem < MenuComponent
|
|
580
|
+
attr_reader :name, :url
|
|
581
|
+
|
|
582
|
+
def initialize(name, url)
|
|
583
|
+
@name = name
|
|
584
|
+
@url = url
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
def render
|
|
588
|
+
"<a href='#{@url}'>#{@name}</a>"
|
|
589
|
+
end
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
# Composite
|
|
593
|
+
class Menu < MenuComponent
|
|
594
|
+
attr_reader :name
|
|
595
|
+
|
|
596
|
+
def initialize(name)
|
|
597
|
+
@name = name
|
|
598
|
+
@children = []
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
def add(component)
|
|
602
|
+
@children << component
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
def render
|
|
606
|
+
html = "<ul class='menu'><li>#{@name}"
|
|
607
|
+
html += "<ul>"
|
|
608
|
+
@children.each { |child| html += "<li>#{child.render}</li>" }
|
|
609
|
+
html += "</ul></li></ul>"
|
|
610
|
+
html
|
|
611
|
+
end
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
# Usage
|
|
615
|
+
main_menu = Menu.new("Main")
|
|
616
|
+
main_menu.add(MenuItem.new("Home", "/"))
|
|
617
|
+
main_menu.add(MenuItem.new("About", "/about"))
|
|
618
|
+
|
|
619
|
+
products_menu = Menu.new("Products")
|
|
620
|
+
products_menu.add(MenuItem.new("Software", "/products/software"))
|
|
621
|
+
products_menu.add(MenuItem.new("Hardware", "/products/hardware"))
|
|
622
|
+
|
|
623
|
+
main_menu.add(products_menu)
|
|
624
|
+
main_menu.render
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
**Rails Example:**
|
|
628
|
+
```ruby
|
|
629
|
+
# app/models/category.rb - Tree structure with acts_as_tree or closure_tree
|
|
630
|
+
class Category < ApplicationRecord
|
|
631
|
+
has_many :children, class_name: "Category", foreign_key: "parent_id"
|
|
632
|
+
belongs_to :parent, class_name: "Category", optional: true
|
|
633
|
+
has_many :products
|
|
634
|
+
|
|
635
|
+
def all_products
|
|
636
|
+
Product.where(category_id: subtree_ids)
|
|
637
|
+
end
|
|
638
|
+
|
|
639
|
+
def subtree_ids
|
|
640
|
+
[id] + children.flat_map(&:subtree_ids)
|
|
641
|
+
end
|
|
642
|
+
|
|
643
|
+
def render_tree(level = 0)
|
|
644
|
+
indent = " " * level
|
|
645
|
+
output = "#{indent}- #{name}\n"
|
|
646
|
+
children.each { |child| output += child.render_tree(level + 1) }
|
|
647
|
+
output
|
|
648
|
+
end
|
|
649
|
+
end
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
---
|
|
653
|
+
|
|
654
|
+
### Decorator
|
|
655
|
+
**Problem:** Add responsibilities to objects dynamically without altering their structure.
|
|
656
|
+
**When to use:** When you need to add behavior to objects without subclassing.
|
|
657
|
+
|
|
658
|
+
```ruby
|
|
659
|
+
# Component interface
|
|
660
|
+
class Coffee
|
|
661
|
+
def cost
|
|
662
|
+
raise NotImplementedError
|
|
663
|
+
end
|
|
664
|
+
|
|
665
|
+
def description
|
|
666
|
+
raise NotImplementedError
|
|
667
|
+
end
|
|
668
|
+
end
|
|
669
|
+
|
|
670
|
+
# Concrete component
|
|
671
|
+
class SimpleCoffee < Coffee
|
|
672
|
+
def cost
|
|
673
|
+
2.0
|
|
674
|
+
end
|
|
675
|
+
|
|
676
|
+
def description
|
|
677
|
+
"Simple coffee"
|
|
678
|
+
end
|
|
679
|
+
end
|
|
680
|
+
|
|
681
|
+
# Decorator base
|
|
682
|
+
class CoffeeDecorator < Coffee
|
|
683
|
+
def initialize(coffee)
|
|
684
|
+
@coffee = coffee
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
def cost
|
|
688
|
+
@coffee.cost
|
|
689
|
+
end
|
|
690
|
+
|
|
691
|
+
def description
|
|
692
|
+
@coffee.description
|
|
693
|
+
end
|
|
694
|
+
end
|
|
695
|
+
|
|
696
|
+
# Concrete decorators
|
|
697
|
+
class Milk < CoffeeDecorator
|
|
698
|
+
def cost
|
|
699
|
+
@coffee.cost + 0.5
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
def description
|
|
703
|
+
"#{@coffee.description}, milk"
|
|
704
|
+
end
|
|
705
|
+
end
|
|
706
|
+
|
|
707
|
+
class Sugar < CoffeeDecorator
|
|
708
|
+
def cost
|
|
709
|
+
@coffee.cost + 0.2
|
|
710
|
+
end
|
|
711
|
+
|
|
712
|
+
def description
|
|
713
|
+
"#{@coffee.description}, sugar"
|
|
714
|
+
end
|
|
715
|
+
end
|
|
716
|
+
|
|
717
|
+
# Usage
|
|
718
|
+
coffee = SimpleCoffee.new
|
|
719
|
+
coffee = Milk.new(coffee)
|
|
720
|
+
coffee = Sugar.new(coffee)
|
|
721
|
+
coffee.cost # => 2.7
|
|
722
|
+
coffee.description # => "Simple coffee, milk, sugar"
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
**Rails Example:**
|
|
726
|
+
```ruby
|
|
727
|
+
# Using SimpleDelegator
|
|
728
|
+
class UserPresenter < SimpleDelegator
|
|
729
|
+
def full_name
|
|
730
|
+
"#{first_name} #{last_name}"
|
|
731
|
+
end
|
|
732
|
+
|
|
733
|
+
def formatted_created_at
|
|
734
|
+
created_at.strftime("%B %d, %Y")
|
|
735
|
+
end
|
|
736
|
+
|
|
737
|
+
def avatar_url
|
|
738
|
+
avatar.attached? ? avatar : "default_avatar.png"
|
|
739
|
+
end
|
|
740
|
+
end
|
|
741
|
+
|
|
742
|
+
# Usage in view
|
|
743
|
+
presenter = UserPresenter.new(@user)
|
|
744
|
+
presenter.email # delegates to user
|
|
745
|
+
presenter.full_name # decorator method
|
|
746
|
+
```
|
|
747
|
+
|
|
748
|
+
---
|
|
749
|
+
|
|
750
|
+
### Facade
|
|
751
|
+
**Problem:** Provide a simplified interface to a complex subsystem.
|
|
752
|
+
**When to use:** When you need a simple interface to a complex set of classes.
|
|
753
|
+
|
|
754
|
+
```ruby
|
|
755
|
+
# Complex subsystem classes
|
|
756
|
+
class Inventory
|
|
757
|
+
def check_stock(product_id)
|
|
758
|
+
# Check inventory
|
|
759
|
+
end
|
|
760
|
+
|
|
761
|
+
def reserve(product_id, quantity)
|
|
762
|
+
# Reserve stock
|
|
763
|
+
end
|
|
764
|
+
end
|
|
765
|
+
|
|
766
|
+
class PaymentProcessor
|
|
767
|
+
def authorize(amount, card)
|
|
768
|
+
# Authorize payment
|
|
769
|
+
end
|
|
770
|
+
|
|
771
|
+
def capture(authorization_id)
|
|
772
|
+
# Capture payment
|
|
773
|
+
end
|
|
774
|
+
end
|
|
775
|
+
|
|
776
|
+
class ShippingService
|
|
777
|
+
def calculate_cost(address, weight)
|
|
778
|
+
# Calculate shipping
|
|
779
|
+
end
|
|
780
|
+
|
|
781
|
+
def create_shipment(order, address)
|
|
782
|
+
# Create shipment
|
|
783
|
+
end
|
|
784
|
+
end
|
|
785
|
+
|
|
786
|
+
class NotificationService
|
|
787
|
+
def send_confirmation(user, order)
|
|
788
|
+
# Send email
|
|
789
|
+
end
|
|
790
|
+
end
|
|
791
|
+
|
|
792
|
+
# Facade
|
|
793
|
+
class OrderFacade
|
|
794
|
+
def initialize
|
|
795
|
+
@inventory = Inventory.new
|
|
796
|
+
@payment = PaymentProcessor.new
|
|
797
|
+
@shipping = ShippingService.new
|
|
798
|
+
@notification = NotificationService.new
|
|
799
|
+
end
|
|
800
|
+
|
|
801
|
+
def place_order(user, cart, address, payment_info)
|
|
802
|
+
# Check stock for all items
|
|
803
|
+
cart.items.each do |item|
|
|
804
|
+
unless @inventory.check_stock(item.product_id)
|
|
805
|
+
raise OutOfStockError, item.name
|
|
806
|
+
end
|
|
807
|
+
end
|
|
808
|
+
|
|
809
|
+
# Reserve inventory
|
|
810
|
+
cart.items.each { |item| @inventory.reserve(item.product_id, item.quantity) }
|
|
811
|
+
|
|
812
|
+
# Process payment
|
|
813
|
+
auth = @payment.authorize(cart.total, payment_info)
|
|
814
|
+
@payment.capture(auth.id)
|
|
815
|
+
|
|
816
|
+
# Create shipment
|
|
817
|
+
@shipping.create_shipment(order, address)
|
|
818
|
+
|
|
819
|
+
# Notify user
|
|
820
|
+
@notification.send_confirmation(user, order)
|
|
821
|
+
|
|
822
|
+
order
|
|
823
|
+
end
|
|
824
|
+
end
|
|
825
|
+
|
|
826
|
+
# Usage - simple interface to complex process
|
|
827
|
+
facade = OrderFacade.new
|
|
828
|
+
facade.place_order(current_user, cart, address, payment_info)
|
|
829
|
+
```
|
|
830
|
+
|
|
831
|
+
---
|
|
832
|
+
|
|
833
|
+
### Proxy
|
|
834
|
+
**Problem:** Provide a surrogate or placeholder for another object.
|
|
835
|
+
**When to use:** For lazy loading, access control, logging, or caching.
|
|
836
|
+
|
|
837
|
+
```ruby
|
|
838
|
+
# Subject interface
|
|
839
|
+
class Image
|
|
840
|
+
def display
|
|
841
|
+
raise NotImplementedError
|
|
842
|
+
end
|
|
843
|
+
end
|
|
844
|
+
|
|
845
|
+
# Real subject
|
|
846
|
+
class RealImage < Image
|
|
847
|
+
def initialize(filename)
|
|
848
|
+
@filename = filename
|
|
849
|
+
load_from_disk
|
|
850
|
+
end
|
|
851
|
+
|
|
852
|
+
def display
|
|
853
|
+
puts "Displaying #{@filename}"
|
|
854
|
+
end
|
|
855
|
+
|
|
856
|
+
private
|
|
857
|
+
|
|
858
|
+
def load_from_disk
|
|
859
|
+
puts "Loading #{@filename} from disk..."
|
|
860
|
+
sleep(2) # Expensive operation
|
|
861
|
+
end
|
|
862
|
+
end
|
|
863
|
+
|
|
864
|
+
# Proxy - lazy loading
|
|
865
|
+
class ImageProxy < Image
|
|
866
|
+
def initialize(filename)
|
|
867
|
+
@filename = filename
|
|
868
|
+
@real_image = nil
|
|
869
|
+
end
|
|
870
|
+
|
|
871
|
+
def display
|
|
872
|
+
@real_image ||= RealImage.new(@filename)
|
|
873
|
+
@real_image.display
|
|
874
|
+
end
|
|
875
|
+
end
|
|
876
|
+
|
|
877
|
+
# Usage
|
|
878
|
+
image = ImageProxy.new("large_photo.jpg")
|
|
879
|
+
# Image not loaded yet
|
|
880
|
+
image.display # Loads and displays
|
|
881
|
+
image.display # Just displays (cached)
|
|
882
|
+
```
|
|
883
|
+
|
|
884
|
+
**Rails Example:**
|
|
885
|
+
```ruby
|
|
886
|
+
# Caching proxy
|
|
887
|
+
class CachedProductRepository
|
|
888
|
+
def initialize(repository = ProductRepository.new)
|
|
889
|
+
@repository = repository
|
|
890
|
+
@cache = Rails.cache
|
|
891
|
+
end
|
|
892
|
+
|
|
893
|
+
def find(id)
|
|
894
|
+
@cache.fetch("product:#{id}", expires_in: 1.hour) do
|
|
895
|
+
@repository.find(id)
|
|
896
|
+
end
|
|
897
|
+
end
|
|
898
|
+
|
|
899
|
+
def all
|
|
900
|
+
@cache.fetch("products:all", expires_in: 15.minutes) do
|
|
901
|
+
@repository.all
|
|
902
|
+
end
|
|
903
|
+
end
|
|
904
|
+
|
|
905
|
+
def invalidate(id)
|
|
906
|
+
@cache.delete("product:#{id}")
|
|
907
|
+
@cache.delete("products:all")
|
|
908
|
+
end
|
|
909
|
+
end
|
|
910
|
+
```
|
|
911
|
+
|
|
912
|
+
---
|
|
913
|
+
|
|
914
|
+
## Behavioral Patterns
|
|
915
|
+
|
|
916
|
+
### Chain of Responsibility
|
|
917
|
+
**Problem:** Pass requests along a chain of handlers.
|
|
918
|
+
**When to use:** When multiple objects may handle a request and the handler isn't known beforehand.
|
|
919
|
+
|
|
920
|
+
```ruby
|
|
921
|
+
class Handler
|
|
922
|
+
attr_accessor :next_handler
|
|
923
|
+
|
|
924
|
+
def handle(request)
|
|
925
|
+
if can_handle?(request)
|
|
926
|
+
process(request)
|
|
927
|
+
elsif next_handler
|
|
928
|
+
next_handler.handle(request)
|
|
929
|
+
else
|
|
930
|
+
default_handle(request)
|
|
931
|
+
end
|
|
932
|
+
end
|
|
933
|
+
|
|
934
|
+
def can_handle?(request)
|
|
935
|
+
raise NotImplementedError
|
|
936
|
+
end
|
|
937
|
+
|
|
938
|
+
def process(request)
|
|
939
|
+
raise NotImplementedError
|
|
940
|
+
end
|
|
941
|
+
|
|
942
|
+
def default_handle(request)
|
|
943
|
+
raise "No handler for request: #{request}"
|
|
944
|
+
end
|
|
945
|
+
end
|
|
946
|
+
|
|
947
|
+
class SmallOrderHandler < Handler
|
|
948
|
+
def can_handle?(request)
|
|
949
|
+
request[:amount] < 100
|
|
950
|
+
end
|
|
951
|
+
|
|
952
|
+
def process(request)
|
|
953
|
+
puts "Processing small order: $#{request[:amount]}"
|
|
954
|
+
:approved
|
|
955
|
+
end
|
|
956
|
+
end
|
|
957
|
+
|
|
958
|
+
class MediumOrderHandler < Handler
|
|
959
|
+
def can_handle?(request)
|
|
960
|
+
request[:amount] < 1000
|
|
961
|
+
end
|
|
962
|
+
|
|
963
|
+
def process(request)
|
|
964
|
+
puts "Processing medium order: $#{request[:amount]} - needs review"
|
|
965
|
+
:pending_review
|
|
966
|
+
end
|
|
967
|
+
end
|
|
968
|
+
|
|
969
|
+
class LargeOrderHandler < Handler
|
|
970
|
+
def can_handle?(request)
|
|
971
|
+
request[:amount] >= 1000
|
|
972
|
+
end
|
|
973
|
+
|
|
974
|
+
def process(request)
|
|
975
|
+
puts "Processing large order: $#{request[:amount]} - needs approval"
|
|
976
|
+
:pending_approval
|
|
977
|
+
end
|
|
978
|
+
end
|
|
979
|
+
|
|
980
|
+
# Setup chain
|
|
981
|
+
small = SmallOrderHandler.new
|
|
982
|
+
medium = MediumOrderHandler.new
|
|
983
|
+
large = LargeOrderHandler.new
|
|
984
|
+
|
|
985
|
+
small.next_handler = medium
|
|
986
|
+
medium.next_handler = large
|
|
987
|
+
|
|
988
|
+
# Usage
|
|
989
|
+
small.handle({ amount: 50 }) # Small handler
|
|
990
|
+
small.handle({ amount: 500 }) # Medium handler
|
|
991
|
+
small.handle({ amount: 5000 }) # Large handler
|
|
992
|
+
```
|
|
993
|
+
|
|
994
|
+
**Rails Example:**
|
|
995
|
+
```ruby
|
|
996
|
+
# Middleware-style chain
|
|
997
|
+
class AuthenticationMiddleware
|
|
998
|
+
def initialize(app)
|
|
999
|
+
@app = app
|
|
1000
|
+
end
|
|
1001
|
+
|
|
1002
|
+
def call(env)
|
|
1003
|
+
if authenticated?(env)
|
|
1004
|
+
@app.call(env)
|
|
1005
|
+
else
|
|
1006
|
+
[401, {}, ["Unauthorized"]]
|
|
1007
|
+
end
|
|
1008
|
+
end
|
|
1009
|
+
end
|
|
1010
|
+
|
|
1011
|
+
# Or as service objects
|
|
1012
|
+
class ValidationChain
|
|
1013
|
+
def self.build
|
|
1014
|
+
chain = RequiredFieldsValidator.new
|
|
1015
|
+
chain.next_handler = EmailFormatValidator.new
|
|
1016
|
+
chain.next_handler.next_handler = UniqueEmailValidator.new
|
|
1017
|
+
chain
|
|
1018
|
+
end
|
|
1019
|
+
end
|
|
1020
|
+
```
|
|
1021
|
+
|
|
1022
|
+
---
|
|
1023
|
+
|
|
1024
|
+
### Command
|
|
1025
|
+
**Problem:** Encapsulate a request as an object.
|
|
1026
|
+
**When to use:** For undo/redo, queuing operations, or logging requests.
|
|
1027
|
+
|
|
1028
|
+
```ruby
|
|
1029
|
+
# Command interface
|
|
1030
|
+
class Command
|
|
1031
|
+
def execute
|
|
1032
|
+
raise NotImplementedError
|
|
1033
|
+
end
|
|
1034
|
+
|
|
1035
|
+
def undo
|
|
1036
|
+
raise NotImplementedError
|
|
1037
|
+
end
|
|
1038
|
+
end
|
|
1039
|
+
|
|
1040
|
+
# Concrete commands
|
|
1041
|
+
class AddItemCommand < Command
|
|
1042
|
+
def initialize(cart, item)
|
|
1043
|
+
@cart = cart
|
|
1044
|
+
@item = item
|
|
1045
|
+
end
|
|
1046
|
+
|
|
1047
|
+
def execute
|
|
1048
|
+
@cart.add(@item)
|
|
1049
|
+
end
|
|
1050
|
+
|
|
1051
|
+
def undo
|
|
1052
|
+
@cart.remove(@item)
|
|
1053
|
+
end
|
|
1054
|
+
end
|
|
1055
|
+
|
|
1056
|
+
class RemoveItemCommand < Command
|
|
1057
|
+
def initialize(cart, item)
|
|
1058
|
+
@cart = cart
|
|
1059
|
+
@item = item
|
|
1060
|
+
end
|
|
1061
|
+
|
|
1062
|
+
def execute
|
|
1063
|
+
@cart.remove(@item)
|
|
1064
|
+
end
|
|
1065
|
+
|
|
1066
|
+
def undo
|
|
1067
|
+
@cart.add(@item)
|
|
1068
|
+
end
|
|
1069
|
+
end
|
|
1070
|
+
|
|
1071
|
+
# Invoker with history
|
|
1072
|
+
class CommandInvoker
|
|
1073
|
+
def initialize
|
|
1074
|
+
@history = []
|
|
1075
|
+
end
|
|
1076
|
+
|
|
1077
|
+
def execute(command)
|
|
1078
|
+
command.execute
|
|
1079
|
+
@history << command
|
|
1080
|
+
end
|
|
1081
|
+
|
|
1082
|
+
def undo
|
|
1083
|
+
return if @history.empty?
|
|
1084
|
+
command = @history.pop
|
|
1085
|
+
command.undo
|
|
1086
|
+
end
|
|
1087
|
+
end
|
|
1088
|
+
|
|
1089
|
+
# Usage
|
|
1090
|
+
invoker = CommandInvoker.new
|
|
1091
|
+
invoker.execute(AddItemCommand.new(cart, product))
|
|
1092
|
+
invoker.execute(AddItemCommand.new(cart, another_product))
|
|
1093
|
+
invoker.undo # Removes another_product
|
|
1094
|
+
```
|
|
1095
|
+
|
|
1096
|
+
**Rails Example:**
|
|
1097
|
+
```ruby
|
|
1098
|
+
# app/commands/create_order_command.rb
|
|
1099
|
+
class CreateOrderCommand
|
|
1100
|
+
def initialize(user:, cart:, address:)
|
|
1101
|
+
@user = user
|
|
1102
|
+
@cart = cart
|
|
1103
|
+
@address = address
|
|
1104
|
+
end
|
|
1105
|
+
|
|
1106
|
+
def execute
|
|
1107
|
+
ActiveRecord::Base.transaction do
|
|
1108
|
+
@order = Order.create!(
|
|
1109
|
+
user: @user,
|
|
1110
|
+
address: @address,
|
|
1111
|
+
total: @cart.total
|
|
1112
|
+
)
|
|
1113
|
+
|
|
1114
|
+
@cart.items.each do |item|
|
|
1115
|
+
@order.order_items.create!(
|
|
1116
|
+
product: item.product,
|
|
1117
|
+
quantity: item.quantity,
|
|
1118
|
+
price: item.price
|
|
1119
|
+
)
|
|
1120
|
+
end
|
|
1121
|
+
|
|
1122
|
+
@cart.clear
|
|
1123
|
+
OrderMailer.confirmation(@order).deliver_later
|
|
1124
|
+
end
|
|
1125
|
+
|
|
1126
|
+
@order
|
|
1127
|
+
end
|
|
1128
|
+
|
|
1129
|
+
def undo
|
|
1130
|
+
return unless @order
|
|
1131
|
+
@order.cancel!
|
|
1132
|
+
end
|
|
1133
|
+
end
|
|
1134
|
+
```
|
|
1135
|
+
|
|
1136
|
+
---
|
|
1137
|
+
|
|
1138
|
+
### Iterator
|
|
1139
|
+
**Problem:** Access elements of a collection sequentially without exposing underlying representation.
|
|
1140
|
+
**When to use:** When you need to traverse a collection in different ways.
|
|
1141
|
+
|
|
1142
|
+
```ruby
|
|
1143
|
+
# Ruby's Enumerable module provides iterator pattern
|
|
1144
|
+
class Library
|
|
1145
|
+
include Enumerable
|
|
1146
|
+
|
|
1147
|
+
def initialize
|
|
1148
|
+
@books = []
|
|
1149
|
+
end
|
|
1150
|
+
|
|
1151
|
+
def add(book)
|
|
1152
|
+
@books << book
|
|
1153
|
+
end
|
|
1154
|
+
|
|
1155
|
+
def each(&block)
|
|
1156
|
+
@books.each(&block)
|
|
1157
|
+
end
|
|
1158
|
+
|
|
1159
|
+
# Custom iterators
|
|
1160
|
+
def each_by_author(author, &block)
|
|
1161
|
+
@books.select { |b| b.author == author }.each(&block)
|
|
1162
|
+
end
|
|
1163
|
+
|
|
1164
|
+
def each_published_after(year, &block)
|
|
1165
|
+
@books.select { |b| b.year > year }.each(&block)
|
|
1166
|
+
end
|
|
1167
|
+
end
|
|
1168
|
+
|
|
1169
|
+
# Usage
|
|
1170
|
+
library = Library.new
|
|
1171
|
+
library.add(Book.new("1984", "Orwell", 1949))
|
|
1172
|
+
library.add(Book.new("Brave New World", "Huxley", 1932))
|
|
1173
|
+
|
|
1174
|
+
library.each { |book| puts book.title }
|
|
1175
|
+
library.map(&:title)
|
|
1176
|
+
library.select { |b| b.year > 1940 }
|
|
1177
|
+
library.each_by_author("Orwell") { |b| puts b.title }
|
|
1178
|
+
```
|
|
1179
|
+
|
|
1180
|
+
**Rails Example:**
|
|
1181
|
+
```ruby
|
|
1182
|
+
# ActiveRecord already implements Enumerable
|
|
1183
|
+
User.all.each { |user| process(user) }
|
|
1184
|
+
User.find_each(batch_size: 1000) { |user| process(user) } # Memory efficient
|
|
1185
|
+
|
|
1186
|
+
# Custom iterator for API pagination
|
|
1187
|
+
class PaginatedIterator
|
|
1188
|
+
include Enumerable
|
|
1189
|
+
|
|
1190
|
+
def initialize(client, resource)
|
|
1191
|
+
@client = client
|
|
1192
|
+
@resource = resource
|
|
1193
|
+
end
|
|
1194
|
+
|
|
1195
|
+
def each
|
|
1196
|
+
page = 1
|
|
1197
|
+
loop do
|
|
1198
|
+
response = @client.get(@resource, page: page)
|
|
1199
|
+
break if response.data.empty?
|
|
1200
|
+
|
|
1201
|
+
response.data.each { |item| yield item }
|
|
1202
|
+
page += 1
|
|
1203
|
+
end
|
|
1204
|
+
end
|
|
1205
|
+
end
|
|
1206
|
+
```
|
|
1207
|
+
|
|
1208
|
+
---
|
|
1209
|
+
|
|
1210
|
+
### Mediator
|
|
1211
|
+
**Problem:** Reduce chaotic dependencies between objects by having them communicate through a mediator.
|
|
1212
|
+
**When to use:** When objects have complex many-to-many relationships.
|
|
1213
|
+
|
|
1214
|
+
```ruby
|
|
1215
|
+
class ChatRoom
|
|
1216
|
+
def initialize
|
|
1217
|
+
@users = []
|
|
1218
|
+
end
|
|
1219
|
+
|
|
1220
|
+
def register(user)
|
|
1221
|
+
@users << user
|
|
1222
|
+
user.chat_room = self
|
|
1223
|
+
end
|
|
1224
|
+
|
|
1225
|
+
def send_message(sender, message, recipient = nil)
|
|
1226
|
+
if recipient
|
|
1227
|
+
# Private message
|
|
1228
|
+
recipient.receive(message, sender)
|
|
1229
|
+
else
|
|
1230
|
+
# Broadcast
|
|
1231
|
+
@users.each do |user|
|
|
1232
|
+
user.receive(message, sender) unless user == sender
|
|
1233
|
+
end
|
|
1234
|
+
end
|
|
1235
|
+
end
|
|
1236
|
+
end
|
|
1237
|
+
|
|
1238
|
+
class User
|
|
1239
|
+
attr_reader :name
|
|
1240
|
+
attr_accessor :chat_room
|
|
1241
|
+
|
|
1242
|
+
def initialize(name)
|
|
1243
|
+
@name = name
|
|
1244
|
+
end
|
|
1245
|
+
|
|
1246
|
+
def send(message, recipient = nil)
|
|
1247
|
+
@chat_room.send_message(self, message, recipient)
|
|
1248
|
+
end
|
|
1249
|
+
|
|
1250
|
+
def receive(message, sender)
|
|
1251
|
+
puts "#{@name} received from #{sender.name}: #{message}"
|
|
1252
|
+
end
|
|
1253
|
+
end
|
|
1254
|
+
|
|
1255
|
+
# Usage
|
|
1256
|
+
room = ChatRoom.new
|
|
1257
|
+
alice = User.new("Alice")
|
|
1258
|
+
bob = User.new("Bob")
|
|
1259
|
+
|
|
1260
|
+
room.register(alice)
|
|
1261
|
+
room.register(bob)
|
|
1262
|
+
|
|
1263
|
+
alice.send("Hello everyone!") # Broadcast
|
|
1264
|
+
alice.send("Hi Bob!", bob) # Private
|
|
1265
|
+
```
|
|
1266
|
+
|
|
1267
|
+
**Rails Example:**
|
|
1268
|
+
```ruby
|
|
1269
|
+
# Event-driven mediator using ActiveSupport::Notifications
|
|
1270
|
+
class OrderMediator
|
|
1271
|
+
def self.setup
|
|
1272
|
+
ActiveSupport::Notifications.subscribe("order.placed") do |*args|
|
|
1273
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
1274
|
+
order = event.payload[:order]
|
|
1275
|
+
|
|
1276
|
+
InventoryService.reserve(order)
|
|
1277
|
+
PaymentService.capture(order)
|
|
1278
|
+
NotificationService.confirm(order)
|
|
1279
|
+
end
|
|
1280
|
+
end
|
|
1281
|
+
end
|
|
1282
|
+
|
|
1283
|
+
# In order creation
|
|
1284
|
+
ActiveSupport::Notifications.instrument("order.placed", order: @order)
|
|
1285
|
+
```
|
|
1286
|
+
|
|
1287
|
+
---
|
|
1288
|
+
|
|
1289
|
+
### Memento
|
|
1290
|
+
**Problem:** Capture and restore an object's internal state.
|
|
1291
|
+
**When to use:** For undo/redo, snapshots, or checkpoints.
|
|
1292
|
+
|
|
1293
|
+
```ruby
|
|
1294
|
+
# Memento
|
|
1295
|
+
class EditorMemento
|
|
1296
|
+
attr_reader :content, :cursor_position
|
|
1297
|
+
|
|
1298
|
+
def initialize(content, cursor_position)
|
|
1299
|
+
@content = content.dup
|
|
1300
|
+
@cursor_position = cursor_position
|
|
1301
|
+
end
|
|
1302
|
+
end
|
|
1303
|
+
|
|
1304
|
+
# Originator
|
|
1305
|
+
class TextEditor
|
|
1306
|
+
attr_accessor :content, :cursor_position
|
|
1307
|
+
|
|
1308
|
+
def initialize
|
|
1309
|
+
@content = ""
|
|
1310
|
+
@cursor_position = 0
|
|
1311
|
+
end
|
|
1312
|
+
|
|
1313
|
+
def type(text)
|
|
1314
|
+
@content.insert(@cursor_position, text)
|
|
1315
|
+
@cursor_position += text.length
|
|
1316
|
+
end
|
|
1317
|
+
|
|
1318
|
+
def save
|
|
1319
|
+
EditorMemento.new(@content, @cursor_position)
|
|
1320
|
+
end
|
|
1321
|
+
|
|
1322
|
+
def restore(memento)
|
|
1323
|
+
@content = memento.content
|
|
1324
|
+
@cursor_position = memento.cursor_position
|
|
1325
|
+
end
|
|
1326
|
+
end
|
|
1327
|
+
|
|
1328
|
+
# Caretaker
|
|
1329
|
+
class EditorHistory
|
|
1330
|
+
def initialize
|
|
1331
|
+
@mementos = []
|
|
1332
|
+
end
|
|
1333
|
+
|
|
1334
|
+
def push(memento)
|
|
1335
|
+
@mementos << memento
|
|
1336
|
+
end
|
|
1337
|
+
|
|
1338
|
+
def pop
|
|
1339
|
+
@mementos.pop
|
|
1340
|
+
end
|
|
1341
|
+
end
|
|
1342
|
+
|
|
1343
|
+
# Usage
|
|
1344
|
+
editor = TextEditor.new
|
|
1345
|
+
history = EditorHistory.new
|
|
1346
|
+
|
|
1347
|
+
editor.type("Hello")
|
|
1348
|
+
history.push(editor.save)
|
|
1349
|
+
|
|
1350
|
+
editor.type(" World")
|
|
1351
|
+
history.push(editor.save)
|
|
1352
|
+
|
|
1353
|
+
editor.type("!!!")
|
|
1354
|
+
puts editor.content # => "Hello World!!!"
|
|
1355
|
+
|
|
1356
|
+
editor.restore(history.pop)
|
|
1357
|
+
puts editor.content # => "Hello World"
|
|
1358
|
+
```
|
|
1359
|
+
|
|
1360
|
+
**Rails Example:**
|
|
1361
|
+
```ruby
|
|
1362
|
+
# Using paper_trail gem or custom versioning
|
|
1363
|
+
class Document < ApplicationRecord
|
|
1364
|
+
has_paper_trail
|
|
1365
|
+
|
|
1366
|
+
def restore_version(version_id)
|
|
1367
|
+
version = versions.find(version_id)
|
|
1368
|
+
version.reify.save!
|
|
1369
|
+
end
|
|
1370
|
+
end
|
|
1371
|
+
|
|
1372
|
+
# Or manual memento
|
|
1373
|
+
class DocumentMemento < ApplicationRecord
|
|
1374
|
+
belongs_to :document
|
|
1375
|
+
serialize :state, coder: JSON
|
|
1376
|
+
end
|
|
1377
|
+
|
|
1378
|
+
class Document < ApplicationRecord
|
|
1379
|
+
has_many :mementos, class_name: "DocumentMemento"
|
|
1380
|
+
|
|
1381
|
+
def create_snapshot
|
|
1382
|
+
mementos.create!(state: attributes)
|
|
1383
|
+
end
|
|
1384
|
+
|
|
1385
|
+
def restore_snapshot(memento)
|
|
1386
|
+
update!(memento.state.except("id", "created_at", "updated_at"))
|
|
1387
|
+
end
|
|
1388
|
+
end
|
|
1389
|
+
```
|
|
1390
|
+
|
|
1391
|
+
---
|
|
1392
|
+
|
|
1393
|
+
### Observer
|
|
1394
|
+
**Problem:** Define a one-to-many dependency so that when one object changes state, all dependents are notified.
|
|
1395
|
+
**When to use:** For event handling, notifications, or decoupled updates.
|
|
1396
|
+
|
|
1397
|
+
```ruby
|
|
1398
|
+
# Using Ruby's Observable module
|
|
1399
|
+
require 'observer'
|
|
1400
|
+
|
|
1401
|
+
class Stock
|
|
1402
|
+
include Observable
|
|
1403
|
+
|
|
1404
|
+
attr_reader :name, :price
|
|
1405
|
+
|
|
1406
|
+
def initialize(name, price)
|
|
1407
|
+
@name = name
|
|
1408
|
+
@price = price
|
|
1409
|
+
end
|
|
1410
|
+
|
|
1411
|
+
def price=(new_price)
|
|
1412
|
+
@price = new_price
|
|
1413
|
+
changed
|
|
1414
|
+
notify_observers(self)
|
|
1415
|
+
end
|
|
1416
|
+
end
|
|
1417
|
+
|
|
1418
|
+
class StockAlert
|
|
1419
|
+
def update(stock)
|
|
1420
|
+
puts "Alert: #{stock.name} is now $#{stock.price}"
|
|
1421
|
+
end
|
|
1422
|
+
end
|
|
1423
|
+
|
|
1424
|
+
class StockLogger
|
|
1425
|
+
def update(stock)
|
|
1426
|
+
puts "[LOG] #{Time.now}: #{stock.name} changed to $#{stock.price}"
|
|
1427
|
+
end
|
|
1428
|
+
end
|
|
1429
|
+
|
|
1430
|
+
# Usage
|
|
1431
|
+
stock = Stock.new("AAPL", 150)
|
|
1432
|
+
stock.add_observer(StockAlert.new)
|
|
1433
|
+
stock.add_observer(StockLogger.new)
|
|
1434
|
+
|
|
1435
|
+
stock.price = 155 # Both observers notified
|
|
1436
|
+
```
|
|
1437
|
+
|
|
1438
|
+
**Rails Example:**
|
|
1439
|
+
```ruby
|
|
1440
|
+
# ActiveRecord callbacks as observers
|
|
1441
|
+
class Order < ApplicationRecord
|
|
1442
|
+
after_create :notify_observers
|
|
1443
|
+
|
|
1444
|
+
private
|
|
1445
|
+
|
|
1446
|
+
def notify_observers
|
|
1447
|
+
OrderCreatedJob.perform_later(id)
|
|
1448
|
+
SlackNotifier.order_placed(self)
|
|
1449
|
+
AnalyticsService.track("order_created", order_id: id)
|
|
1450
|
+
end
|
|
1451
|
+
end
|
|
1452
|
+
|
|
1453
|
+
# Using ActiveSupport::Notifications
|
|
1454
|
+
class Order < ApplicationRecord
|
|
1455
|
+
after_create do
|
|
1456
|
+
ActiveSupport::Notifications.instrument("order.created", order: self)
|
|
1457
|
+
end
|
|
1458
|
+
end
|
|
1459
|
+
|
|
1460
|
+
# Subscribers
|
|
1461
|
+
ActiveSupport::Notifications.subscribe("order.created") do |*args|
|
|
1462
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
1463
|
+
OrderMailer.confirmation(event.payload[:order]).deliver_later
|
|
1464
|
+
end
|
|
1465
|
+
```
|
|
1466
|
+
|
|
1467
|
+
---
|
|
1468
|
+
|
|
1469
|
+
### State
|
|
1470
|
+
**Problem:** Allow an object to alter its behavior when its internal state changes.
|
|
1471
|
+
**When to use:** When an object's behavior depends on its state and must change at runtime.
|
|
1472
|
+
|
|
1473
|
+
```ruby
|
|
1474
|
+
# State interface
|
|
1475
|
+
class OrderState
|
|
1476
|
+
def process(order)
|
|
1477
|
+
raise NotImplementedError
|
|
1478
|
+
end
|
|
1479
|
+
|
|
1480
|
+
def ship(order)
|
|
1481
|
+
raise NotImplementedError
|
|
1482
|
+
end
|
|
1483
|
+
|
|
1484
|
+
def cancel(order)
|
|
1485
|
+
raise NotImplementedError
|
|
1486
|
+
end
|
|
1487
|
+
end
|
|
1488
|
+
|
|
1489
|
+
# Concrete states
|
|
1490
|
+
class PendingState < OrderState
|
|
1491
|
+
def process(order)
|
|
1492
|
+
puts "Processing payment..."
|
|
1493
|
+
order.state = PaidState.new
|
|
1494
|
+
end
|
|
1495
|
+
|
|
1496
|
+
def ship(order)
|
|
1497
|
+
puts "Cannot ship - payment pending"
|
|
1498
|
+
end
|
|
1499
|
+
|
|
1500
|
+
def cancel(order)
|
|
1501
|
+
puts "Order cancelled"
|
|
1502
|
+
order.state = CancelledState.new
|
|
1503
|
+
end
|
|
1504
|
+
end
|
|
1505
|
+
|
|
1506
|
+
class PaidState < OrderState
|
|
1507
|
+
def process(order)
|
|
1508
|
+
puts "Already paid"
|
|
1509
|
+
end
|
|
1510
|
+
|
|
1511
|
+
def ship(order)
|
|
1512
|
+
puts "Shipping order..."
|
|
1513
|
+
order.state = ShippedState.new
|
|
1514
|
+
end
|
|
1515
|
+
|
|
1516
|
+
def cancel(order)
|
|
1517
|
+
puts "Refunding and cancelling..."
|
|
1518
|
+
order.state = CancelledState.new
|
|
1519
|
+
end
|
|
1520
|
+
end
|
|
1521
|
+
|
|
1522
|
+
class ShippedState < OrderState
|
|
1523
|
+
def process(order)
|
|
1524
|
+
puts "Already processed and shipped"
|
|
1525
|
+
end
|
|
1526
|
+
|
|
1527
|
+
def ship(order)
|
|
1528
|
+
puts "Already shipped"
|
|
1529
|
+
end
|
|
1530
|
+
|
|
1531
|
+
def cancel(order)
|
|
1532
|
+
puts "Cannot cancel - already shipped"
|
|
1533
|
+
end
|
|
1534
|
+
end
|
|
1535
|
+
|
|
1536
|
+
# Context
|
|
1537
|
+
class Order
|
|
1538
|
+
attr_accessor :state
|
|
1539
|
+
|
|
1540
|
+
def initialize
|
|
1541
|
+
@state = PendingState.new
|
|
1542
|
+
end
|
|
1543
|
+
|
|
1544
|
+
def process
|
|
1545
|
+
@state.process(self)
|
|
1546
|
+
end
|
|
1547
|
+
|
|
1548
|
+
def ship
|
|
1549
|
+
@state.ship(self)
|
|
1550
|
+
end
|
|
1551
|
+
|
|
1552
|
+
def cancel
|
|
1553
|
+
@state.cancel(self)
|
|
1554
|
+
end
|
|
1555
|
+
end
|
|
1556
|
+
|
|
1557
|
+
# Usage
|
|
1558
|
+
order = Order.new
|
|
1559
|
+
order.process # => "Processing payment..."
|
|
1560
|
+
order.ship # => "Shipping order..."
|
|
1561
|
+
order.cancel # => "Cannot cancel - already shipped"
|
|
1562
|
+
```
|
|
1563
|
+
|
|
1564
|
+
**Rails Example:**
|
|
1565
|
+
```ruby
|
|
1566
|
+
# Using AASM gem
|
|
1567
|
+
class Order < ApplicationRecord
|
|
1568
|
+
include AASM
|
|
1569
|
+
|
|
1570
|
+
aasm column: :status do
|
|
1571
|
+
state :pending, initial: true
|
|
1572
|
+
state :paid, :shipped, :delivered, :cancelled
|
|
1573
|
+
|
|
1574
|
+
event :pay do
|
|
1575
|
+
transitions from: :pending, to: :paid
|
|
1576
|
+
after { OrderMailer.payment_received(self).deliver_later }
|
|
1577
|
+
end
|
|
1578
|
+
|
|
1579
|
+
event :ship do
|
|
1580
|
+
transitions from: :paid, to: :shipped
|
|
1581
|
+
after { ShippingService.create_label(self) }
|
|
1582
|
+
end
|
|
1583
|
+
|
|
1584
|
+
event :deliver do
|
|
1585
|
+
transitions from: :shipped, to: :delivered
|
|
1586
|
+
end
|
|
1587
|
+
|
|
1588
|
+
event :cancel do
|
|
1589
|
+
transitions from: [:pending, :paid], to: :cancelled
|
|
1590
|
+
after { RefundService.process(self) if paid? }
|
|
1591
|
+
end
|
|
1592
|
+
end
|
|
1593
|
+
end
|
|
1594
|
+
```
|
|
1595
|
+
|
|
1596
|
+
---
|
|
1597
|
+
|
|
1598
|
+
### Strategy
|
|
1599
|
+
**Problem:** Define a family of algorithms and make them interchangeable.
|
|
1600
|
+
**When to use:** When you have multiple ways to do something and want to switch at runtime.
|
|
1601
|
+
|
|
1602
|
+
```ruby
|
|
1603
|
+
# Strategy interface
|
|
1604
|
+
class ShippingStrategy
|
|
1605
|
+
def calculate(package)
|
|
1606
|
+
raise NotImplementedError
|
|
1607
|
+
end
|
|
1608
|
+
end
|
|
1609
|
+
|
|
1610
|
+
# Concrete strategies
|
|
1611
|
+
class GroundShipping < ShippingStrategy
|
|
1612
|
+
def calculate(package)
|
|
1613
|
+
package.weight * 1.5
|
|
1614
|
+
end
|
|
1615
|
+
end
|
|
1616
|
+
|
|
1617
|
+
class AirShipping < ShippingStrategy
|
|
1618
|
+
def calculate(package)
|
|
1619
|
+
package.weight * 3.0
|
|
1620
|
+
end
|
|
1621
|
+
end
|
|
1622
|
+
|
|
1623
|
+
class ExpressShipping < ShippingStrategy
|
|
1624
|
+
def calculate(package)
|
|
1625
|
+
package.weight * 5.0 + 10.0
|
|
1626
|
+
end
|
|
1627
|
+
end
|
|
1628
|
+
|
|
1629
|
+
# Context
|
|
1630
|
+
class ShippingCalculator
|
|
1631
|
+
attr_accessor :strategy
|
|
1632
|
+
|
|
1633
|
+
def initialize(strategy)
|
|
1634
|
+
@strategy = strategy
|
|
1635
|
+
end
|
|
1636
|
+
|
|
1637
|
+
def calculate(package)
|
|
1638
|
+
@strategy.calculate(package)
|
|
1639
|
+
end
|
|
1640
|
+
end
|
|
1641
|
+
|
|
1642
|
+
# Usage
|
|
1643
|
+
package = OpenStruct.new(weight: 10)
|
|
1644
|
+
calculator = ShippingCalculator.new(GroundShipping.new)
|
|
1645
|
+
puts calculator.calculate(package) # => 15.0
|
|
1646
|
+
|
|
1647
|
+
calculator.strategy = ExpressShipping.new
|
|
1648
|
+
puts calculator.calculate(package) # => 60.0
|
|
1649
|
+
```
|
|
1650
|
+
|
|
1651
|
+
**Rails Example:**
|
|
1652
|
+
```ruby
|
|
1653
|
+
# app/strategies/pricing_strategy.rb
|
|
1654
|
+
class PricingStrategy
|
|
1655
|
+
def self.for(user)
|
|
1656
|
+
case user.membership
|
|
1657
|
+
when "premium"
|
|
1658
|
+
PremiumPricing.new
|
|
1659
|
+
when "wholesale"
|
|
1660
|
+
WholesalePricing.new
|
|
1661
|
+
else
|
|
1662
|
+
StandardPricing.new
|
|
1663
|
+
end
|
|
1664
|
+
end
|
|
1665
|
+
end
|
|
1666
|
+
|
|
1667
|
+
class StandardPricing
|
|
1668
|
+
def calculate(product)
|
|
1669
|
+
product.base_price
|
|
1670
|
+
end
|
|
1671
|
+
end
|
|
1672
|
+
|
|
1673
|
+
class PremiumPricing
|
|
1674
|
+
def calculate(product)
|
|
1675
|
+
product.base_price * 0.9 # 10% discount
|
|
1676
|
+
end
|
|
1677
|
+
end
|
|
1678
|
+
|
|
1679
|
+
class WholesalePricing
|
|
1680
|
+
def calculate(product)
|
|
1681
|
+
product.base_price * 0.7 # 30% discount
|
|
1682
|
+
end
|
|
1683
|
+
end
|
|
1684
|
+
|
|
1685
|
+
# Usage in service
|
|
1686
|
+
class CartService
|
|
1687
|
+
def total(user, cart)
|
|
1688
|
+
strategy = PricingStrategy.for(user)
|
|
1689
|
+
cart.items.sum { |item| strategy.calculate(item.product) * item.quantity }
|
|
1690
|
+
end
|
|
1691
|
+
end
|
|
1692
|
+
```
|
|
1693
|
+
|
|
1694
|
+
---
|
|
1695
|
+
|
|
1696
|
+
### Template Method
|
|
1697
|
+
**Problem:** Define the skeleton of an algorithm and let subclasses override specific steps.
|
|
1698
|
+
**When to use:** When you have a common algorithm with varying implementations.
|
|
1699
|
+
|
|
1700
|
+
```ruby
|
|
1701
|
+
# Abstract class with template method
|
|
1702
|
+
class DataExporter
|
|
1703
|
+
# Template method
|
|
1704
|
+
def export(data)
|
|
1705
|
+
validate(data)
|
|
1706
|
+
formatted = format(data)
|
|
1707
|
+
output = generate_output(formatted)
|
|
1708
|
+
save(output)
|
|
1709
|
+
notify
|
|
1710
|
+
end
|
|
1711
|
+
|
|
1712
|
+
private
|
|
1713
|
+
|
|
1714
|
+
def validate(data)
|
|
1715
|
+
raise ArgumentError, "Data cannot be empty" if data.empty?
|
|
1716
|
+
end
|
|
1717
|
+
|
|
1718
|
+
def format(data)
|
|
1719
|
+
raise NotImplementedError
|
|
1720
|
+
end
|
|
1721
|
+
|
|
1722
|
+
def generate_output(formatted)
|
|
1723
|
+
raise NotImplementedError
|
|
1724
|
+
end
|
|
1725
|
+
|
|
1726
|
+
def save(output)
|
|
1727
|
+
File.write(filename, output)
|
|
1728
|
+
end
|
|
1729
|
+
|
|
1730
|
+
def notify
|
|
1731
|
+
puts "Export completed: #{filename}"
|
|
1732
|
+
end
|
|
1733
|
+
|
|
1734
|
+
def filename
|
|
1735
|
+
raise NotImplementedError
|
|
1736
|
+
end
|
|
1737
|
+
end
|
|
1738
|
+
|
|
1739
|
+
# Concrete implementations
|
|
1740
|
+
class CsvExporter < DataExporter
|
|
1741
|
+
def format(data)
|
|
1742
|
+
data.map { |row| row.values }
|
|
1743
|
+
end
|
|
1744
|
+
|
|
1745
|
+
def generate_output(formatted)
|
|
1746
|
+
formatted.map { |row| row.join(",") }.join("\n")
|
|
1747
|
+
end
|
|
1748
|
+
|
|
1749
|
+
def filename
|
|
1750
|
+
"export.csv"
|
|
1751
|
+
end
|
|
1752
|
+
end
|
|
1753
|
+
|
|
1754
|
+
class JsonExporter < DataExporter
|
|
1755
|
+
def format(data)
|
|
1756
|
+
data
|
|
1757
|
+
end
|
|
1758
|
+
|
|
1759
|
+
def generate_output(formatted)
|
|
1760
|
+
JSON.pretty_generate(formatted)
|
|
1761
|
+
end
|
|
1762
|
+
|
|
1763
|
+
def filename
|
|
1764
|
+
"export.json"
|
|
1765
|
+
end
|
|
1766
|
+
end
|
|
1767
|
+
|
|
1768
|
+
# Usage
|
|
1769
|
+
data = [{ name: "Alice", age: 30 }, { name: "Bob", age: 25 }]
|
|
1770
|
+
CsvExporter.new.export(data)
|
|
1771
|
+
JsonExporter.new.export(data)
|
|
1772
|
+
```
|
|
1773
|
+
|
|
1774
|
+
**Rails Example:**
|
|
1775
|
+
```ruby
|
|
1776
|
+
# app/services/base_importer.rb
|
|
1777
|
+
class BaseImporter
|
|
1778
|
+
def import(file)
|
|
1779
|
+
records = parse(file)
|
|
1780
|
+
validate_records(records)
|
|
1781
|
+
|
|
1782
|
+
imported = 0
|
|
1783
|
+
records.each do |record|
|
|
1784
|
+
next unless valid?(record)
|
|
1785
|
+
create_record(record)
|
|
1786
|
+
imported += 1
|
|
1787
|
+
end
|
|
1788
|
+
|
|
1789
|
+
finalize(imported)
|
|
1790
|
+
end
|
|
1791
|
+
|
|
1792
|
+
private
|
|
1793
|
+
|
|
1794
|
+
def parse(file)
|
|
1795
|
+
raise NotImplementedError
|
|
1796
|
+
end
|
|
1797
|
+
|
|
1798
|
+
def validate_records(records)
|
|
1799
|
+
raise ImportError, "No records found" if records.empty?
|
|
1800
|
+
end
|
|
1801
|
+
|
|
1802
|
+
def valid?(record)
|
|
1803
|
+
true # Override in subclass
|
|
1804
|
+
end
|
|
1805
|
+
|
|
1806
|
+
def create_record(record)
|
|
1807
|
+
raise NotImplementedError
|
|
1808
|
+
end
|
|
1809
|
+
|
|
1810
|
+
def finalize(count)
|
|
1811
|
+
Rails.logger.info "Imported #{count} records"
|
|
1812
|
+
end
|
|
1813
|
+
end
|
|
1814
|
+
|
|
1815
|
+
class UserImporter < BaseImporter
|
|
1816
|
+
private
|
|
1817
|
+
|
|
1818
|
+
def parse(file)
|
|
1819
|
+
CSV.read(file, headers: true).map(&:to_h)
|
|
1820
|
+
end
|
|
1821
|
+
|
|
1822
|
+
def valid?(record)
|
|
1823
|
+
record["email"].present?
|
|
1824
|
+
end
|
|
1825
|
+
|
|
1826
|
+
def create_record(record)
|
|
1827
|
+
User.create!(
|
|
1828
|
+
email: record["email"],
|
|
1829
|
+
name: record["name"]
|
|
1830
|
+
)
|
|
1831
|
+
end
|
|
1832
|
+
end
|
|
1833
|
+
```
|
|
1834
|
+
|
|
1835
|
+
---
|
|
1836
|
+
|
|
1837
|
+
### Visitor
|
|
1838
|
+
**Problem:** Add new operations to existing object structures without modifying them.
|
|
1839
|
+
**When to use:** When you need to perform many unrelated operations on objects.
|
|
1840
|
+
|
|
1841
|
+
```ruby
|
|
1842
|
+
# Visitor interface
|
|
1843
|
+
class ShapeVisitor
|
|
1844
|
+
def visit_circle(circle)
|
|
1845
|
+
raise NotImplementedError
|
|
1846
|
+
end
|
|
1847
|
+
|
|
1848
|
+
def visit_rectangle(rectangle)
|
|
1849
|
+
raise NotImplementedError
|
|
1850
|
+
end
|
|
1851
|
+
end
|
|
1852
|
+
|
|
1853
|
+
# Concrete visitors
|
|
1854
|
+
class AreaCalculator < ShapeVisitor
|
|
1855
|
+
def visit_circle(circle)
|
|
1856
|
+
Math::PI * circle.radius ** 2
|
|
1857
|
+
end
|
|
1858
|
+
|
|
1859
|
+
def visit_rectangle(rectangle)
|
|
1860
|
+
rectangle.width * rectangle.height
|
|
1861
|
+
end
|
|
1862
|
+
end
|
|
1863
|
+
|
|
1864
|
+
class DrawingVisitor < ShapeVisitor
|
|
1865
|
+
def visit_circle(circle)
|
|
1866
|
+
puts "Drawing circle with radius #{circle.radius}"
|
|
1867
|
+
end
|
|
1868
|
+
|
|
1869
|
+
def visit_rectangle(rectangle)
|
|
1870
|
+
puts "Drawing rectangle #{rectangle.width}x#{rectangle.height}"
|
|
1871
|
+
end
|
|
1872
|
+
end
|
|
1873
|
+
|
|
1874
|
+
# Elements
|
|
1875
|
+
class Circle
|
|
1876
|
+
attr_reader :radius
|
|
1877
|
+
|
|
1878
|
+
def initialize(radius)
|
|
1879
|
+
@radius = radius
|
|
1880
|
+
end
|
|
1881
|
+
|
|
1882
|
+
def accept(visitor)
|
|
1883
|
+
visitor.visit_circle(self)
|
|
1884
|
+
end
|
|
1885
|
+
end
|
|
1886
|
+
|
|
1887
|
+
class Rectangle
|
|
1888
|
+
attr_reader :width, :height
|
|
1889
|
+
|
|
1890
|
+
def initialize(width, height)
|
|
1891
|
+
@width = width
|
|
1892
|
+
@height = height
|
|
1893
|
+
end
|
|
1894
|
+
|
|
1895
|
+
def accept(visitor)
|
|
1896
|
+
visitor.visit_rectangle(self)
|
|
1897
|
+
end
|
|
1898
|
+
end
|
|
1899
|
+
|
|
1900
|
+
# Usage
|
|
1901
|
+
shapes = [Circle.new(5), Rectangle.new(4, 6), Circle.new(3)]
|
|
1902
|
+
|
|
1903
|
+
area_calc = AreaCalculator.new
|
|
1904
|
+
total_area = shapes.sum { |shape| shape.accept(area_calc) }
|
|
1905
|
+
puts "Total area: #{total_area}"
|
|
1906
|
+
|
|
1907
|
+
drawer = DrawingVisitor.new
|
|
1908
|
+
shapes.each { |shape| shape.accept(drawer) }
|
|
1909
|
+
```
|
|
1910
|
+
|
|
1911
|
+
**Rails Example:**
|
|
1912
|
+
```ruby
|
|
1913
|
+
# Reporting visitor for different model types
|
|
1914
|
+
class ReportVisitor
|
|
1915
|
+
def visit_order(order)
|
|
1916
|
+
{
|
|
1917
|
+
type: "order",
|
|
1918
|
+
total: order.total,
|
|
1919
|
+
items: order.items.count
|
|
1920
|
+
}
|
|
1921
|
+
end
|
|
1922
|
+
|
|
1923
|
+
def visit_refund(refund)
|
|
1924
|
+
{
|
|
1925
|
+
type: "refund",
|
|
1926
|
+
amount: refund.amount,
|
|
1927
|
+
reason: refund.reason
|
|
1928
|
+
}
|
|
1929
|
+
end
|
|
1930
|
+
|
|
1931
|
+
def visit_subscription(subscription)
|
|
1932
|
+
{
|
|
1933
|
+
type: "subscription",
|
|
1934
|
+
plan: subscription.plan.name,
|
|
1935
|
+
mrr: subscription.monthly_amount
|
|
1936
|
+
}
|
|
1937
|
+
end
|
|
1938
|
+
end
|
|
1939
|
+
|
|
1940
|
+
# Models include visitable concern
|
|
1941
|
+
module Visitable
|
|
1942
|
+
def accept(visitor)
|
|
1943
|
+
method_name = "visit_#{self.class.name.underscore}"
|
|
1944
|
+
visitor.send(method_name, self)
|
|
1945
|
+
end
|
|
1946
|
+
end
|
|
1947
|
+
|
|
1948
|
+
class Order < ApplicationRecord
|
|
1949
|
+
include Visitable
|
|
1950
|
+
end
|
|
1951
|
+
|
|
1952
|
+
# Generate report
|
|
1953
|
+
visitor = ReportVisitor.new
|
|
1954
|
+
transactions = Order.all + Refund.all + Subscription.all
|
|
1955
|
+
report = transactions.map { |t| t.accept(visitor) }
|
|
1956
|
+
```
|
|
1957
|
+
|
|
1958
|
+
---
|
|
1959
|
+
|
|
1960
|
+
## Pattern Selection Guide
|
|
1961
|
+
|
|
1962
|
+
| Problem | Pattern |
|
|
1963
|
+
|---------|---------|
|
|
1964
|
+
| Create objects without specifying class | Factory Method |
|
|
1965
|
+
| Create families of related objects | Abstract Factory |
|
|
1966
|
+
| Construct complex objects step by step | Builder |
|
|
1967
|
+
| Single instance with global access | Singleton |
|
|
1968
|
+
| Clone existing objects | Prototype |
|
|
1969
|
+
| Make incompatible interfaces work together | Adapter |
|
|
1970
|
+
| Separate abstraction from implementation | Bridge |
|
|
1971
|
+
| Tree structures with uniform interface | Composite |
|
|
1972
|
+
| Add behavior dynamically | Decorator |
|
|
1973
|
+
| Simplify complex subsystem | Facade |
|
|
1974
|
+
| Control access, lazy loading, caching | Proxy |
|
|
1975
|
+
| Pass request through handler chain | Chain of Responsibility |
|
|
1976
|
+
| Encapsulate request as object | Command |
|
|
1977
|
+
| Traverse collections uniformly | Iterator |
|
|
1978
|
+
| Reduce dependencies between objects | Mediator |
|
|
1979
|
+
| Capture and restore state | Memento |
|
|
1980
|
+
| Notify dependents of changes | Observer |
|
|
1981
|
+
| Behavior depends on state | State |
|
|
1982
|
+
| Interchangeable algorithms | Strategy |
|
|
1983
|
+
| Algorithm skeleton with customizable steps | Template Method |
|
|
1984
|
+
| Add operations without modifying objects | Visitor |
|