Ruby Science
Mixin
Inheritance is a common method of reuse in object-oriented software. Ruby supports single inheritance using subclasses and multiple inheritance using Mixins. Mixins can be used to package common helpers or provide a common public interface.
However, mixins have some drawbacks:
- They use the same namespace as classes they’re mixed into, which can cause naming conflicts.
- Although they have access to instance variables from classes they’re mixed into, mixins can’t easily accept initializer arguments, so they can’t have their own state.
- They inflate the number of methods available in a class.
- They’re not easy to add and remove at runtime.
- They’re difficult to test in isolation, since they can’t be instantiated.
Symptoms
- Methods in mixins that accept the same parameters over and over.
- Methods in mixins that don’t reference the state of the class they’re mixed into.
- Business logic that can’t be used without using the mixin.
- Classes that have few public methods except those from a mixin.
- Inverting dependencies is difficult because mixins can’t accept parameters.
Example
In our example application, users can invite their friends by email to take surveys. If an invited email matches an existing user, a private message will be created. Otherwise, a message is sent to that email address with a link.
The logic to generate the invitation message is the same regardless of the delivery mechanism, so this behavior is encapsulated in a mixin:
# app/models/inviter.rb
module Inviter
extend ActiveSupport::Concern
do
included include AbstractController::Rendering
include Rails.application.routes.url_helpers
self.view_paths = 'app/views'
self.default_url_options = ActionMailer::Base.default_url_options
end
private
def render_message_body
template: 'invitations/message'
render end
end
Each delivery strategy mixes in Inviter
and calls
render_message_body
:
# app/models/message_inviter.rb
class MessageInviter < AbstractController::Base
include Inviter
def initialize(invitation, recipient)
@invitation = invitation
@recipient = recipient
end
def deliver
Message.create!(
recipient: @recipient,
sender: @invitation.sender,
body: render_message_body
)end
end
Although the mixin does a good job of preventing duplicated code, it’s difficult to test or understand in isolation, it obfuscates the inviter classes, and it tightly couples the inviter classes to a particular message body implementation.
Solutions
- Extract class to liberate business logic trapped in mixins.
- Replace mixin with composition to improve testability, flexibility and readability.
Prevention
Mixins are a form of inheritance. By following composition over inheritance, you’ll be less likely to introduce mixins.
Reserve mixins for reusable framework code like common associations and callbacks, and you’ll end up with a more flexible and comprehensible system.
Ruby Science
The canonical reference for writing fantastic Rails applications from authors who have created hundreds.