Rails Concerns: Wanneer Ze Code Opschonen en Wanneer Ze Complexiteit Verbergen
Rails concerns hebben een slechte reputatie die ze maar half verdienen. De ActiveSupport::Concern module wordt meegeleverd met elke Rails-app, de generators plaatsen concerns/ directories in zowel app/models/ als app/controllers/, en toch discussiëren ervaren Rails-developers regelmatig of ze überhaupt in productiecode thuishoren.
Het antwoord is niet zwart-wit. Concerns lossen specifieke problemen goed op en creëren andere problemen als ze verkeerd worden ingezet. Na het onderhouden van Rails-applicaties van 20-model startups tot 400-model enterprise codebases, heb ik concrete regels opgesteld voor wanneer concerns hun plek verdienen en wanneer ze vervangen moeten worden.
Wat een concern eigenlijk doet
Een concern is een Ruby module met wat syntactische suiker van ActiveSupport::Concern. Die suiker is minimaal maar betekenisvol:
# app/models/concerns/searchable.rb
module Searchable
extend ActiveSupport::Concern
included do
scope :search, ->(query) {
where("title ILIKE :q OR body ILIKE :q", q: "%#{query}%")
}
end
class_methods do
def most_searched_fields
%i[title body]
end
end
def search_summary
"#{title}: #{body.truncate(100)}"
end
end
Zonder ActiveSupport::Concern zou je dit schrijven met self.included, def self.extended en een geneste ClassMethods module. De concern-versie leest schoner, maar het echte voordeel is het included block — dat draait in de context van de includerende klasse, zodat scope, validates, has_many en andere class-level macro’s gewoon werken.
Het andere dat concerns afhandelen is dependency-resolutie. Als concern B afhankelijk is van concern A, lost Rails de dependency-graph voor je op — zonder ActiveSupport::Concern doet de include-volgorde ertoe en krijg je cryptische fouten.
Wanneer concerns goed werken
1. Gedeeld gedrag over niet-gerelateerde modellen
Het standaard voorbeeld. Je hebt Article, Comment en Product modellen die allemaal soft-delete functionaliteit nodig hebben:
# app/models/concerns/soft_deletable.rb
module SoftDeletable
extend ActiveSupport::Concern
included do
scope :kept, -> { where(deleted_at: nil) }
scope :discarded, -> { where.not(deleted_at: nil) }
default_scope { kept }
end
def discard
update_column(:deleted_at, Time.current)
end
def undiscard
update_column(:deleted_at, nil)
end
def discarded?
deleted_at.present?
end
end
Dit is een schoon gebruik omdat:
- Het gedrag oprecht gedeeld is (niet geforceerd passend gemaakt)
- De concern op zichzelf staat — het reikt niet in de internals van het host-model
- Elk model met een
deleted_atkolom kan het includen en het werkt gewoon - Testen is eenvoudig: test de concern één keer met een dummy model
2. Framework-integratie boilerplate extraheren
Wanneer meerdere modellen dezelfde ActiveRecord configuratie nodig hebben voor een gem of service, houdt een concern de duplicatie beheersbaar.
3. Query-interfaces groeperen
Wanneer een model veel scopes heeft gerelateerd aan een specifiek domein-concept, houdt een concern het modelbestand leesbaar. Denk aan een Publishable concern die published, draft, scheduled scopes bundelt met bijbehorende instance methods.
Wanneer concerns fout gaan
1. Het “god model in vermomming” patroon
Dit is het meest voorkomende misbruik. Een User model bereikt 500 regels, dus splitst iemand het in concerns:
class User < ApplicationRecord
include Authenticatable
include Profileable
include Billable
include Notifiable
include Searchable
include Reportable
end
Het modelbestand is nu 6 regels. De complexiteit is niet afgenomen — ze is verspreid over 6 bestanden. Elke concern hangt waarschijnlijk af van attributen en methoden in andere concerns of het model zelf. Je kunt Billable niet begrijpen zonder ook User, Authenticatable en waarschijnlijk Notifiable te lezen.
Dit is horizontale decompositie: een klasse in stukken snijden langs willekeurige lijnen. Het aantal regels per bestand daalt, maar de cognitieve belasting stijgt.
2. Concerns die afhangen van host-model internals
Als een concern company, admin? of andere methoden aanroept die het zelf niet definieert, heeft het onzichtbare afhankelijkheden. Het kan niet geïsoleerd getest worden, niet hergebruikt worden, en wijzigingen aan het host-model kunnen het op niet-voor-de-hand-liggende manieren breken.
3. Callback-ketens over meerdere concerns
Wanneer meerdere concerns callbacks registreren, wordt de uitvoeringsvolgorde moeilijk te voorspellen. Callbacks draaien in include-volgorde, maar dat is niet duidelijk uit het lezen van één enkel bestand. Ik heb productie-bugs gezien die dagen kostten om op te sporen omdat een callback in de ene concern stilzwijgend afhing van side effects van een callback in een andere.
Alternatieven als concerns niet het juiste gereedschap zijn
Service objects voor businesslogica
Wanneer een concern eigenlijk een bedrijfsproces verbergt, extraheer het naar een service object. De dependencies zijn zichtbaar in de constructor, testen is eenvoudig. Ik behandelde service object patronen in detail in een eerdere post.
Gewone Ruby modules voor gedeelde utilities
Niet alles heeft ActiveSupport::Concern nodig. Als je utility-methoden deelt die geen included blocks nodig hebben, is een gewone module eenvoudiger en heeft geen framework-afhankelijkheid.
Compositie met delegatie
Voor het “vet model gesplitst in concerns” probleem, overweeg een apart object te extraheren:
class User < ApplicationRecord
def billing
@billing ||= UserBilling.new(self)
end
end
Nu leest user.billing.current_plan duidelijk, is UserBilling geïsoleerd testbaar, en is de afhankelijkheid van User expliciet. Deze aanpak combineert goed met de delegatie-patronen die beschikbaar zijn in Ruby.
Mijn regels voor concerns in productiecode
Gebruik een concern als:
- Het gedrag oprecht gedeeld is over 2+ niet-gerelateerde modellen
- De concern op zichzelf staat (geen onzichtbare afhankelijkheden van de host)
- Het framework-integraties wrapt die gewone modules niet aankunnen
- Je het kunt testen met een dummy model
Gebruik iets anders als:
- Je een enkel model splitst om bestandsgrootte te verminderen (compositie of service objects)
- De concern in de associaties of methoden van het host-model reikt (service object)
- Het gedrag een bedrijfsproces met sequentiële stappen voorstelt (service object)
- De gedeelde code geen ActiveRecord macro’s nodig heeft (gewone module)
Debugging van concern-problemen in Rails 8
# Bekijk alle ancestors (concerns verschijnen in de keten)
User.ancestors
# Check waar een methode gedefinieerd is
User.instance_method(:search_summary).source_location
# Lijst alle callbacks van een specifiek type
User._create_callbacks.map { |cb| [cb.filter, cb.kind] }
De source_location truc is het onthouden waard. Wanneer een model 5 concerns includet en je moet vinden waar een methode leeft, is dit sneller dan grep.
FAQ
Hoeveel concerns mag een enkel model includen?
Er is geen harde limiet, maar als een model meer dan 3-4 concerns includet, is dat een code smell. Het betekent meestal dat het model te veel verantwoordelijkheden heeft en structurele refactoring nodig heeft — niet meer bestandssplitsing.
Moet ik controller concerns in app/controllers/concerns plaatsen?
Ja, maar dezelfde regels gelden. Authenticatie-checks, paginatie-setup en API-response formatting zijn goede controller concern kandidaten. Businesslogica in controller concerns is een teken dat de logica in een service object of model thuishoort. De custom Rack middleware aanpak is vaak beter voor cross-cutting HTTP concerns.
Beïnvloeden concerns Rails autoloading of performance?
In Rails 8 met Zeitwerk worden concerns in conventionele directories autoloaded zoals elk ander Ruby-bestand. Er is geen performance-penalty van het gebruiken van concerns versus inline code — Ruby’s method dispatch maakt het niet uit of een methode direct gedefinieerd is of ingemixed vanuit een module.
Hoe test ik een concern zonder koppeling aan een specifiek model?
Maak een tijdelijk testmodel met een anonieme klasse:
RSpec.describe SoftDeletable do
let(:model_class) do
Class.new(ApplicationRecord) do
self.table_name = "articles"
include SoftDeletable
end
end
it "soft deletes een record" do
record = model_class.create!(title: "Test", deleted_at: nil)
record.discard
expect(record.deleted_at).to be_present
end
end
About the Author
Roger Heykoop is een senior Ruby on Rails ontwikkelaar met 19+ jaar Rails ervaring en 35+ jaar ervaring in softwareontwikkeling. Hij is gespecialiseerd in Rails modernisering, performance optimalisatie, en AI-ondersteunde ontwikkeling.
Get in TouchRelated Articles
Need Expert Rails Development?
Let's discuss how we can help you build or modernize your Rails application with 19+ years of expertise
Schedule a Free Consultation