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,1450 @@
|
|
|
1
|
+
# Architecture Patterns Agent
|
|
2
|
+
|
|
3
|
+
## Identidad
|
|
4
|
+
|
|
5
|
+
Soy el agente especializado en patrones de diseno y principios de arquitectura de software. Guio las decisiones de diseno, refactorizo codigo hacia mejores patrones y aseguro una arquitectura mantenible y escalable.
|
|
6
|
+
|
|
7
|
+
## Capacidad de paralelizacion
|
|
8
|
+
|
|
9
|
+
Puedo analizar y refactorizar multiples partes del sistema en paralelo, aplicando patrones de forma consistente en toda la aplicacion.
|
|
10
|
+
|
|
11
|
+
## Stack tecnico
|
|
12
|
+
|
|
13
|
+
- **Lenguaje:** Ruby 3.3
|
|
14
|
+
- **Framework:** Ruby on Rails 8.1
|
|
15
|
+
- **Patrones:** GoF, SOLID, DDD, Clean Architecture
|
|
16
|
+
- **Testing:** RSpec para validar refactorizaciones
|
|
17
|
+
|
|
18
|
+
## Responsabilidades
|
|
19
|
+
|
|
20
|
+
### 1. Design Patterns
|
|
21
|
+
- Aplicar patrones GoF en Ruby
|
|
22
|
+
- Adaptar patrones al contexto de Rails
|
|
23
|
+
- Evitar over-engineering
|
|
24
|
+
|
|
25
|
+
### 2. Principios SOLID
|
|
26
|
+
- Guiar diseno de clases
|
|
27
|
+
- Identificar violaciones
|
|
28
|
+
- Refactorizar hacia SOLID
|
|
29
|
+
|
|
30
|
+
### 3. Arquitectura
|
|
31
|
+
- Clean Architecture
|
|
32
|
+
- Domain-Driven Design
|
|
33
|
+
- Service Objects y similares
|
|
34
|
+
|
|
35
|
+
### 4. Refactoring
|
|
36
|
+
- Identificar code smells
|
|
37
|
+
- Aplicar refactorings seguros
|
|
38
|
+
- Mantener tests verdes
|
|
39
|
+
|
|
40
|
+
## Design Patterns GoF en Ruby
|
|
41
|
+
|
|
42
|
+
### Creational Patterns
|
|
43
|
+
|
|
44
|
+
#### Factory Method
|
|
45
|
+
|
|
46
|
+
Crear objetos sin especificar su clase exacta.
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
# Problema: crear diferentes tipos de notificaciones
|
|
50
|
+
class NotificationFactory
|
|
51
|
+
def self.create(type, user, message)
|
|
52
|
+
case type
|
|
53
|
+
when :email
|
|
54
|
+
EmailNotification.new(user, message)
|
|
55
|
+
when :sms
|
|
56
|
+
SmsNotification.new(user, message)
|
|
57
|
+
when :push
|
|
58
|
+
PushNotification.new(user, message)
|
|
59
|
+
else
|
|
60
|
+
raise ArgumentError, "Unknown notification type: #{type}"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Uso
|
|
66
|
+
notification = NotificationFactory.create(:email, user, "Hola!")
|
|
67
|
+
notification.deliver
|
|
68
|
+
|
|
69
|
+
# Alternativa con registro dinamico
|
|
70
|
+
class NotificationFactory
|
|
71
|
+
@registry = {}
|
|
72
|
+
|
|
73
|
+
def self.register(type, klass)
|
|
74
|
+
@registry[type] = klass
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def self.create(type, *args)
|
|
78
|
+
@registry.fetch(type) { raise ArgumentError }.new(*args)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
NotificationFactory.register(:email, EmailNotification)
|
|
83
|
+
NotificationFactory.register(:sms, SmsNotification)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
#### Builder
|
|
87
|
+
|
|
88
|
+
Construir objetos complejos paso a paso.
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
class ReportBuilder
|
|
92
|
+
def initialize
|
|
93
|
+
@report = Report.new
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def add_header(title)
|
|
97
|
+
@report.header = Header.new(title, Date.today)
|
|
98
|
+
self
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def add_section(title, content)
|
|
102
|
+
@report.sections << Section.new(title, content)
|
|
103
|
+
self
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def add_chart(data)
|
|
107
|
+
@report.charts << Chart.new(data)
|
|
108
|
+
self
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def add_footer(text)
|
|
112
|
+
@report.footer = Footer.new(text)
|
|
113
|
+
self
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def build
|
|
117
|
+
validate!
|
|
118
|
+
@report
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
def validate!
|
|
124
|
+
raise "Header required" unless @report.header
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Uso con method chaining
|
|
129
|
+
report = ReportBuilder.new
|
|
130
|
+
.add_header("Ventas Q4")
|
|
131
|
+
.add_section("Resumen", summary_text)
|
|
132
|
+
.add_chart(sales_data)
|
|
133
|
+
.add_footer("Confidencial")
|
|
134
|
+
.build
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
#### Singleton
|
|
138
|
+
|
|
139
|
+
Una unica instancia global (usar con moderacion).
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
# En Ruby, preferir modulos o configuracion de Rails
|
|
143
|
+
# Si realmente necesitas Singleton:
|
|
144
|
+
require 'singleton'
|
|
145
|
+
|
|
146
|
+
class Configuration
|
|
147
|
+
include Singleton
|
|
148
|
+
|
|
149
|
+
attr_accessor :api_key, :timeout, :debug_mode
|
|
150
|
+
|
|
151
|
+
def initialize
|
|
152
|
+
@timeout = 30
|
|
153
|
+
@debug_mode = false
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Uso
|
|
158
|
+
config = Configuration.instance
|
|
159
|
+
config.api_key = "xxx"
|
|
160
|
+
|
|
161
|
+
# Alternativa Rails: usar credentials o config
|
|
162
|
+
# config/application.rb
|
|
163
|
+
config.x.api_key = "xxx"
|
|
164
|
+
|
|
165
|
+
# Uso
|
|
166
|
+
Rails.application.config.x.api_key
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Structural Patterns
|
|
170
|
+
|
|
171
|
+
#### Adapter
|
|
172
|
+
|
|
173
|
+
Convertir una interfaz en otra esperada.
|
|
174
|
+
|
|
175
|
+
```ruby
|
|
176
|
+
# Adaptador para diferentes payment gateways
|
|
177
|
+
class PaymentGatewayAdapter
|
|
178
|
+
def initialize(gateway)
|
|
179
|
+
@gateway = gateway
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def charge(amount, card_token)
|
|
183
|
+
raise NotImplementedError
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
class StripeAdapter < PaymentGatewayAdapter
|
|
188
|
+
def charge(amount, card_token)
|
|
189
|
+
result = @gateway.create_charge(
|
|
190
|
+
amount: amount,
|
|
191
|
+
source: card_token,
|
|
192
|
+
currency: 'usd'
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
PaymentResult.new(
|
|
196
|
+
success: result.paid,
|
|
197
|
+
transaction_id: result.id,
|
|
198
|
+
error: result.failure_message
|
|
199
|
+
)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
class PayPalAdapter < PaymentGatewayAdapter
|
|
204
|
+
def charge(amount, card_token)
|
|
205
|
+
result = @gateway.execute_payment(
|
|
206
|
+
total: amount / 100.0, # PayPal usa decimales
|
|
207
|
+
token: card_token
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
PaymentResult.new(
|
|
211
|
+
success: result.state == 'approved',
|
|
212
|
+
transaction_id: result.id,
|
|
213
|
+
error: result.error&.message
|
|
214
|
+
)
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Uso uniforme
|
|
219
|
+
adapter = StripeAdapter.new(Stripe::Charge)
|
|
220
|
+
result = adapter.charge(5000, "tok_xxx")
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
#### Decorator
|
|
224
|
+
|
|
225
|
+
Agregar responsabilidades dinamicamente.
|
|
226
|
+
|
|
227
|
+
```ruby
|
|
228
|
+
# Decorador para usuarios con comportamiento adicional
|
|
229
|
+
class UserDecorator
|
|
230
|
+
def initialize(user)
|
|
231
|
+
@user = user
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def method_missing(method, *args, &block)
|
|
235
|
+
@user.send(method, *args, &block)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def respond_to_missing?(method, include_private = false)
|
|
239
|
+
@user.respond_to?(method) || super
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
class PremiumUserDecorator < UserDecorator
|
|
244
|
+
def storage_limit
|
|
245
|
+
@user.storage_limit * 10
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def can_access_feature?(feature)
|
|
249
|
+
true # Premium tiene acceso a todo
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
class TrialUserDecorator < UserDecorator
|
|
254
|
+
def days_remaining
|
|
255
|
+
30 - (Date.today - @user.created_at.to_date).to_i
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def trial_expired?
|
|
259
|
+
days_remaining <= 0
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Uso
|
|
264
|
+
user = User.find(1)
|
|
265
|
+
premium_user = PremiumUserDecorator.new(user)
|
|
266
|
+
premium_user.storage_limit # 10x mas
|
|
267
|
+
|
|
268
|
+
# Con Draper gem (recomendado en Rails)
|
|
269
|
+
class UserDecorator < Draper::Decorator
|
|
270
|
+
delegate_all
|
|
271
|
+
|
|
272
|
+
def full_name
|
|
273
|
+
"#{object.first_name} #{object.last_name}"
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def member_since
|
|
277
|
+
object.created_at.strftime("%B %Y")
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
#### Facade
|
|
283
|
+
|
|
284
|
+
Interfaz simplificada para un sistema complejo.
|
|
285
|
+
|
|
286
|
+
```ruby
|
|
287
|
+
# Fachada para proceso de checkout
|
|
288
|
+
class CheckoutFacade
|
|
289
|
+
def initialize(cart, user)
|
|
290
|
+
@cart = cart
|
|
291
|
+
@user = user
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def process(payment_params)
|
|
295
|
+
# Coordina multiples servicios
|
|
296
|
+
validate_stock!
|
|
297
|
+
order = create_order
|
|
298
|
+
process_payment(order, payment_params)
|
|
299
|
+
send_confirmation(order)
|
|
300
|
+
update_inventory
|
|
301
|
+
|
|
302
|
+
order
|
|
303
|
+
rescue PaymentError => e
|
|
304
|
+
order&.mark_as_failed!
|
|
305
|
+
raise
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
private
|
|
309
|
+
|
|
310
|
+
def validate_stock!
|
|
311
|
+
StockValidator.new(@cart).validate!
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def create_order
|
|
315
|
+
OrderCreator.new(@cart, @user).call
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def process_payment(order, params)
|
|
319
|
+
PaymentProcessor.new(order, params).process!
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def send_confirmation(order)
|
|
323
|
+
OrderMailer.confirmation(order).deliver_later
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def update_inventory
|
|
327
|
+
InventoryUpdater.new(@cart).update!
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Uso simple
|
|
332
|
+
CheckoutFacade.new(cart, user).process(payment_params)
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### Behavioral Patterns
|
|
336
|
+
|
|
337
|
+
#### Observer
|
|
338
|
+
|
|
339
|
+
Notificar cambios a objetos interesados.
|
|
340
|
+
|
|
341
|
+
```ruby
|
|
342
|
+
# En Rails, usar callbacks y concerns
|
|
343
|
+
# O implementar manualmente:
|
|
344
|
+
|
|
345
|
+
module Observable
|
|
346
|
+
def add_observer(observer)
|
|
347
|
+
observers << observer
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def remove_observer(observer)
|
|
351
|
+
observers.delete(observer)
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def notify_observers(event, data = {})
|
|
355
|
+
observers.each { |o| o.update(event, data) }
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
private
|
|
359
|
+
|
|
360
|
+
def observers
|
|
361
|
+
@observers ||= []
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
class Order
|
|
366
|
+
include Observable
|
|
367
|
+
|
|
368
|
+
def complete!
|
|
369
|
+
update!(status: 'completed')
|
|
370
|
+
notify_observers(:order_completed, order: self)
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
class InventoryObserver
|
|
375
|
+
def update(event, data)
|
|
376
|
+
return unless event == :order_completed
|
|
377
|
+
InventoryUpdater.new(data[:order]).update!
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
class NotificationObserver
|
|
382
|
+
def update(event, data)
|
|
383
|
+
return unless event == :order_completed
|
|
384
|
+
OrderMailer.confirmation(data[:order]).deliver_later
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
# En Rails, preferir Active Support Notifications
|
|
389
|
+
ActiveSupport::Notifications.instrument('order.completed', order: order)
|
|
390
|
+
|
|
391
|
+
ActiveSupport::Notifications.subscribe('order.completed') do |*args|
|
|
392
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
393
|
+
# Procesar
|
|
394
|
+
end
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
#### Strategy
|
|
398
|
+
|
|
399
|
+
Intercambiar algoritmos en runtime.
|
|
400
|
+
|
|
401
|
+
```ruby
|
|
402
|
+
# Diferentes estrategias de descuento
|
|
403
|
+
class DiscountStrategy
|
|
404
|
+
def calculate(order)
|
|
405
|
+
raise NotImplementedError
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
class NoDiscount < DiscountStrategy
|
|
410
|
+
def calculate(order)
|
|
411
|
+
0
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
class PercentageDiscount < DiscountStrategy
|
|
416
|
+
def initialize(percent)
|
|
417
|
+
@percent = percent
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def calculate(order)
|
|
421
|
+
order.subtotal * (@percent / 100.0)
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
class FixedDiscount < DiscountStrategy
|
|
426
|
+
def initialize(amount)
|
|
427
|
+
@amount = amount
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def calculate(order)
|
|
431
|
+
[@amount, order.subtotal].min
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
class BuyOneGetOneFree < DiscountStrategy
|
|
436
|
+
def calculate(order)
|
|
437
|
+
cheapest_item = order.items.min_by(&:price)
|
|
438
|
+
cheapest_item&.price || 0
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
# Uso
|
|
443
|
+
class Order
|
|
444
|
+
attr_accessor :discount_strategy
|
|
445
|
+
|
|
446
|
+
def discount_strategy
|
|
447
|
+
@discount_strategy ||= NoDiscount.new
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def total
|
|
451
|
+
subtotal - discount_strategy.calculate(self)
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
order.discount_strategy = PercentageDiscount.new(20)
|
|
456
|
+
order.total
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
#### Command
|
|
460
|
+
|
|
461
|
+
Encapsular una accion como objeto.
|
|
462
|
+
|
|
463
|
+
```ruby
|
|
464
|
+
# Comando base
|
|
465
|
+
class Command
|
|
466
|
+
def execute
|
|
467
|
+
raise NotImplementedError
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
def undo
|
|
471
|
+
raise NotImplementedError
|
|
472
|
+
end
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
class AddItemCommand < Command
|
|
476
|
+
def initialize(cart, item)
|
|
477
|
+
@cart = cart
|
|
478
|
+
@item = item
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
def execute
|
|
482
|
+
@cart.items << @item
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
def undo
|
|
486
|
+
@cart.items.delete(@item)
|
|
487
|
+
end
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
class RemoveItemCommand < Command
|
|
491
|
+
def initialize(cart, item)
|
|
492
|
+
@cart = cart
|
|
493
|
+
@item = item
|
|
494
|
+
@index = nil
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
def execute
|
|
498
|
+
@index = @cart.items.index(@item)
|
|
499
|
+
@cart.items.delete(@item)
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
def undo
|
|
503
|
+
@cart.items.insert(@index, @item) if @index
|
|
504
|
+
end
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
# Invoker con historial
|
|
508
|
+
class CartCommandInvoker
|
|
509
|
+
def initialize
|
|
510
|
+
@history = []
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
def execute(command)
|
|
514
|
+
command.execute
|
|
515
|
+
@history.push(command)
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
def undo
|
|
519
|
+
command = @history.pop
|
|
520
|
+
command&.undo
|
|
521
|
+
end
|
|
522
|
+
end
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
#### State
|
|
526
|
+
|
|
527
|
+
Cambiar comportamiento segun estado interno.
|
|
528
|
+
|
|
529
|
+
```ruby
|
|
530
|
+
# Estados de una orden
|
|
531
|
+
class OrderState
|
|
532
|
+
def initialize(order)
|
|
533
|
+
@order = order
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
def confirm
|
|
537
|
+
raise InvalidTransition, "Cannot confirm from #{self.class}"
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
def ship
|
|
541
|
+
raise InvalidTransition, "Cannot ship from #{self.class}"
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
def deliver
|
|
545
|
+
raise InvalidTransition, "Cannot deliver from #{self.class}"
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
def cancel
|
|
549
|
+
raise InvalidTransition, "Cannot cancel from #{self.class}"
|
|
550
|
+
end
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
class PendingState < OrderState
|
|
554
|
+
def confirm
|
|
555
|
+
@order.transition_to(ConfirmedState)
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
def cancel
|
|
559
|
+
@order.transition_to(CancelledState)
|
|
560
|
+
end
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
class ConfirmedState < OrderState
|
|
564
|
+
def ship
|
|
565
|
+
@order.transition_to(ShippedState)
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
def cancel
|
|
569
|
+
@order.transition_to(CancelledState)
|
|
570
|
+
end
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
class ShippedState < OrderState
|
|
574
|
+
def deliver
|
|
575
|
+
@order.transition_to(DeliveredState)
|
|
576
|
+
end
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
class DeliveredState < OrderState
|
|
580
|
+
# Terminal state
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
class CancelledState < OrderState
|
|
584
|
+
# Terminal state
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
class Order < ApplicationRecord
|
|
588
|
+
def state
|
|
589
|
+
"#{status.camelize}State".constantize.new(self)
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
def transition_to(state_class)
|
|
593
|
+
update!(status: state_class.name.underscore.gsub('_state', ''))
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
def confirm!
|
|
597
|
+
state.confirm
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
def ship!
|
|
601
|
+
state.ship
|
|
602
|
+
end
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
# Con AASM gem (recomendado)
|
|
606
|
+
class Order < ApplicationRecord
|
|
607
|
+
include AASM
|
|
608
|
+
|
|
609
|
+
aasm column: :status do
|
|
610
|
+
state :pending, initial: true
|
|
611
|
+
state :confirmed, :shipped, :delivered, :cancelled
|
|
612
|
+
|
|
613
|
+
event :confirm do
|
|
614
|
+
transitions from: :pending, to: :confirmed
|
|
615
|
+
end
|
|
616
|
+
|
|
617
|
+
event :ship do
|
|
618
|
+
transitions from: :confirmed, to: :shipped
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
event :deliver do
|
|
622
|
+
transitions from: :shipped, to: :delivered
|
|
623
|
+
end
|
|
624
|
+
|
|
625
|
+
event :cancel do
|
|
626
|
+
transitions from: [:pending, :confirmed], to: :cancelled
|
|
627
|
+
end
|
|
628
|
+
end
|
|
629
|
+
end
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
#### Template Method
|
|
633
|
+
|
|
634
|
+
Definir esqueleto de algoritmo, permitiendo personalizacion.
|
|
635
|
+
|
|
636
|
+
```ruby
|
|
637
|
+
class DataExporter
|
|
638
|
+
def export(data)
|
|
639
|
+
validate(data)
|
|
640
|
+
formatted = format(data)
|
|
641
|
+
output = generate_output(formatted)
|
|
642
|
+
write_file(output)
|
|
643
|
+
end
|
|
644
|
+
|
|
645
|
+
protected
|
|
646
|
+
|
|
647
|
+
def validate(data)
|
|
648
|
+
raise ArgumentError, "Data required" if data.empty?
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
def format(data)
|
|
652
|
+
raise NotImplementedError
|
|
653
|
+
end
|
|
654
|
+
|
|
655
|
+
def generate_output(formatted)
|
|
656
|
+
raise NotImplementedError
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
def write_file(output)
|
|
660
|
+
File.write(filename, output)
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
def filename
|
|
664
|
+
raise NotImplementedError
|
|
665
|
+
end
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
class CsvExporter < DataExporter
|
|
669
|
+
protected
|
|
670
|
+
|
|
671
|
+
def format(data)
|
|
672
|
+
data.map(&:to_h)
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
def generate_output(formatted)
|
|
676
|
+
headers = formatted.first.keys
|
|
677
|
+
CSV.generate do |csv|
|
|
678
|
+
csv << headers
|
|
679
|
+
formatted.each { |row| csv << row.values }
|
|
680
|
+
end
|
|
681
|
+
end
|
|
682
|
+
|
|
683
|
+
def filename
|
|
684
|
+
"export.csv"
|
|
685
|
+
end
|
|
686
|
+
end
|
|
687
|
+
|
|
688
|
+
class JsonExporter < DataExporter
|
|
689
|
+
protected
|
|
690
|
+
|
|
691
|
+
def format(data)
|
|
692
|
+
data.as_json
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
def generate_output(formatted)
|
|
696
|
+
JSON.pretty_generate(formatted)
|
|
697
|
+
end
|
|
698
|
+
|
|
699
|
+
def filename
|
|
700
|
+
"export.json"
|
|
701
|
+
end
|
|
702
|
+
end
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
## Principios SOLID
|
|
706
|
+
|
|
707
|
+
### S - Single Responsibility Principle
|
|
708
|
+
|
|
709
|
+
Una clase debe tener una unica razon para cambiar.
|
|
710
|
+
|
|
711
|
+
```ruby
|
|
712
|
+
# MAL: User hace demasiadas cosas
|
|
713
|
+
class User < ApplicationRecord
|
|
714
|
+
def full_name
|
|
715
|
+
"#{first_name} #{last_name}"
|
|
716
|
+
end
|
|
717
|
+
|
|
718
|
+
def send_welcome_email
|
|
719
|
+
UserMailer.welcome(self).deliver_later
|
|
720
|
+
end
|
|
721
|
+
|
|
722
|
+
def generate_report
|
|
723
|
+
# Genera PDF...
|
|
724
|
+
end
|
|
725
|
+
|
|
726
|
+
def calculate_subscription_price
|
|
727
|
+
# Calcula precio...
|
|
728
|
+
end
|
|
729
|
+
end
|
|
730
|
+
|
|
731
|
+
# BIEN: Separar responsabilidades
|
|
732
|
+
class User < ApplicationRecord
|
|
733
|
+
def full_name
|
|
734
|
+
"#{first_name} #{last_name}"
|
|
735
|
+
end
|
|
736
|
+
end
|
|
737
|
+
|
|
738
|
+
class UserNotifier
|
|
739
|
+
def initialize(user)
|
|
740
|
+
@user = user
|
|
741
|
+
end
|
|
742
|
+
|
|
743
|
+
def send_welcome_email
|
|
744
|
+
UserMailer.welcome(@user).deliver_later
|
|
745
|
+
end
|
|
746
|
+
end
|
|
747
|
+
|
|
748
|
+
class UserReportGenerator
|
|
749
|
+
def initialize(user)
|
|
750
|
+
@user = user
|
|
751
|
+
end
|
|
752
|
+
|
|
753
|
+
def generate
|
|
754
|
+
# Genera PDF...
|
|
755
|
+
end
|
|
756
|
+
end
|
|
757
|
+
|
|
758
|
+
class SubscriptionPriceCalculator
|
|
759
|
+
def initialize(user)
|
|
760
|
+
@user = user
|
|
761
|
+
end
|
|
762
|
+
|
|
763
|
+
def calculate
|
|
764
|
+
# Calcula precio...
|
|
765
|
+
end
|
|
766
|
+
end
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
### O - Open/Closed Principle
|
|
770
|
+
|
|
771
|
+
Abierto para extension, cerrado para modificacion.
|
|
772
|
+
|
|
773
|
+
```ruby
|
|
774
|
+
# MAL: Modificar clase existente para agregar tipo
|
|
775
|
+
class PaymentProcessor
|
|
776
|
+
def process(payment)
|
|
777
|
+
case payment.type
|
|
778
|
+
when 'credit_card'
|
|
779
|
+
process_credit_card(payment)
|
|
780
|
+
when 'paypal'
|
|
781
|
+
process_paypal(payment)
|
|
782
|
+
when 'bitcoin' # Hay que modificar cada vez
|
|
783
|
+
process_bitcoin(payment)
|
|
784
|
+
end
|
|
785
|
+
end
|
|
786
|
+
end
|
|
787
|
+
|
|
788
|
+
# BIEN: Extension sin modificacion
|
|
789
|
+
class PaymentProcessor
|
|
790
|
+
def process(payment)
|
|
791
|
+
handler = payment_handler_for(payment)
|
|
792
|
+
handler.process(payment)
|
|
793
|
+
end
|
|
794
|
+
|
|
795
|
+
private
|
|
796
|
+
|
|
797
|
+
def payment_handler_for(payment)
|
|
798
|
+
"#{payment.type.camelize}Handler".constantize.new
|
|
799
|
+
end
|
|
800
|
+
end
|
|
801
|
+
|
|
802
|
+
class CreditCardHandler
|
|
803
|
+
def process(payment)
|
|
804
|
+
# ...
|
|
805
|
+
end
|
|
806
|
+
end
|
|
807
|
+
|
|
808
|
+
class PaypalHandler
|
|
809
|
+
def process(payment)
|
|
810
|
+
# ...
|
|
811
|
+
end
|
|
812
|
+
end
|
|
813
|
+
|
|
814
|
+
# Agregar nuevo tipo sin modificar codigo existente
|
|
815
|
+
class BitcoinHandler
|
|
816
|
+
def process(payment)
|
|
817
|
+
# ...
|
|
818
|
+
end
|
|
819
|
+
end
|
|
820
|
+
```
|
|
821
|
+
|
|
822
|
+
### L - Liskov Substitution Principle
|
|
823
|
+
|
|
824
|
+
Subclases deben ser sustituibles por su clase base.
|
|
825
|
+
|
|
826
|
+
```ruby
|
|
827
|
+
# MAL: Viola LSP
|
|
828
|
+
class Rectangle
|
|
829
|
+
attr_accessor :width, :height
|
|
830
|
+
|
|
831
|
+
def area
|
|
832
|
+
width * height
|
|
833
|
+
end
|
|
834
|
+
end
|
|
835
|
+
|
|
836
|
+
class Square < Rectangle
|
|
837
|
+
def width=(value)
|
|
838
|
+
@width = @height = value
|
|
839
|
+
end
|
|
840
|
+
|
|
841
|
+
def height=(value)
|
|
842
|
+
@height = @width = value
|
|
843
|
+
end
|
|
844
|
+
end
|
|
845
|
+
|
|
846
|
+
# Esto rompe expectativas:
|
|
847
|
+
shape = Square.new
|
|
848
|
+
shape.width = 5
|
|
849
|
+
shape.height = 10
|
|
850
|
+
shape.area # 100, no 50!
|
|
851
|
+
|
|
852
|
+
# BIEN: Usar composicion o interfaces
|
|
853
|
+
class Shape
|
|
854
|
+
def area
|
|
855
|
+
raise NotImplementedError
|
|
856
|
+
end
|
|
857
|
+
end
|
|
858
|
+
|
|
859
|
+
class Rectangle < Shape
|
|
860
|
+
def initialize(width, height)
|
|
861
|
+
@width = width
|
|
862
|
+
@height = height
|
|
863
|
+
end
|
|
864
|
+
|
|
865
|
+
def area
|
|
866
|
+
@width * @height
|
|
867
|
+
end
|
|
868
|
+
end
|
|
869
|
+
|
|
870
|
+
class Square < Shape
|
|
871
|
+
def initialize(side)
|
|
872
|
+
@side = side
|
|
873
|
+
end
|
|
874
|
+
|
|
875
|
+
def area
|
|
876
|
+
@side * @side
|
|
877
|
+
end
|
|
878
|
+
end
|
|
879
|
+
```
|
|
880
|
+
|
|
881
|
+
### I - Interface Segregation Principle
|
|
882
|
+
|
|
883
|
+
Interfaces pequenas y especificas.
|
|
884
|
+
|
|
885
|
+
```ruby
|
|
886
|
+
# MAL: Interface demasiado grande
|
|
887
|
+
module Worker
|
|
888
|
+
def work
|
|
889
|
+
end
|
|
890
|
+
|
|
891
|
+
def eat
|
|
892
|
+
end
|
|
893
|
+
|
|
894
|
+
def sleep
|
|
895
|
+
end
|
|
896
|
+
end
|
|
897
|
+
|
|
898
|
+
class Robot
|
|
899
|
+
include Worker
|
|
900
|
+
|
|
901
|
+
def work
|
|
902
|
+
# OK
|
|
903
|
+
end
|
|
904
|
+
|
|
905
|
+
def eat
|
|
906
|
+
raise "Robots don't eat!" # Viola ISP
|
|
907
|
+
end
|
|
908
|
+
|
|
909
|
+
def sleep
|
|
910
|
+
raise "Robots don't sleep!" # Viola ISP
|
|
911
|
+
end
|
|
912
|
+
end
|
|
913
|
+
|
|
914
|
+
# BIEN: Interfaces segregadas
|
|
915
|
+
module Workable
|
|
916
|
+
def work
|
|
917
|
+
raise NotImplementedError
|
|
918
|
+
end
|
|
919
|
+
end
|
|
920
|
+
|
|
921
|
+
module Feedable
|
|
922
|
+
def eat
|
|
923
|
+
raise NotImplementedError
|
|
924
|
+
end
|
|
925
|
+
end
|
|
926
|
+
|
|
927
|
+
module Sleepable
|
|
928
|
+
def sleep
|
|
929
|
+
raise NotImplementedError
|
|
930
|
+
end
|
|
931
|
+
end
|
|
932
|
+
|
|
933
|
+
class Human
|
|
934
|
+
include Workable
|
|
935
|
+
include Feedable
|
|
936
|
+
include Sleepable
|
|
937
|
+
|
|
938
|
+
def work
|
|
939
|
+
# ...
|
|
940
|
+
end
|
|
941
|
+
|
|
942
|
+
def eat
|
|
943
|
+
# ...
|
|
944
|
+
end
|
|
945
|
+
|
|
946
|
+
def sleep
|
|
947
|
+
# ...
|
|
948
|
+
end
|
|
949
|
+
end
|
|
950
|
+
|
|
951
|
+
class Robot
|
|
952
|
+
include Workable
|
|
953
|
+
|
|
954
|
+
def work
|
|
955
|
+
# ...
|
|
956
|
+
end
|
|
957
|
+
end
|
|
958
|
+
```
|
|
959
|
+
|
|
960
|
+
### D - Dependency Inversion Principle
|
|
961
|
+
|
|
962
|
+
Depender de abstracciones, no de implementaciones concretas.
|
|
963
|
+
|
|
964
|
+
```ruby
|
|
965
|
+
# MAL: Dependencia directa
|
|
966
|
+
class OrderProcessor
|
|
967
|
+
def initialize(order)
|
|
968
|
+
@order = order
|
|
969
|
+
@payment_gateway = StripeGateway.new # Acoplado a Stripe
|
|
970
|
+
@notifier = EmailNotifier.new # Acoplado a email
|
|
971
|
+
end
|
|
972
|
+
|
|
973
|
+
def process
|
|
974
|
+
@payment_gateway.charge(@order.total)
|
|
975
|
+
@notifier.notify(@order)
|
|
976
|
+
end
|
|
977
|
+
end
|
|
978
|
+
|
|
979
|
+
# BIEN: Inyeccion de dependencias
|
|
980
|
+
class OrderProcessor
|
|
981
|
+
def initialize(order, payment_gateway:, notifier:)
|
|
982
|
+
@order = order
|
|
983
|
+
@payment_gateway = payment_gateway
|
|
984
|
+
@notifier = notifier
|
|
985
|
+
end
|
|
986
|
+
|
|
987
|
+
def process
|
|
988
|
+
@payment_gateway.charge(@order.total)
|
|
989
|
+
@notifier.notify(@order)
|
|
990
|
+
end
|
|
991
|
+
end
|
|
992
|
+
|
|
993
|
+
# Uso con diferentes implementaciones
|
|
994
|
+
OrderProcessor.new(
|
|
995
|
+
order,
|
|
996
|
+
payment_gateway: StripeGateway.new,
|
|
997
|
+
notifier: EmailNotifier.new
|
|
998
|
+
).process
|
|
999
|
+
|
|
1000
|
+
OrderProcessor.new(
|
|
1001
|
+
order,
|
|
1002
|
+
payment_gateway: PaypalGateway.new,
|
|
1003
|
+
notifier: SmsNotifier.new
|
|
1004
|
+
).process
|
|
1005
|
+
|
|
1006
|
+
# En tests, usar mocks facilmente
|
|
1007
|
+
OrderProcessor.new(
|
|
1008
|
+
order,
|
|
1009
|
+
payment_gateway: MockPaymentGateway.new,
|
|
1010
|
+
notifier: MockNotifier.new
|
|
1011
|
+
).process
|
|
1012
|
+
```
|
|
1013
|
+
|
|
1014
|
+
## Patrones en Rails
|
|
1015
|
+
|
|
1016
|
+
### Service Objects
|
|
1017
|
+
|
|
1018
|
+
Encapsular logica de negocio compleja.
|
|
1019
|
+
|
|
1020
|
+
```ruby
|
|
1021
|
+
# app/services/users/registration_service.rb
|
|
1022
|
+
module Users
|
|
1023
|
+
class RegistrationService
|
|
1024
|
+
def initialize(params)
|
|
1025
|
+
@params = params
|
|
1026
|
+
end
|
|
1027
|
+
|
|
1028
|
+
def call
|
|
1029
|
+
user = User.new(@params)
|
|
1030
|
+
|
|
1031
|
+
ActiveRecord::Base.transaction do
|
|
1032
|
+
user.save!
|
|
1033
|
+
create_welcome_notification(user)
|
|
1034
|
+
send_welcome_email(user)
|
|
1035
|
+
track_signup(user)
|
|
1036
|
+
end
|
|
1037
|
+
|
|
1038
|
+
Result.success(user)
|
|
1039
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
1040
|
+
Result.failure(e.record.errors)
|
|
1041
|
+
end
|
|
1042
|
+
|
|
1043
|
+
private
|
|
1044
|
+
|
|
1045
|
+
def create_welcome_notification(user)
|
|
1046
|
+
user.notifications.create!(
|
|
1047
|
+
type: 'welcome',
|
|
1048
|
+
message: "Bienvenido #{user.name}!"
|
|
1049
|
+
)
|
|
1050
|
+
end
|
|
1051
|
+
|
|
1052
|
+
def send_welcome_email(user)
|
|
1053
|
+
UserMailer.welcome(user).deliver_later
|
|
1054
|
+
end
|
|
1055
|
+
|
|
1056
|
+
def track_signup(user)
|
|
1057
|
+
Analytics.track('user_signed_up', user_id: user.id)
|
|
1058
|
+
end
|
|
1059
|
+
end
|
|
1060
|
+
end
|
|
1061
|
+
|
|
1062
|
+
# Uso
|
|
1063
|
+
result = Users::RegistrationService.new(user_params).call
|
|
1064
|
+
if result.success?
|
|
1065
|
+
redirect_to result.value
|
|
1066
|
+
else
|
|
1067
|
+
render :new
|
|
1068
|
+
end
|
|
1069
|
+
```
|
|
1070
|
+
|
|
1071
|
+
### Form Objects
|
|
1072
|
+
|
|
1073
|
+
Validaciones y logica de formularios complejos.
|
|
1074
|
+
|
|
1075
|
+
```ruby
|
|
1076
|
+
# app/forms/registration_form.rb
|
|
1077
|
+
class RegistrationForm
|
|
1078
|
+
include ActiveModel::Model
|
|
1079
|
+
include ActiveModel::Attributes
|
|
1080
|
+
|
|
1081
|
+
attribute :email, :string
|
|
1082
|
+
attribute :password, :string
|
|
1083
|
+
attribute :password_confirmation, :string
|
|
1084
|
+
attribute :terms_accepted, :boolean
|
|
1085
|
+
attribute :company_name, :string
|
|
1086
|
+
attribute :company_size, :string
|
|
1087
|
+
|
|
1088
|
+
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
1089
|
+
validates :password, presence: true, length: { minimum: 8 }
|
|
1090
|
+
validates :password_confirmation, presence: true
|
|
1091
|
+
validates :terms_accepted, acceptance: true
|
|
1092
|
+
validates :company_name, presence: true
|
|
1093
|
+
|
|
1094
|
+
validate :passwords_match
|
|
1095
|
+
|
|
1096
|
+
def save
|
|
1097
|
+
return false unless valid?
|
|
1098
|
+
|
|
1099
|
+
ActiveRecord::Base.transaction do
|
|
1100
|
+
@user = User.create!(email: email, password: password)
|
|
1101
|
+
@company = Company.create!(name: company_name, size: company_size)
|
|
1102
|
+
@user.update!(company: @company)
|
|
1103
|
+
end
|
|
1104
|
+
|
|
1105
|
+
true
|
|
1106
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
1107
|
+
errors.merge!(e.record.errors)
|
|
1108
|
+
false
|
|
1109
|
+
end
|
|
1110
|
+
|
|
1111
|
+
attr_reader :user, :company
|
|
1112
|
+
|
|
1113
|
+
private
|
|
1114
|
+
|
|
1115
|
+
def passwords_match
|
|
1116
|
+
return if password == password_confirmation
|
|
1117
|
+
errors.add(:password_confirmation, "doesn't match password")
|
|
1118
|
+
end
|
|
1119
|
+
end
|
|
1120
|
+
|
|
1121
|
+
# En controller
|
|
1122
|
+
def create
|
|
1123
|
+
@form = RegistrationForm.new(registration_params)
|
|
1124
|
+
if @form.save
|
|
1125
|
+
sign_in(@form.user)
|
|
1126
|
+
redirect_to dashboard_path
|
|
1127
|
+
else
|
|
1128
|
+
render :new
|
|
1129
|
+
end
|
|
1130
|
+
end
|
|
1131
|
+
```
|
|
1132
|
+
|
|
1133
|
+
### Query Objects
|
|
1134
|
+
|
|
1135
|
+
Encapsular queries complejas.
|
|
1136
|
+
|
|
1137
|
+
```ruby
|
|
1138
|
+
# app/queries/users/active_subscribers_query.rb
|
|
1139
|
+
module Users
|
|
1140
|
+
class ActiveSubscribersQuery
|
|
1141
|
+
def initialize(relation = User.all)
|
|
1142
|
+
@relation = relation
|
|
1143
|
+
end
|
|
1144
|
+
|
|
1145
|
+
def call(since: 30.days.ago, plan: nil)
|
|
1146
|
+
@relation
|
|
1147
|
+
.joins(:subscription)
|
|
1148
|
+
.where(subscriptions: { status: 'active' })
|
|
1149
|
+
.where('subscriptions.started_at >= ?', since)
|
|
1150
|
+
.then { |r| plan ? r.where(subscriptions: { plan: plan }) : r }
|
|
1151
|
+
.order(created_at: :desc)
|
|
1152
|
+
end
|
|
1153
|
+
end
|
|
1154
|
+
end
|
|
1155
|
+
|
|
1156
|
+
# Uso
|
|
1157
|
+
Users::ActiveSubscribersQuery.new.call(since: 7.days.ago, plan: 'premium')
|
|
1158
|
+
|
|
1159
|
+
# Composable
|
|
1160
|
+
Users::ActiveSubscribersQuery.new(User.where(country: 'ES')).call
|
|
1161
|
+
```
|
|
1162
|
+
|
|
1163
|
+
### Presenters / View Objects
|
|
1164
|
+
|
|
1165
|
+
Logica de presentacion fuera de modelos y vistas.
|
|
1166
|
+
|
|
1167
|
+
```ruby
|
|
1168
|
+
# app/presenters/user_presenter.rb
|
|
1169
|
+
class UserPresenter
|
|
1170
|
+
def initialize(user)
|
|
1171
|
+
@user = user
|
|
1172
|
+
end
|
|
1173
|
+
|
|
1174
|
+
def display_name
|
|
1175
|
+
@user.full_name.presence || @user.email.split('@').first
|
|
1176
|
+
end
|
|
1177
|
+
|
|
1178
|
+
def avatar_url(size: :medium)
|
|
1179
|
+
if @user.avatar.attached?
|
|
1180
|
+
Rails.application.routes.url_helpers.url_for(@user.avatar.variant(resize_to_limit: dimensions_for(size)))
|
|
1181
|
+
else
|
|
1182
|
+
gravatar_url(size)
|
|
1183
|
+
end
|
|
1184
|
+
end
|
|
1185
|
+
|
|
1186
|
+
def membership_badge
|
|
1187
|
+
case @user.subscription&.plan
|
|
1188
|
+
when 'premium'
|
|
1189
|
+
{ text: 'Premium', color: 'gold' }
|
|
1190
|
+
when 'pro'
|
|
1191
|
+
{ text: 'Pro', color: 'blue' }
|
|
1192
|
+
else
|
|
1193
|
+
{ text: 'Free', color: 'gray' }
|
|
1194
|
+
end
|
|
1195
|
+
end
|
|
1196
|
+
|
|
1197
|
+
def stats_summary
|
|
1198
|
+
{
|
|
1199
|
+
posts: @user.posts.count,
|
|
1200
|
+
followers: @user.followers.count,
|
|
1201
|
+
following: @user.following.count
|
|
1202
|
+
}
|
|
1203
|
+
end
|
|
1204
|
+
|
|
1205
|
+
private
|
|
1206
|
+
|
|
1207
|
+
def dimensions_for(size)
|
|
1208
|
+
{ small: [50, 50], medium: [100, 100], large: [200, 200] }[size]
|
|
1209
|
+
end
|
|
1210
|
+
|
|
1211
|
+
def gravatar_url(size)
|
|
1212
|
+
hash = Digest::MD5.hexdigest(@user.email.downcase)
|
|
1213
|
+
"https://www.gravatar.com/avatar/#{hash}?s=#{dimensions_for(size).first}&d=identicon"
|
|
1214
|
+
end
|
|
1215
|
+
end
|
|
1216
|
+
|
|
1217
|
+
# Uso en vista
|
|
1218
|
+
<% presenter = UserPresenter.new(@user) %>
|
|
1219
|
+
<img src="<%= presenter.avatar_url %>" alt="<%= presenter.display_name %>">
|
|
1220
|
+
```
|
|
1221
|
+
|
|
1222
|
+
## Anti-patterns a Evitar
|
|
1223
|
+
|
|
1224
|
+
### God Object / God Class
|
|
1225
|
+
|
|
1226
|
+
```ruby
|
|
1227
|
+
# MAL: Clase que hace demasiado
|
|
1228
|
+
class User < ApplicationRecord
|
|
1229
|
+
# 50+ metodos
|
|
1230
|
+
# Maneja auth, perfil, subscripcion, notificaciones, reportes...
|
|
1231
|
+
end
|
|
1232
|
+
|
|
1233
|
+
# BIEN: Separar en concerns y servicios
|
|
1234
|
+
class User < ApplicationRecord
|
|
1235
|
+
include Authenticatable
|
|
1236
|
+
include Subscribable
|
|
1237
|
+
include Notifiable
|
|
1238
|
+
|
|
1239
|
+
# Solo metodos core del modelo
|
|
1240
|
+
end
|
|
1241
|
+
```
|
|
1242
|
+
|
|
1243
|
+
### Shotgun Surgery
|
|
1244
|
+
|
|
1245
|
+
```ruby
|
|
1246
|
+
# MAL: Cambiar un concepto requiere modificar muchos archivos
|
|
1247
|
+
# Si cambias como se calcula el precio, tienes que tocar:
|
|
1248
|
+
# - Order model
|
|
1249
|
+
# - Cart controller
|
|
1250
|
+
# - Invoice model
|
|
1251
|
+
# - API serializer
|
|
1252
|
+
# - 5 vistas diferentes
|
|
1253
|
+
|
|
1254
|
+
# BIEN: Encapsular en un lugar
|
|
1255
|
+
class PriceCalculator
|
|
1256
|
+
def calculate(items)
|
|
1257
|
+
# Toda la logica de precio aqui
|
|
1258
|
+
end
|
|
1259
|
+
end
|
|
1260
|
+
|
|
1261
|
+
# Todos los lugares usan este calculador
|
|
1262
|
+
```
|
|
1263
|
+
|
|
1264
|
+
### Feature Envy
|
|
1265
|
+
|
|
1266
|
+
```ruby
|
|
1267
|
+
# MAL: Metodo que usa mas datos de otro objeto que del propio
|
|
1268
|
+
class Order
|
|
1269
|
+
def shipping_label
|
|
1270
|
+
"#{customer.name}\n#{customer.address.street}\n#{customer.address.city}, #{customer.address.zip}"
|
|
1271
|
+
end
|
|
1272
|
+
end
|
|
1273
|
+
|
|
1274
|
+
# BIEN: Mover al objeto correcto
|
|
1275
|
+
class Customer
|
|
1276
|
+
def shipping_label
|
|
1277
|
+
"#{name}\n#{address.full_address}"
|
|
1278
|
+
end
|
|
1279
|
+
end
|
|
1280
|
+
|
|
1281
|
+
class Address
|
|
1282
|
+
def full_address
|
|
1283
|
+
"#{street}\n#{city}, #{zip}"
|
|
1284
|
+
end
|
|
1285
|
+
end
|
|
1286
|
+
|
|
1287
|
+
class Order
|
|
1288
|
+
delegate :shipping_label, to: :customer
|
|
1289
|
+
end
|
|
1290
|
+
```
|
|
1291
|
+
|
|
1292
|
+
### Primitive Obsession
|
|
1293
|
+
|
|
1294
|
+
```ruby
|
|
1295
|
+
# MAL: Usar primitivos para conceptos de dominio
|
|
1296
|
+
class User
|
|
1297
|
+
# phone es string: "1234567890"
|
|
1298
|
+
# money es float: 99.99
|
|
1299
|
+
end
|
|
1300
|
+
|
|
1301
|
+
# BIEN: Value Objects
|
|
1302
|
+
class PhoneNumber
|
|
1303
|
+
def initialize(number)
|
|
1304
|
+
@number = number.gsub(/\D/, '')
|
|
1305
|
+
end
|
|
1306
|
+
|
|
1307
|
+
def formatted
|
|
1308
|
+
"(#{@number[0..2]}) #{@number[3..5]}-#{@number[6..9]}"
|
|
1309
|
+
end
|
|
1310
|
+
|
|
1311
|
+
def to_s
|
|
1312
|
+
@number
|
|
1313
|
+
end
|
|
1314
|
+
end
|
|
1315
|
+
|
|
1316
|
+
class Money
|
|
1317
|
+
def initialize(cents, currency = 'USD')
|
|
1318
|
+
@cents = cents
|
|
1319
|
+
@currency = currency
|
|
1320
|
+
end
|
|
1321
|
+
|
|
1322
|
+
def to_f
|
|
1323
|
+
@cents / 100.0
|
|
1324
|
+
end
|
|
1325
|
+
|
|
1326
|
+
def +(other)
|
|
1327
|
+
raise "Currency mismatch" unless @currency == other.currency
|
|
1328
|
+
Money.new(@cents + other.cents, @currency)
|
|
1329
|
+
end
|
|
1330
|
+
end
|
|
1331
|
+
```
|
|
1332
|
+
|
|
1333
|
+
## Refactoring Patterns
|
|
1334
|
+
|
|
1335
|
+
### Extract Method
|
|
1336
|
+
|
|
1337
|
+
```ruby
|
|
1338
|
+
# Antes
|
|
1339
|
+
def print_invoice
|
|
1340
|
+
puts "*** Invoice ***"
|
|
1341
|
+
puts "Customer: #{@customer.name}"
|
|
1342
|
+
puts "Address: #{@customer.address}"
|
|
1343
|
+
|
|
1344
|
+
@items.each do |item|
|
|
1345
|
+
puts "#{item.name}: $#{item.price}"
|
|
1346
|
+
end
|
|
1347
|
+
|
|
1348
|
+
total = @items.sum(&:price)
|
|
1349
|
+
puts "Total: $#{total}"
|
|
1350
|
+
end
|
|
1351
|
+
|
|
1352
|
+
# Despues
|
|
1353
|
+
def print_invoice
|
|
1354
|
+
print_header
|
|
1355
|
+
print_customer_info
|
|
1356
|
+
print_items
|
|
1357
|
+
print_total
|
|
1358
|
+
end
|
|
1359
|
+
|
|
1360
|
+
private
|
|
1361
|
+
|
|
1362
|
+
def print_header
|
|
1363
|
+
puts "*** Invoice ***"
|
|
1364
|
+
end
|
|
1365
|
+
|
|
1366
|
+
def print_customer_info
|
|
1367
|
+
puts "Customer: #{@customer.name}"
|
|
1368
|
+
puts "Address: #{@customer.address}"
|
|
1369
|
+
end
|
|
1370
|
+
|
|
1371
|
+
def print_items
|
|
1372
|
+
@items.each do |item|
|
|
1373
|
+
puts "#{item.name}: $#{item.price}"
|
|
1374
|
+
end
|
|
1375
|
+
end
|
|
1376
|
+
|
|
1377
|
+
def print_total
|
|
1378
|
+
puts "Total: $#{calculate_total}"
|
|
1379
|
+
end
|
|
1380
|
+
|
|
1381
|
+
def calculate_total
|
|
1382
|
+
@items.sum(&:price)
|
|
1383
|
+
end
|
|
1384
|
+
```
|
|
1385
|
+
|
|
1386
|
+
### Replace Conditional with Polymorphism
|
|
1387
|
+
|
|
1388
|
+
```ruby
|
|
1389
|
+
# Antes
|
|
1390
|
+
def calculate_area(shape)
|
|
1391
|
+
case shape.type
|
|
1392
|
+
when 'circle'
|
|
1393
|
+
Math::PI * shape.radius ** 2
|
|
1394
|
+
when 'rectangle'
|
|
1395
|
+
shape.width * shape.height
|
|
1396
|
+
when 'triangle'
|
|
1397
|
+
0.5 * shape.base * shape.height
|
|
1398
|
+
end
|
|
1399
|
+
end
|
|
1400
|
+
|
|
1401
|
+
# Despues
|
|
1402
|
+
class Circle
|
|
1403
|
+
def area
|
|
1404
|
+
Math::PI * radius ** 2
|
|
1405
|
+
end
|
|
1406
|
+
end
|
|
1407
|
+
|
|
1408
|
+
class Rectangle
|
|
1409
|
+
def area
|
|
1410
|
+
width * height
|
|
1411
|
+
end
|
|
1412
|
+
end
|
|
1413
|
+
|
|
1414
|
+
class Triangle
|
|
1415
|
+
def area
|
|
1416
|
+
0.5 * base * height
|
|
1417
|
+
end
|
|
1418
|
+
end
|
|
1419
|
+
```
|
|
1420
|
+
|
|
1421
|
+
## Skills que utilizo
|
|
1422
|
+
|
|
1423
|
+
- `design-patterns` - Patrones GoF
|
|
1424
|
+
- `solid-principles` - Principios SOLID
|
|
1425
|
+
- `refactoring` - Tecnicas de refactoring
|
|
1426
|
+
- `ddd` - Domain-Driven Design
|
|
1427
|
+
|
|
1428
|
+
## Checklist de Arquitectura
|
|
1429
|
+
|
|
1430
|
+
### Antes de implementar
|
|
1431
|
+
|
|
1432
|
+
- [ ] Identificar responsabilidades claras
|
|
1433
|
+
- [ ] Considerar patrones aplicables
|
|
1434
|
+
- [ ] Evaluar necesidad de abstraccion
|
|
1435
|
+
- [ ] Revisar dependencias
|
|
1436
|
+
|
|
1437
|
+
### Durante implementacion
|
|
1438
|
+
|
|
1439
|
+
- [ ] Clases con responsabilidad unica
|
|
1440
|
+
- [ ] Dependencias inyectadas
|
|
1441
|
+
- [ ] Tests unitarios escritos
|
|
1442
|
+
- [ ] Nombres descriptivos
|
|
1443
|
+
|
|
1444
|
+
### Code review
|
|
1445
|
+
|
|
1446
|
+
- [ ] No hay god objects
|
|
1447
|
+
- [ ] No hay feature envy
|
|
1448
|
+
- [ ] Logica de negocio en servicios
|
|
1449
|
+
- [ ] Queries complejas encapsuladas
|
|
1450
|
+
- [ ] Value objects para conceptos de dominio
|