Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Presenters and Decorators: A Code Tour

blowmage
April 24, 2012

Presenters and Decorators: A Code Tour

Presenter and Decorators are design approaches that can be used in Rails applications outside of the standard Models, Views and Controllers. These approaches are becoming more and more popular as teams search for new ways to identify and manage the complexity within their applications.

In this session Mike Moore will defined the Presenter and Decorator approaches using simple and clear terminology. Common design problems in Rails applications will be shown using real-life code examples and refactored toward Presenters and Decorators. Code will be improved and strengthened by identifying and respecting the dependencies within large applications.

blowmage

April 24, 2012
Tweet

More Decks by blowmage

Other Decks in Programming

Transcript

  1. Presenters
    and Decorators
    A Code Tour

    View full-size slide

  2. Presenters
    should ease
    PAIN

    View full-size slide

  3. Components...

    View full-size slide

  4. <%= render_component(@component) %>
    app/views/dashboard/index.html.erb

    View full-size slide

  5. def render_component(component)
    render :partial => "shared/component",
    :locals => {:component => component}
    end
    app/controllers/application_controller.rb

    View full-size slide

  6. <%= render_component(component) %>
    app/views/shared/_component.html.erb

    View full-size slide

  7. module ComponentHelper
    def render_component(component)
    return "" if component.nil?
    if component.respond_to?(:render)
    raw component.render(self)
    else
    raw component.to_s
    end
    end
    end
    app/helpers/component_helper.rb

    View full-size slide

  8. class DashboardController < ApplicationController
    before_filter :require_user
    def index
    @component = DashboardComponentBuilder.build(current_user)
    end
    end
    app/controller/dashboard_controller.rb

    View full-size slide

  9. class DashboardComponentBuilder
    def self.build(user)
    my_posts = user.posts.recent
    DashboardComponent.new(
    :post_items =>
    PostItemComponentBuilder.build_list(my_posts),
    :author_stats_component =>
    AuthorAnalyticsComponentBuilder.build(user)
    )
    end
    end
    lib/dashboard_component_builder.rb

    View full-size slide

  10. class PostItemComponentBuilder < ComponentBuilder
    def self.build(post)
    post_path = UrlGenerator.post_path(post)
    PostItemComponent.new(
    :title_link => Link.new(post.name, post_path),
    :description => post.description,
    :views => post.view_count,
    :post_thumbnail =>
    PostThumbnailComponentBuilder.build(post),
    :comments => post.comments.published.count,
    :edit_link => Link.new("Edit Post",
    UrlGenerator.edit_post_path(post)),
    :status_class => build_status_class(post),
    :status_text => build_status_text(post),
    :delete_button =>
    DeleteButtonComponentBuilder.build(post)
    )
    end
    #...
    end
    lib/post_item_component_builder.rb

    View full-size slide

  11. class ComponentBuilder
    def self.build_list(items,*args)
    return [] if items.nil? or items.empty?
    if self.respond_to?(:build)
    items.map do |item|
    self.build(item,*args)
    end.compact
    else
    raise "ComponentBuilder must implement .build"
    end
    end
    end
    app/components/component_builder.rb

    View full-size slide

  12. class PostItemComponentBuilder < ComponentBuilder
    def self.build(post)
    post_path = UrlGenerator.post_path(post)
    PostItemComponent.new(
    :title_link => Link.new(post.name, post_path),
    :description => post.description,
    :views => post.view_count,
    :post_thumbnail =>
    PostThumbnailComponentBuilder.build(post),
    :comments => post.comments.published.count,
    :edit_link => Link.new("Edit Post",
    UrlGenerator.edit_post_path(post)),
    :status_class => build_status_class(post),
    :status_text => build_status_text(post),
    :delete_button =>
    DeleteButtonComponentBuilder.build(post)
    )
    end
    #...
    end
    lib/post_item_component_builder.rb

    View full-size slide

  13. PostItemComponent = PartialComponent.define(
    "posts/post_item",
    :title_link,
    :description,
    :views,
    :comments,
    :delete_button,
    :edit_link,
    :post_thumbnail,
    :status_class,
    :status_text)
    app/components/post_item_component.rb

    View full-size slide

  14. class PartialComponent
    def self.define(partial, *var_names)
    component_class = ConstructorStruct.new(
    *[var_names + [:partial]].flatten)
    component_class.send(:define_method, :locals) do
    locals = {}
    var_names.each do |k|
    locals[k] = instance_variable_get("@#{k}")
    end
    locals
    end
    component_class.send(:define_method, :render) do |template|
    template.send(:render,
    :partial => @partial || partial,
    :locals => locals)
    end
    component_class
    end
    end
    app/components/partial_component.rb

    View full-size slide

  15. PostItemComponent = PartialComponent.define(
    "posts/post_item",
    :title_link,
    :description,
    :views,
    :comments,
    :delete_button,
    :edit_link,
    :post_thumbnail,
    :status_class,
    :status_text)
    app/components/post_item_component.rb

    View full-size slide

  16. .post_item{ :class => status_class }
    = render_component(post_thumbnail)
    .text
    %h5
    - if status_text
    %span= status_text
    = render_link(title_link)
    .description= description
    .stats_and_actions
    .item_stats
    %p.views.stat_display
    %strong.stat_count= views
    Views
    %p.comments.stat_display
    %strong.stat_count= comments
    Comments
    .item_actions
    .edit
    %a.edit{:href => edit_link.url}
    %span Edit
    .delete
    = render_component(delete_button)
    app/views/posts/_post_item.html.haml

    View full-size slide

  17. Welcome to
    the team!

    View full-size slide

  18. Won’t somebody
    please think of
    the new
    developers?!?!

    View full-size slide

  19. View ⬌ Bikeshed

    View full-size slide

  20. Typical
    Summary
    Example...

    View full-size slide

  21. <% published_count = current_user.posts.published.count +
    current_user.videos.published.count %>
    <% if published_count > 0 %>

    Published: <%= published_count %>

    <% end %>
    <% draft_count = current_user.posts.unpublished.count +
    current_user.videos.unpublished.count %>
    <% if draft_count > 0 %>

    Drafted:
    <%= draft_count %>

    <% end %>
    app/shared/nav/_author_details.html.erb

    View full-size slide

  22. <% if current_user.admin? %>
    <% approval_count =
    current_user.organization.posts.unapproved.count %>
    <% if approval_count.to_i > 0 %>

    Needing Approval:
    <%= approval_count %>

    <% end %>
    <% end %>
    app/shared/nav/_author_details.html.erb (cont)

    View full-size slide

  23. class User < ActiveRecord::Base
    def published_count
    self.posts.published.count +
    self.videos.published.count
    end
    def draft_count
    self.posts.unpublished.count +
    self.videos.unpublished.count
    end
    def approval_count
    if self.admin?
    self.organization.posts.unapproved.count
    else
    0
    end
    end
    # ...
    end
    app/models/user.rb

    View full-size slide

  24. class AuthorSummary
    def initialize(author)
    @author = author
    end
    def published_count
    @author.posts.published.count +
    @author.videos.published.count
    end
    def draft_count
    @author.posts.unpublished.count +
    @author.videos.unpublished.count
    end
    def approval_count
    if @author.admin?
    @author.organization.posts.unapproved.count
    else
    0
    end
    end
    end
    lib/author_summary.rb

    View full-size slide

  25. <% summary = AuthorSummary.new(current_user) %>
    <% if summary.published_count > 0 %>

    Published:
    <%= summary.published_count %>

    <% end %>
    <% if summary.draft_count > 0 %>

    Drafted:
    <%= summary.draft_count %>

    <% end %>
    <% if summary.approval_count > 0 %>

    Needing Approval:
    <%= summary.approval_count %>

    <% end %>
    app/shared/nav/_author_details.html.erb

    View full-size slide

  26. ActiveDecorator
    github.com/amatsuda/active_decorator

    View full-size slide

  27. module UserDecorator
    def published_count
    self.posts.published.count +
    self.videos.published.count
    end
    def draft_count
    self.posts.unpublished.count +
    self.videos.unpublished.count
    end
    def approval_count
    if self.admin?
    self.organization.posts.unapproved.count
    else
    0
    end
    end
    end
    app/decorators/user_decorator

    View full-size slide

  28. <% if current_user.published_count > 0 %>

    Published:
    <%= current_user.published_count %>

    <% end %>
    <% if current_user.draft_count > 0 %>

    Drafted:
    <%= current_user.draft_count %>

    <% end %>
    <% if current_user.approval_count > 0 %>

    Needing Approval:
    <%= current_user.approval_count %>

    <% end %>
    app/shared/nav/_author_details.html.erb

    View full-size slide

  29. Typical
    Serialization
    Example...

    View full-size slide

  30. class PostController < ApplicationController
    before_filter :require_user
    def show
    @post = current_organization.posts.find(params[:id])
    respond_to do |format|
    format.html
    format.json { render :json => @post }
    end
    end
    end
    app/controllers/posts_controller.rb

    View full-size slide

  31. object @post
    attributes :id, :title
    # look up author_name on the model, but use author in the JSON
    attributes :author_name => :author
    if current_user.admin?
    # only show views, popularity to admins
    attributes :view_count => :views,
    :popularity_index => :popularity
    end
    app/views/posts/show.json.rabl

    View full-size slide

  32. class PostController < ApplicationController
    before_filter :require_user
    def show
    @post = current_organization.posts.find(params[:id])
    respond_to do |format|
    format.html
    format.json
    end
    end
    end
    app/controllers/posts_controller.rb

    View full-size slide

  33. class PostSerializer
    def initialize(post, user)
    @post, @user = post, user
    end
    def to_json
    if @user && @user.admin?
    full_detail
    else
    summary
    end
    end
    def full_detail
    summary.merge({ :views => @post.view_count,
    :popularity => @post.popularity_index })
    end
    def summary
    { :id => @post.id,
    :title => @post.title,
    :author => @post.author_name }
    end
    end
    lib/post_serializer.rb

    View full-size slide

  34. class PostController < ApplicationController
    before_filter :require_user
    def show
    @post = current_organization.posts.find(params[:id])
    respond_to do |format|
    format.html
    format.json { s = PostSerializer.new(@post, current_user)
    render :json => s.to_json }
    end
    end
    end
    app/controllers/posts_controller.rb

    View full-size slide

  35. ActiveModel::
    Serializers
    github.com/josevalim/
    active_model_serializers

    View full-size slide

  36. class PostSerializer < ActiveModel::Serializer
    attributes :id, :title
    # look up author_name on the model, but use author in the JSON
    attribute :author_name, :key => :author
    # @scoped is set to current_user by default
    if @scoped.admin?
    attribute :view_count, :key => :views
    attribute :popularity_index, :key => :popularity
    end
    end
    app/serializers/post_serializer.rb

    View full-size slide

  37. class PostController < ApplicationController
    before_filter :require_user
    def show
    @post = current_organization.posts.find(params[:id])
    respond_to do |format|
    format.html
    format.json { render :json => @post }
    end
    end
    end
    app/controllers/posts_controller.rb

    View full-size slide

  38. Pretty cool!

    View full-size slide

  39. Presenter
    Definition?

    View full-size slide

  40. Design Patterns

    View full-size slide

  41. Decorator
    Design Pattern
    Attach additional
    responsibilities to an object
    dynamically. Decorators
    provide a flexible alternative
    to subclassing for extending
    functionality.

    View full-size slide

  42. Mediator
    Design Pattern
    Define an object that
    encapsulates how a set of
    objects interact. Mediator
    promotes loose coupling by
    keeping objects from referring
    to each other explicitly, and
    lets you vary their interaction
    independently.

    View full-size slide

  43. Presenter
    Spectrum

    View full-size slide

  44. Presenter Spectrum
    Models
    Views
    Decorators
    Presenters?

    View full-size slide

  45. Rails: Presenter
    Pattern
    blog.jayfields.com/2007/03/rails-
    presenter-pattern.html

    View full-size slide

  46. Presentation
    Model
    martinfowler.com/eaaDev/
    PresentationModel.html

    View full-size slide

  47. Presentation
    Model
    Presentation Model may
    interact with several domain
    objects, but Presentation
    Model is not a GUI friendly
    facade to a specific domain
    object...

    View full-size slide

  48. Presentation
    Model
    Instead it is easier to
    consider Presentation Model
    as an abstract of the view
    that is not dependent on a
    specific GUI framework...

    View full-size slide

  49. Presentation
    Model
    While several views can
    utilize the same Presentation
    Model, each view should
    require only one
    Presentation Model...

    View full-size slide

  50. Presentation
    Model
    In the case of composition a
    Presentation Model may
    contain one or many child
    Presentation Model
    instances, but each child
    control will also have only
    one Presentation Model.

    View full-size slide

  51. “Representation
    of the State of
    the View”

    View full-size slide

  52. Presenter ==
    Presentation
    Model?

    View full-size slide

  53. ViewModel ==
    Presentation
    Model

    View full-size slide

  54. A Slightly
    Better
    Example...

    View full-size slide

  55. <% if @course.available? &&
    @course.self_enrollment &&
    @course.open_enrollment &&
    (!@course_enrollment || !@course_enrollment.active?) &&
    !session["role_course_#{@course.id}"] %>
    <%= render :partial => "join_course", :object => @course %>
    <% elsif @course_enrollment &&
    @course_enrollment.self_enrolled &&
    @course_enrollment.active? &&
    (!session["role_course_#{@course.id}"]) %>
    <%= render :partial => "drop_course",
    :locals => { :course => @course,
    :enrollment => @course_enrollment } %>
    <% elsif temp_type = session["role_course_#{@course.id}"] %>
    <%= render :partial => "temp_access",
    :object => temp_type %>
    <% end %>
    app/views/course.html.erb

    View full-size slide

  56. <% if @view.show_join_course? %>
    <%= render :partial => "join_course",
    :object => @view.course %>
    <% elsif @view.show_drop_course? %>
    <%= render :partial => "drop_course",
    :locals => { :course => @view.course,
    :enrollment => @view.enrollment } %>
    <% elsif @view.show_temp_access? %>
    <%= render :partial => "temp_access",
    :object => @view.temp_access %>
    <% end %>
    app/views/course.html.erb

    View full-size slide

  57. class CourseShowPresenter
    attr_accessor :course, :enrollment, :user, :session
    def show_join_course?
    @course.available? &&
    @course.self_enrollment &&
    @course.open_enrollment &&
    (!@enrollment || [email protected]?) &&
    !show_temp_access?
    end
    def show_drop_course?
    @enrollment &&
    @enrollment.self_enrolled &&
    @enrollment.active? &&
    !show_temp_access?
    end
    def show_temp_access?
    @session["role_course_#{@course.id}"]
    end
    alias :temp_access, :show_temp_access?
    end
    app/presenters/course_show_presenter.rb

    View full-size slide

  58. class CourseShowPresenter
    attr_accessor :course, :enrollment, :user, :session
    def show_join_course?
    @course.can_user_join?(@user) &&
    !show_temp_access?
    end
    def show_drop_course?
    @course.can_user_drop?(@user) &&
    !show_temp_access?
    end
    def show_temp_access?
    @session["role_course_#{@course.id}"]
    end
    alias :temp_access, :show_temp_access?
    end
    app/presenters/course_show_presenter.rb

    View full-size slide

  59. <% if @view.show_join_course? %>
    <%= render :partial => "join_course",
    :object => @view.course %>
    <% elsif @view.show_drop_course? %>
    <%= render :partial => "drop_course",
    :locals => { :course => @view.course,
    :enrollment => @view.enrollment } %>
    <% elsif @view.show_temp_access? %>
    <%= render :partial => "temp_access",
    :object => @view.temp_access %>
    <% end %>
    app/views/course.html.erb

    View full-size slide

  60. A Good
    Example...

    View full-size slide

  61. <% if current_organization.feature?(:categories) %>
    <% if current_site.setting?(:categories) %>
    <% if user_logged_in? ||
    current_site.setting?(:show_all_content) %>
    categories = current_site.top_categories
    <% else %>
    categories = current_site.top_public_categories
    <% end %>
    <% if categories.any? %>


    Top Categories


    <%= render :partial => "category_item",
    :collection => categories %>


    <% end %>
    <% end %>
    <% end %>
    app/views/home/_sidebar.html.erb

    View full-size slide

  62. class HomepagePresenter
    attr_accessor :organization, :site, :user
    # Categories
    def show_categories?
    @organization.feature?(:categories) &&
    @site.setting?(:categories) &&
    self.categories.any?
    end
    def categories
    if @user.present? || @site.setting?(:show_all_content)
    @categories ||= @organization.top_categories
    else
    @categories ||= @organization.top_public_categories
    end
    end
    #...
    end
    app/presenters/homepage_presenter.rb

    View full-size slide

  63. <% if current_organization.feature?(:categories) %>
    <% if current_site.setting?(:categories) %>
    <% if user_logged_in? ||
    current_site.setting?(:show_all_content) %>
    categories = current_site.top_categories
    <% else %>
    categories = current_site.top_public_categories
    <% end %>
    <% if categories.any? %>


    Top Categories


    <%= render :partial => "category_item",
    :collection => categories %>


    <% end %>
    <% end %>
    <% end %>
    app/views/home/_sidebar.html.erb

    View full-size slide

  64. <% if @view.show_categories? %>


    Top Categories


    <%= render :partial => "category_item",
    :collection => @view.categories %>


    <% end %>
    app/views/home/_sidebar.html.erb

    View full-size slide

  65. class HomepagePresenter
    attr_accessor :organization, :site, :user
    # Categories
    def show_categories?
    @organization.feature?(:categories) &&
    @site.setting?(:categories) &&
    self.categories.any?
    end
    def categories
    if @user.present? || @site.setting?(:show_all_content)
    @categories ||= @organization.top_categories
    else
    @categories ||= @organization.top_public_categories
    end
    end
    #...
    end
    app/presenters/homepage_presenter.rb

    View full-size slide

  66. class HomepagePresenter
    #...
    def categories
    if show_all_content?
    @categories ||= @organization.top_categories
    else
    @categories ||= @organization.top_public_categories
    end
    end
    protected
    def show_all_content?
    @user.present? || @site.setting?(:show_all_content)
    end
    #...
    end
    app/presenters/homepage_presenter.rb

    View full-size slide

  67. # Presenter when user is logged in
    class HomepageUserPresenter
    #...
    end
    # Presenter when site shows only public content
    class HomepagePublicPresenter
    #...
    end
    # Presenter when site shows all content, all the time
    class HomepagePermissivePresenter
    #...
    end
    app/presenters/homepage_*_presenter.rb

    View full-size slide

  68. class HomepageController < ApplicationController
    def index
    @view = build_homepage_presenter
    respond_to do |format|
    format.html
    end
    end
    def build_homepage_presenter
    view = if user_logged_in?
    HomepageUserPresenter.new
    elsif current_site.setting?(:show_all_content)
    HomepagePermissivePresenter.new
    else
    HomepagePublicPresenter.new
    end
    view.organization = current_organization
    view.site = current_site
    view.user = current_user
    view
    end
    end
    app/controllers/homepage_controller.rb

    View full-size slide

  69. HIRE ME!!!
    blowmage.com
    Questions?

    View full-size slide