93
SOLID Ruby - SOLID Rails Establishing a sustainable codebase Michael Mahlberg, Consulting Guild AG Jens-Christian Fischer, InVisible GmbH 1

SOLID Ruby SOLID Rails

Embed Size (px)

DESCRIPTION

Establishing a sustainable codebase

Citation preview

Page 1: SOLID Ruby SOLID Rails

SOLID Ruby - SOLID RailsEstablishing a sustainable codebase

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

1

Page 2: SOLID Ruby SOLID Rails

Michael Mahlberg

Who?

2

Page 3: SOLID Ruby SOLID Rails

roughly a dozen companies over the last two decades

Founder of

3

Page 4: SOLID Ruby SOLID Rails

>> relevance=> nil

4

Page 5: SOLID Ruby SOLID Rails

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

Working as

5

Page 6: SOLID Ruby SOLID Rails

>> relevance != nil=> true

6

Page 7: SOLID Ruby SOLID Rails

Jens-Christian Fischer

Who?

7

Page 8: SOLID Ruby SOLID Rails

and generally interested in waytoo many things

Tinkerer, Practician, Author

8

Page 9: SOLID Ruby SOLID Rails

What is SOLID?

9

Page 10: SOLID Ruby SOLID Rails

SOLIDis

nota

Law

10

Page 11: SOLID Ruby SOLID Rails

Agile Software Development, Principles, Patterns, and Practices

PPP(by Robert C. Martin)

11

Page 12: SOLID Ruby SOLID Rails

You know - more like guidelines

Principles!

12

Page 13: SOLID Ruby SOLID Rails

OL DS IOL D

SRP OCP LSP ISP DIP

S I

13

Page 14: SOLID Ruby SOLID Rails

OL DS I

SRP OCP LSP ISP DIP

14

Page 15: SOLID Ruby SOLID Rails

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

SingleResponsibilityPrinciple

SRP

15

Page 16: SOLID Ruby SOLID Rails

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

Page 17: SOLID Ruby SOLID Rails

So what‘s wrong with this?

17

Page 18: SOLID Ruby SOLID Rails

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

Page 19: SOLID Ruby SOLID Rails

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

Page 20: SOLID Ruby SOLID Rails

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

Page 21: SOLID Ruby SOLID Rails

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

def countries [self.country] end

From: user.rb

21

Page 22: SOLID Ruby SOLID Rails

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

Page 23: SOLID Ruby SOLID Rails

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

Page 24: SOLID Ruby SOLID Rails

Anyone notice a pattern?

24

Page 25: SOLID Ruby SOLID Rails

Neither do we

25

Page 26: SOLID Ruby SOLID Rails

Separation of Concerns

26

Page 27: SOLID Ruby SOLID Rails

AuthenticationRoles

MailersState

...

27

Page 28: SOLID Ruby SOLID Rails

So how?

Mixins

28

Page 29: SOLID Ruby SOLID Rails

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

Page 30: SOLID Ruby SOLID Rails

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

Page 31: SOLID Ruby SOLID Rails

Methods

31

Page 32: SOLID Ruby SOLID Rails

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

Page 33: SOLID Ruby SOLID Rails

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

Page 34: SOLID Ruby SOLID Rails

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

Page 35: SOLID Ruby SOLID Rails

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

Page 36: SOLID Ruby SOLID Rails

Add a 16-band equalizer & a

BlueRay®player to this...

36

Page 37: SOLID Ruby SOLID Rails

And now to this...

37

Page 38: SOLID Ruby SOLID Rails

OL DS I

SRP OCP LSP ISP DIP

38

Page 39: SOLID Ruby SOLID Rails

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

OpenClosedPrinciple

OCP

39

Page 40: SOLID Ruby SOLID Rails

40

Page 41: SOLID Ruby SOLID Rails

41

Page 42: SOLID Ruby SOLID Rails

42

Page 43: SOLID Ruby SOLID Rails

43

Page 44: SOLID Ruby SOLID Rails

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

Page 45: SOLID Ruby SOLID Rails

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

Page 46: SOLID Ruby SOLID Rails

Design Sketch

46

Page 47: SOLID Ruby SOLID Rails

class Outputter

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

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

...end

out = Outputter.newout.puts "Testing"

47

Page 48: SOLID Ruby SOLID Rails

OL DS I

SRP OCP LSP ISP DIP

48

Page 49: SOLID Ruby SOLID Rails

Derived classes must be substitutable for their base classes.

LiskovSubstitutionPrinciple

LSP

49

Page 50: SOLID Ruby SOLID Rails

Or so it seems...

No Problem in Ruby

50

Page 51: SOLID Ruby SOLID Rails

no problem?

No Interface...

51

Page 52: SOLID Ruby SOLID Rails

Wrong !

52

Page 53: SOLID Ruby SOLID Rails

The classic violation

53

Page 54: SOLID Ruby SOLID Rails

A square is a rectangle

54

Page 55: SOLID Ruby SOLID Rails

Rectangle

setX setY

Square

setX setY

55

Page 56: SOLID Ruby SOLID Rails

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

Page 57: SOLID Ruby SOLID Rails

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

Page 58: SOLID Ruby SOLID Rails

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

Page 59: SOLID Ruby SOLID Rails

CCD Common Conceptual Denominator

59

Page 60: SOLID Ruby SOLID Rails

dup

60

Page 62: SOLID Ruby SOLID Rails

OL DS I

SRP OCP LSP ISP DIP

62

Page 63: SOLID Ruby SOLID Rails

Make fine grained interfaces that are client specific.

InterfaceSegregationPrinciple

ISP

63

Page 64: SOLID Ruby SOLID Rails

64

Page 65: SOLID Ruby SOLID Rails

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

Page 66: SOLID Ruby SOLID Rails

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

Page 67: SOLID Ruby SOLID Rails

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

Page 68: SOLID Ruby SOLID Rails

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

Page 69: SOLID Ruby SOLID Rails

OL DS I

SRP OCP LSP ISP DIP

69

Page 70: SOLID Ruby SOLID Rails

Depend on abstractions, not on concretions.

DependencyInversionPrinciple

DIP

70

Page 71: SOLID Ruby SOLID Rails

71

Page 72: SOLID Ruby SOLID Rails

out = Outputter.newout.puts "Testing"

From our OCP example to DIP

72

Page 73: SOLID Ruby SOLID Rails

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

The code we wish we had

73

Page 74: SOLID Ruby SOLID Rails

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

TSTTCPW

74

Page 75: SOLID Ruby SOLID Rails

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

Later...

75

Page 76: SOLID Ruby SOLID Rails

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

Page 77: SOLID Ruby SOLID Rails

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

Page 78: SOLID Ruby SOLID Rails

78

Page 79: SOLID Ruby SOLID Rails

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

Page 80: SOLID Ruby SOLID Rails

80

Page 81: SOLID Ruby SOLID Rails

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

Page 82: SOLID Ruby SOLID Rails

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

Page 83: SOLID Ruby SOLID Rails

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

Page 84: SOLID Ruby SOLID Rails

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

Page 85: SOLID Ruby SOLID Rails

85

Page 86: SOLID Ruby SOLID Rails

OL DS IOL D

SRP OCP LSP ISP DIP

S I

86

Page 87: SOLID Ruby SOLID Rails

SRP OCP LSP ISP DIP

87

Page 88: SOLID Ruby SOLID Rails

SRP OCP LSP ISP DIP

DOLS IQuestions?

88

Page 89: SOLID Ruby SOLID Rails

Vielen Dank!

89

Page 93: SOLID Ruby SOLID Rails

93

Michael Mahlberg

Consulting Guild AG

@MMahlberg

[email protected]

http://agile-aspects.blogspot.com

Jens-Christian Fischer

InVisible GmbH

@jcfischer

[email protected]

http://blog.invisible.ch

93