Advertisement
Guest User

Untitled

a guest
Aug 31st, 2015
71
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 9.13 KB | None | 0 0
  1. Inheritance
  2. ===
  3.  
  4. Using inheritance in Ruby is extremely powerful and can greatly reduce complexity in the code. In this blog post, I'm going to walk through a use case for inheritance in the context of Ruby on Rails.
  5.  
  6.  
  7. Example Use Case: Form Parsing
  8. ---
  9.  
  10. We have an apartment listings website. Users can search for apartments in their area and refine the search with filters like # of bedrooms, min and max price, and lease start date.
  11.  
  12. When a user submits the form to filter apartment listings, it would come in the controller params like so:
  13.  
  14. ```ruby
  15. {
  16. search: { bedrooms: "3", min_price: "$500", max_price: "$800", lease_start_date: "09/01/2015" }
  17. }
  18. ```
  19.  
  20. As simple as this form is, we still have some work to do to clean the data before we can query our database. For example, the following will **not** work right now:
  21.  
  22. ```ruby
  23. Apartment.where(lease_start_date: params[:search][:lease_start_date])
  24. ```
  25.  
  26. Any idea why? This is because the lease_start_date column in our apartments table is a Date type. We need to convert the "09/01/2015" string to a Date object before we can perform this query.
  27.  
  28.  
  29. The Quick Way Out
  30. ---
  31.  
  32. The quickest solution is to do something like...
  33.  
  34. ```ruby
  35. # controllers/search_controller.rb
  36.  
  37. def filter_results
  38. @bedrooms = params[:search][:bedrooms].to_i
  39. @min_price = params[:search][:min_price].to_f
  40. @max_price = params[:search][:max_price].to_f
  41. @lease_start_date = Date.parse(params[:search][:lease_start_date])
  42.  
  43. @apartments = Apartment.where(bedrooms: @bedrooms).where("price >= ?", @min_price).where("price <= ?", @max_price).where(lease_start_date: @lease_start_date)
  44. end
  45. ```
  46.  
  47. This actually won't even work correctly because min_price and max_price may come in looking like "$500" instead of a clean "500", depending on what the user puts in the field ("$500".to_f turns into 0.0 ...yikes)
  48.  
  49. Further, should the filtering become more complex in the future, this controller action would get unweildy very quickly.
  50.  
  51.  
  52. Service Object
  53. ---
  54.  
  55. Let's create an object to handle the filtering of our results.
  56.  
  57. ```ruby
  58. # controllers/search_controller.rb
  59. def filter_results
  60. @apartments = Forms::FilterApartments.new(params[:search]).apartments
  61. end
  62. ```
  63.  
  64. ```ruby
  65. # classes/forms/filter_apartments.rb
  66.  
  67. class Forms::FilterApartments
  68.  
  69. attr_reader :bedrooms, :min_price, :max_price, :lease_start_date
  70.  
  71. def initialize(args = {})
  72. @bedroooms = args[:bedrooms].to_i
  73. @min_price = string_to_float(args[:min_price])
  74. @max_price = string_to_float(args[:max_price])
  75. @lease_start_date = string_to_date(args[:lease_start_date])
  76. end
  77.  
  78. def apartments
  79. @apartments = Apartment.where(bedrooms: bedrooms)
  80. @apartments = @apartments.where("price >= ?", min_price)
  81. @apartments = @apartments.where("price <= ?", max_price)
  82. @apartments = @apartments.where(lease_start_date: lease_start_date)
  83. end
  84.  
  85. private
  86.  
  87. def string_to_float(currency_string)
  88. float_regex = /(\d|[.])/
  89. currency_string.scan(float_regex).join.try(:to_f)
  90. end
  91.  
  92. def string_to_date(date_string)
  93. Date.parse(date_string)
  94. end
  95.  
  96. end
  97. ```
  98.  
  99. Note that this service object even accounts for the situation where min_price and max_price comes in as $500 instead of 500.
  100.  
  101. This service object is certainly an enhancement. It provides a place where we can perform more advanced parsing and logic and separates the responsibility of the results filtering into one object rather than a controller action that is sure to become too unweildy to manage over time as new filters are added.
  102.  
  103.  
  104. Yikes - New Feature Request
  105. ---
  106.  
  107. Our app has taken off and users want another page to see house listings. Let's start to write the controller action and corresponding service object to filter results for houses. Unlike apartments, houses come with some additional filtering options: :acreage, and :has_front_porch
  108.  
  109. ```ruby
  110. # controllers/search_controller.rb
  111.  
  112. def filter_houses
  113. @houses = Forms::FilterHouses.new(params[:search]).houses
  114. end
  115. ```
  116.  
  117. ```ruby
  118. # classes/forms/filter_houses.rb
  119.  
  120. class Forms::FilterHouses
  121.  
  122. attr_reader :bedrooms, :min_price, :max_price, :lease_start_date, :acreage, :has_front_porch
  123.  
  124. def initialize(args = {})
  125. @bedroooms = args[:bedrooms].to_i
  126. @min_price = string_to_float(args[:min_price])
  127. @max_price = string_to_float(args[:max_price])
  128. @lease_start_date = string_to_date(args[:lease_start_date])
  129. @acreage = string_to_float(args[:acreage])
  130. @has_front_porch = string_to_boolean(args[:has_front_porch])
  131. end
  132.  
  133. def houses
  134. @houses = House.where(bedrooms: bedrooms)
  135. @houses = @houses.where("price >= ?", min_price)
  136. @houses = @houses.where("price <= ?", max_price)
  137. @houses = @houses.where(lease_start_date: lease_start_date)
  138. @houses = @houses.where("acreage >= ?", acreage)
  139. @houses = @houses.where(has_front_porch: has_front_porch)
  140. end
  141.  
  142. private
  143.  
  144. def string_to_float(currency_string)
  145. float_regex = /(\d|[.])/
  146. currency_string.scan(float_regex).join.try(:to_f)
  147. end
  148.  
  149. def string_to_date(date_string)
  150. Date.parse(date_string)
  151. end
  152.  
  153. def string_to_boolean(boolean_string)
  154. boolean_string == "true"
  155. end
  156.  
  157. end
  158. ```
  159.  
  160. Man, the Houses service object seems very similar to the Apartments service object. Both have a method for converting strings to floats. Both are converting the incoming lease_start_date string to a date as well. Further, both are filtering by a lot of the same values. Intuitively, we know that searching for houses and searching for apartments are fundamentally similar experiences. So it is no surprise to me we are seeing these commonalities in the code.
  161.  
  162. How can we dry this up? Let's give inheritance a shot.
  163.  
  164.  
  165. Implementing Inheritance
  166. ---
  167.  
  168. ```ruby
  169. # classes/forms/property_filter.rb
  170.  
  171. class Forms::PropertyFilter
  172.  
  173. # this object will be used by both the
  174. # apartments and housing filter objects
  175.  
  176. attr_reader :bedrooms, :min_price, :max_price, :lease_start_date
  177.  
  178. def initialize(args = {})
  179. @bedroooms = args[:bedrooms].to_i
  180. @min_price = string_to_float(args[:min_price])
  181. @max_price = string_to_float(args[:max_price])
  182. @lease_start_date = string_to_date(args[:lease_start_date])
  183. after_initialize(args) # implemented by houses and apartments filters
  184. end
  185.  
  186. def results
  187. @results = model.where(bedrooms: bedrooms)
  188. @results = @results.where("price >= ?", min_price)
  189. @results = @results.where("price <= ?", max_price)
  190. @results = @results.where(lease_start_date: lease_start_date)
  191. end
  192.  
  193. private
  194.  
  195. def after_initialize(args = {})
  196. # implemented by subclasses
  197. end
  198.  
  199. def model
  200. # required method for subclasses
  201. # need to know what table to find the data
  202. raise "#{self.class.name} must implement model method."
  203. # i.e. "Forms::Filter::Apartments must implement model method"
  204. end
  205.  
  206. def string_to_float(currency_string)
  207. float_regex = /(\d|[.])/
  208. currency_string.scan(float_regex).join.try(:to_f)
  209. end
  210.  
  211. def string_to_date(date_string)
  212. Date.parse(date_string)
  213. end
  214.  
  215. end
  216. ```
  217.  
  218. ```ruby
  219. # classes/forms/property_filter/apartments.rb
  220.  
  221. class Forms::PropertyFilter::Apartments < Forms::PropertyFilter
  222.  
  223. def results
  224. super # runs results method in Forms::PropertyFilter
  225. end
  226.  
  227. private
  228.  
  229. def model
  230. Apartment
  231. end
  232.  
  233. end
  234. ```
  235.  
  236. ```ruby
  237. # classes/forms/property_filter/houses.rb
  238.  
  239. class Forms::PropertyFilter::Houses < Forms::PropertyFilter
  240.  
  241. attr_reader :acreage, :has_front_porch
  242.  
  243. def results
  244. super # runs results method in Forms::PropertyFilter
  245. @results = @results.where("acreage >= ?", acreage)
  246. @results = @results.where(has_front_porch: has_front_porch)
  247. end
  248.  
  249. private
  250.  
  251. def after_initialize(args = {})
  252. @acreage = string_to_float(args[:acreage])
  253. @has_front_porch = string_to_boolean(args[:acreage])
  254. end
  255.  
  256. def model
  257. House
  258. end
  259.  
  260. def string_to_boolean(boolean_string)
  261. boolean_string == "true"
  262. end
  263.  
  264. end
  265. ```
  266.  
  267. With these three classes in place, we have abstracted the commonalities between FilterApartments and FilterHouses into one class, PropertyFilter. Further, since the Houses filter has every filter that Apartments has plus acreage and has_front_porch, we see that the Houses filter only needs to worry about acreage and has_front porch.
  268.  
  269. In my opinion, implementing inheritance in this situation is absolutely critical. What happens, for example, if the bedrooms filter changes? Maybe users want to see apartments and houses with **more than** 3 bedrooms instead of filtering exactly on the bedroom number. This type of change would be cumbersome if the logic for filtering bedrooms was in both the Apartments filtering class and the Houses filtering class (let alone separate controller actions if we didn't implement service objects).
  270.  
  271. Actually, if I was implementing a website with this functionality, I would use Single Table Inheritance:
  272.  
  273. ```ruby
  274. # models/apartment.rb
  275. def Apartment < Property
  276. end
  277.  
  278. # models/house.rb
  279. class House < Property
  280. end
  281.  
  282. def Property < ActiveRecord::Base
  283. end
  284. ```
  285.  
  286. With this modeling, we would not need the "model" method in our PropertyFilter service object. To me, a house and an apartment are too similar of entities to not inherit from a common model, which intuitively is a property. Further, I can envision a situation in which both apartments and houses are shown as results to users in the same list.
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement