35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English 35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English
Rails Concerns: Wanneer Ze Code Opschonen en Wanneer Ze Complexiteit Verbergen

Rails Concerns: Wanneer Ze Code Opschonen en Wanneer Ze Complexiteit Verbergen

Roger Heykoop
Ruby on Rails
Een praktische gids voor Rails ActiveSupport::Concern met echte voorbeelden van goed en slecht gebruik. Leer wanneer concerns duplicatie verminderen, wanneer ze verborgen afhankelijkheden creëren, en welke alternatieven beter werken.

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_at kolom 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
#rails 8 #architectuur #concerns #refactoring #code-organisatie
R

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 Touch

Share this article

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