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 Slide

  2. View Slide

  3. Mike Moore

    View Slide

  4. @blowmage

    View Slide

  5. Disclaimer

    View Slide

  6. Presenters
    should ease
    PAIN

    View Slide

  7. Components...

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

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

    View Slide

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

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

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

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

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

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

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

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

  21. Welcome to
    the team!

    View Slide

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

    View Slide

  23. View ⬌ Bikeshed

    View Slide

  24. Typical
    Summary
    Example...

    View Slide

  25. <% 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 Slide

  26. <% 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 Slide

  27. Solutions?

    View Slide

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

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

  30. <% 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 Slide

  31. ActiveDecorator
    github.com/amatsuda/active_decorator

    View Slide

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

  33. <% 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 Slide

  34. It helped!

    View Slide

  35. Typical
    Serialization
    Example...

    View Slide

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

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

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

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

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

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

    View Slide

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

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

  44. Pretty cool!

    View Slide

  45. But...

    View Slide

  46. Is that it?

    View Slide

  47. Presenter
    Definition?

    View Slide

  48. Design Patterns

    View Slide

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

    View Slide

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

  51. Presenter
    Spectrum

    View Slide

  52. Presenter Spectrum
    Models
    Views
    Decorators
    Presenters?

    View Slide

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

    View Slide

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

    View Slide

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

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

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

    View Slide

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

  59. Definition?

    View Slide

  60. “Representation
    of the State of
    the View”

    View Slide

  61. Presenter ==
    Presentation
    Model?

    View Slide

  62. ViewModel ==
    Presentation
    Model

    View Slide

  63. A Slightly
    Better
    Example...

    View Slide

  64. <% if @course.available? &&
    @course.self_enrollment &&
    @course.open_enrollment &&
    ([email protected]_enrollment || [email protected]_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 Slide

  65. <% 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 Slide

  66. class CourseShowPresenter
    attr_accessor :course, :enrollment, :user, :session
    def show_join_course?
    @course.available? &&
    @course.self_enrollment &&
    @course.open_enrollment &&
    ([email protected] || [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 Slide

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

  68. <% 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 Slide

  69. A Good
    Example...

    View Slide

  70. View Slide

  71. View Slide

  72. <% 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 Slide

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

  74. <% 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 Slide

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


    Top Categories


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


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

    View Slide

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

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

  78. # 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 Slide

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

  80. Warning!

    View Slide

  81. View Slide

  82. View Slide

  83. View Slide

  84. Thank you!

    View Slide

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

    View Slide