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

SOLID Principles Through Tests

SOLID Principles Through Tests

We care about writing quality code, we have read the definition of SOLID principles several times and we know how important they are for writing good OO code, but are we really following those principles? Is there a pragmatic way of following them in our day to day jobs or are they just some principles a few computer scientists wrote?

Fortunately there is, SOLID principles are not just good ideas , they are intended to help us write better code, enjoy our jobs more and be happy programmers. So, where should we start? We should start where we always do. By writing tests, yes, for real.

As Kent Beck says "TDD doesn't drive good design. TDD gives you immediate feedback about what is likely to be bad design", so we need to go a step further. In this talk we will see how writing tests is not just doing TDD is about having good test coverage, it's also about driving our code towards good design, one that follows SOLID principles.

Presented at: LA RubyConf 2014

Sebastian Sogamoso

February 08, 2014
Tweet

More Decks by Sebastian Sogamoso

Other Decks in Programming

Transcript

  1. “TDD doesn't drive good design. TDD gives you immediate feedback

    about what is likely to be bad design” Kent Beck
  2. =

  3. Based on: - The amount of items to ship, excluding

    digital items - A fixed rate that depends on the amount of items - The distance between origin and destination
  4. describe Shipping::StandardMethod do describe "#cost" do context "when all items

    in the order are digital" do…end context "when only 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
  5. describe Shipping::StandardMethod do describe "#cost" do context "when all items

    in the order are digital" do…end context "when only 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 end ... end
  6. describe Shipping::StandardMethod do describe "#cost" do let(:items) { double(:items) }

    let(:order) { double(:order) } let(:origin) { double(:origin).as_null_object } let(:destination) { double(:destination) } subject { Shipping::StandardMethod.new(order, origin, destination) } context "when all items in the order are digital" do it "returns zero" do items.stub(:reject).with(:digital?).and_return([]) expect(subject.cost).to be_zero end end context "when only one item in the order is not digital" do before { items.stub(:reject).with(:digital?).and_return([1]) } context "when the origin and destination are in the same state" do it "returns the correspondent shipping cost" do origin.stub(same_state?: true) expect(subject.cost).to eq (1 * 9.99 * 1) end end context "when the origin and destination are in the same country" do it "returns the correspondent shipping cost" do origin.stub(same_country?: true) expect(subject.cost).to eq (1 * 9.99 * 2) end end context "when the origin and destination are in different countries" do it "returns the correspondent shipping cost" do expect(subject.cost).to eq (1 * 9.99 * 3) end end end context "when more than one item in the order are not digital" do before { items.stub(:reject).with(:digital?).and_return([1]) } context "when the origin and destination are in the same state" do it "returns the correspondent shipping cost" do origin.stub(same_state?: true) expect(subject.cost).to eq (1 * 5.99 * 1) end end ! context "when the origin and destination are in the same country" do it "returns the correspondent shipping cost" do origin.stub(same_country?: true) expect(subject.cost).to eq (1 * 5.99 * 2) end end ! context "when the origin and destination are in different countries" do it "returns the correspondent shipping cost" do expect(subject.cost).to eq (1 * 5.99 * 3) end end end end ... end
  7. module Shipping class StandardMethod attr_reader :order, :origin, :destination def cost

    items_quantity * rate * distance end ... private def items_quantity order.items.reject(:digital?).length end def rate if items_quantity > 1 5.99 else 9.99 end end def distance return 1 if origin.same_state? destination return 2 if origin.same_country? destination 3 end end end
  8. module Shipping class StandardMethod attr_reader :order, :origin, :destination def cost

    items_quantity * rate * distance end ... private def items_quantity order.items.reject(:digital?).length end def rate if items_quantity > 1 5.99 else 9.99 end end def distance return 1 if origin.same_state? destination return 2 if origin.same_country? destination 3 end end end
  9. describe Shipping::StandardMethod do describe "#cost" do let(:order) { double(:order) }

    let(:origin) { double(:origin) } let(:destination) { double(:destination) } subject { Shipping::StandardMethod.new(order, origin, destination) } context "when all items in the order are digital" do it "returs zero" do order.stub(non_digital_items: 0) expect(subject.cost).to be_zero end end ! context "when at least one item in the order is not digital" do it "calculates the cost of shipping" do Shipping::Rate.stub(:new).with(1) { double(calculate: 10) } Shipping::Distance.stub(:new).with(origin, destination) { double(calculate: 2) } order.stub(non_digital_items: 1) expect(subject.cost).to eq (1 * 10 * 2) end end end ... end
  10. context "when all items in the order are digital" do

    it "returns zero" do order.stub(non_digital_items: 0) expect(subject.cost).to be_zero end end
  11. context "when at least one item in the order is

    not digital" do it "calculates the cost of shipping" do Shipping::Rate.stub(:new).with(1) { double(calculate: 10) } Shipping::Distance.stub(:new).with(origin, destination) { double(calculate: 2) } order.stub(non_digital_items: 1) expect(subject.cost).to eq (1 * 10 * 2) end end
  12. module Shipping class StandardMethod attr_reader :order, :origin, :destination def cost

    items_quantity * rate * distance end ... private def items_quantity order.non_digital_items end def rate Shipping::Rate.new(items_quantity).calculate end ! def distance Shipping::Distance.new(origin, destination).calculate end end end
  13. describe Shipping::StandardMethod do describe "#cost" do ... end describe "#packing_days"

    do ... end describe "#shipping_estimated_date" do ... end describe "#delivery_estimated_date" do ... end end describe Shipping::ExpressMethod do describe "#cost" do ... end ! describe "#shipping_estimated_date" do ... end describe "#delivery_estimated_date" do ... end end
  14. describe Shipping::StandardMethod do describe "#cost" do ... end describe "#packing_days"

    do ... end describe "#shipping_estimated_date" do ... end describe "#delivery_estimated_date" do ... end end describe Shipping::ExpressMethod do describe "#cost" do ... end ! ! ! ! describe "#shipping_estimated_date" do ... end describe "#delivery_estimated_date" do ... end end
  15. describe Shipping::StandardMethod do describe "#cost" do ... end describe "#packing_days"

    do ... end describe "#shipping_estimated_date" do ... end describe "#delivery_estimated_date" do ... end end describe Shipping::ExpressMethod do describe "#cost" do ... end ! ! ! ! describe "#shipping_estimated_date" do ... end describe "#delivery_estimated_date" do ... end end
  16. shared_examples "a shipping method" do it { should respond_to(:cost) }

    it { should respond_to(:packing_days) } it { should respond_to(:shipping_estimated_date) } it { should respond_to(:delivery_estimated_date) } end
  17. describe Shipping::StandardMethod do ! it_should_behave_like "a shipping method" describe "#cost"

    do ... end describe "#packing_days" do ... end describe "#shipping_estimated_date" do ... end describe "#delivery_estimated_date" do ... end end describe Shipping::ExpressMethod do ! it_should_behave_like "a shipping method" describe "#cost" do ... end ! ! ! ! ! describe "#shipping_estimated_date" do ... end describe "#delivery_estimated_date" do ... end end
  18. describe Shipping::StandardMethod do ! it_should_behave_like "a shipping method" describe "#cost"

    do ... end describe "#packing_days" do ... end describe "#shipping_estimated_date" do ... end describe "#delivery_estimated_date" do ... end end describe Shipping::ExpressMethod do ! it_should_behave_like "a shipping method" describe "#cost" do ... end ! describe "#packing_days" do ... end ! describe "#shipping_estimated_date" do ... end describe "#delivery_estimated_date" do ... end end
  19. We need to be able to ship an Order using

    any of the existing shipping methods
  20. describe Order do describe "#ship" do context "when shipping method

    is a Shipping::StandardMethod" do … end context "when shipping method is a Shipping::ExpressMethod" do … end context "when shipping method is a Shipping::SameDayMethod" do … end end end
  21. class Order attr_reader :seller, :buyer, :items, :method ! def ship

    case method.class when Shipping::RegularMethod origin = seller.address destination = buyer.shipping_address method.ship(self, origin, destination) when Shipping::ExpressMethod origin = seller.address destination = "#{buyer.shipping_address}, #{buyer.zip}" method.send_items(items, origin, destination) when Shipping::SameDayMethod method.ship(items: items, origin: seller, destination: buyer) end end ... end
  22. case method.class when Shipping::RegularMethod origin = seller.address destination = buyer.shipping_address

    method.ship(self, origin, destination) when Shipping::ExpressMethod origin = seller.address destination = "#{buyer.shipping_address}, #{buyer.zip}" method.send_items(items, origin, destination) when Shipping::SameDayMethod method.ship(items: items, origin: seller, destination: buyer) end
  23. class Order attr_reader :seller, :buyer, :items, :method ! def ship

    case method.class when Shipping::RegularMethod origin = seller.address destination = buyer.shipping_address method.ship(self, origin, destination) when Shipping::ExpressMethod origin = seller.address destination = "#{buyer.shipping_address}, #{buyer.zip}" method.send_items(items, origin, destination) when Shipping::SameDayMethod method.ship(items: items, origin: seller, destination: buyer) end end ... end
  24. class Order attr_reader :seller, :buyer, :items, :method ! def ship

    case method.class when Shipping::RegularMethod origin = seller.address destination = buyer.shipping_address method.ship(self, origin, destination) when Shipping::ExpressMethod origin = seller.address destination = "#{buyer.shipping_address}, #{buyer.zip}" method.send_items(items, origin, destination) when Shipping::SameDayMethod method.ship(items: items, origin: seller, destination: buyer) when Shipping::NextDayMethod method.ship(items, seller, buyer) end end ... end
  25. class Order attr_reader :seller, :buyer, :items, :method ! def ship

    case method.class when Shipping::RegularMethod origin = seller.address destination = buyer.shipping_address method.ship(self, origin, destination) when Shipping::ExpressMethod origin = seller.address destination = "#{buyer.shipping_address}, #{buyer.zip}" method.send_items(items, origin, destination) when Shipping::SameDayMethod method.ship(items: items, origin: seller, destination: buyer) when Shipping::NextDayMethod method.ship(items, seller, buyer) end end ... end
  26. class Order attr_reader :seller, :buyer, :items, :method ! def ship

    case method.class when Shipping::RegularMethod origin = seller.address destination = buyer.shipping_address method.ship(self, origin, destination) when Shipping::ExpressMethod origin = seller.address destination = "#{buyer.shipping_address}, #{buyer.zip}" method.send_items(items, origin, destination) when Shipping::SameDayMethod method.ship(items: items, origin: seller, destination: buyer) when Shipping::NextDayMethod method.ship(items, seller, buyer) end end ... end
  27. describe Order do describe "#ship" do it "ships the order

    using the shipping method" do items = double(:items) seller = double(:seller) buyer = double(:buyer) method = double(:shipping_method) order = Order.new(items, seller, buyer) order.ship(method) ! expect(method). to have_received(:ship). with(items, seller, buyer) end end end
  28. describe Order do describe "#ship" do it "ships the order

    using the shipping method" do items = double(:items) seller = double(:seller) buyer = double(:buyer) method = double(:shipping_method) order = Order.new(items, seller, buyer) order.ship(method) ! expect(method). to have_received(:ship). with(items, seller, buyer) end end end
  29. describe Order do describe "#ship" do it "ships the order

    using the shipping method" do items = double(:items) seller = double(:seller) buyer = double(:buyer) method = double(:shipping_method) order = Order.new(items, seller, buyer) order.ship(method) ! expect(method). to have_received(:ship). with(items, seller, buyer) end end end
  30. class Order attr_reader :items, :seller, :buyer ! def initialize(args) …

    end def ship(method) method.ship(items, buyer, items) end ... end
  31. > ActiveRecord::Base.instance_methods(false).sort => [:<=>, :==, :[], :[]=, :_accessible_attributes, :_accessible_attributes=, :_accessible_attributes?,

    :_active_authorizer, :_active_authorizer=, :_active_authorizer?, :_attr_readonly, :_attr_readonly?, :_commit_callbacks, :_commit_callbacks=, :_commit_callbacks?, :_create_callbacks, :_create_callbacks=, :_create_callbacks?, :_destroy_callbacks, ...] > ActiveRecord::Base.instance_methods(false).count => 123
  32. > ActiveRecord::Base.instance_methods(false).sort => [:<=>, :==, :[], :[]=, :_accessible_attributes, :_accessible_attributes=, :_accessible_attributes?,

    :_active_authorizer, :_active_authorizer=, :_active_authorizer?, :_attr_readonly, :_attr_readonly?, :_commit_callbacks, :_commit_callbacks=, :_commit_callbacks?, :_create_callbacks, :_create_callbacks=, :_create_callbacks?, :_destroy_callbacks, ...] > ActiveRecord::Base.instance_methods(false).count => 123
  33. shared_examples "an ActiveRecord::Base instance" do it { should respond_to(:<=>) }

    it { should respond_to(:==) } it { should respond_to(:[]) } it { should respond_to(:[]=) } it { should respond_to(:_accessible_attributes) } ... end
  34. shared_examples "an ActiveRecord::Base instance" do it { should respond_to(:<=>) }

    it { should respond_to(:==) } it { should respond_to(:[]) } it { should respond_to(:[]=) } it { should respond_to(:_accessible_attributes) } ... end 118 to go
  35. shared_examples "an ActiveRecord::Base instance" do it { should respond_to(:<=>) }

    it { should respond_to(:==) } it { should respond_to(:[]) } it { should respond_to(:[]=) } it { should respond_to(:_accessible_attributes) } ... end
  36. shared_examples "a shipping method" do it { should respond_to(:cost) }

    it { should respond_to(:packing_days) } it { should respond_to(:shipping_estimated_date) } it { should respond_to(:delivery_estimated_date) } end
  37. shared_examples "a shipping method" do it { should respond_to(:cost) }

    it { should respond_to(:packing_days) } it { should respond_to(:shipping_estimated_date) } it { should respond_to(:delivery_estimated_date) } end
  38. =

  39. Credits • http://www.flickr.com/photos/pulpolux/192250352/ - Title • http://www.flickr.com/photos/pulpolux/3318002502/ - Single responsibility

    • http://www.flickr.com/photos/pulpolux/1784671520/ - Liskov substitution • http://www.flickr.com/photos/pulpolux/757214357/ - Open closed • http://www.flickr.com/photos/zdenadel/2422697310/ - Dependency inversion • http://www.flickr.com/photos/pan-o/4987143590/ - Interface segregation • http://www.socwall.com/desktop-wallpaper/19991/underwater-iceberg-by-fanois/ - Iceberg class • http://little--vampire.tumblr.com/post/57019786320 - Titanic movie pic • http://www.dailymail.co.uk/news/article-2114836/Hilarious-DIY-car-repair-bodge-jobs-carried-amateur-mechanics.html - Car with trolley wheel • http://www.etsy.com/listing/172435308/double-sided-open-closed-funny-retail?ref=market - “Closed but still awesome” • http://www.flickr.com/photos/jydesign/2086002622/in/photostream/ - Smileys • http://memegenerator.net/Uncle-Bob - Uncle Bob’s meme • http://www.flickr.com/photos/rivalee/3234174656/ - 3D glasses • http://www.quickmeme.com/Grandma-finds-the-Internet/page/205/ - Store closed meme • http://picyou.com/UFGfN4 - Rose? meme