Upgrade to Pro — share decks privately, control downloads, hide ads and more …

POROs To The Rescue - Tropical Ruby 2015

POROs To The Rescue - Tropical Ruby 2015

Being tasked with rescuing an ancient codebase and turning it into something workable and manageable is a predicament most of us have already found ourselves in. In the 10 years since we began using Ruby to write Web applications, a lot has changed concerning architectures and design patterns. How do you take your code in a time travel from the olden days to more current practices such as POROs, lean models, presenters and decorators? This talk will walk you through some easy practices that can make that trip less bumpy and allow you to survive to tell the story.

03cd6fd48d43c786c6693cbaae63b3e1?s=128

Marcelo De Polli

March 06, 2015
Tweet

Transcript

  1. POROs to
 the Rescue Marcelo De Polli @mdepolli

  2. None
  3. None
  4. None
  5. Todo código é código legado

  6. 2005 Já escrevemos aplicações Web em Ruby desde

  7. None
  8. Muita coisa mudou

  9. None
  10. Mas era mesmo diferente?

  11. None
  12. a heresia do James Golick

  13. None
  14. Plain Old Ruby Object

  15. class User attr_accessor :first_name, :last_name, :email def initialize(first_name, last_name, email)

    @first_name = first_name @last_name = last_name @email = email end def full_name "#{first_name} #{last_name}" end end
  16. class User attr_accessor :first_name, :last_name, :email def initialize(first_name, last_name, email)

    @first_name = first_name @last_name = last_name @email = email end def full_name "#{first_name} #{last_name}" end end
  17. None
  18. 1

  19. class CsvImport < ActiveRecord::Base def order_data(csv_line) order_params = {} order_params[:account_id]

    = account.id order_params[:user_id] = account.user.id order_params[:retailer_id] = retailer.id order_params[:status_id] = 1 order_confirmed = true category_supported = true case retailer.name when "Amazon" # Get order data from csv line else "eBay" # Get order data from csv line end order = Order.new(order_params) order_shipped = order_params[:shipped_at].present? shipped_days_ago = if order_shipped (user.timezone.now - user.timezone.parse(order_params[:shipped_at]))/1.day end if category_supported && order_confirmed && order_shipped && shipped_days_ago < 30 order.save end end end
  20. when "Amazon" category_name = line[8].gsub('Category_', '') if order_category = Category.find_by(name:

    category_name) if line[18].strip == 'Confirmed' order_params[:shipped_at] = account.user.timezone.parse(line[18]) order_params[:shipping_name] = line[49] order_params[:shipping_company] = line[50] order_params[:shipping_street] = line[51] order_params[:shipping_city] = line[52] order_params[:shipping_state] = line[53] order_params[:shipping_zip] = line[54] order_params[:shipping_country] = line[55] order_params[:shipping_phone] = line[17] order_params[:billing_name] = line[4] order_params[:billing_company] = line[5] order_params[:billing_street] = line[6] order_params[:billing_city] = line[7] order_params[:billing_state] = line[8] order_params[:billing_zip] = line[9] order_params[:billing_country] = line[15] order_params[:billing_phone] = line[17] else order_confirmed = false end else category_supported = false end else "eBay" # ...
  21. # ... else "eBay" category_name = line[2].gsub('eBay ', '').strip if

    order_category = Category.find_by(name: category_name) if line[20].strip == 'CF' shipped_date = Date.parse(line[5]) shipped_time = line[6] order_params[:shipped_at] = account.user.timezone.parse("#{shipped_date} #{shipped_time}") order_params[:shipping_name] = line[8] order_params[:shipping_company] = line[9] order_params[:shipping_street] = line[10] order_params[:shipping_city] = line[11] order_params[:shipping_state] = line[12] order_params[:shipping_zip] = line[13] order_params[:shipping_country] = line[14] order_params[:shipping_phone] = line[15] order_params[:billing_name] = line[24] order_params[:billing_company] = line[25] order_params[:billing_street] = line[26] order_params[:billing_city] = line[27] order_params[:billing_state] = line[28] order_params[:billing_zip] = line[29] order_params[:billing_country] = line[30] order_params[:billing_phone] = line[31] else order_confirmed = false end else category_supported = false end end
  22. class CsvImport < ActiveRecord::Base def order_data(csv_line) order_params = {} order_params[:account_id]

    = account.id order_params[:user_id] = account.user.id order_params[:retailer_id] = retailer.id order_params[:status_id] = 1 order_confirmed = true category_supported = true case retailer.name when "Amazon" category_name = line[8].gsub('Category_', '') if order_category = Category.find_by(name: category_name) if line[18].strip == 'Confirmed' order_params[:shipped_at] = account.user.timezone.parse(line[18]) order_params[:shipping_name] = line[49] order_params[:shipping_company] = line[50] order_params[:shipping_street] = line[51] order_params[:shipping_city] = line[52] order_params[:shipping_state] = line[53] order_params[:shipping_zip] = line[54] order_params[:shipping_country] = line[55] order_params[:shipping_phone] = line[17] order_params[:billing_name] = line[4] order_params[:billing_company] = line[5] order_params[:billing_street] = line[6] order_params[:billing_city] = line[7] order_params[:billing_state] = line[8] order_params[:billing_zip] = line[9] order_params[:billing_country] = line[15] order_params[:billing_phone] = line[17] else order_confirmed = false end else category_supported = false end else "eBay" category_name = line[2].gsub('eBay ', '').strip if order_category = Category.find_by(name: category_name) if line[20].strip == 'CF' shipped_date = Date.parse(line[5]) shipped_time = line[6] order_params[:shipped_at] = account.user.timezone.parse("#{shipped_date} #{shipped_time}") order_params[:shipping_name] = line[8] order_params[:shipping_company] = line[9] order_params[:shipping_street] = line[10] order_params[:shipping_city] = line[11] order_params[:shipping_state] = line[12] order_params[:shipping_zip] = line[13] order_params[:shipping_country] = line[14] order_params[:shipping_phone] = line[15] order_params[:billing_name] = line[24] order_params[:billing_company] = line[25] order_params[:billing_street] = line[26] order_params[:billing_city] = line[27] order_params[:billing_state] = line[28] order_params[:billing_zip] = line[29] order_params[:billing_country] = line[30] order_params[:billing_phone] = line[31] else order_confirmed = false end else category_supported = false end end order = Order.new(order_params) order_shipped = order_params[:shipped_at].present? shipped_days_ago = if order_shipped (user.timezone.now - user.timezone.parse(order_params[:shipped_at]))/1.day end if category_supported && order_confirmed && order_shipped && shipped_days_ago < 30 order.save end end end
  23. None
  24. class CsvImport < ActiveRecord::Base def order_data(csv_line) order_params = {} order_params[:account_id]

    = account.id order_params[:user_id] = account.user.id order_params[:retailer_id] = retailer.id order_params[:status_id] = 1 order_confirmed = true category_supported = true case retailer.name when "Amazon" category_name = line[8].gsub('Category_', '') if order_category = Category.find_by(name: category_name) if line[18].strip == 'Confirmed' order_params[:shipped_at] = account.user.timezone.parse(line[18]) order_params[:shipping_name] = line[49] order_params[:shipping_company] = line[50] order_params[:shipping_street] = line[51] order_params[:shipping_city] = line[52] order_params[:shipping_state] = line[53] order_params[:shipping_zip] = line[54] order_params[:shipping_country] = line[55] order_params[:shipping_phone] = line[17] order_params[:billing_name] = line[4] order_params[:billing_company] = line[5] order_params[:billing_street] = line[6] order_params[:billing_city] = line[7] order_params[:billing_state] = line[8] order_params[:billing_zip] = line[9] order_params[:billing_country] = line[15] order_params[:billing_phone] = line[17] else order_confirmed = false end else category_supported = false end else "eBay" category_name = line[2].gsub('eBay ', '').strip if order_category = Category.find_by(name: category_name) if line[20].strip == 'CF' shipped_date = Date.parse(line[5]) shipped_time = line[6] order_params[:shipped_at] = account.user.timezone.parse("#{shipped_date} #{shipped_time}") order_params[:shipping_name] = line[8] order_params[:shipping_company] = line[9] order_params[:shipping_street] = line[10] order_params[:shipping_city] = line[11] order_params[:shipping_state] = line[12] order_params[:shipping_zip] = line[13] order_params[:shipping_country] = line[14] order_params[:shipping_phone] = line[15] order_params[:billing_name] = line[24] order_params[:billing_company] = line[25] order_params[:billing_street] = line[26] order_params[:billing_city] = line[27] order_params[:billing_state] = line[28] order_params[:billing_zip] = line[29] order_params[:billing_country] = line[30] order_params[:billing_phone] = line[31] else order_confirmed = false end else category_supported = false end end order = Order.new(order_params) order_shipped = order_params[:shipped_at].present? shipped_days_ago = if order_shipped (user.timezone.now - user.timezone.parse(order_params[:shipped_at]))/1.day end if category_supported && order_confirmed && order_shipped && shipped_days_ago < 30 order.save end end end Atribuição de valores
  25. class CsvImport < ActiveRecord::Base def order_data(csv_line) order_params = {} order_params[:account_id]

    = account.id order_params[:user_id] = account.user.id order_params[:retailer_id] = retailer.id order_params[:status_id] = 1 order_confirmed = true category_supported = true case retailer.name when "Amazon" category_name = line[8].gsub('Category_', '') if order_category = Category.find_by(name: category_name) if line[18].strip == 'Confirmed' order_params[:shipped_at] = account.user.timezone.parse(line[18]) order_params[:shipping_name] = line[49] order_params[:shipping_company] = line[50] order_params[:shipping_street] = line[51] order_params[:shipping_city] = line[52] order_params[:shipping_state] = line[53] order_params[:shipping_zip] = line[54] order_params[:shipping_country] = line[55] order_params[:shipping_phone] = line[17] order_params[:billing_name] = line[4] order_params[:billing_company] = line[5] order_params[:billing_street] = line[6] order_params[:billing_city] = line[7] order_params[:billing_state] = line[8] order_params[:billing_zip] = line[9] order_params[:billing_country] = line[15] order_params[:billing_phone] = line[17] else order_confirmed = false end else category_supported = false end else "eBay" category_name = line[2].gsub('eBay ', '').strip if order_category = Category.find_by(name: category_name) if line[20].strip == 'CF' shipped_date = Date.parse(line[5]) shipped_time = line[6] order_params[:shipped_at] = account.user.timezone.parse("#{shipped_date} #{shipped_time}") order_params[:shipping_name] = line[8] order_params[:shipping_company] = line[9] order_params[:shipping_street] = line[10] order_params[:shipping_city] = line[11] order_params[:shipping_state] = line[12] order_params[:shipping_zip] = line[13] order_params[:shipping_country] = line[14] order_params[:shipping_phone] = line[15] order_params[:billing_name] = line[24] order_params[:billing_company] = line[25] order_params[:billing_street] = line[26] order_params[:billing_city] = line[27] order_params[:billing_state] = line[28] order_params[:billing_zip] = line[29] order_params[:billing_country] = line[30] order_params[:billing_phone] = line[31] else order_confirmed = false end else category_supported = false end end order = Order.new(order_params) order_shipped = order_params[:shipped_at].present? shipped_days_ago = if order_shipped (user.timezone.now - user.timezone.parse(order_params[:shipped_at]))/1.day end if category_supported && order_confirmed && order_shipped && shipped_days_ago < 30 order.save end end end Atribuição de valores Guarda (critério de aceitação)
  26. PORO!

  27. module AmazonParser private def category_name data[8].gsub('Category_', '') end def order_confirmed?

    data[18].strip == 'Confirmed' end def shipped_at timezone.parse(data[18]) end def number data[2].strip end def shipping_name data[49] end def shipping_company data[50] end # ... end
  28. class RetailerParser attr_accessor :data, :import, :timezone def initialize(data, import) self.data

    = data self.import = import self.timezone = import.timezone include_retailer_methods end ALLOWED_ATTRIBUTES_FROM_DATA = %i( shipped_at number shipping_name shipping_company shipping_street shipping_city shipping_state shipping_zip shipping_country shipping_phone billing_name billing_company billing_street billing_city billing_state billing_zip billing_country billing_phone ) def order import.orders.build(order_attributes_from_data) end private def order_attributes_from_data ALLOWED_ATTRIBUTES_FROM_DATA.each_with_object({}) { |a, e| a[e] = send(e) } end def include_retailer_methods include Object.const_get("#{import.retailer.name}Parser") end end Value Object
  29. class Order < ActiveRecord::Base SHIPPING_LIMIT_IN_DAYS = 30 validates :shipped_at, presence:

    true validate :shipping_must_be_recent def shipping_must_be_recent days_ago = (Time.zone.now - shipped_at.in_time_zone) / 1.day return unless days_ago <= SHIPPING_LIMIT_IN_DAYS errors.add(:shipped_at, "cannot be older than #{SHIPPING_LIMIT_IN_DAYS} days") end end
  30. order = Order.new(RetailerParser.new(data, import).order_attributes) if order.save # ... else #

    ... end
  31. O que pertence a um model?

  32. Tudo que modela o seu domínio pode ser um model


    (ou não)
  33. Tirar as classes do app/models pode te impedir de ver

    a complexidade
  34. Esconder é muito diferente de organizar

  35. 2

  36. class CsvImport < ActiveRecord::Base def import_data(data) order_params = {} order_params[:account_id]

    = account.id order_params[:user_id] = user.id order_params[:retailer_id] = retailer.id case retailer.name when "Amazon" # Get order data from csv data line else "eBay" # Get order data from csv data line end order = Order.new(order_params) order_shipped = order_params[:shipped_at].present? shipped_days_ago = if order_shipped (user.timezone.now - user.timezone.parse(order_params[:shipped_at])) / 1.day end if order_shipped && shipped_days_ago < 30 order.save end end end
  37. class CsvImport < ActiveRecord::Base # ... def validate_address(data) # ...

    end def shipping_date_in_user_timezone(data) # ... end def order_delivered?(data) # ... end def order_confirmed?(data) # ... end def order_shipped?(data) # ... end def order_shipped_days_ago(data) # ... end def shipping_address_from_data(data) # ... end def billing_address_from_data(data) # ... end end
  38. class CsvImport < ActiveRecord::Base # ... def validate_address(data) # ...

    end def shipping_date_in_user_timezone(data) # ... end def order_delivered?(data) # ... end def order_confirmed?(data) # ... end def order_shipped?(data) # ... end def order_shipped_days_ago(data) # ... end def shipping_address_from_data(data) # ... end def billing_address_from_data(data) # ... end end Compartilhamento de estado + Desacoplamento natural
  39. PORO!

  40. SimpleDelegator

  41. class OrderDecorator < SimpleDelegator attr_accessor :data def initialize(order, data) super(order)

    self.data = data end def address_valid? # ... end def shipping_date_in_user_timezone # ... end def order_delivered? # ... end def order_confirmed? # ... end def order_shipped? # ... end def shipping_address_from_data # ... end end Decorator
  42. decorated_order = OrderDecorator.new(order) decorated_order.shipping_company #=> "Acme" decorated_order.shipping_street #=> "164 Roca

    St" decorated_order.order_delivered? #=> false
  43. 3

  44. class OrderHelper < ApplicationHelper def validate_address(data) # ... end def

    shipping_date_in_user_timezone(data) # ... end def order_delivered?(data) # ... end def order_confirmed?(data) # ... end def order_shipped_days_ago(data) # ... end def shipping_address_from_data(data) # ... end end
  45. PORO!

  46. class OrderPresenter < SimpleDelegator attr_accessor :data, :order def initialize(template, order,

    data) super(template) self.order = order self.data = data end def address_valid? # ... end def shipping_date_in_user_timezone # ... end def order_delivered? # ... end def order_confirmed? # ... end def order_shipped? # ... end def shipping_address_from_data # ... end end Presenter
  47. <% @order_presenter = OrderPresenter.new(self, @order, @data) %> <p> <b>Shipping date:</b>

    <%= @order_presenter.shipping_date_in_user_timezone %> </p>
  48. None
  49. Em resumo

  50. Don’t overengineer (não resolva problemas que você não tem)

  51. Não tenha medo de criar objetos

  52. Quem vai modelar o seu domínio é
 você,
 não o

    Rails
  53. Obrigado! @mdepolli