Share
Embed
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
Sebastian Sogamoso
August 28, 2014
Transcript
93 slides
-
Cats follow the 6 harmonies to practice Kungfu We should
follow principles when writing code RECAP -
E-commerce system • Initial status: supports a few shipping methods
• Requirement: painlessly add new shipping methods as needed -
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 -
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 -
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 -
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 -
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 -
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 -
“TDD doesn’t drive good design. TDD gives you immediate feedback
about what’s likely to be bad design” Kent Beck -
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 -
module Shipping class RegularMethod < BaseMethod ... def self.ship_order(order) new.
pack(order.items, order.origin, order.destination) .ship end ... end end -
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 -
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 -
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 -
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 -
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 -
module Shipping class WikedCrazyMethod < BaseMethod ... def fold(items, origin,
destination) ... end def fly ... end ... end end -
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 -
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 -
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 -
• 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 -
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 -
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 -
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 -
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 -
• "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" -
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 -
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 -
• "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" -
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 -
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 -
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 -
A class should have one, and only one, reason to
change RECAP -
“I’m not a great programmer; I’m just a good programmer
with great habits” Kent Beck -
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 -
• 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