Public Service Announcement
There are errors in the code examples.
When I get a chance, I'll fix them and remove this slide.
And then you won't know about them.
Or this slide.
Or anything really.
Presenter Workshop
Or how I'm learning to use object
oriented designs with Rails controllers
Jason Wieringa
jason [at] codesherpas.com
jason [at] codesherpas.com
Jr. Software Engineer
Introduction
- I'd like this to be more of a discussion
- This isn't Gospel
- If you ask me six months from now my thoughts, I hope they will have matured more
- We'll be talking through the Why's of opinions
Goal
Think about ways to make it easier to understand
obtuse Rails controller and helper code
by using objected oriented designs.
obtuse Rails controller and helper code
by using objected oriented designs.
Where it all Began
THe First time I saw this Code
Now I think...
Dear past self or former developer:
I don't understand what you are trying to tell me.
Would you be willing to explain?
Past self: "I'm sorry, I don't remember."
Me: "I don't like you right now."
Or if you are on a take over project like me...
Former Developer: **crickets**
Check the tests!
That will tell me what's happening.
No tests
That sucks.
I'll write the tests!
HOurs later...
But then I think...
I've written Lots of tests before..
Why is it so hard to write tests for this?
This is not Great Ruby
We can make this easier to test.
Let's take a look at a simpler example.
An Evolution
Part 1
def index
@surveys = Survey.all
end
Part 2
Add a parameter to choose different sets of data
def index
view = params[:view]
case view
when "submitted"
@surveys = Surveys.submitted
when "open"
@surveys = Surveys.open
else
@surveys.all
end
end
Part 3
Add a custom title
def index
view = params[:view]
case view
when "submitted"
@surveys = Surveys.submitted
when "open"
@surveys = Surveys.open
else
@surveys.all
end
case user.role
when "Admin"
@title = "Admin's Surveys"
when "Minion"
@title = "Minion's Surveys"
end
end
Part 4
def index
view = params[:view]
case view
when "submitted"
@surveys = Surveys.submitted
when "open"
@surveys = Surveys.open
else
@surveys.all
end
end
class SurveyHelper
def title(user)
case user.role
when "Admin"
"Admin's Surveys"
when "Minion"
"Minion's Surveys"
end
end
end
Part 5...
- Minion's first name and last name together
- Admin's first and last name together
- List of all the tools in the system for Admins
- List of all the tools belonging to a Minion
- A Minion's workplace logo
- An Admin's organization logo
- Question sets for surveys
- Survey cycle periods
- Number of allowed survey questions
- And it just keeps going....
Reflection
- The SurveyHelper is hundred plus lines long
- The SurveyHelper has methods for multiple controllers
- The controller action is 25 lines and has 6 instance variables
- The functional test suite is taking a long time to run
- I have 6+ private methods in my controller
- Lots of silly presentation bugs are being reported
I am no longer happy.
I cannot explain what is going on.
I've felt this way before...
But I thought it was the other person's fault...
Turns out, That's not Great Ruby
You are trying to do it procedurally, try to do it with objects.*- Aaron Kromer
*paraphrased
Presenters
Some Definitions
Martin Fowler
Supervising (Presenter) Controller
Supervising Controller uses a controller both to handle input response but also to manipulate the view to handle more complex view logic. It leaves simple view behavior to the declarative system, intervening only when effects are needed that are beyond what can be achieved declaratively.
http://martinfowler.com/eaaDev/SupervisingPresenter.html
Jay Fields
Rails Presenter Pattern
The Presenter pattern addresses bloated controllers and views containing logic in concert by creating a class representation of the state of the view.
http://blog.jayfields.com/2007/03/rails-presenter-pattern.html
Steve Klabnik
On Presenters... but not really on Presenters...
...I want you to write better code, and so I'm here to 'break the secret,' if you will, and share this awesome, powerful technique with you.
It's called the 'Plain Old Ruby Domain Object.'
http://blog.steveklabnik.com/posts/2011-09-06-the-secret-to-rails-oo-design
Let's make some PORO's
Does this domain concept deserve its
own class?
- Hard to test
- Bugs
- Many instance variables
- Very complex view code
- Logic spread among Helper, View, Controller
Let's change our index action
Before
def index
@surveys = Survey.all
end
Afterdef index
@survey_presenter = SurveyPresenter.new(params[:view], current_user)
end
Plain Old Ruby Object
class SurveyPresenter
def initialize(view = "all", user)
@view = view
@user = user
end
def surveys
case @view
when "submitted"
@surveys ||= Survey.submitted
when "open"
@surveys ||= Survey.open
when "all"
@surveys ||= Survey.all
end
end
end
PLAIN OLD RUBY OBJECT
<h1><%= @survey_presenter.title %></h1>
<% @survey_presenter.surveys.each do |survey|
#....
<% end %>
PLAIN OLD RUBY OBJECT
What about tests?
Before
test "should get index as a minion" do
UserSession.create(users(:minion))
get :index, view: 'submitted'
assert_response :success
assert_assigns :surveys
assert_equal 'Minion's Surveys', assigns :title
end
PLAIN OLD RUBY OBJECT
What about tests?
After
test "should return minion title" do
user = User.new
user.expects(:role).returns("Minion")
survey = SurveyPresenter.new(user)
assert_equal "Minion's Surveys", survey.title
end
Benefits
I've found that...
- I have a lot more test coverage
- I can think about problems easier
- Since the code is one place, I see duplication faster
- Adding new features to the view is easier
- I'm moving domain logic into Models or other classes
Strategies
- You don't need to re-write the controller all at once
- Start small with one action or instance variable
- Leave the old methods around and note them as depreciated
- Do write tests for your new code! :)
Strategies
Seek Incremental Clarity.
Besides, you'll probably understand it better
next week.
Let's work on Some Code
I'd like to see examples of how others have
thought about these challenges.
-
Break up into groups of three.
Thank You!
Presenter Workshop
By Jason Wieringa
Presenter Workshop
- 1,667