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
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.

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
Plain Old Ruby Objects

Let's change our index action

Before
def index
@surveys = Survey.all
end
After
def 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



What does this look like in the view?
<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!