Mixin to Object
It’s easy for code to become deeply dependent on mixin modules in Ruby. Especially in the context of Rails view code calling out to helper modules, disentangling dependencies can feel like untying a thick knot. In this episode you’ll learn a Ruby trick that will enable you to treat mixin modules as separate objects, and draw clear lines of encapsulation in the process.
Video transcript & code
In a typical Ruby on Rails application, we often create helper modules for methods that format domain objects for display. For instance, here's a helper module with some methods for pretty-printing playing card objects.
# encoding: UTF-8
module CardHelper
RANK_NAMES = [nil, "Ace", *(2..10), "Jack", "Queen", "King"]
SUIT_SYMBOLS = {
"Hearts" => "♠",
"Clubs" => "♣",
"Spades" => "♠",
"Diamonds" => "♦",
}
def pretty_card(card)
symbol = SUIT_SYMBOLS[card.suit]
rank_char = RANK_NAMES[card.rank].to_s[0]
"#{symbol}#{rank_char}"
end
def long_card(card)
"#{RANK_NAMES[card.rank]} of #{card.suit}"
end
end
Let's imagine a web action in which we might use this helper. Maybe we are writing a blackjack game, and we need an action for when the player asks the dealer to hit them.
For this action, we'll need a deck , and a draw pile. will also need a hand for the player , which will start off with two cards.
We take the next card off draw pile , and move it to the hand .
In a real rails application, we would be putting all this code in a controller action. For the sake of this example, we'll keep things simple and just write some code to print to the console.
require "./cards"
require "./helpers"
@deck = Deck.new
@draw_pile = @deck.stack(:draw_pile)
@hand = @deck.stack(:hand)
@draw_pile.deal(2, to: @hand)
@new_card = @draw_pile.first
@new_card.move_to(@hand)
Now that we've performed the hit, and we have instance variables for all of the domain objects involved, we need to render a view to show the user what happened.
We will need the ERB library to render our view template.
To simulate the way that helpers are typically ubiquitously available in rails views, we'll include the CardHelper
module at the top level.
Let's write our view template, making use of the card helpers to display both the new card drawn and the new state of the player's hand.
When we load and run this template, we get to see the card helpers in action.
require "./controller"
require "erb"
include CardHelper
hit_view = <<'EOF'
New card: <%= long_card(@new_card) %>
(Now in your hand: <%= @hand.map{|card| pretty_card(card)}.join(", ") %>)
EOF
ERB.new(hit_view).run
# >> New card: 3 of Clubs
# >>
# >> (Now in your hand: ♠4, ♠7, ♣3)
Now let's fast forward a bit. Our app has grown in size, and in order to make our views better testable and maintainable, we are pursuing a strategy used by many mature Rails shops: we're factoring our view templates into <a id="tapas__rails_pre href="https://www.rubytapas.com/out/rails-presenter-pattern">Presenter objects. As part of that process, we want to extract out our Blackjack "hit" view into its own presenter.
So we create a new HitPresenter
class. We give it constructor parameters for the model objects it needs to know about. And we give it a method to render
itself.
Into this method, we copy the original template code.
require "erb" class HitPresenter def initialize(hand, new_card) @hand = hand @new_card = new_card end def render ERB.new(<<~'EOF').result(binding) New card: <%= long_card(@new_card) %>
Back in the "controller" part of our code, we get rid of the template rendering. Now all we need to do is create a(Now in your hand: <%= @hand.map{|card| pretty_card(card)}.join(", ") %>) EOF
end end
HitPresenter
, give it the@hand
and@new_card
, and tell it to render itself. Since we're no longer using theCardHelper
methods here, we'll remove theinclude
for it as well.There's just one problem: the template code we copied
HitPresenter
is still trying to use thoseCardHelper
methods, and now it can't find them.require "./controller" require "./presenter" puts HitPresenter.new(@hand, @new_card).render # ~> NoMethodError: undefined method `long_card' for #<HitPresenter:0x0000000...
~> NoMethodError
~> undefined method `long_card' for #<HitPresenter:0x00000002a28148>
~>
~> (erb):1:in `render'
~> C:/tools/ruby23/lib/ruby/2.3.0/erb.rb:864:in `eval'
~> C:/tools/ruby23/lib/ruby/2.3.0/erb.rb:864:in `result'
~> c:/Users/avdi_000/Dropbox/rubytapas-shared/working-episodes/481-mixin-to...
~> xmptmp-in256321RF.rb:5:in `<main>'
The most obvious solution is to just include the
CardHelper
module into theHitPresenter
.require "./presenter" class HitPresenter include CardHelper # ... end
require "./controller" require "./presenter2" puts HitPresenter.new(@hand, @new_card).render
>> New card: King of Clubs
>>
>> (Now in your hand: ♠3, ♠3, ♣K)
The trouble with this route is that it somewhat defeats the purpose of our moving templates into presenters. The point of having presenters is to have small objects with tightly constrained interfaces. They should be well encapsulated, easy to test, and have limited, well-defined dependencies.
One thing we really don't want to see is presenters that contain templates which in turn contain myriad, uncounted dependencies on a big amorphous blob of module includes.
Instead, we'd rather the templates contained within the presenters only reference methods provided by those presenters.
To put this in concrete terms, our
HitPresenter
should contain private methods for formatting cards.The question is, how should we implement these private methods?
class HitPresenter # ... private def pretty_card(card) # ??? end def long_card(card) # ??? end end
Of course, we could just copy and paste the relevant methods from the
CardHelper
module. But then we would probably find that those methods depend on other methods, which we would have to copy and paste in turn. Not to mention that we'd really rather not see duplicate implementations of card formatting code diverge over time. All in all, it's a rabbit hole we'd rather not go down.Anoher possibility is that we go ahead and include the
CardHelper
module, and for the time being just define the explicitHitPresenter
versions to delegate to the module ancestor versions.class HitPresenter include CardHelper # ... private def pretty_card(card) super end def long_card(card) super end end
Or we could rename our presenter versions of the methods, including in the template, and have the renamed methods delegate to the module originals.
class HitPresenter include CardHelper # ... def render ERB.new(<<~'EOF').result(binding) New card: <%= present_long_card(@new_card) %>
But both of these approaches fail to deal with the fact that we're still inheriting the entire interface of(Now in your hand: <%= @hand.map{|card| present_pretty_card(card)}.join(", ") %>) EOF
end
private
def present_pretty_card(card) pretty_card(card) end
def present_long_card(card) long_card(card) end end
CardHelper
intoHitPresenter
. Any templates insideHitPresenter
can silently depend on anyCardHelper
methods. This isn't really moving us in the direction of separation of responsibilities.There is another factor that we haven't talked about yet: our effort to extract presenters is part of a larger architectural shift that we're trying to make. We are trying to move away from the situation where some responsibilities are handled by objects, and others are handled by mixing modules. Instead, we've set an intention to have each refactoring move us towards a more consistent design where every role has an object responsible for it.
Ideally, each of our
HitPresenter
private formatter methods would delegate to an object responsible for formatting cards.class HitPresenter # ... private def pretty_card(card) @card_formatter.pretty_card(card) end def long_card(card) @card_formatter.long_card(card) end end
Now, we could take a detour here and start up another refactoring where we convert
CardHelper
into a class. But that would mean either introducing duplication, in the form of a new class paralleling theCardHelper
module; or else it would mean rewriting every single view and presenter in the system that currently depends onCardHelper
as a module.And there's a more immediate problem with that idea: we have a policy of not starting new refactorings in the midst of current ones.
We don't want to make our presenter inherit legacy helper modules. But we also don't want to embark on a massive rewrite of our helpers. What do we do?
As it happens, there's a quick and very Ruby-ish solution to this conundrum.
In our presenter initializer, we can assign the
@card_formatter
instance variable a new blank object. Then, we can extend this object with theCardHelper
module.And that's it: our formatter methods now have a
@card_formatter
object that they can delegate to.require "./presenter" require "./helpers" class HitPresenter def initialize(hand, new_card) @hand = hand @new_card = new_card @card_formatter = Object.new @card_formatter.extend(CardHelper) end private def pretty_card(card) @card_formatter.pretty_card(card) end def long_card(card) @card_formatter.long_card(card) end end
Let's run this code and verify that it works.
require "./controller" require "./presenter3" puts HitPresenter.new(@hand, @new_card).render
>> New card: King of Hearts
>>
>> (Now in your hand: ♠Q, ♦Q, ♠K)
Consider what we've accomplished here: we've enabled our
HitPresenter
methods to pretend that there is already a dedicated card formatter object of them to work with. Meanwhile, we've been able to leave the actual currentCardHelper
module unchanged and complete our refactoring. All with two lines of code.@card_formatter = Object.new @card_formatter.extend(CardHelper)
These lines of code also give us a convenient "seam" for future evolution. For instance, the next step we take towards converting
CardHelper
to an object might go like this:We create a
CardFormatter
class. We include the currentCardHelper
module into it.class CardFormatter include CardHelper end
Then, in our presenter objects, we switch to using this new class.
require "./presenter" require "./helpers" require "./card_formatter" class HitPresenter def initialize(hand, new_card) @hand = hand @new_card = new_card @card_formatter = CardFormatter.new end # ... end
From here, we can start to gradually pull methods from the
CardHelper
module over to theCardFormatter
class.class CardFormatter include CardHelper def pretty_card(card) # ... end end
Right now, that's all in the future. But by dynamically extending a blank object with a module, we've taken the first step.
@card_formatter = Object.new @card_formatter.extend(CardHelper)
Any time we have a module that we want to treat like an object, we can use this trick. We can lean on Ruby's dynamic nature to create encapsulation even where it wasn't originally planned. And that's a useful option to have. Happy hacking!
Responses