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

Fat Models Must Die

Ju Liu
June 14, 2013

Fat Models Must Die

Il concetto “Fat Models, Skinny controller” è da sempre uno dei cavalli di battaglia di Rails ed è uno dei principi fondamentali intorno a cui ruota il suo stack. Purtroppo, seguire ciecamente questo pattern spesso porta ad una crescita smisurata delle responsabilità dei modelli, che col passare del tempo e dei commit si trasformano in matasse di codice ingarbugliato e ingestibile.

In questo talk verranno esplorate differenti metodologie che si possono seguire nella pratica per mantenere il controllo del proprio progetto. Si descriveranno i pattern più diffusi proposti dalla community Rails per risolvere il problema della crescita del codice nel medio-lungo periodo: incominciando con concerns e presenters per passare a service objects e DCI, verranno spiegati i pregi dell’utilizzare pratiche più OOP per gestire con soddisfazione la complessità delle nostre applicazioni.

Lanyrd: http://lanyrd.com/2013/rubyday13/sckdyg/

Ju Liu

June 14, 2013
Tweet

More Decks by Ju Liu

Other Decks in Programming

Transcript

  1. $ wc -l app/models/* | sort -rn | sed -n

    '2,4p' 625 app/models/activity.rb 407 app/models/task.rb 364 app/models/user.rb
  2. SRP Every class should have a single responsibility, and that

    responsibility should be entirely encapsulated by the class.
  3. require 'active_support/concern' module NameGreeter extend ActiveSupport::Concern included do attr_accessor :name

    end def greet puts "Hi! I'm #{name}!" end module ClassMethods def build(name) self.new.tap do |instance| instance.name = name end end end end class User include NameGreeter end user = User.build("Mark") user.greet >> “Hi! I’m Mark!”
  4. class Match < ActiveRecord::Base def first_half_win? fh_made > fh_taken end

    def second_half_win? sh_made > sh_taken end def first_half_loss?; ... ; end def second_half_loss?; ... ; end def first_half_draw?; ... ; end def second_half_draw?; ... ; end end
  5. class Score < Struct.new(:goals_made, :goals_taken) def win? goals_made > goals_taken

    end def draw? goals_made == goals_taken end def loss? goals_made < goals_taken end end
  6. class EventsController < ApplicationController def index @events = Event.published if

    params[:lat] && params[:lng] && params[:distance] @events = @events.near(lat, lng, distance) end if params[:query].present? @events = @events.matching(params[:query]) end if params[:category_id].present? @events = @events.in_category(params[:category_id]) end end end
  7. class EventsQuery < OpenStruct def scope(scope = Events.scoped) scope =

    scope.published if lat && lng && distance scope = scope.near(lat, lng, distance) end if query.present? scope = scope.matching(query) end if category_id.present? scope = scope.of_category(category_id) end scope end end
  8. <%= form_for @query, as: :query, url: events_path, method: :get do

    |f| %> <%= f.text_field :query %> <% # ... %> <% end %>
  9. class CommentsController < ApplicationController def create @comment = Comment.new(params[:comment]) if

    @comment.save redirect_to blog_path, notice: "Comment was posted." else render "new" end end end
  10. class CommentsController < ApplicationController def create @comment = Notifier.new(Comment.new(params[:comment])) if

    @comment.save redirect_to blog_path, notice: "Comment was posted." else render "new" end end end
  11. class Notifier < Struct(:comment) def save comment.save && send_notification! end

    private def send_notification! AppMailer.comment_submission(comment).deliver true end end class Comment < ActiveRecord::Base end
  12. The primary goal of exhibits is to insert a model

    object within a rendering context. Purpose
  13. require 'delegate' class UserPresenter < SimpleDelegator def initialize(user, context) @context

    = context super(user) # Set up delegation end def gravatar_url md5 = Digest::MD5.hexdigest(email.downcase) "http://gravatar.com/avatar/#{md5}.png" end def gravatar @context.image_tag(gravatar_url) end end
  14. bob_account = Account.new("Bob", 100.0) alice_account = Account.new("Alice", 50.0) context =

    MoneyTransferContext.new( bob_account, alice_account ) context.transfer(10.0) puts bob_account.amount # => 90.0 puts alice_account.amount # => 60.0
  15. class MoneyTransferContext < Struct.new(:source, :destination) module SourceRole def withdraw(amount) self.balance

    -= amount end end module DestinationRole def deposit(amount) self.amount += amount end end end
  16. bob_account = Account.new("Bob", 100.0) alice_account = Account.new("Alice", 50.0) bob_account.extend SourceRole

    bob_account.withdraw(amount) # => 90.0 alice_account.withdraw(amount) # NoMethodError: undefined method...
  17. class MoneyTransferContext < Struct.new(:source, :destination) # ... def transfer(amount) source.extend

    SourceRole destination.extend DestinationRole source.withdraw(amount) destination.deposit(amount) end end