Skip to content

Instantly share code, notes, and snippets.

@damncabbage
Last active August 29, 2015 13:56
Show Gist options
  • Save damncabbage/8877353 to your computer and use it in GitHub Desktop.
Save damncabbage/8877353 to your computer and use it in GitHub Desktop.
Mixins: Not Very Encapsulatey.

(In reply to https://twitter.com/gregmcintyre/status/432012568966619137)

Mixins (included Modules) all share one object's state and method set; you need to know everything about every module a class includes or extends before touching it, or risk breaking your code unintentionally. This is practically antithetical to Encapsulation, which hides state behind defined interfaces.

A good quote from a good article:

Using mixins like this is akin to “cleaning” a messy room by dumping the clutter into six separate junk drawers and slamming them shut. Sure, it looks cleaner at the surface, but the junk drawers actually make it harder to identify and implement the decompositions and extractions necessary to clarify the domain model.

module Searching
def search(value)
@cache ||= ExpensiveSearchOperation.do(value)
# ...
end
end
module Formatting
def format(value)
@cache ||= ExpensiveFormattingOperation.do(value)
# ...
end
end
class Post
include Searching
include Formatting
end
module Titling
attr_reader :title
def title=(value)
@title = normalize(value)
end
private
def normalize(title)
title.split(' ').select {|w| w.capitalize! || w }.join(' ');
end
end
module Slugging
attr_reader :slug
def slug=(value)
@slug = normalize(value)
end
private
def normalize(slug)
slug.downcase.gsub /[^a-z0-9]/, '-'
end
end
class Post
include Titling
include Slugging
end
p = Post.new
p.title = "Foo Bar"
p.slug = "Foo Bar"
puts p.title # => "foo-bar"
puts p.slug # => "foo-bar"
module Slugging
def self.included(klass)
klass.instance_eval do
validates :slug, :presence => true, :uniqueness => true
before_create do
self.slug = calculate_slug(self.title) # Permanent, set once at creation
end
end
end
def calculate_slug(title)
title.downcase.gsub /[^a-z0-9]/, '-'
end
end
module SKU
def generate_sku
save! unless persisted? # Needs an ID first
"#{id}#{title.upcase.gsub(/[^A-Z]/, '')[0..10]}"
end
end
class Product < ActiveRecord::Base
include Slugging
include SKU
end
# In a controller somewhere...
p = Product.new
p.generate_sku # Saves, calls before_create from Slugging, explodes with validation error.
p.title = "Red Shoe" # ... Never makes it this far.
# What happened? Something in a module messed with the state of the object in
# a way you didn't expect; the Product got saved earlier than expected, which
# started a chain of actions that resulted in an invalid Product and an exception.
#
# "Just don't do that, then! Show some discipline!"
# Sure, but then why expose yourself to the possibility of it happening at all?
# Hive off the slugging and the SKU building into their own classes, keeping them
# isolated so they can't mess the object itself.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment