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

SOLID Principles through tests

SOLID Principles through tests

A practical approach on how to integrating the SOLID principles to the way we write code will pay off in the long the term.

Presented at: RubyConf Brazil 2014

92d08794b535e41a4082c57ea547546e?s=128

Sebastian Sogamoso

August 28, 2014
Tweet

Transcript

  1. SOLID Principles Through Tests

  2. @sebasoga Sebastián Sogamoso

  3. None
  4. None
  5. Kung-fu

  6. Shaolin Kungfu

  7. The most well known principle is called Six Harmonies

  8. Three External Harmonies

  9. Three Internal Harmonies

  10. We all follow some sort of principle

  11. SOLID Principles Through Tests

  12. Set of 5 object design level principles

  13. None
  14. S O L I D

  15. Prevent our systems from rotting

  16. Code that will save us time and money

  17. Cats follow the 6 harmonies to practice Kungfu We should

    follow principles when writing code RECAP
  18. Story Time

  19. Some facts of this story are true and some are

    made up
  20. We’ll call him Steven Seagull

  21. E-commerce system • Initial status: supports a few shipping methods

    • Requirement: painlessly add new shipping methods as needed
  22. class Order ... def ship case @store.shipping_method when Shipping::RegularMethod @store.shipping_method.pack(@items,

    @buyer, @seller).ship when Shipping::OvernightMethod origin = @seller.address destination = “#{@buyer.address_line1}, #{@buyer.zip}" @store.shipping_method.ship_order(origin, destination, @items) when Shipping::SameDayMethod addresses = [@seller.address, @buyer.complete_address] @store.shipping_method.pack(@items, addresses).send end end ... end
  23. def ship case @store.shipping_method when Shipping::RegularMethod @store.shipping_method.pack(@items, @buyer, @seller).ship when

    Shipping::OvernightMethod origin = @seller.address destination = “#{@buyer.address_line1}, #{@buyer.zip}" @store.shipping_method.ship_order(origin, destination, @items) when Shipping::SameDayMethod addresses = [@seller.address, @buyer.complete_address] @store.shipping_method.pack(@items, addresses).send when Shipping::NewMethod ... end end
  24. Shotgun surgery

  25. class Order ... def ship(shipping_method) case shipping_method when Shipping::RegularMethod shipping_method.pack(@items,

    @buyer, @seller).ship when Shipping::OvernightMethod origin = @seller.address destination = “#{@buyer.address_line1}, #{@buyer.zip}" shipping_method.ship_order(origin, destination, @items) when Shipping::SameDayMethod addresses = [@seller.address, @buyer.complete_address] @store.shipping_method.pack(@items, addresses).send else shipping_method.ship_order(self) end end ... end
  26. We were able to extend a class behavior, without modifying

    it RECAP
  27. S O L I D Open Closed

  28. class Order ... def ship(shipping_method) case shipping_method when Shipping::RegularMethod shipping_method.ship_order(self)

    when Shipping::OvernightMethod shipping_method.ship_order(self) when Shipping::SameDayMethod shipping_method.ship_order(self) else shipping_method.ship_order(self) end end ... end
  29. .................................. .................................. ....................... Finished in 129600.024296s 91 examples, 21 failures,

    0 skips
  30. describe Order do describe "#ship" do ... subject { Order.new

    } context "when shipping method is a Shipping::RegularMethod" do it "uses Shipping::RegularMethod to ship the order" do allow(shipping_method).to receive(class: Shipping::RegularMethod) subject.ship expect(shipping_method). to have_received_message_chain(:pack, :ship). with(items, seller, buyer) end end context "when shipping method is a Shipping::OvernightMethod" do it "uses Shipping::OvernightMethod to ship the order" do allow(shipping_method).to receive(class: Shipping::OvernightMethod) subject.ship expect(shipping_method). to have_received(:ship_order) end end context "when shipping method is a Shipping::SameDayMethod" do it "uses Shipping::SameDayMethod to ship the order" do allow(shipping_method).to receive(class: Shipping::SameDayMethod) subject.ship expect(shipping_method). to have_received_message_chain(:pack, :send). end end end end
  31. context "when shipping method is a Shipping::OvernightMethod" do it "uses

    Shipping::OvernightMethod to ship the order" do allow(shipping_method).to receive(class: Shipping::OvernightMethod) subject.ship expect(shipping_method). to have_received(:ship_order) end end
  32. Listen to your tests

  33. Tests are the first client of the system

  34. “TDD doesn’t drive good design. TDD gives you immediate feedback

    about what’s likely to be bad design” Kent Beck
  35. Order Shipping method

  36. Concrete things change a lot, abstract things change much less

    frequently
  37. class Order ... def ship(shipping_method) case shipping_method when Shipping::RegularMethod shipping_method.pack(@items,

    @buyer, @seller).ship when Shipping::OvernightMethod origin = @seller.address destination = “#{@buyer.address_line1}, #{@buyer.zip}" shipping_method.ship_order(origin, destination, @items) when Shipping::SameDayMethod addresses = [@seller.address, @buyer.complete_address] @store.shipping_method.pack(@items, addresses).send else shipping_method.ship_order(self) end end ... end
  38. module Shipping class RegularMethod < BaseMethod ... def self.ship_order(order) new.

    pack(order.items, order.origin, order.destination) .ship end ... end end
  39. class Order ... def ship(shipping_method) case shipping_method when Shipping::RegularMethod shipping_method.ship_order(self)

    when Shipping::OvernightMethod origin = @seller.address destination = “#{@buyer.address_line1}, #{@buyer.zip}" shipping_method.ship_order(origin, destination, @items) when Shipping::SameDayMethod addresses = [@seller.address, @buyer.complete_address] @store.shipping_method.pack(@items, addresses).send else shipping_method.ship_order(self) end end ... end
  40. class Order ... def ship(shipping_method) case shipping_method when Shipping::OvernightMethod origin

    = @seller.address destination = “#{@buyer.address_line1}, #{@buyer.zip}" shipping_method.ship_order(origin, destination, @items) when Shipping::SameDayMethod addresses = [@seller.address, @buyer.complete_address] @store.shipping_method.pack(@items, addresses).send else shipping_method.ship_order(self) end end ... end
  41. describe Order do describe "#ship" do ... subject { Order.new

    } context "when shipping method is a Shipping::OvernightMethod" do it “ships the order using the given shipping method" do order = Order.new allow(shipping_method).to receive(class: Shipping::OvernightMethod) allow(shipping_method).to receive(:ship_order) { ‘ordered shipped’ } expect(order.ship(shipping_method).to eq ‘ordered shipped’ end end context "when shipping method is a Shipping::SameDayMethod" do it “ships the order using the given shipping method" do order = Order.new allow(shipping_method).to receive(class: Shipping::SameDayMethod) allow(shipping_method).to receive(:ship_order) { ‘ordered shipped’ } expect(order.ship(shipping_method).to eq ‘ordered shipped’ end end context "when shipping method is a different type" do it "uses Shipping::SameDayMethod to ship the order" do allow(shipping_method).to receive(class: nil) subject.ship expect(shipping_method). to have_received_message_chain(:pack, :send). end end end end
  42. describe Order do ... describe "#ship" do let(:shipping_method) { double(:shipping_method)

    } subject { Order.new } it “ships the order using the given shipping method" do order = Order.new.pack allow(shipping_method).to receive(:ship_order) { ‘ordered shipped’ } expect(order.ship(shipping_method).to eq ‘ordered shipped’ end ... end
  43. class Order ... def ship(shipping_method) case shipping_method.class when Shipping::RegularMethod shipping_method.ship_order(self)

    when Shipping::OvernightMethod shipping_method.ship_order(self) when Shipping::SameDayMethod shipping_method.ship_order(self) end end ... end
  44. class Order ... def ship(shipping_method) shipping_method.ship_order(self) end ... end

  45. Started to depend on an abstractions, not on concrete implementations

    RECAP
  46. S O L I D Dependency Inversion

  47. module Shipping class WikedCrazyMethod < BaseMethod ... def fold(items, origin,

    destination) ... end def fly ... end ... end end
  48. Contract tests

  49. shared_examples "a shipping method" do it do should respond_to(:ship_order). with(1).argument

    end it “can be used to ship an order” do order = Order.new shipping_method = subject expect(order).to_not be_shipped order.ship(shipping_method) expect(order).to be_shipped end end
  50. describe Shipping::StandardMethod do subject { Shipping::WikedCrazyMethod } it_should_behave_like "a shipping

    method” ... end
  51. .................................. .................................. ....................... Finished in 129600.024296s 91 examples, 1 failure,

    0 skips
  52. module Shipping class WikedCrazyMethod < BaseMethod def self.ship_order(order) items =

    order.items * Math::PI origin = order.seller.name.reverse new.fold(items, origin, order.buyer).fly end ... end end
  53. .................................. .................................. ....................... Finished in 129600.024296s 91 examples, 0 failures,

    0 skips
  54. All clases conform with the same contract RECAP

  55. S O L I D Liskov Substitution

  56. Subclasses should be substitutable for their base classes

  57. Shipping cost calculation Formula that considers: • The amount of

    items (except for digital items). • A value that represents the distance between the origin and the destination . • A value that represents a fixed rate. items_quantity * distance * fixed_rate
  58. • Initial status: a change on any formula items, requires

    changing the class • Requirement: if only one formula item is changed the rest of the items shouldn’t be affected Shipping cost calculation
  59. describe Shipping::Cost do describe "#calculate" do ... subject { Shipping::StandardMethod.new(order,

    origin, destination) } context "when all items in the order are digital" do ... end context "when exactly one item in the order is not digital" do context "when the origin and destination are in the same state" do ... end context "when the origin and destination are in the same country" do ... end context "when the origin and destination are in different countries" do ... end end context "when more than one item in the order are not digital" do context "when the origin and destination are in the same state" do ... end context "when the origin and destination are in the same country" do ... end context "when the origin and destination are in different countries" do ... end end context "when origin city doesn't ship to the destination" do ... end end ... end
  60. module Shipping class Cost def initialize(order, origin, destination) ... end

    def calculate items_quantity * rate * distance end ... private def items_quantity @order.items.reject(:digital?).lenght end def rate if items_quantity > 1 9.99 else 5.99 end end def distance return 0 if @origin.does_not_ship_to? @destination return 1 if @origin.same_state? @destination return 2 if @origin.same_country? @destination 3 end end end
  61. Burroteca

  62. None
  63. Donkey + Discotheque

  64. None
  65. describe Shipping::Cost do ... describe "#calculate" do subject { Shipping::StandardMethod.new(order,

    origin, destination) } context "when all items in the order are digital" do ... end context "when exactly one item in the order is not digital" do context "when the origin and destination are in the same state" do ... end context "when the origin and destination are in the same country" do ... end context "when the origin and destination are in different countries" do ... end end context "when more than one item in the order are not digital" do context "when the origin and destination are in the same state" do ... end context "when the origin and destination are in the same country" do ... end context "when the origin and destination are in different countries" do ... end end context "when the origin city doesn't ship to the destination" do ... end end ... end
  66. describe Shipping::Cost do ... describe "#calculate" do subject { Shipping::StandardMethod.new(order,

    origin, destination) } context "when all items in the order are digital" do ... end context "when exactly one item in the order is not digital" do context "when the origin and destination are in the same state" do ... end context "when the origin and destination are in the same country" do ... end context "when the origin and destination are in different countries" do ... end end context "when more than one item in the order are not digital" do context "when the origin and destination are in the same state" do ... end context "when the origin and destination are in the same country" do ... end context "when the origin and destination are in different countries" do ... end end context "when the origin city doesn't ship to the destination" do ... end end ... end
  67. • "when all items in the order are digital” •

    "when exactly one item in the order is not digital" • "when more than one item in the order are not digital"
  68. describe Shipping::Items do describe "#quantity" do context "when all items

    in the order are digital" do ... end context "when exactly one item in the order is not digital" do ... end context "when more than one item in the order are not digital" do ... end end end
  69. describe Shipping::Cost do ... describe "#calculate" do subject { Shipping::StandardMethod.new(order,

    origin, destination) } context "when all items in the order are digital" do ... end context "when exactly one item in the order is not digital" do context "when the origin and destination are in the same state" do ... end context "when the origin and destination are in the same country" do ... end context "when the origin and destination are in different countries" do ... end end context "when more than one item in the order are not digital" do context "when the origin and destination are in the same state" do ... end context "when the origin and destination are in the same country" do ... end context "when the origin and destination are in different countries" do ... end end context "when the origin city doesn't ship to the destination" do ... end end ... end
  70. • "when the origin and destination are in the same

    state” • "when the origin and destination are in the same country” • "when the origin and destination are in different countries” • "when the origin city doesn't ship to the destination"
  71. describe Shipping::Distance do describe "#calculate" do context "when the origin

    and destination are in the same state" do ... end context "when the origin and destination are in the same country" do ... end context "when the origin and destination are in different countries" do ... end context "when the origin city doesn't ship to the destination" do ... end end end
  72. module Shipping class Cost def initialize(order, origin, destination) ... end

    def calculate items_quantity * rate * distance end private def items_quantity Shipping::Items.new(@order).quantity end def rate if items_quantity > 1 9.99 else 5.99 end end def distance Shipping::Distance.new(origin, destination).calculate end end end
  73. .................................. .................................. ....................... Finished in 129600.024296s 91 examples, 0 failures,

    0 skips
  74. module Shipping class Cost def initialize(order, origin, destination) ... end

    def calculate items_quantity * rate * distance end private def items_quantity Shipping::Items.new(@order).quantity end def rate Shipping::Rate.new(items_quantity).calculate end def distance Shipping::Distance.new(origin, destination).calculate end end end
  75. A class should have one, and only one, reason to

    change RECAP
  76. S O L I D Single Responsibility

  77. Steven Seagull was very happy and so were we

  78. S O L I D Interface Segregation

  79. Make fine-grained interfaces that are client-specific

  80. Why should you care?

  81. Respond to changing requirements

  82. Manage dependencies

  83. “I’m not a great programmer; I’m just a good programmer

    with great habits” Kent Beck
  84. The SOLID principles will help you acquire great object design

    habits
  85. None
  86. None
  87. Use your best judgement

  88. None
  89. We all follow some sort of principle

  90. Choose yours wisely

  91. Obrigado @sebasoga

  92. Credits • Me at RubyConf Uruguay: https://secure.flickr.com/photos/94260666@N05/14251620082/ • James: https://www.flickr.com/photos/125635394@N08/14894541332/in/photolist-oGbqdL-9neeBd-9neeD3-o8YsWp-osB7nF-oqTkmd-osef7e-9nfoAb-oDxDMY-

    o9mymh-ooPmHN-o9mAa9-o9mwaf-o9mBVn-o9mBf4-o9nDYM-o9mEds-o9nEaZ-o9mD9L-oqPJSm-ooPvhG-o9nCTk-o9nKtV-ooPnL9-oqz7uD-ooPvxw-o9mAu6- oqz4o8-o9mCpZ-ooPzR1-oqPGfY-oqD4hS-oqz4TM-osB7Kz-ooPv5s-osBfmB-o9mzY5-osBdax-oqz8ar-o9mJj4-o9mH3f-o9mAoy-oqReWV-o9mwVU-o9mEAJ-oqz2Tz- o9mv9N-o9mBia-o9nESR-oqD2xE • Golden Gate Bridge: https://www.flickr.com/photos/salim/402620871/in/photolist-gxzCVN-3gHR9W-5Wkky-7L2Rdu-7uys1-5R11B1- e7CfhP-6DZhWn-38B3gv-4tN2Xq-kAVAG-dUSUQy-6zLofj-Bzxaa-4jmZiD-Bxufq-bfCFgT-5V84Y-n3nkmB-Bzxus-Bzwuu-bVnpiR-n8Vxzv-axiND-ekFJyQ-h881MJ- bdn9iB-dX8ZeH-ba8H-dZcoQ1-5eikhM-cdFJjh-BzwPa-iW215d-5cTsTq-aPkLxV-6u7c4x-eD5M4-hFTq1W-78nwcX-sXd8e-fCMz9o-813icF-39Wstq-ev17J3-nPu9UG- c6bssC-6EDRSs-dQjTmN-axiXV • Gatto Mimmo: https://www.flickr.com/photos/gattomimmo/2125052913/in/photolist-3eqRoo-bRHqDi-akDPPi-9znES-482wnh-57PniE-7k88aj-2eEDEV-4eMsck- NEZ6A-eAR9nm-6c4a3f-c6p92h-2Y3Xp-78ATbn-9cdK3N-48ERpQ-6bANod-guvqG4-NEZ7f-DV4uM-47Xq5i-95D52m-7GfyjG-3E8qR-2vnfir-6qkkAk-d6XrpW-Ey2hB- NEZ6L-8mNVog-fEgakj-7oGZng-7YpL32-6ScqrQ-qPBk1-4Pvgs-93vS3Y-h1Gipq-47XHfB-PsFae-7YsZMh-8mPMr2-h1G7cr-gux5xc-2xcrmY-owS2Tg-8Jt8ro- ddKAb-332pLa • Shaolin Kungfu 1: https://www.flickr.com/photos/larique/3235902917/in/ photolist-5VWRcB-5KpggW-5KpioE-5VWDwg-5Kk2Qp-5Gf7j-4Jntb-4Jnio-4JoKG-4JoeM-4MZ6M-4JnKm-4JoTZ-4Johi-4JoEA-3xWQA- gsor-7t8exA-5VWPb4-5W24MC-gvWkgi-yKgw6-5W226u-5W1U5w-oUZif-8Mo9FE-8Mk5bg-8Modbf-5Kk3Wa-gwdV3T-fAcBQo-xTuHS-fFtqEg-5W1M8y-5W1KgG- oUZii-oV14w-crzuKN-9Hpikx-9HpdF6-9Hsgeh-9Hsa5Q-9HpiUD-9HsaBC-9HpkEg-9HskyJ-9Hs5jC-9Hpgiv-9HpvH2-9Hpd9k • Shaolin Kungfu 2: https://www.flickr.com/photos/larique/3236693046/in/photolist-5W1U5w-oUZif-8Mo9FE-8Mk5bg-8Modbf-5Kk3Wa-gwdV3T-fAcBQo-xTuHS- fFtqEg-5W1M8y-5W1KgG-oUZii-oV14w- crzuKN-9Hpikx-9HpdF6-9Hsgeh-9Hsa5Q-9HpiUD-9HsaBC-9HpkEg-9HskyJ-9Hs5jC-9Hpgiv-9HpvH2-9Hpd9k-9HsjxW-9Hse2b-9HpucM-9Hs7rj-9Hsigj-9Hpnki-9Hpfn K-9HpbAX-9HppSF-9Hs8xd-9HsiEQ-9HscSS-9Hscgw-9HpuJc-9Hps8M-9Hptd4-9Hprfp-9Hso4o-9HpmM8-9HpoSp-9HppnX-9Hs6X9-9Hs3Lm
  93. • Shaolin Kungfu 3: https://www.flickr.com/photos/larique/3235896099/in/photolist-5VWPb4-5W24MC-gvWkgi-yKgw6-5W226u-5W1U5w- oUZif-8Mo9FE-8Mk5bg-8Modbf-5Kk3Wa-gwdV3T-fAcBQo-xTuHS-fFtqEg-5W1M8y-5W1KgG-oUZii-oV14w- crzuKN-9Hpikx-9HpdF6-9Hsgeh-9Hsa5Q-9HpiUD-9HsaBC-9HpkEg-9HskyJ-9Hs5jC-9Hpgiv-9HpvH2-9Hpd9k-9HsjxW-9Hse2b-9HpucM-9Hs7rj-9Hsigj-9Hpnki-9Hpfn K-9HpbAX-9HppSF-9Hs8xd-9HsiEQ-9HscSS-9Hscgw-9HpuJc-9Hps8M-9Hptd4-9Hprfp-9Hso4o • Uncle

    Bob: https://www.flickr.com/photos/moofbong/4206618799/in/ photolist-7pJ2dk-7pMCyY-7pMWo5-7pHWfe-7pMJ7s-7pMSoJ-7pJ16V-7pHSMt-7pHXpr-7pMNVw-7pHL6Z-7pHHPt-7pMGow-7pHMqv-seGMM-seGNW-7j2wCX- seHCx-seHbn-seHx4-seHmT-seHod-seGDJ-seHFq-seHDM-seGEB-seGL1-seHK6-seGHD-seGYT-seHBi-seGFC-seHHY-seH4e-seHiT-seGJp-seGK7-seHsD-seHkn- seHH7-seHxv-seH5f-seH6s-seGGF-seH3p-seHy8-seH9M-seHzx-b1hoEp-Gj1Wx/ • Kent Beck: https://www.flickr.com/photos/skrb/238102518/in/photolist-Hfu8-n3kzQ-n3kWh-n3kkG-7Jnjvu-6foWR2-6foWRa-djMXPd-cWkWKS-oF4y3- oCY4q-7Tx3yN-3p6hoi-4SrHSK-3Uu3Z3-8QyWVd-3paQF1-3p6hcT-3o4296-3o423c-3p62PD-3p62FM-3p62ze-3p6aQz-3p6h26-3paA55-3payDY-3p5VdK-3p5TaV-3p5S x6-3p5Wnc-3p5YdM-3pavp9-3p5Y4B-3pawg1-3pauEh-3pay1L-3patuL-3p5UiB-3p61zD-3paqDq-3parJE-3p5XKn-3paujq-3pavM9-3p5ZFR-3p5SEk-3pavdN-3p5YMZ-3 pauxq • Superheroes: https://www.flickr.com/photos/charlot17/4648822207 • Beware: https://www.flickr.com/photos/atoach/3922401551/in/photolist-AmPFE-2xwi8T-64GFek-6kWVRg-9vAqpD-jiG1mG-4tj1U-cvyg1f-4eHMx9-65XzLZ- czjMeb-6Y6JC8-5Hja23-6YBkjX-7tRTC3-64VB6C-mmd6zP-9LaWU3-7T61c5-kDDFs3-gmYDTg-ekvzx5-9mrc-7JphLV-6x1xw1-atzvpP-5Nz7gs-5y9TPm-nxj3e1- hHNuw9-bpnnzS-7B43Lf-9BMD7M-bvZDbg-jwss4F-bUhfe1-8ype7F-51LBV-n9rWsV-KugGj-dCPvvp-cECRaC-7GjgEU-jMaCY-h66jGg-jGjbF-9jF4P-diSx2r-nsVjAr- ceUeoG • Burroteca: http://www.jackmag.com.co/articles/burroteca/ Credits