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

    DashboardComponentBuilder.build(current_user) end end app/controller/dashboard_controller.rb
  3. 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
  4. 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
  5. 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
  6. 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
  7. 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
  8. .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
  9. <% 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
  10. <% 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)
  11. 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
  12. 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
  13. <% 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
  14. 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
  15. <% 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
  16. 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
  17. 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
  18. 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
  19. 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
  20. 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
  21. 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
  22. 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
  23. Decorator Design Pattern Attach additional responsibilities to an object dynamically.

    Decorators provide a flexible alternative to subclassing for extending functionality.
  24. 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.
  25. Presentation Model Presentation Model may interact with several domain objects,

    but Presentation Model is not a GUI friendly facade to a specific domain object...
  26. 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...
  27. Presentation Model While several views can utilize the same Presentation

    Model, each view should require only one Presentation Model...
  28. 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.
  29. <% 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
  30. <% 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
  31. 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
  32. 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
  33. <% 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
  34. <% 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
  35. 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
  36. <% 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
  37. <% 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
  38. 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
  39. 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
  40. # 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
  41. 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