Michael Mahlberg, Consulting Guild AG
Jens-Christian Fischer, InVisible GmbH

SOLID Ruby -
SOLID Rails
Establishing a sustainable
codebase




                                         1
Who?

       Michael Mahlberg




                          2
Founder of

     roughly a dozen companies
     over the last two decades



                                 3
>> relevance
=> nil




               4
Working as

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


                                 5
>> relevance != nil
=> true




                      6
Who?

       Jens-Christian Fischer




                                7
Tinkerer, Practician,
      Author
            and generally
          interested in way
           too many things


                              8
What is
SOLID?

          9
SOLID
  is
 not
  a
 Law

        10
PPP
(by Robert C. Martin)




                        Agile Software
                        Development,
                        Principles, Patterns,
                        and Practices




                                                11
Principles!

        You know - more
         like guidelines




                           12
SOLID

SRP OCP LSP ISP DIP

                      13
S OL I D

SRP OCP LSP ISP DIP

                      14
SRP
      Single
      Responsibility
      Principle


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




                                         15
require	
  'digest/sha1'

class	
  User	
  <	
  ActiveRecord::Base
	
  	
  include	
  Authentication
                                         User Class
	
  	
  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	
  	
  	
                                        16
So what‘s wrong with
        this?


                       17
From: user.rb
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?
...




                                                                    18
From: user.rb
  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




                                                                19
From: user.rb
  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




                                                                20
From: user.rb
def is_admin?
  self.roles.collect{|role| role.title}.include?('admin')
end

def countries
  [self.country]
end




                                                            21
From: user.rb
 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




                                                             22
From: user.rb
  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




                                                        23
Anyone notice a pattern?



                           24
Neither do we



                25
Separation of Concerns



                         26
Authentication
    Roles
   Mailers
    State
     ...

                 27
So how?


  Mixins




           28
New User Model
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




                                          29
UserMessages
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])
  end
end
end




                                               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
SRP Transfer
def transfer data
  open_connection
  post data
  return location
end

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




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

def response_code
  @response.code.to_i
end

def created?
  response_code == 201
end

def get_location
  @response['Location']
end

def error
  @response.body
end




                                                           35
Add a 16-band
 equalizer & a
   BlueRay
player to this...




                    36
And now to
  this...




             37
S OL I D

SRP OCP LSP ISP DIP

                      38
OCP
  Open
  Closed
  Principle


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



                                    39
40
41
42
43
def makemove(map)
                                     From the Google
  x, y = map.my_position
  # calculate a move ...               AI Challenge
  if(valid_moves.size == 0)
    map.make_move( :NORTH )
                                        (Tronbot)
  else
    # choose move ...
    puts move # debug (like in the old days)
    map.make_move( move )
  end
end

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



                                                                         44
From the Google AI Challenge (Tronbot)
def puts(*args)
  $stderr.puts *args
end

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

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




                                         45
Design Sketch




                46
class Outputter

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

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

  ...
end

out = Outputter.new
out.puts "Testing"




                                 47
S OL I D

SRP OCP LSP ISP DIP

                      48
LSP
      Liskov
      Substitution
      Principle


                     Derived classes
                     must be substitutable
                     for their base
                     classes.



                                             49
No Problem
  in Ruby

        Or so it seems...




                            50
No Interface...

            no problem?




                          51
Wrong !



          52
The classic violation



                        53
A square is a rectangle



                          54
Rectangle

setX
setY




        Square

setX
setY




                   55
Rectange
>>   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



                                       56
Square
>> class Square
?>   def width
>>     @dimension
                            ?> shape = Square.new
>>   end
                            => #<Square:0x101107e88>
?>   def height
                            ?> puts shape.width
>>     @dimension
                            nil
>>   end
                            ?> shape.width=3
?>   def width= n
                            => 3
>>     @dimension = n
                            ?> shape.width
>>   end
                            => 3
?>   def height= n
                            ?> shape.height
>>     @dimension = n
                            => 3
>>   end
>> end




                                                       57
A Problem...
>>   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
                                Text
>>   a_rectangle.height
=>   3




                                                         58
CCD Common Conceptual
     Denominator


                    59
dup



      60
irb 1:0> 5.respond_to? :dup
=> true
irb 2:0> 5.dup
TypeError: can't dup Fixnum
         from (irb):1:in `dup'
         from (irb):1
irb 3:0>




           http://blog.objectmentor.com/articles/2007/02/17/
           liskov-substitution-principle-and-the-ruby-core-libraries



                                                                  61
S OL I D

SRP OCP LSP ISP DIP

                      62
ISP
      Interface
      Segregation
      Principle


                    Make fine grained
                    interfaces that are
                    client specific.




                                          63
64
Users Controller
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
...


                                                                                  65
more UsersController
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



                                                                         66
User Class Revisited
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




                                                        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
S OL I D

SRP OCP LSP ISP DIP

                      69
DIP
      Dependency
      Inversion
      Principle


                   Depend on
                   abstractions, not on
                   concretions.




                                          70
71
From our OCP example to DIP



out = Outputter.new
out.puts "Testing"




                               72
The code we wish we had
class TronBot
  def initialize
    @@out = TRON_ENVIRONMENT[:debugger]
  end

  def some_method
    ...
    @@out.puts "Testing"
    ...
  end

end




                                          73
TSTTCPW


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




                                                   74
Later...


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




                                                         75
DIP Violation in Controller
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
  end
end




                                                                      76
DIP Violation in Controller
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
  end
end


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

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

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

                                                                           79
80
2nd Refactoring
                                             (wiring)
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]
  end
end




                                                                          81
class EspGoalReplacmenent
                                                   2nd Refactoring
  def self.my_class_sym

  end
      EspGoal.to_sym                               (Behaviour)
  def partial_definition child
  { :partial => "edit_esp_goal_descriptor",
       :locals => {:esp_goal_descriptor => child,
                   :parent_object => child.esp_goal}
    }
  end
end

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}
    }
  end
end



                                                                 82
DIP Violation in Controller
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
  end
end



                                                                      83
2nd Refactoring
                   - the Controller -
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
  end
end




                                                                                84
85
SOLID

SRP OCP LSP ISP DIP

                      86
SRP OCP LSP ISP DIP

                      87
Questions?
      S OL ID

SRP OCP LSP ISP DIP

                        88
Vielen Dank!



               89
Credits (1/2)
PPP-Article (online)
http://butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod

Photos
http://www.flickr.com/photos/dieterkarner/370967891/
http://www.flickr.com/photos/popcorncx/2221630487/sizes/l/
http://www.flickr.com/photos/bdesham/2432400623/
http://www.flickr.com/photos/popcorncx/2221630487/
http://www.flickr.com/photos/glennbatuyong/4081599002/in/photostream/
http://www.flickr.com/photos/glennbatuyong/4081599168/in/photostream/
http://www.flickr.com/photos/renfield/3865907619/


                                                                        90
                                                                             90
Credits (2/2)
Photos
http://www.flickr.com/photos/renfield/3865907619/
http://www.flickr.com/photos/maxpower/5160699/
http://programmer.97things.oreilly.com/wiki/index.php/Uncle_Bob
http://www.flickr.com/photos/georgivar/3288942086/
http://www.everystockphoto.com/photo.php?imageId=237523
http://www.flickr.com/photos/pasukaru76/3992935923/




                                                                  91
                                                                       91
Lizense


http://creativecommons.org/licenses/by-sa/
  3.0/de/




                                         92
                                              92
Jens-Christian Fischer        Michael Mahlberg

InVisible GmbH                Consulting Guild AG



@jcfischer                     @MMahlberg

jens-christian@invisible.ch   mm@michaelmahlberg.de

http://blog.invisible.ch      http://agile-aspects.blogspot.com




                                                             93
                                                                  93

SOLID Ruby SOLID Rails

  • 1.
    Michael Mahlberg, ConsultingGuild AG Jens-Christian Fischer, InVisible GmbH SOLID Ruby - SOLID Rails Establishing a sustainable codebase 1
  • 2.
    Who? Michael Mahlberg 2
  • 3.
    Founder of roughly a dozen companies over the last two decades 3
  • 4.
  • 5.
    Working as A consultant on software processes, architecture & design for > 2 decades 5
  • 6.
    >> relevance !=nil => true 6
  • 7.
    Who? Jens-Christian Fischer 7
  • 8.
    Tinkerer, Practician, Author and generally interested in way too many things 8
  • 9.
  • 10.
    SOLID is not a Law 10
  • 11.
    PPP (by Robert C.Martin) Agile Software Development, Principles, Patterns, and Practices 11
  • 12.
    Principles! You know - more like guidelines 12
  • 13.
  • 14.
    S OL ID SRP OCP LSP ISP DIP 14
  • 15.
    SRP Single Responsibility Principle A class should have one, and only one, reason to change. 15
  • 16.
    require  'digest/sha1' class  User  <  ActiveRecord::Base    include  Authentication User Class    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       16
  • 17.
    So what‘s wrongwith this? 17
  • 18.
    From: user.rb 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? ... 18
  • 19.
    From: user.rb 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 19
  • 20.
    From: user.rb 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 20
  • 21.
    From: user.rb def is_admin? self.roles.collect{|role| role.title}.include?('admin') end def countries [self.country] end 21
  • 22.
    From: user.rb defboards 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 22
  • 23.
    From: user.rb 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 23
  • 24.
    Anyone notice apattern? 24
  • 25.
  • 26.
  • 27.
    Authentication Roles Mailers State ... 27
  • 28.
    So how? Mixins 28
  • 29.
    New User Model classUser < ActiveRecord::Base include Authentication include Authentication::ByPassword include Authentication::ByCookieToken include Project::UserStates include Project::UserMailer include Project::UserForum include Project::UserMessages ... end 29
  • 30.
    UserMessages 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]) end end end 30
  • 31.
  • 32.
    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
  • 33.
    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
  • 34.
    SRP Transfer def transferdata open_connection post data return location end 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 34
  • 35.
    def location get_location if created? # returns nil if not created? end def response_code @response.code.to_i end def created? response_code == 201 end def get_location @response['Location'] end def error @response.body end 35
  • 36.
    Add a 16-band equalizer & a BlueRay player to this... 36
  • 37.
    And now to this... 37
  • 38.
    S OL ID SRP OCP LSP ISP DIP 38
  • 39.
    OCP Open Closed Principle You should be able to extend a classes behavior, without modifying it. 39
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
    def makemove(map) From the Google x, y = map.my_position # calculate a move ... AI Challenge if(valid_moves.size == 0) map.make_move( :NORTH ) (Tronbot) else # choose move ... puts move # debug (like in the old days) map.make_move( move ) end end class Map ... def make_move(direction) $stdout << ({:NORTH=>1, :SOUTH=>3, :EAST=>2, :WEST=>4}[direction]) $stdout << "n" $stdout.flush end end 44
  • 45.
    From the GoogleAI Challenge (Tronbot) def puts(*args) $stderr.puts *args end def p(*args) args.map!{|arg| arg.inspect} puts args end def print(*args) $stderr.print *args end 45
  • 46.
  • 47.
    class Outputter def initialize(io = $stderr) @io = io end def puts(*args) @io.puts *args end ... end out = Outputter.new out.puts "Testing" 47
  • 48.
    S OL ID SRP OCP LSP ISP DIP 48
  • 49.
    LSP Liskov Substitution Principle Derived classes must be substitutable for their base classes. 49
  • 50.
    No Problem in Ruby Or so it seems... 50
  • 51.
    No Interface... no problem? 51
  • 52.
  • 53.
  • 54.
    A square isa rectangle 54
  • 55.
    Rectangle setX setY Square setX setY 55
  • 56.
    Rectange >> 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 56
  • 57.
    Square >> class Square ?> def width >> @dimension ?> shape = Square.new >> end => #<Square:0x101107e88> ?> def height ?> puts shape.width >> @dimension nil >> end ?> shape.width=3 ?> def width= n => 3 >> @dimension = n ?> shape.width >> end => 3 ?> def height= n ?> shape.height >> @dimension = n => 3 >> end >> end 57
  • 58.
    A Problem... >> 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 Text >> a_rectangle.height => 3 58
  • 59.
    CCD Common Conceptual Denominator 59
  • 60.
    dup 60
  • 61.
    irb 1:0> 5.respond_to?:dup => true irb 2:0> 5.dup TypeError: can't dup Fixnum from (irb):1:in `dup' from (irb):1 irb 3:0> http://blog.objectmentor.com/articles/2007/02/17/ liskov-substitution-principle-and-the-ruby-core-libraries 61
  • 62.
    S OL ID SRP OCP LSP ISP DIP 62
  • 63.
    ISP Interface Segregation Principle Make fine grained interfaces that are client specific. 63
  • 64.
  • 65.
    Users Controller 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 ... 65
  • 66.
    more UsersController 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 66
  • 67.
    User Class Revisited classUser < 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 67
  • 68.
    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
  • 69.
    S OL ID SRP OCP LSP ISP DIP 69
  • 70.
    DIP Dependency Inversion Principle Depend on abstractions, not on concretions. 70
  • 71.
  • 72.
    From our OCPexample to DIP out = Outputter.new out.puts "Testing" 72
  • 73.
    The code wewish we had class TronBot def initialize @@out = TRON_ENVIRONMENT[:debugger] end def some_method ... @@out.puts "Testing" ... end end 73
  • 74.
    TSTTCPW TRON_ENVIRONMENT = { :debugger => Outputter.new ($stderr), :game_engine => Outputter.new ($stdout), :user_io => Outputter.new ($stderr) } 74
  • 75.
    Later... TRON_ENVIRONMENT = { :debugger => Outputter.new ($stderr), :game_engine => Outputter.new (TCP_OUTPUTTER), :user_io => Outputter.new ($stderr) } 75
  • 76.
    DIP Violation inController 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 end end 76
  • 77.
    DIP Violation inController 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 end end 77
  • 78.
  • 79.
    1st Refactoring def show ... format.js do render :update do |page| page.replace_html "descriptor_#{@current_object.id}", @parent_object.page_replacement(@current_object) end end end class EspGoal def page_replacement child { :partial => "edit_esp_goal_descriptor", :locals => {:esp_goal_descriptor => child, :parent_object => self} } end end class Goal def page_replacement child { :partial => "edit_goal_descriptor", :locals => {:goal_descriptor => child, :parent_object => self} } end end 79
  • 80.
  • 81.
    2nd Refactoring (wiring) 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] end end 81
  • 82.
    class EspGoalReplacmenent 2nd Refactoring def self.my_class_sym end EspGoal.to_sym (Behaviour) def partial_definition child { :partial => "edit_esp_goal_descriptor", :locals => {:esp_goal_descriptor => child, :parent_object => child.esp_goal} } end end 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} } end end 82
  • 83.
    DIP Violation inController 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 end end 83
  • 84.
    2nd Refactoring - the Controller - 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 end end 84
  • 85.
  • 86.
  • 87.
    SRP OCP LSPISP DIP 87
  • 88.
    Questions? S OL ID SRP OCP LSP ISP DIP 88
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
    Jens-Christian Fischer Michael Mahlberg InVisible GmbH Consulting Guild AG @jcfischer @MMahlberg jens-christian@invisible.ch mm@michaelmahlberg.de http://blog.invisible.ch http://agile-aspects.blogspot.com 93 93