temp variable 的問題在於他只存在這個 scope,把它抽成 method 的話在整個 class 裡面都可以用到
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
base_price = @quantity * @item_price if base_price > 1000 base_price * 0.95 else base_price * 0.8 end
# After if base_price > 1000 base_price * 0.95 else base_price * 0.8 end
defbase_price @quantity * @item_price end
Replace Temp with Chain
有另一個 refactoring method 是 Hide Delegate,兩者乍看衝突,但他們之間最大的差別在於 Replace Temp with Chain 回傳的東西都對同樣的 API 回應,而 Hide Delegation 回傳的東西每次可能都是不同的物件,也就是有沒有違反 Law of Demeter
classSearchCriteria definitialize(hash) @author_id = hash[:author_id] @publisher_id = hash[:publisher_id] @isbn = hash[:isbn] end end
## After classSearchCriteria hash_initializer :author_id, :publisher_id, :isbn end
moduleCustominitializers defhash_initializer(*attr_names) define_method(:initialize) do|*args| data = args.first || {} attr_names.each do|attr_name| instance_variable_set "@{attr_name}", data[attr_name] end end end end
classPostData definitialize(post_data) (class << self;self; end).class_eval do post_data.each_pair do|key, value| define_method key.to_sym do value end end end end end
classCustomer attr_reader:name definitialize(name) @name = name end end
classOrder definitialize(customer_name) @customer = Customer.new(customer_name) end end
# Step1 Replace Constructor with Factory Method classCustomer defself.create(name) Customer.new(name) end attr_reader:name definitialize(name) @name = name end end
classOrder definitialize(customer_name) @customer = Customer.create(customer_name) end end
classMountainBike attr_writer:type_code defprice case @type_code when:front_suspension ... when:full_suspension ... end end end # 用法: bike = MountainBike.new(type_code: :rigid) # bike.type_code = :front_suspension
# 改成 classMountainBike attr_reader:type_code deftype_code=(value) @type_code = value case type_code when:front_suspension: extend(FrontSuspensionMountainBike) when:full_suspension: extend(FullSuspensionMountainBike) end end end
moduleFrontSuspensionMountainBike defprice ... end end
moduleFullSuspensionMountainBike defprice ... end end
Introduce Null Object
使用 null object 是一個減少 conditional 的一個方式
做一個 null object class 出來,然後在他身上還有原本的 source class 身上做出一個 missing? method
# 原本丟出 nil 的地方改成丟出 null object # 原本 classSite attr_reader:customer end # 改成 classSite defcustomer @customer || Customer.new_missing end end classCustomer defself.new_missing MissingCustomer.new end end
# 最難的部分是找出原本哪些地方去檢查 nil 然後改成 call missing?
plan = customer? customer.plan : BillingPlan.basic # 改成 plan = customer.missing? ? customer.plan : BillingPlan.basic
defexecute request = Het::HTTP::Post.new(url.path) attribute_hash = attributes.inject({}) do|result, attribute| result[attribute.to_s] = subject.send attribute result end request.set_form_data(attribute_hash) Net::HTTP.new(url.host, url.port).start { |http| http.request(request) } end
defurl URI.parse(to) end end
# 把 person 改寫 classPerson attr_accessor:first_name, :last_name, :ssn defsave Gateway.save do|persist| persist.subject = self persist.attributes = [:first_name, :last_name, :ssn] persist.to = 'http://www.example.com/person' end end end
# 還有另一個 class 需要整合 classCompany attr_accessor:name, :tax_id
defsave url = URI.parse('http://www.example.com/companies') request = Net::HTTP::Get.new(url.path + "?name=#{name}&tax_id=#{tax_id}") Net::HTTP.new(url.host, url.port).start { |http| http.request(request) } end end
# 因為分別是 get 跟 post 所以可以把他分成不同的 type classGateway defself.new gateway = self.new yield gateway gateway.execute end
defexecute Net::HTTP.new(url.host, url.port).start do|http| http.request(build_request) end end end
classPostGateway defbuild_request request = Het::HTTP::Post.new(url.path) attribute_hash = attributes.inject({}) do|result, attribute| result[attribute.to_s] = subject.send attribute result end request.set_form_data(attribute_hash) end end
classGetGateway defbuild_request parameters = attributes.collect do|attribute| "#{attribute}=#{subject.send(attribute)}" end Net::HTTP::Get.new("#{url.path}?#{parameters.join("&")}") end end
classCompany attr_accessor:name, :tax_id defsave GetGateway.save do|persist| persist.subject = self persist.attributes = [:name, :tax_id] persist.to = 'http:ww.example.com/companies' end end end
Introduce Expression Builder
Expression Builder 的功能就是讓我們用一些 public API 用起來更上手,提供更便利的介面給使用者使用
classCustomer defstatement result = "Rental Record for #{name}\n" @rentals.each do|rental| # show figures for this rental result << "\t#{rental.movie.title}\t#{rental.charge}\n" end # add footer lines result << "Amount owed is #{total_charge}\n" result << "You earned #{total_frequent_renter_points} frequent renter points" result end
defhtml_statement result = "<H1>Rentals for <EM>#{name}</EM></H1><P>\n" @rentals.each do|rental| # show figures for this rental result << "#{rental.movie.title}: \t#{rental.charge}<BR/>\n" end # add footer lines result << "<P>You owe <EM>#{total_charge}</EM></P>\n" result << "On this rental you earned <EM>#{total_frequent_renter_points}</\ EM> frequent renter points</P>" end end
classTextStatement < Statement defvalue(customer) result = "Rental Record for #{customer.name}\n" customer.rentals.each do|rental| # show figures for this rental result << "\t#{rental.movie.title}\t#{rental.charge}\n" end # add footer lines result << "Amount owed is #{customer.total_charge}\n" result << "You earned #{customer.total_frequent_renter_points} frequent renter points" result end end
classHtmlStatement < Statement defvalue(customer) result = "<H1>Rentals for <EM>#{customer.name}</EM></H1><P>\n" customer.rentals.each do|rental| # show figures for this rental result << "#{rental.movie.title}: \t#{rental.charge}<BR/>\n" end # add footer lines result << "<P>You owe <EM>#{customer.total_charge}</EM></P>\n" result << "On this rental you earned <EM>#{customer.total_frequent_renter_points}</\ EM> frequent renter points</P>" end end
classCustomer defstatement TextStatement.value(self) end
defhtml_statement HtmlStatement.value(self) end end
我們看得出來中間的過程都是 header / body / footer,所以把他們抽成一樣的外型放到 super class
classStatement defvalue(customer) result = header_string(customer) customer.rentals.each do|rental| result << each_rental_string(rental) end result << footer_string(customer) end end
classTextStatement < Statement defheader_string(customer) "Rental Record for #{customer.name}\n" end
defeach_rental_string(rental) "\t#{rental.movie.title}\t#{rental.charge}\n" end
deffooter_string(customer) <<-EOS Amount owed is #{customer.total_charge}\n" You earned #{customer.total_frequent_renter_points} frequent renter points" EOS end end
classHtmlStatement < Statement defheader_string(customer) #... end
classStatement defvalue(customer) result = header_string(customer) customer.rentals.each do|rental| result << each_rental_string(rental) end result << footer_string(customer) end end
moduleTextStatement defheader_string(customer) "Rental Record for #{customer.name}\n" end
defeach_rental_string(rental) "\t#{rental.movie.title}\t#{rental.charge}\n" end
deffooter_string(customer) <<-EOS Amount owed is #{customer.total_charge}\n" You earned #{customer.total_frequent_renter_points} frequent renter points" EOS end end
moduleHtmlStatement defheader_string(customer) #... end
defeach_rental_string(rental) #... end
deffooter_string(customer) #... end end
然後接口會長的比較特別,用 instance 去 extend module
1 2 3 4 5 6 7 8
classCustomer defstatement Statement.new.extend(TextStatement).value(self) end defhtml_statement Statement.new.extend(HtmlStatement).value(self) end end
這樣做有什麼好處呢?我們想像如果之後有另一個需求,但他的步驟跟正常的 Statement 不同,我們要做出另一個 class
1 2 3 4 5 6 7 8 9 10 11 12
classMonthlyStatement defvalue(customer) result = header_string(customer) rentals = customer.rentals.select do|rental| rental.date > DateTime.now -30 end rentals.each do|rental| result << each_rental_string(rental) end result << footer_string(customer) end end
classCustomer defstatement Statement.new.extend(TextStatement).value(self) end defhtml_statement Statement.new.extend(HtmlStatement).value(self) end defmonthly_statement MonthlyStatement.new.extend(TextStatement).value(self) end defmonthly_html_statement MonthlyStatement.new.extend(HtmlStatement).value(self) end end
Replace Inheritance with Delegation
常常我們看到繼承的 subclass ,但奇怪的是這個 subclass 只有用到少數 super class 的功能,這時候可以考慮改用 delegation
其中一個常見的情況是繼承 collection
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
classPolicy < Hash attr_reader:name definitialize(name) @name = name end
classModule defdeprecate(method_name, &block) module_eval <<-END alias_method :deprecated_#{method_name}, :#{method_name} def#{method_name}(*args, &block) $stderr.puts "Warning: calling deprecated method\ #{self}.#{method_name}. This method will be removed in a future release." deprecated_#{method_name}(*args, &block) end END end end