SOLID Ruby SOLID Rails

Preview:

DESCRIPTION

Establishing a sustainable codebase

Citation preview

SOLID Ruby - SOLID RailsEstablishing a sustainable codebase

Michael Mahlberg, Consulting Guild AGJens-Christian Fischer, InVisible GmbH

1

Michael Mahlberg

Who?

2

roughly a dozen companies over the last two decades

Founder of

3

>> relevance=> nil

4

A consultant on software processes, architecture & design for > 2 decades

Working as

5

>> relevance != nil=> true

6

Jens-Christian Fischer

Who?

7

and generally interested in waytoo many things

Tinkerer, Practician, Author

8

What is SOLID?

9

SOLIDis

nota

Law

10

Agile Software Development, Principles, Patterns, and Practices

PPP(by Robert C. Martin)

11

You know - more like guidelines

Principles!

12

OL DS IOL D

SRP OCP LSP ISP DIP

S I

13

OL DS I

SRP OCP LSP ISP DIP

14

A class should have one, and only one, reason to change.

SingleResponsibilityPrinciple

SRP

15

User Classrequire  'digest/sha1'

class  User  <  ActiveRecord::Base    include  Authentication    include  Authentication::ByPassword    include  Authentication::ByCookieToken

   #TODO  Check  login  redirect  if  this  filter  is  skipped    #skip_after_filter  :store_location

   #  Virtual  attribute  for  the  unencrypted  password    attr_accessor  :password

   belongs_to  :country

   has_one  :user_profile,  :dependent  =>  :destroy    has_one  :note            has_many  :queries    has_many  :tags,  :foreign_key  =>  "created_by"    has_many  :taggings,  :as  =>  :tagger    has_many  :organizations,  :through  =>  :affiliations    has_many  :affiliations    has_many  :locations,  :through  =>  :affiliations    has_many  :projects,  :through  =>  :memberships    has_many  :memberships    has_many  :public_assets,  :through  =>  :privileges    has_many  :descriptions,  :through  =>  :privileges    has_many  :assessments,  :through  =>  :privileges    has_many  :description_profiles,  :through  =>  :privileges    has_many  :privileges    has_many  :diaries    has_many  :roles,  :through  =>  :commitments    has_many  :commitments    has_many  :activities    has_many  :messages    has_many  :fellowships    has_many  :user_groups,  :through  =>  :fellowships    has_many  :survey_responses          has_many  :favorites    has_many  :assets,  :as  =>  :object    has_many  :links,  :as  =>  :object    has_many  :finders,  :as  =>  :findable    has_many  :keywords,  :through  =>  :finders    has_many  :tasks

   accepts_nested_attributes_for  :user_profile,                                                                :allow_destroy  =>  true,                                                                :reject_if  =>  proc  {  |attrs|  attrs.all?  {  |k,  v|  v.blank?  }  }

   #  prevents  a  user  from  submitting  a  crafted  form  that  bypasses  activation    #  anything  else  you  want  your  user  to  change  should  be  added  here.

   attr_accessible  :login,  :email,  :password,  :password_confirmation,  :city,                                    :zip,  :province,  :country_id,  :adr1,  :phone1,  :adr2,  :phone2,  :organization,  :locale,  :time_zone,                                    :receive_newsletter,  :last_login_at,  :last_login_ip,  :sign_up_context,  :last_logout_context,  :password_reset_code,                                    :lock_version,  :created_at,  :updated_at,  :created_by,  :updated_by,  :will_participate,  :first_name,  :lastname,  :keyword_ids

   validates_presence_of          :login,  :email,  :country_id    validates_presence_of          :password,                                      :if  =>  :password_required?    validates_length_of              :password,  :within  =>  6..20,  :if  =>  :password_required?    validates_presence_of          :password_confirmation,            :if  =>  :password_required?    validates_confirmation_of  :password,                                      :if  =>  :password_required?    validates_uniqueness_of      :login,  :email,  :case_sensitive  =>  false    validates_length_of              :login,        :within  =>  3..20    validates_format_of              :login,        :with  =>  Authentication.login_regex,  :message  =>  Authentication.bad_login_message    validates_length_of              :email,        :within  =>  5..100    validates_format_of              :email,        :with  =>  Authentication.email_regex,  :message  =>  Authentication.bad_email_message

   before_save  :encrypt_password    after_create  :create_profile

   acts_as_state_machine  :initial  =>  :pending

   state  :pending,  :enter  =>  :make_activation_code    state  :active,    :enter  =>  :do_activate    state  :suspended    state  :published,  :enter  =>  :do_publish    state  :suspended,  :enter  =>  :do_suspend    state  :deleted,  :enter  =>  :do_delete

   event  :register  do        transitions  :from  =>  :passive,  :to  =>  :pending,  :guard  =>  Proc.new  {|u|  !(u.crypted_password.blank?  &&  u.password.blank?)  }    end

   event  :activate  do        transitions  :from  =>  :pending,  :to  =>  :active    end

   event  :suspend  do        transitions  :from  =>  [:pending,  :active,  :published],  :to  =>  :suspended    end

   event  :delete  do        transitions  :from  =>  [:pending,  :active,  :suspended,],  :to  =>  :deleted    end        event  :publish  do        transitions  :from  =>  :active,  :to  =>  :published    end        event  :unpublish  do          transitions  :from  =>  :published,  :to  =>  :active    end

   event  :unsuspend  do        transitions  :from  =>  :suspended,  :to  =>  :active,    :guard  =>  Proc.new  {|u|  !u.activated_at.blank?  }        transitions  :from  =>  :suspended,  :to  =>  :pending,  :guard  =>  Proc.new  {|u|  !u.activation_code.blank?  }        #transitions  :from  =>  :suspended,  :to  =>  :published    end

   event  :system_activate  do        transitions  :from  =>  :pending,  :to  =>  :active    end

   #  TODO  BEFORE/AFTER  MIGRATION

   delegate  :description,  :to  =>  :user_profile,  :default  =>  "n/a"    delegate  :education,  :to  =>  :user_profile,  :default  =>  "n/a"    delegate  :competence,  :to  =>  :user_profile,  :default  =>  "n/a"    delegate  :interests,  :to  =>  :user_profile,  :default  =>  "n/a"        def  message_threads        self.message_threads  +  self.message_threads    end        def  contacts        (self.message_threads.collect  {  |mt|  mt.receiver}  +  self.message_threads.collect  {  |mt|  mt.sender}).uniq    end

   def  available_user_groups        self.fellowships.all(:conditions  =>  ["state  =  ?  OR  state  =  ?",'published','active']).collect  {  |f|  f.user_group  }    end

   def  user_groups        self.fellowships.collect  {  |f|  f.user_group  }    end

   #  Authenticates  a  user  by  their  login  name  and  unencrypted  password.    Returns  the  user  or  nil.    def  self.authenticate(login,  password)        #  u  =  find_in_state  :first,  :active,  :conditions  =>  {:login  =>  login}  #  need  to  get  the  salt        u  =  User.first  :conditions  =>  ['(state  =  ?  OR  state  =  ?  OR  state  =  ?  OR  state  =  ?)  AND  login  =  ?',  'active',  'published',  'hidden',  'private',  login]        u  &&  u.authenticated?(password)  ?  u  :  nil    end

   def  login=(value)        write_attribute  :login,  (value  ?  value.downcase  :  nil)    end

   def  email=(value)        write_attribute  :email,  (value  ?  value.downcase  :  nil)    end

   #  Encrypts  some  data  with  the  salt.    def  self.encrypt(password,  salt)        Digest::SHA1.hexdigest("-­‐-­‐#{salt}-­‐-­‐#{password}-­‐-­‐")    end

   #  Encrypts  the  password  with  the  user  salt    def  encrypt(password)        self.class.encrypt(password,  salt)    end

   def  authenticated?(password)        crypted_password  ==  encrypt(password)    end

   def  remember_token?        remember_token_expires_at  &&  Time.now.utc  <  remember_token_expires_at    end

   #  These  create  and  unset  the  fields  required  for  remembering  users  between  browser  closes    def  remember_me        remember_me_for  2.weeks    end

   def  remember_me_for(time)        remember_me_until  time.from_now.utc    end

   def  remember_me_until(time)        self.remember_token_expires_at  =  time        self.remember_token  =  encrypt("#{email}-­‐-­‐#{remember_token_expires_at}")        save(false)    end

   def  forget_me        self.remember_token_expires_at  =  nil        self.remember_token  =  nil        save(false)    end

   def  forgot_password        @forgotten_password  =  true        self.make_password_reset_code    end

   def  reset_password        #  First  update  the  password_reset_code  before  setting  the        #  reset_password  flag  to  avoid  duplicate  email  notifications.        update_attributes(:password_reset_code  =>  nil)        @reset_password  =  true    end

   #used  in  user_observer    def  recently_forgot_password?        @forgotten_password    end

   def  recently_reset_password?        @reset_password    end

   def  recently_activated?        @recent_active    end

   def  forum_nickname        self.user_profile.nickname.blank?  ?  "#{self.first_name}  #{self.last_name}"  :  self.user_profile.nickname    end

   def  name        "#{self.first_name}  #{self.last_name}"  rescue  'n/a'    end

   def  email_with_name        "#{self.first_name}  #{self.last_name}  <#{self.email}>"    end

   def  is_admin?        self.roles.collect{|role|  role.title}.include?('admin')    end        def  countries        [self.country]    end

   #def  tags    #    tag_names  =  Tag.all(:conditions  =>  {:created_by  =>  self}).collect  {|t|  t.name.split(",")}.flatten.uniq    #    tags  =  tag_names.collect  {|t|  t.strip}.sort    #end

   def  update_roles(role_hash)        return  unless  role_hash        new_roles  =  role_hash.keys        self.roles  =  Role.find(new_roles)    end

   def  private_locations        self.locations.select{  |l|  l.organization_id.nil?  }.sort_by{  |l|  l.name  }    end

   def  organization_locations        self.affiliations.select{  |a|  !a.organization_id.nil?  }.sort_by{  |a|  a.position_in_user_context  }    end

   def  avatar        self.user_profile.avatar    end

   def  readable_by?(user=nil)        self.created_by  ==  user    end

   def  editable_by?(user=nil)        self.created_by  ==  user    end

   def  readable_by?(user=nil)        self.created_by  ==  user    end

   def  boards        Board.all  :conditions  =>  {  :user_group_id  =>  self.user_groups.collect{  |g|  g.id  }}    end            def  discussions          Discussion.all  :conditions  =>  {  :board_id  =>  self.boards.collect{  |b|  b.id  }}      end        def  organization_roles        role_ids  =  Affiliation.all(:conditions  =>  {:user_id  =>  self.id}).collect{|a|  a.role_id}.uniq        roles  =  Role.find(role_ids)    end        def  user_group_roles        role_ids  =  Fellowship.all(:conditions  =>  {:user_id  =>  self.id}).collect{|a|  a.role_id}.uniq        roles  =  Role.find(role_ids)    end        def  project_roles        role_ids  =  Membership.all(:conditions  =>  {:user_id  =>  self.id}).collect{|a|  a.role_id}.uniq        roles  =  Role.find(role_ids)    end        def  all_roles        roles  =  (self.organization_roles  +  self.user_group_roles  +  self.project_roles).uniq    end

   def  tags_of(user)        taggings  =  Tagging.all  :conditions  =>  {:taggable_type  =>  "User",  :taggable_id  =>  self.id,  :tagger_type  =>  "User",  :tagger_id  =>  user.id}        tags  =  taggings.collect  {|t|  t.tag.name}.uniq.sort    end

   def  self.tags_of(user)        taggings  =  Tagging.all  :conditions  =>  {:taggable_type  =>  "User",  :tagger_type  =>  "User",  :tagger_id  =>  user.id}        tags  =  taggings.collect  {|t|  t.tag.name}.uniq.sort    end        def  avatar        self.user_profile.avatar  ?  self.user_profile.avatar  :  ""    end        def  self.keyword_categories        KeywordCategory.all(:conditions  =>  {  :for_users  =>  1  })      end        def  keywords_by_category(keyword_category)        self.keywords.select{  |k|  k.keyword_category.id  ==  keyword_category.id  }.collect    end        def  message_threads        MessageThread.all(:conditions  =>  ["sender_id  =  ?  or  receiver_id  =  ?",  self.id,  self.id])    end        def  contacts        self.message_threads.collect  {|mt|  [mt.sender,  mt.receiver]}.flatten.uniq  -­‐  [self]    end

   protected

   def  create_profile        UserProfile.create(:user  =>  self,  :created_by  =>  self.created_by)    end

   #  before  filter    def  encrypt_password        return  if  password.blank?        self.salt  =  Digest::SHA1.hexdigest("-­‐-­‐#{Time.now.to_s}-­‐-­‐#{login}-­‐-­‐")  if  new_record?        self.crypted_password  =  encrypt(password)    end

   def  password_required?        crypted_password.blank?  ||  !password.blank?    end

   def  make_activation_code        self.deleted_at  =  nil        self.activation_code  =  Digest::SHA1.hexdigest(  Time.now.to_s.split(//).sort_by  {rand}.join  )    end

   def  do_delete        self.deleted_at  =  Time.now.utc    end

   def  do_activate        self.activated_at  =  Time.now.utc        self.deleted_at  =  self.activation_code  =  nil        @recent_active  =  true    end        def  do_publish        self.published_at  =  Time.now    end        def  do_suspend        self.suspended_at  =  Time.now    end

   def  make_password_reset_code        self.password_reset_code  =  Digest::SHA1.hexdigest(  Time.now.to_s.split(//).sort_by  {rand}.join  )

   end        def  self.published_users        User.all(:conditions  =>  ['state  =  ?',  'published'],  :order  =>  'login  ASC',  :include  =>  [:user_profile])    end

end

16

So what‘s wrong with this?

17

class User < ActiveRecord::Base include Authentication include Authentication::ByPassword include Authentication::ByCookieToken... belongs_to :country... has_one :user_profile, :dependent => :destroy has_many :queries has_many :tags, :foreign_key => "created_by"... validates_presence_of :login, :email, :country_id validates_presence_of :password, :if => :password_required?...

From: user.rb

18

acts_as_state_machine :initial => :pending

state :pending, :enter => :make_activation_code state :active, :enter => :do_activate... event :register do transitions :from => :passive, :to => :pending, :guard => Proc.new {|u| !(u.crypted_password.blank? && u.password.blank?) } end... def message_threads self.message_threads + self.message_threads end

From: user.rb

19

def forum_nickname self.user_profile.nickname.blank? ? "#{self.first_name} #{self.last_name}" : self.user_profile.nickname end

def name "#{self.first_name} #{self.last_name}" rescue 'n/a' end

def email_with_name "#{self.first_name} #{self.last_name} <#{self.email}>" end

From: user.rb

20

def is_admin? self.roles.collect{|role| role.title}.include?('admin') end

def countries [self.country] end

From: user.rb

21

def boards Board.all :conditions => { :user_group_id => self.user_groups.collect{ |g| g.id }} end

def discussions Discussion.all :conditions => { :board_id => self.boards.collect{ |b| b.id }} end

def organization_roles role_ids = Affiliation.all(:conditions => {:user_id => self.id}).collect{|a| a.role_id}.uniq roles = Role.find(role_ids) end

From: user.rb

22

def make_password_reset_code self.password_reset_code = Digest::SHA1.hexdigest( Time.now.to_s.split(//).sort_by {rand}.join ) end

def self.published_users User.all(:conditions => ['state = ?', 'published'], :order => 'login ASC', :include => [:user_profile]) end

From: user.rb

23

Anyone notice a pattern?

24

Neither do we

25

Separation of Concerns

26

AuthenticationRoles

MailersState

...

27

So how?

Mixins

28

class User < ActiveRecord::Base include Authentication include Authentication::ByPassword include Authentication::ByCookieToken

include Project::UserStates include Project::UserMailer include Project::UserForum include Project::UserMessages...end

New User Model

29

module Project module UserMessages # to be included in User Model

has_many :messages def message_threads MessageThread.all(:conditions => ["sender_id = ? or receiver_id = ?", self.id, self.id]) endendend

UserMessages

30

Methods

31

def transfer(data, url) h = Net::HTTP.new(self.uri.host, self.uri.port) RAILS_DEFAULT_LOGGER.debug "connecting to CL: #{self.uri}" RAILS_DEFAULT_LOGGER.debug "connecting to CL: #{url}"

resp = h.post(url, data, {'Content-Type' => 'application/xml'}) response_code = resp.code.to_i location = if response_code == 201 resp['Location'] else RAILS_DEFAULT_LOGGER.debug "error from CL: #{response_code}" RAILS_DEFAULT_LOGGER.debug "error from CL: #{resp.body}" @error = resp.body nil end [response_code, location] end

32

def transfer(data, document)

if document.cl_document_url != nil self.uri = URI.parse(document.cl_document_url ) h = Net::HTTP.new(self.uri.host, self.uri.port) response = h.post(self.uri, data, {'Content-Type' => 'application/xml'}) else h = Net::HTTP.new(self.uri.host, self.uri.port) response = h.post("/tasks", data, {'Content-Type' => 'application/xml'}) end response_code = response.code.to_i if response_code == 201 location = response['Location'] document.cl_document_url = location document.save! else nil end [response_code, location]end

33

def transfer data open_connection post data return locationend

def open_connection @http = Net::HTTP.new(self.uri.host, self.uri.port)end

def post data @response = http.post(self.url, data, {'Content-Type' => 'application/xml'})end

SRP Transfer

34

def location get_location if created? # returns nil if not created?end

def response_code @response.code.to_iend

def created? response_code == 201end

def get_location @response['Location']end

def error @response.bodyend

35

Add a 16-band equalizer & a

BlueRay®player to this...

36

And now to this...

37

OL DS I

SRP OCP LSP ISP DIP

38

You should be able to extend a classes behavior, without modifying it.

OpenClosedPrinciple

OCP

39

40

41

42

43

def makemove(map) x, y = map.my_position # calculate a move ...

if(valid_moves.size == 0) map.make_move( :NORTH ) else # choose move ... puts move # debug (like in the old days) map.make_move( move ) endend

class Map ... def make_move(direction) $stdout << ({:NORTH=>1, :SOUTH=>3, :EAST=>2, :WEST=>4}[direction]) $stdout << "\n" $stdout.flush endend

From the Google AI Challenge

(Tronbot)

44

def puts(*args) $stderr.puts *argsend

def p(*args) args.map!{|arg| arg.inspect} puts argsend

def print(*args) $stderr.print *argsend

From the Google AI Challenge (Tronbot)

45

Design Sketch

46

class Outputter

def initialize(io = $stderr) @io = io end

def puts(*args) @io.puts *args end

...end

out = Outputter.newout.puts "Testing"

47

OL DS I

SRP OCP LSP ISP DIP

48

Derived classes must be substitutable for their base classes.

LiskovSubstitutionPrinciple

LSP

49

Or so it seems...

No Problem in Ruby

50

no problem?

No Interface...

51

Wrong !

52

The classic violation

53

A square is a rectangle

54

Rectangle

setX setY

Square

setX setY

55

>> class Rectangle>> attr_accessor :width, :height>> end=> nil>> ?> shape = Rectangle.new=> #<Rectangle:0x10114fad0>>> shape.width=> nil>> shape.width=3>> shape.width=> 3>> shape.height=5>> shape.height=> 5>> shape.width=> 3

Rectange

56

>> class Square?> def width>> @dimension>> end?> def height>> @dimension>> end?> def width= n>> @dimension = n>> end?> def height= n>> @dimension = n>> end>> end

?> shape = Square.new=> #<Square:0x101107e88>?> puts shape.widthnil?> shape.width=3=> 3?> shape.width=> 3?> shape.height=> 3

Square

57

>> s = [Rectangle.new, Square.new]=> [#<Rectangle:0x1005642e8>, #<Square:0x100564298>]>> a_rectangle = s[rand(2)]=> #<Square:0x100564298>>> a_rectangle.height=1=> 1>> a_rectangle.width=3=> 3>> a_rectangle.height=>

A Problem...

Text

3

58

CCD Common Conceptual Denominator

59

dup

60

OL DS I

SRP OCP LSP ISP DIP

62

Make fine grained interfaces that are client specific.

InterfaceSegregationPrinciple

ISP

63

64

class UsersController < ApplicationController

ssl_required :new, :create, :edit, :update, :destroy, :activate, :change_passwort, :forgot_password, :reset_password, :make_profile, :my_contacts ssl_allowed :eula, :index, :show

access_control [:suspend, :unsuspend, :destroy, :purge, :delete, :admin, :ban, :remove_ban] => 'admin'

before_filter :find_user

skip_after_filter :store_location

def show unless @user == current_user redirect_to access_denied_path(@locale) else respond_to do |format| format.html format.js { render :partial => "users/#{@context.title}/#{@partial}" } end end end...

Users Controller

65

def activate logout_keeping_session! user = User.find_by_activation_code(params[:activation_code]) unless params[:activation_code].blank?

case when (!params[:activation_code].blank?) && user && !user.active? user.activate! flash[:notice] = t(:message_sign_up_complete) unless params[:context].blank? redirect_to login_path(:context => params[:context]) else redirect_to "/login" end when params[:activation_code].blank? flash[:error] = t(:message_activation_code_missing) redirect_back_or_default("/") else flash[:error] = t(:message_user_with_that_activation_code_missing) redirect_back_or_default("/") end end

more UsersController

66

class User < ActiveRecord::Base ...end

class Registration < ActiveRecord::Base set_table_name "users"

acts_as_state_machine :initial => :pending

state :pending, :enter => :make_activation_code state :active, :enter => :do_activate ...

event :activate do transitions :from => :pending, :to => :active end ...end

User Class Revisited

67

class RegistrationController < ApplicationController ... def activate logout_keeping_session! code_is_blank = params[:activation_code].blank? registration = Registration.find_by_activation_code(params[:activation_code]) unless code_is_blank

case when (!code_is_blank) && registration && !registratio.active? registration.activate! flash[:notice] = t(:message_sign_up_complete) unless params[:context].blank? redirect_to login_path(:context => params[:context]) else redirect_to "/login" end when code_is_blank flash[:error] = t(:message_activation_code_missing) redirect_back_or_default("/") else flash[:error] = t(:message_user_with_that_activation_code_missing) redirect_back_or_default("/") end end ...end

68

OL DS I

SRP OCP LSP ISP DIP

69

Depend on abstractions, not on concretions.

DependencyInversionPrinciple

DIP

70

71

out = Outputter.newout.puts "Testing"

From our OCP example to DIP

72

class TronBot def initialize @@out = TRON_ENVIRONMENT[:debugger] end def some_method ... @@out.puts "Testing" ... end end

The code we wish we had

73

TRON_ENVIRONMENT = { :debugger => Outputter.new ($stderr), :game_engine => Outputter.new ($stdout), :user_io => Outputter.new ($stderr) }

TSTTCPW

74

TRON_ENVIRONMENT = { :debugger => Outputter.new ($stderr), :game_engine => Outputter.new (TCP_OUTPUTTER), :user_io => Outputter.new ($stderr) }

Later...

75

format.js do render :update do |page| if @parent_object.class == EspGoal @esp_goal_descriptor = @current_object page.replace_html "descriptor_#{@current_object.id}", :partial => "edit_esp_goal_descriptor", :locals => {:esp_goal_descriptor => @esp_goal_descriptor, :parent_object => @parent_object} else @goal_descriptor = @current_object page.replace_html "descriptor_#{@current_object.id}", :partial => "edit_goal_descriptor", :locals => {:goal_descriptor => @goal_descriptor, :parent_object => @parent_object} end endend

DIP Violation in Controller

76

format.js do render :update do |page| if @parent_object.class == EspGoal @esp_goal_descriptor = @current_object page.replace_html "descriptor_#{@current_object.id}", :partial => "edit_esp_goal_descriptor", :locals => {:esp_goal_descriptor => @esp_goal_descriptor, :parent_object => @parent_object} else if @parent_object.class == Goal @goal_descriptor = @current_object page.replace_html "descriptor_#{@current_object.id}", :partial => "edit_goal_descriptor", :locals => {:goal_descriptor => @goal_descriptor, :parent_object => @parent_object} else if @parent_object.class == LearningGoal ... ... end endend

DIP Violation in Controller

77

78

def show ... format.js do render :update do |page| page.replace_html "descriptor_#{@current_object.id}", @parent_object.page_replacement(@current_object) end endend

class EspGoal def page_replacement child { :partial => "edit_esp_goal_descriptor", :locals => {:esp_goal_descriptor => child, :parent_object => self} } endend

class Goal def page_replacement child { :partial => "edit_goal_descriptor", :locals => {:goal_descriptor => child, :parent_object => self} } endend

1st Refactoring

79

80

class PartialContainer def add class_symbol, partial_replacement @@partinal_replacements.add( class_symbol => partial_replacement) end

def self.partial_replacement an_object unless @@partial_replacments self.add( EspGoalReplacement.my_class_sym, EspGoalReplacment.new) self.add( GoalReplacement.my_class_sym, GoalReplacment.new) end @@partial_replacement[an_object.class] endend

2nd Refactoring(wiring)

81

class EspGoalReplacmenent def self.my_class_sym EspGoal.to_sym end def partial_definition child { :partial => "edit_esp_goal_descriptor", :locals => {:esp_goal_descriptor => child, :parent_object => child.esp_goal} } endend

class GoalReplacmenent def self.my_class_sym Goal.to_sym end def partial_definition child { :partial => "edit_goal_descriptor", :locals => {:goal_descriptor => child, :parent_object => child.goal} } endend

2nd Refactoring(Behaviour)

82

format.js do render :update do |page| if @parent_object.class == EspGoal @esp_goal_descriptor = @current_object page.replace_html "descriptor_#{@current_object.id}", :partial => "edit_esp_goal_descriptor", :locals => {:esp_goal_descriptor => @esp_goal_descriptor, :parent_object => @parent_object} else @goal_descriptor = @current_object page.replace_html "descriptor_#{@current_object.id}", :partial => "edit_goal_descriptor", :locals => {:goal_descriptor => @goal_descriptor, :parent_object => @parent_object} end endend

DIP Violation in Controller

83

def show ... format.js do render :update do |page| page.replace_html "descriptor_#{@current_object.id}", PartialContainer.partial_replacement(@parent_object). partial_definition(@current_object) end endend

2nd Refactoring- the Controller -

84

85

OL DS IOL D

SRP OCP LSP ISP DIP

S I

86

SRP OCP LSP ISP DIP

87

SRP OCP LSP ISP DIP

DOLS IQuestions?

88

Vielen Dank!

89

93

Michael Mahlberg

Consulting Guild AG

@MMahlberg

mm@michaelmahlberg.de

http://agile-aspects.blogspot.com

Jens-Christian Fischer

InVisible GmbH

@jcfischer

jens-christian@invisible.ch

http://blog.invisible.ch

93