Download - SOLID Ruby SOLID Rails
![Page 1: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/1.jpg)
SOLID Ruby - SOLID RailsEstablishing a sustainable codebase
Michael Mahlberg, Consulting Guild AGJens-Christian Fischer, InVisible GmbH
1
![Page 2: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/2.jpg)
Michael Mahlberg
Who?
2
![Page 3: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/3.jpg)
roughly a dozen companies over the last two decades
Founder of
3
![Page 4: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/4.jpg)
>> relevance=> nil
4
![Page 5: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/5.jpg)
A consultant on software processes, architecture & design for > 2 decades
Working as
5
![Page 6: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/6.jpg)
>> relevance != nil=> true
6
![Page 7: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/7.jpg)
Jens-Christian Fischer
Who?
7
![Page 8: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/8.jpg)
and generally interested in waytoo many things
Tinkerer, Practician, Author
8
![Page 9: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/9.jpg)
What is SOLID?
9
![Page 10: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/10.jpg)
SOLIDis
nota
Law
10
![Page 11: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/11.jpg)
Agile Software Development, Principles, Patterns, and Practices
PPP(by Robert C. Martin)
11
![Page 12: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/12.jpg)
You know - more like guidelines
Principles!
12
![Page 13: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/13.jpg)
OL DS IOL D
SRP OCP LSP ISP DIP
S I
13
![Page 14: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/14.jpg)
OL DS I
SRP OCP LSP ISP DIP
14
![Page 15: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/15.jpg)
A class should have one, and only one, reason to change.
SingleResponsibilityPrinciple
SRP
15
![Page 16: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/16.jpg)
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](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/17.jpg)
So what‘s wrong with this?
17
![Page 18: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/18.jpg)
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](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/19.jpg)
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](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/20.jpg)
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](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/21.jpg)
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](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/22.jpg)
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](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/23.jpg)
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](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/24.jpg)
Anyone notice a pattern?
24
![Page 25: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/25.jpg)
Neither do we
25
![Page 26: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/26.jpg)
Separation of Concerns
26
![Page 27: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/27.jpg)
AuthenticationRoles
MailersState
...
27
![Page 28: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/28.jpg)
So how?
Mixins
28
![Page 29: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/29.jpg)
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](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/30.jpg)
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](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/31.jpg)
Methods
31
![Page 32: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/32.jpg)
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](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/33.jpg)
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](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/34.jpg)
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](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/35.jpg)
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](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/36.jpg)
Add a 16-band equalizer & a
BlueRay®player to this...
36
![Page 37: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/37.jpg)
And now to this...
37
![Page 38: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/38.jpg)
OL DS I
SRP OCP LSP ISP DIP
38
![Page 39: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/39.jpg)
You should be able to extend a classes behavior, without modifying it.
OpenClosedPrinciple
OCP
39
![Page 40: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/40.jpg)
40
![Page 41: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/41.jpg)
41
![Page 42: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/42.jpg)
42
![Page 43: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/43.jpg)
43
![Page 44: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/44.jpg)
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](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/45.jpg)
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](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/46.jpg)
Design Sketch
46
![Page 47: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/47.jpg)
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](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/48.jpg)
OL DS I
SRP OCP LSP ISP DIP
48
![Page 49: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/49.jpg)
Derived classes must be substitutable for their base classes.
LiskovSubstitutionPrinciple
LSP
49
![Page 50: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/50.jpg)
Or so it seems...
No Problem in Ruby
50
![Page 51: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/51.jpg)
no problem?
No Interface...
51
![Page 52: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/52.jpg)
Wrong !
52
![Page 53: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/53.jpg)
The classic violation
53
![Page 54: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/54.jpg)
A square is a rectangle
54
![Page 55: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/55.jpg)
Rectangle
setX setY
Square
setX setY
55
![Page 56: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/56.jpg)
>> 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](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/57.jpg)
>> 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](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/58.jpg)
>> 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](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/59.jpg)
CCD Common Conceptual Denominator
59
![Page 60: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/60.jpg)
dup
60
![Page 61: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/61.jpg)
irb 1:0> 5.respond_to? :dup=> trueirb 2:0> 5.dupTypeError: can't dup Fixnum from (irb):1:in `dup' from (irb):1irb 3:0>
http://blog.objectmentor.com/articles/2007/02/17/liskov-substitution-principle-and-the-ruby-core-libraries
61
![Page 62: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/62.jpg)
OL DS I
SRP OCP LSP ISP DIP
62
![Page 63: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/63.jpg)
Make fine grained interfaces that are client specific.
InterfaceSegregationPrinciple
ISP
63
![Page 64: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/64.jpg)
64
![Page 65: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/65.jpg)
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](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/66.jpg)
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](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/67.jpg)
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](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/68.jpg)
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](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/69.jpg)
OL DS I
SRP OCP LSP ISP DIP
69
![Page 70: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/70.jpg)
Depend on abstractions, not on concretions.
DependencyInversionPrinciple
DIP
70
![Page 71: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/71.jpg)
71
![Page 72: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/72.jpg)
out = Outputter.newout.puts "Testing"
From our OCP example to DIP
72
![Page 73: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/73.jpg)
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](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/74.jpg)
TRON_ENVIRONMENT = { :debugger => Outputter.new ($stderr), :game_engine => Outputter.new ($stdout), :user_io => Outputter.new ($stderr) }
TSTTCPW
74
![Page 75: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/75.jpg)
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](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/76.jpg)
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](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/77.jpg)
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](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/78.jpg)
78
![Page 79: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/79.jpg)
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](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/80.jpg)
80
![Page 81: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/81.jpg)
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](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/82.jpg)
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](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/83.jpg)
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](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/84.jpg)
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](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/85.jpg)
85
![Page 86: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/86.jpg)
OL DS IOL D
SRP OCP LSP ISP DIP
S I
86
![Page 87: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/87.jpg)
SRP OCP LSP ISP DIP
87
![Page 88: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/88.jpg)
SRP OCP LSP ISP DIP
DOLS IQuestions?
88
![Page 89: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/89.jpg)
Vielen Dank!
89
![Page 90: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/90.jpg)
Credits (1/2)
PPP-Article (online)http://butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod
Photoshttp://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/
9090
![Page 91: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/91.jpg)
Credits (2/2)
Photoshttp://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/
9191
![Page 92: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/92.jpg)
Lizense
http://creativecommons.org/licenses/by-sa/3.0/de/
9292
![Page 93: SOLID Ruby SOLID Rails](https://reader034.vdocuments.us/reader034/viewer/2022042510/55493c9cb4c9050f4d8b4d22/html5/thumbnails/93.jpg)
93
Michael Mahlberg
Consulting Guild AG
@MMahlberg
http://agile-aspects.blogspot.com
Jens-Christian Fischer
InVisible GmbH
@jcfischer
http://blog.invisible.ch
93