Presenters and Decorators: A Code Tour

Cd8da976054ea4915eafc5d9dd096d38?s=47 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.

Cd8da976054ea4915eafc5d9dd096d38?s=128

blowmage

April 24, 2012
Tweet

Transcript

  1. Presenters and Decorators A Code Tour

  2. None
  3. Mike Moore

  4. @blowmage

  5. Disclaimer

  6. Presenters should ease PAIN

  7. Components...

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

  9. def render_component(component) render :partial => "shared/component", :locals => {:component =>

    component} end app/controllers/application_controller.rb
  10. <%= render_component(component) %> app/views/shared/_component.html.erb

  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
  12. class DashboardController < ApplicationController before_filter :require_user def index @component =

    DashboardComponentBuilder.build(current_user) end end app/controller/dashboard_controller.rb
  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
  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
  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
  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
  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
  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
  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
  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
  21. Welcome to the team!

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

  23. View ⬌ Bikeshed

  24. Typical Summary Example...

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

    > 0 %> <li class='published'> <strong>Published:</strong> <%= published_count %> </li> <% end %> <% draft_count = current_user.posts.unpublished.count + current_user.videos.unpublished.count %> <% if draft_count > 0 %> <li class='draft'> <strong>Drafted:</strong> <%= draft_count %> </li> <% end %> app/shared/nav/_author_details.html.erb
  26. <% if current_user.admin? %> <% approval_count = current_user.organization.posts.unapproved.count %> <%

    if approval_count.to_i > 0 %> <li class='approval'> <strong>Needing Approval:</strong> <%= approval_count %> </li> <% end %> <% end %> app/shared/nav/_author_details.html.erb (cont)
  27. Solutions?

  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
  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
  30. <% summary = AuthorSummary.new(current_user) %> <% if summary.published_count > 0

    %> <li class='published'> <strong>Published:</strong> <%= summary.published_count %> </li> <% end %> <% if summary.draft_count > 0 %> <li class='draft'> <strong>Drafted:</strong> <%= summary.draft_count %> </li> <% end %> <% if summary.approval_count > 0 %> <li class='approval'> <strong>Needing Approval:</strong> <%= summary.approval_count %> </li> <% end %> app/shared/nav/_author_details.html.erb
  31. ActiveDecorator github.com/amatsuda/active_decorator

  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
  33. <% if current_user.published_count > 0 %> <li class='published'> <strong>Published:</strong> <%=

    current_user.published_count %> </li> <% end %> <% if current_user.draft_count > 0 %> <li class='draft'> <strong>Drafted:</strong> <%= current_user.draft_count %> </li> <% end %> <% if current_user.approval_count > 0 %> <li class='approval'> <strong>Needing Approval:</strong> <%= current_user.approval_count %> </li> <% end %> app/shared/nav/_author_details.html.erb
  34. It helped!

  35. Typical Serialization Example...

  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
  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
  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
  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
  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
  41. ActiveModel:: Serializers github.com/josevalim/ active_model_serializers

  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
  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
  44. Pretty cool!

  45. But...

  46. Is that it?

  47. Presenter Definition?

  48. Design Patterns

  49. Decorator Design Pattern Attach additional responsibilities to an object dynamically.

    Decorators provide a flexible alternative to subclassing for extending functionality.
  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.
  51. Presenter Spectrum

  52. Presenter Spectrum Models Views Decorators Presenters?

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

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

  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...
  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...
  57. Presentation Model While several views can utilize the same Presentation

    Model, each view should require only one Presentation Model...
  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.
  59. Definition?

  60. “Representation of the State of the View”

  61. Presenter == Presentation Model?

  62. ViewModel == Presentation Model

  63. A Slightly Better Example...

  64. <% 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
  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
  66. class CourseShowPresenter attr_accessor :course, :enrollment, :user, :session def show_join_course? @course.available?

    && @course.self_enrollment && @course.open_enrollment && (!@enrollment || !@enrollment.active?) && !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
  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
  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
  69. A Good Example...

  70. None
  71. None
  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? %> <section class="top_categories"> <header> <h6>Top Categories</h6> </header> <ul> <%= render :partial => "category_item", :collection => categories %> </ul> </section> <% end %> <% end %> <% end %> app/views/home/_sidebar.html.erb
  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
  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? %> <section class="top_categories"> <header> <h6>Top Categories</h6> </header> <ul> <%= render :partial => "category_item", :collection => categories %> </ul> </section> <% end %> <% end %> <% end %> app/views/home/_sidebar.html.erb
  75. <% if @view.show_categories? %> <section class="top_categories"> <header> <h6>Top Categories</h6> </header>

    <ul> <%= render :partial => "category_item", :collection => @view.categories %> </ul> </section> <% end %> app/views/home/_sidebar.html.erb
  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
  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
  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
  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
  80. Warning!

  81. None
  82. None
  83. None
  84. Thank you!

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