rails-hyperdrive-layered-rails 0.1.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.
Files changed (62) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +30 -0
  3. data/README.md +89 -0
  4. data/UPSTREAM +4 -0
  5. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/SKILL.md.erb +315 -0
  6. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/examples/authorization-to-policy.md +79 -0
  7. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/examples/callbacks-to-service.md +72 -0
  8. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/examples/complex-input-to-form-object.md +119 -0
  9. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/examples/current-from-model.md +63 -0
  10. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/examples/god-object-decomposition.md +81 -0
  11. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/examples/implicit-to-explicit-state-machine.md +77 -0
  12. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/examples/query-to-query-object.md +63 -0
  13. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/examples/view-logic-to-presenter.md +60 -0
  14. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/anti-patterns/callbacks.md +114 -0
  15. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/anti-patterns/concerns.md +92 -0
  16. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/anti-patterns/helpers.md +66 -0
  17. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/anti-patterns/jobs.md +71 -0
  18. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/anti-patterns/layer-violations.md +176 -0
  19. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/anti-patterns/service-objects.md +131 -0
  20. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/anti-patterns/testing.md +37 -0
  21. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/core/architecture-layers.md +204 -0
  22. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/core/extraction-signals.md +314 -0
  23. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/core/specification-test.md +229 -0
  24. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/gems/action-policy.md +437 -0
  25. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/gems/active-agent.md +275 -0
  26. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/gems/active-delivery.md +221 -0
  27. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/gems/active-job-performs.md +171 -0
  28. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/gems/alba.md +257 -0
  29. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/gems/anyway-config.md +212 -0
  30. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/gems/rubanok.md +246 -0
  31. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/gems/view-component.md +223 -0
  32. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/gems/workflow.md +305 -0
  33. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/collaborator-objects.md +209 -0
  34. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/concerns.md +381 -0
  35. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/filter-objects.md +222 -0
  36. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/form-objects.md +267 -0
  37. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/policy-objects.md +298 -0
  38. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/presenters.md +257 -0
  39. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/query-objects.md +208 -0
  40. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/repositories.md +382 -0
  41. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/serializers.md +256 -0
  42. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/service-objects.md +170 -0
  43. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/state-machines.md +368 -0
  44. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/value-objects.md +267 -0
  45. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/topics/ai-integration.md +372 -0
  46. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/topics/authorization.md +398 -0
  47. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/topics/callbacks.md +336 -0
  48. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/topics/configuration.md +383 -0
  49. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/topics/current-attributes.md +307 -0
  50. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/topics/instrumentation.md +386 -0
  51. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/topics/notifications.md +373 -0
  52. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/topics/view-components.md +454 -0
  53. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/workflows/analyze-callbacks.md +255 -0
  54. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/workflows/analyze-gods.md +279 -0
  55. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/workflows/analyze-services.md +1444 -0
  56. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/workflows/analyze.md +376 -0
  57. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/workflows/plan.md +249 -0
  58. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/workflows/review.md +473 -0
  59. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/workflows/spec-test.md +282 -0
  60. data/lib/rails-hyperdrive-layered-rails/version.rb +3 -0
  61. data/lib/rails-hyperdrive-layered-rails.rb +7 -0
  62. metadata +106 -0
@@ -0,0 +1,119 @@
1
+ # Form Object for Complex Input
2
+
3
+ Replace a controller orchestrating multiple models with a form object that owns the registration workflow.
4
+
5
+ ## Contents
6
+
7
+ - Before (fat controller)
8
+ - After (form object)
9
+
10
+ ## Before
11
+
12
+ ```ruby
13
+ class RegistrationsController < ApplicationController
14
+ def create
15
+ @user = User.new(user_params)
16
+ @user.profile = Profile.new(profile_params)
17
+
18
+ if @user.email.end_with?("@company.com")
19
+ @user.role = :employee
20
+ @user.team = Team.find_by(department: profile_params[:department])
21
+ end
22
+
23
+ if @user.save
24
+ UserMailer.welcome(@user).deliver_later
25
+ redirect_to dashboard_path
26
+ else
27
+ render :new, status: :unprocessable_entity
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def user_params
34
+ params.require(:user).permit(:email, :password, :name)
35
+ end
36
+
37
+ def profile_params
38
+ params.require(:profile).permit(:bio, :department, :avatar)
39
+ end
40
+ end
41
+ ```
42
+
43
+ ## After
44
+
45
+ ```ruby
46
+ # app/forms/registration_form.rb
47
+ class RegistrationForm < ApplicationForm
48
+ attribute :email, :string
49
+ attribute :password, :string
50
+ attribute :name, :string
51
+ attribute :bio, :string
52
+ attribute :department, :string
53
+ attribute :avatar
54
+
55
+ validates :email, :password, :name, presence: true
56
+ validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
57
+ validates :password, length: { minimum: 8 }
58
+
59
+ def save
60
+ return false unless valid?
61
+
62
+ ApplicationRecord.transaction do
63
+ create_user
64
+ create_profile
65
+ assign_team if company_email?
66
+ end
67
+
68
+ true
69
+ rescue ActiveRecord::RecordInvalid => e
70
+ errors.merge!(e.record.errors)
71
+ false
72
+ end
73
+
74
+ attr_reader :user
75
+
76
+ private
77
+
78
+ def create_user
79
+ @user = User.create!(
80
+ email: email,
81
+ password: password,
82
+ name: name,
83
+ role: company_email? ? :employee : :member
84
+ )
85
+ end
86
+
87
+ def create_profile
88
+ @user.create_profile!(bio: bio, department: department, avatar: avatar)
89
+ end
90
+
91
+ def assign_team
92
+ @user.update!(team: Team.find_by(department: department))
93
+ end
94
+
95
+ def company_email?
96
+ email.end_with?("@company.com")
97
+ end
98
+ end
99
+
100
+ # app/controllers/registrations_controller.rb
101
+ class RegistrationsController < ApplicationController
102
+ def create
103
+ @form = RegistrationForm.new(registration_params)
104
+
105
+ if @form.save
106
+ UserMailer.welcome(@form.user).deliver_later
107
+ redirect_to dashboard_path
108
+ else
109
+ render :new, status: :unprocessable_entity
110
+ end
111
+ end
112
+
113
+ private
114
+
115
+ def registration_params
116
+ params.require(:registration).permit(:email, :password, :name, :bio, :department, :avatar)
117
+ end
118
+ end
119
+ ```
@@ -0,0 +1,63 @@
1
+ # Extract Current from Model
2
+
3
+ Remove `Current.user` access from a domain model by moving authorization to a policy and ownership to the controller.
4
+
5
+ ## Before
6
+
7
+ ```ruby
8
+ class Post < ApplicationRecord
9
+ belongs_to :author, class_name: "User"
10
+
11
+ before_validation :set_author, on: :create
12
+
13
+ def can_edit?
14
+ author == Current.user || Current.user&.admin?
15
+ end
16
+
17
+ private
18
+
19
+ def set_author
20
+ self.author = Current.user
21
+ end
22
+ end
23
+ ```
24
+
25
+ ## After
26
+
27
+ ```ruby
28
+ # app/models/post.rb
29
+ class Post < ApplicationRecord
30
+ belongs_to :author, class_name: "User"
31
+ # No Current access - domain is context-agnostic
32
+ end
33
+
34
+ # app/policies/post_policy.rb
35
+ class PostPolicy < ApplicationPolicy
36
+ def edit?
37
+ owner? || admin?
38
+ end
39
+
40
+ private
41
+
42
+ def owner?
43
+ record.author_id == user.id
44
+ end
45
+
46
+ def admin?
47
+ user.admin?
48
+ end
49
+ end
50
+
51
+ # app/controllers/posts_controller.rb
52
+ class PostsController < ApplicationController
53
+ def create
54
+ @post = current_user.posts.build(post_params)
55
+
56
+ if @post.save
57
+ redirect_to @post
58
+ else
59
+ render :new, status: :unprocessable_entity
60
+ end
61
+ end
62
+ end
63
+ ```
@@ -0,0 +1,81 @@
1
+ # Extract God Object with Associated Objects
2
+
3
+ Decompose a sprawling `User` model into focused collaborators using the associated-objects pattern.
4
+
5
+ ## Before
6
+
7
+ ```ruby
8
+ class User < ApplicationRecord
9
+ # 500+ lines with multiple responsibilities
10
+
11
+ # Authentication
12
+ has_secure_password
13
+ def generate_token; end
14
+ def verify_token; end
15
+ def reset_password!; end
16
+
17
+ # Billing
18
+ def subscribe!(plan); end
19
+ def cancel_subscription!; end
20
+ def update_payment_method(token); end
21
+ def invoice_history; end
22
+
23
+ # Notifications
24
+ def notify!(message); end
25
+ def notification_preferences; end
26
+ def unread_notifications_count; end
27
+ end
28
+ ```
29
+
30
+ ## After
31
+
32
+ ```ruby
33
+ # app/models/user.rb
34
+ class User < ApplicationRecord
35
+ has_secure_password
36
+
37
+ has_object :billing
38
+ has_object :notification_settings
39
+
40
+ # Only core user identity logic
41
+ end
42
+
43
+ # app/models/user/billing.rb
44
+ class User::Billing
45
+ attr_reader :user
46
+
47
+ def initialize(user)
48
+ @user = user
49
+ end
50
+
51
+ def subscribe!(plan)
52
+ Stripe::Subscription.create(customer: stripe_customer_id, items: [{ price: plan.stripe_price_id }])
53
+ user.update!(plan: plan, subscribed_at: Time.current)
54
+ end
55
+
56
+ def cancel_subscription!
57
+ Stripe::Subscription.cancel(subscription_id)
58
+ user.update!(plan: nil, subscription_cancelled_at: Time.current)
59
+ end
60
+
61
+ def invoice_history
62
+ Stripe::Invoice.list(customer: stripe_customer_id)
63
+ end
64
+
65
+ private
66
+
67
+ def stripe_customer_id
68
+ user.stripe_customer_id
69
+ end
70
+ end
71
+
72
+ # app/models/user/notification_settings.rb
73
+ class User::NotificationSettings
74
+ include ActiveModel::Model
75
+ include ActiveModel::Attributes
76
+
77
+ attribute :email_enabled, :boolean, default: true
78
+ attribute :push_enabled, :boolean, default: true
79
+ attribute :digest_frequency, :string, default: "daily"
80
+ end
81
+ ```
@@ -0,0 +1,77 @@
1
+ # Replace Implicit State Machine
2
+
3
+ Replace ad-hoc timestamp checks with an explicit state machine using the `workflow` gem.
4
+
5
+ ## Before
6
+
7
+ ```ruby
8
+ class Order < ApplicationRecord
9
+ def status
10
+ return :cancelled if cancelled_at?
11
+ return :delivered if delivered_at?
12
+ return :shipped if shipped_at?
13
+ return :paid if paid_at?
14
+ :pending
15
+ end
16
+
17
+ def can_ship?
18
+ paid_at? && !shipped_at? && !cancelled_at?
19
+ end
20
+
21
+ def ship!
22
+ return false unless can_ship?
23
+ update!(shipped_at: Time.current)
24
+ OrderMailer.shipped(self).deliver_later
25
+ end
26
+ end
27
+ ```
28
+
29
+ ## After
30
+
31
+ ```ruby
32
+ # app/models/order.rb
33
+ class Order < ApplicationRecord
34
+ include WorkflowActiverecord
35
+
36
+ workflow_column :status
37
+
38
+ workflow do
39
+ state :pending do
40
+ event :pay, transitions_to: :paid
41
+ event :cancel, transitions_to: :cancelled
42
+ end
43
+
44
+ state :paid do
45
+ event :ship, transitions_to: :shipped
46
+ event :cancel, transitions_to: :cancelled
47
+ end
48
+
49
+ state :shipped do
50
+ event :deliver, transitions_to: :delivered
51
+ end
52
+
53
+ state :delivered
54
+ state :cancelled
55
+ end
56
+
57
+ def ship
58
+ self.shipped_at = Time.current
59
+ end
60
+
61
+ def deliver
62
+ self.delivered_at = Time.current
63
+ end
64
+
65
+ def cancel
66
+ self.cancelled_at = Time.current
67
+ end
68
+ end
69
+
70
+ # Notifications in service
71
+ class Orders::Ship < ApplicationService
72
+ def call(order)
73
+ order.ship!
74
+ OrderMailer.shipped(order).deliver_later
75
+ end
76
+ end
77
+ ```
@@ -0,0 +1,63 @@
1
+ # Extract Query Logic to Query Object
2
+
3
+ Pull a long Active Record chain out of a controller into a composable query object.
4
+
5
+ ## Before
6
+
7
+ ```ruby
8
+ class ReportsController < ApplicationController
9
+ def sales
10
+ @orders = Order
11
+ .joins(:customer, :line_items)
12
+ .where(status: :completed)
13
+ .where(created_at: params[:start_date]..params[:end_date])
14
+ .where(customers: { region: params[:region] }) if params[:region].present?
15
+ .group("DATE(orders.created_at)")
16
+ .select(
17
+ "DATE(orders.created_at) as date",
18
+ "COUNT(DISTINCT orders.id) as order_count",
19
+ "SUM(line_items.quantity * line_items.price) as revenue"
20
+ )
21
+ .order("date DESC")
22
+ end
23
+ end
24
+ ```
25
+
26
+ ## After
27
+
28
+ ```ruby
29
+ # app/queries/sales_report_query.rb
30
+ class SalesReportQuery < ApplicationQuery
31
+ relation { Order.joins(:customer, :line_items).where(status: :completed) }
32
+
33
+ def by_date_range(start_date, end_date)
34
+ relation.where(created_at: start_date..end_date)
35
+ end
36
+
37
+ def by_region(region)
38
+ return relation if region.blank?
39
+ relation.where(customers: { region: region })
40
+ end
41
+
42
+ def daily_summary
43
+ relation
44
+ .group("DATE(orders.created_at)")
45
+ .select(
46
+ "DATE(orders.created_at) as date",
47
+ "COUNT(DISTINCT orders.id) as order_count",
48
+ "SUM(line_items.quantity * line_items.price) as revenue"
49
+ )
50
+ .order("date DESC")
51
+ end
52
+ end
53
+
54
+ # app/controllers/reports_controller.rb
55
+ class ReportsController < ApplicationController
56
+ def sales
57
+ @orders = SalesReportQuery.new
58
+ .by_date_range(params[:start_date], params[:end_date])
59
+ .by_region(params[:region])
60
+ .daily_summary
61
+ end
62
+ end
63
+ ```
@@ -0,0 +1,60 @@
1
+ # Extract View Logic to Presenter
2
+
3
+ Move complex view computations out of templates into a presenter wrapping the model.
4
+
5
+ ## Before
6
+
7
+ ```erb
8
+ <%# app/views/users/show.html.erb %>
9
+ <div class="profile">
10
+ <span class="badge <%= user.status == 'active' ? 'badge-success' : user.status == 'pending' ? 'badge-warning' : 'badge-secondary' %>">
11
+ <%= user.status.titleize %>
12
+ </span>
13
+
14
+ <h1>
15
+ <%= user.name.squish.split(/\s/).then { |parts| parts[0..-2].map { _1[0] + "." }.join + parts.last } %>
16
+ </h1>
17
+
18
+ <p>Member since <%= user.created_at.strftime("%B %Y") %></p>
19
+ </div>
20
+ ```
21
+
22
+ ## After
23
+
24
+ ```ruby
25
+ # app/presenters/user_presenter.rb
26
+ class UserPresenter < SimpleDelegator
27
+ def status_badge_class
28
+ case status
29
+ when "active" then "badge-success"
30
+ when "pending" then "badge-warning"
31
+ else "badge-secondary"
32
+ end
33
+ end
34
+
35
+ def short_name
36
+ name.squish.split(/\s/).then do |parts|
37
+ parts[0..-2].map { _1[0] + "." }.join + parts.last
38
+ end
39
+ end
40
+
41
+ def member_since
42
+ created_at.strftime("%B %Y")
43
+ end
44
+ end
45
+ ```
46
+
47
+ ```erb
48
+ <%# app/views/users/show.html.erb %>
49
+ <% present(@user) do |user| %>
50
+ <div class="profile">
51
+ <span class="badge <%= user.status_badge_class %>">
52
+ <%= user.status.titleize %>
53
+ </span>
54
+
55
+ <h1><%= user.short_name %></h1>
56
+
57
+ <p>Member since <%= user.member_since %></p>
58
+ </div>
59
+ <% end %>
60
+ ```
@@ -0,0 +1,114 @@
1
+ # Callback Anti-Patterns
2
+
3
+ Misuses of Active Record callbacks that signal a layering problem.
4
+
5
+ ## Contents
6
+
7
+ - Operation Callbacks
8
+ - Skip Callback Anti-Pattern
9
+ - Callback Control Flags
10
+
11
+ ## Operation Callbacks
12
+
13
+ **Problem:** Business process steps disguised as callbacks.
14
+
15
+ ```ruby
16
+ # BAD
17
+ class User < ApplicationRecord
18
+ after_create :generate_initial_project, unless: :admin?
19
+ after_commit :send_welcome_email, on: :create
20
+ after_commit :sync_with_crm
21
+ after_commit :track_signup_analytics
22
+ end
23
+ ```
24
+
25
+ **Fix:** Extract to controller, service, or events.
26
+
27
+ ```ruby
28
+ # GOOD - Controller handles side effects
29
+ class UsersController < ApplicationController
30
+ def create
31
+ @user = User.create!(user_params)
32
+ UserMailer.welcome(@user).deliver_later
33
+ CrmSyncJob.perform_later(@user.id)
34
+ AnalyticsService.track_signup(@user)
35
+ end
36
+ end
37
+
38
+ # OR use events
39
+ class User < ApplicationRecord
40
+ after_commit on: :create do
41
+ UserCreatedEvent.publish(user: self)
42
+ end
43
+ end
44
+ ```
45
+
46
+ ## Skip Callback Anti-Pattern
47
+
48
+ **Problem:** `skip_before_action` creates hidden dependencies.
49
+
50
+ ```ruby
51
+ # BAD
52
+ class ApplicationController < ActionController::Base
53
+ before_action :authenticate_user!
54
+ before_action :set_current_tenant
55
+ end
56
+
57
+ class PublicController < ApplicationController
58
+ skip_before_action :authenticate_user!
59
+ # Now depends on parent's internal callback order
60
+ end
61
+ ```
62
+
63
+ **Fix:** Use explicit inheritance or composition.
64
+
65
+ ```ruby
66
+ # GOOD
67
+ class AuthenticatedController < ApplicationController
68
+ before_action :authenticate_user!
69
+ end
70
+
71
+ class PublicController < ApplicationController
72
+ # No authentication needed
73
+ end
74
+
75
+ class DashboardController < AuthenticatedController
76
+ # Inherits authentication
77
+ end
78
+ ```
79
+
80
+ ## Callback Control Flags
81
+
82
+ **Problem:** Virtual attributes to skip callbacks.
83
+
84
+ ```ruby
85
+ # BAD
86
+ class User < ApplicationRecord
87
+ attr_accessor :skip_welcome_email, :skip_crm_sync
88
+
89
+ after_commit :send_welcome_email, unless: :skip_welcome_email
90
+ after_commit :sync_with_crm, unless: :skip_crm_sync
91
+ end
92
+
93
+ # Usage
94
+ user = User.new(params)
95
+ user.skip_welcome_email = true
96
+ user.save!
97
+ ```
98
+
99
+ **Fix:** Extract callbacks, call explicitly when needed.
100
+
101
+ ```ruby
102
+ # GOOD
103
+ class User < ApplicationRecord
104
+ # No operation callbacks
105
+ end
106
+
107
+ class RegisterUserService
108
+ def call(user_params, send_welcome: true)
109
+ user = User.create!(user_params)
110
+ UserMailer.welcome(user).deliver_later if send_welcome
111
+ user
112
+ end
113
+ end
114
+ ```
@@ -0,0 +1,92 @@
1
+ # Concern Anti-Patterns
2
+
3
+ Misuses of `ActiveSupport::Concern` that hide structural problems.
4
+
5
+ ## Code-Slicing Concerns
6
+
7
+ **Problem:** Splitting model by Rails artifact type, not behavior.
8
+
9
+ ```ruby
10
+ # BAD - Just groups related code, not a behavior
11
+ module Contactable
12
+ extend ActiveSupport::Concern
13
+
14
+ included do
15
+ validates :email, presence: true
16
+ validates :phone, format: { with: PHONE_REGEX }
17
+ before_save :normalize_phone
18
+ end
19
+
20
+ def full_contact_info
21
+ "#{email} / #{phone}"
22
+ end
23
+ end
24
+ ```
25
+
26
+ **Test:** If removing this concern breaks unrelated tests, it's code-slicing.
27
+
28
+ **Fix:** Keep in model or extract to value object.
29
+
30
+ ```ruby
31
+ # GOOD - Behavioral concern (can be tested in isolation)
32
+ module Publishable
33
+ extend ActiveSupport::Concern
34
+
35
+ included do
36
+ scope :published, -> { where.not(published_at: nil) }
37
+ scope :draft, -> { where(published_at: nil) }
38
+ end
39
+
40
+ def published? = published_at.present?
41
+
42
+ def publish!
43
+ update!(published_at: Time.current)
44
+ end
45
+ end
46
+ ```
47
+
48
+ ## Overgrown Concerns
49
+
50
+ **Problem:** Concern has too many responsibilities.
51
+
52
+ ```ruby
53
+ # BAD - Too much in one concern
54
+ module WithMedia
55
+ extend ActiveSupport::Concern
56
+
57
+ included do
58
+ has_many_attached :images
59
+ has_many_attached :videos
60
+ has_one_attached :thumbnail
61
+
62
+ after_commit :process_images
63
+ after_commit :generate_thumbnail
64
+ after_commit :transcode_videos
65
+ end
66
+
67
+ def images_ready? = # ...
68
+ def videos_ready? = # ...
69
+ def thumbnail_url = # ...
70
+ def processing_status = # ...
71
+ # 50 more methods...
72
+ end
73
+ ```
74
+
75
+ **Fix:** Extract to delegate object or separate concerns.
76
+
77
+ ```ruby
78
+ # GOOD
79
+ class Post::MediaProcessor
80
+ def initialize(post)
81
+ @post = post
82
+ end
83
+
84
+ def process_all
85
+ process_images
86
+ generate_thumbnail
87
+ transcode_videos
88
+ end
89
+
90
+ # ...
91
+ end
92
+ ```