In applications using the MVC (Model View Controller) architecture, it is good practice to:
- Keep the business logic in the model
- Keep the controller thin
- Keep logic out of the view
I’ve had few problems with keeping all of the business logic in the model. Keeping the controller thin can be more work, but it’s not usually too difficult. Keeping logic out of the view, however, has been more of a problem. The two easiest ways to get the logic out of the view are to move it into either a “helper function,” or into the model itself. I’d like to explain the problems with these approaches, and introduce the “decorator” pattern as an alternate solution. I will show how the decorate pattern is easily applied in Ruby through the use of dynamically included modules.
Let’s Begin
Our example application will be a simple schedule with a list of events. It will make a schedule with some events, and then display it.
We’ll need some libraries:
require 'date'
require 'delegate'
The models couldn’t be simpler.
Event = Struct.new(:name, :date)
class Schedule
attr_reader :events
def initialize
@events = []
end
end
The view is just a function that prints the schedule to the console:
def show_schedule(schedule)
case schedule.events.size
when 0
puts "Nothing to do. Perfect!"
when 1
puts "Something to do. Sigh."
else
puts "Much too much to do!"
end
schedule.events.each do |event|
puts "#{event.date.strftime("%D")} - #{event.name}"
end
end
This function is used by the controller to make a populated instance of the Schedule model:
def make_schedule
schedule = Schedule.new
schedule.events << Event.new('Mow the lawn', Date.new(2015, 1, 1))
schedule.events << Event.new('Stop watering the lawn', Date.new(2015, 2, 1))
schedule.events << Event.new('Rake up dead lawn', Date.new(2015, 6, 1))
schedule
end
And this code, the controller proper, ties it all together.
schedule = make_schedule
show_schedule schedule
And its outout:
Much too much to do!
01/01/15 - Mow the lawn
02/01/15 - Stop watering the lawn
06/01/15 - Rake up dead lawn
It works, but can it be better? The logic in the view is distracting. It would be better if the view did not have to concern itself with details such as how to format the date, or how many tasks is “Much too much to do!”. If we can move those logics out of the view, the view becomes simpler. Also, there may be other views of the same models that wish to reuse those logics. Let’s explore some different ways to achieve this.
Helper methods
Let’s try moving the view logics into helper functions. This is an approach commonly used in Ruby on Rails applications (although rails calls them “helper methods,” not “helper functions”).
class Bar
def foo
@foo
end
end
Here are our helper functions:
def format_event_date(event)
event.date.strftime("%D %R")
end
def schedule_burden(schedule)
case schedule.events.size
when 0
"Nothing to do. Perfect!"
when 1
"Something to do. Sigh."
else
"Much too much to do!"
end
end
and here is the view that uses them:
def show_schedule(schedule)
puts schedule_burden(schedule)
schedule.events.each do |event|
puts "#{format_event_date(event)} - #{event.name}"
end
end
This isn’t bad. The view is certainly cleaner now. But helper functions have a drawback: Being functions rathern than methods, the helpers need to have all of the state they will act upon passed into them. That’s the difference between a function and a method. A method acts upon an object’s state; a function acts upon its arguments. Being functions, they are disconnected from the models they act upon. One of the principles of OOP is that data, and the code that act upon that data, live together in an object. Now we have some code just “hanging out there.” An object’s methods are discoverable:
p o.methods
But the functions are unattached to the object. You just have to know they are there.
Adding view helpers to the models
One way to avoid the problem of disconnected helper functions is to move the functions into the models they act upon. Our models become:
Event = Struct.new(:name, :date) do
def format_date
date.strftime("%D %R")
end
end
class Schedule
attr_reader :events
def initialize
@events = []
end
def burden
case @events.size
when 0
"Nothing to do. Perfect!"
when 1
"Something to do. Sigh."
else
"Much too much to do!"
end
end
end
and the view:
def show_schedule(schedule)
puts schedule.burden
schedule.events.each do |event|
puts "#{event.format_date} - #{event.name}"
end
end
This is just about ideal for the view. It doesn’t get any easier than that. Things aren’t as good in the model. The model’s business logical is now mixed in with the view’s logic. It’s not so bad with one view, but what if there were several views, each needing their own special formatting and other logic? The model ends up becoming a Caddisfly larvae, covered with little pebbles and twigs that are useful here and there, but a bit of a mess in the whole. What we want is the view we just made here, but without having to add those methods to the model. The decorator pattern can do that.
Decorator using SimpleDelegator
What if we could decorate an instance of a model, adding to it the methods that the view needs? With Ruby’s SimpleDelegator, we can do it.
class DecoratedSchedule < SimpleDelegator
def burden
case events.size
when 0
"Nothing to do. Perfect!"
when 1
"Something to do. Sigh."
else
"Much too much to do!"
end
end
end
A SimpleDelegator has an #initialize method that takes a single argument, the delegatee, or class being delegated to. In this case, it will be an instance of Schedule. The delegator forwards to the delegatee any methods it doesn’t know about. It acts as though it is the delegatee, but with some extra methods. That’s exactly what we need!
A little change to the controller decorates the schedule before showing it:
schedule = make_schedule
schedule = DecoratedSchedule.new(schedule)
show_schedule schedule
We’ll want to decorate the event, too. Unfortunately, there’s not an easy way for us to do this outside the view. We could do it where we make the schedule:
def make_schedule
schedule = Schedule.new
events = [
Event.new('Mow the lawn', Date.new(2015, 1, 1)),
Event.new('Stop watering the lawn', Date.new(2015, 2, 1)),
Event.new('Rake up dead lawn', Date.new(2015, 6, 1))
]
events.each do |event|
event = DecoratedEvent.new(event)
schedule.events << event
end
end
But this is cumbersome. And what if the code that makes the schedules is used by other views? We don’t want to decorate all events, we just want to decorate the events used by this view.
We could also have the view do the decorating:
def show_schedule(schedule)
puts schedule.burden
schedule.events.each do |event|
event = DecoratedEvent.new(event)
puts "#{event.format_date} - #{event.name}"
end
end
but we’re trying to make views simpler, not more complex. If we only needed to decorate a single, outer object, using delegators would be alright. But, in this case, our models are nested: A Schedule has Tasks. We want to decorate both the schedule and its tasks. Which leads us to:
Mixin decorations
What if our helper methods were in modules?
module ScheduleDecoration
def burden
case @events.size
when 0
"Nothing to do. Perfect!"
when 1
"Something to do. Sigh."
else
"Much too much to do!"
end
end
end
module EventDecoration
def format_date
date.strftime("%D %R")
end
end
Static mixins
The usual way people see modules used is at the class level, like this:
class Schedule
include ScheduleDecoration
...
end
This is a little better than just including the method directly into the model: at least the module name serves to group methods according to their purpose. It’s not ideal, though, since every instance of that model has the decorations. What happens when one view needs the date formatted differently? You end up having this:
module EventDecoration
def format_date_for_show
...
end
end
module OtherEventDecoration
def format_date_for_other_purpose
...
end
end
This code is not getting better. But what if you could mix modules into individual objects, so that the model gets decorated by the view as needed?
Dynamic mixins
Let’s have the controller mix in the decorations at runtime, dynamically:
def decorate_schedule(schedule)
schedule.extend ScheduleDecoration
schedule.events.each do |event|
event.extend EventDecoration
end
end
schedule = make_schedule
decorate_schedule schedule
show_schedule schedule
calling extend on an object and passing it a module, as here:
schedule.extend ScheduleDecoration
is almost the same as if the class included the module:
class Schedule
include ScheduleDecoration
...
end
except that the module’s methods are only included in that one instance of Schedule rather than in every instance. This is just what we were looking for. Now our controller can add the extra methods that will make the view simple and clean, without polluting the model class.
There’s one more refinement we can make here. The “decorate_schedule”
method is kind of annoying. It’s detail that our controller should
not have to worry about. Also, if more than one controller needs the
same decoration, they would have to share the decorate_schedule
method. That’s not that bad, but let’s see if we can tidy things up.
Cleaning things up with Module.extended
You can make a module do things when it is extended. Let’s take the
loop out of decorate_schedule
and move it into ScheduleDecoration:
module ScheduleDecoration
def self.extended(schedule)
schedule.events.each do |event|
event.extend EventDecoration
end
end
def burden
case @events.size
when 0
"Nothing to do. Perfect!"
when 1
"Something to do. Sigh."
else
"Much too much to do!"
end
end
end
Now the controller can simply decorate the schedule and the schedule’s tasks will also be decorated:
schedule = make_schedule
schedule.extend ScheduleDecoration
show_schedule schedule
The final result
We’ve made a lot of changes along the way. Here’s what the code looks like now:
require 'date'
require 'delegate'
Event = Struct.new(:name, :date);
class Schedule
attr_reader :events
def initialize
@events = []
end
end
module ScheduleDecoration
def self.extended(schedule)
schedule.events.each do |event|
event.extend EventDecoration
end
end
def burden
case @events.size
when 0
"Nothing to do. Perfect!"
when 1
"Something to do. Sigh."
else
"Much too much to do!"
end
end
end
module EventDecoration
def format_date
date.strftime("%D %R")
end
end
def make_schedule
schedule = Schedule.new
schedule.events << Event.new('Mow the lawn', Date.new(2015, 1, 1))
schedule.events << Event.new('Stop watering the lawn', Date.new(2015, 2, 1))
schedule.events << Event.new('Rake up dead lawn', Date.new(2015, 6, 1))
schedule
end
def show_schedule(schedule)
puts schedule.burden
schedule.events.each do |event|
puts "#{event.format_date} - #{event.name}"
end
end
schedule = make_schedule
schedule.extend ScheduleDecoration
show_schedule schedule
and its output:
Much too much to do!
01/01/15 00:00 - Mow the lawn
02/01/15 00:00 - Stop watering the lawn
06/01/15 00:00 - Rake up dead lawn