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

SOLID and TDD, Sitting in a Tree

99e2a6afab542ba98a9f1d1cae6c9670?s=47 PromptWorks
September 26, 2013

SOLID and TDD, Sitting in a Tree

Watch it online: http://confreaks.com/videos/2653-rockymountainruby2013-solid-and-tdd-sitting-in-a-tree

You’ve heard the claims or know from experience that test-driven development (TDD) produces better code. Perhaps you’ve also heard that to write better code you should be following the SOLID principles. Is there a connection between practicing TDD and writing SOLID code?

In this talk, I’ll use examples gleaned from real life to demonstrate how TDD prompts us to produce code that conforms to the SOLID principles, and how the SOLID principles are where we should turn when our tests are causing us pain. In doing so, we’ll learn what each principle really means and why it’s valuable.

Mike Nicholaides has been a Rails consultant since 2006 and has always obsessed about writing clean, clear, and concise code. He organizes the Code Retreat in Philadelphia where the focus is on learning TDD, communicating with code, and of course, having fun.

Presented by Mike Nicholaides at Rocky Mountain Ruby Conf. https://www.promptworks.com

99e2a6afab542ba98a9f1d1cae6c9670?s=128

PromptWorks

September 26, 2013
Tweet

Transcript

  1. TDD & SOLID Mike Nicholaides

  2. http://promptworks.com! @promptworks

  3. describe Notifier, "#notify" do let(:message_body) do "Hi @#{notifiable_user1.name} @#{notifiable_user2.name}\ @#{

    unnotifiable_user.name }" end ! let(:message) { create(:message, body: message_body) } ! it "FB's the message's mentioned users who prefer to be FB'd" do notifiable_user1 = create(:facebook_connected_user, fb_me_on_mention: true) notifiable_user2 = create(:facebook_connected_user, fb_me_on_mention: true) unnotifiable_user = create(:facebook_connected_user, fb_me_on_mention: false) ! notifiable_user1.facebook_connection.stub(put_connections: true) notifiable_user2.facebook_connection.stub(put_connections: true) unnotifiable_user.facebook_connection.stub(put_connections: true) ! Notifier.notify(message) ! expected_message = "#{message.author.name} said #{message_body}" ! expect(notifiable_user1.facebook_connection).to have_received(:put_connections).with("me", "feed", message: expected_message) expect(notifiable_user2.facebook_connection).to have_received(:put_connections).with("me", "feed", message: expected_message) expect(unnotifiable_user.facebook_connection).not_to have_received(:put_connections) end ! it "emails the message's mentioned users who prefer to be emailed" do notifiable_user1 = create(:user, email_me_on_mention: true) notifiable_user2 = create(:user, email_me_on_mention: true) unnotifiable_user = create(:user, email_me_on_mention: false) ! MessageMailer.stub(:deliver_message) ! Notifier.notify(message) ! expected(MessageMailer).to have_received(:deliver_message).with(message, [notifiable_user1, notifiable_user2]) end ! it "SMS's the message's mentioned users who prefer to be SMS'd" do notifiable_user1 = create(:sms_user, sms_me_on_mention: true) notifiable_user2 = create(:sms_user, sms_me_on_mention: true) unnotifiable_user = create(:sms_user, sms_me_on_mention: false) ! SMSGateway.stub(:deliver) ! Notifier.notify(message) ! expected_message = "#{message.author.name} said #{message_body}" ! expect(SMSGateway).to have_received(:deliver).with(expected_message, notifiable_user1.sms_number) expect(SMSGateway).to have_received(:deliver).with(expected_message, notifiable_user2.sms_number) end end
  4. class Notifier def self.notify(message) message.mentioned_users. select(&:fb_me_on_mention?). select(&:facebook_connection). each do |user|

    user.facebook_connection.put_connections( "me", "feed", message: "#{message.author.name} said: #{message.body}" ) end ! email_users = message.mentioned_users. select(&:email_me_on_mention?). select{|u| u.email.present? } MessageMailer.deliver_message(message, email_users) ! message.mentioned_users. select(&:sms_me_on_mention?). select{|u| u.sms_number.present? }. each { |user| SMSGateway.deliver(message, user.sms_number) } end end
  5. Test first != good code

  6. SOLID

  7. Interface Segregation Principle “many client-specific interfaces are better than one

    general-purpose interface.” ! Robert C. Martin Principles of Object Oriented Design
  8. Dependency Inversion Principle “Depend upon abstractions. Do not depend upon

    concretions.” ! ! Robert C. Martin, Design Principles and Design Patterns
  9. Depending on a Concretion def save_my_game if game_changed? File.open('game.txt', 'w')

    do |file| file.print @game_data end end end ! save_my_game
  10. Depending on an Abstraction def save_my_game persistent_store = FileStore.new persistent_store.save(@game_data)

    if game_changed? end ! save_my_game class FileStore def save(game_data) File.open('game.txt', 'w') do |file| file.print game_data end end end
  11. Dependency Inversion Principle hide concretions behind abstractions

  12. Open/Closed Principle “Software entities … should be open for extension,

    but closed for modification” ! Bertrand Meyer, Object Oriented Software Construction
  13. Open/Closed Principle • dependencies • collaborators • logic Swappable pieces

    for
  14. Open/Closed Principle • blocks • inheritance • dependency injection Techniques

  15. Open/Closed Principle def save_my_game(persistent_store) persistent_store.save(@game_data) if game_changed? end ! save_my_game(FileStore.new)

  16. Open/Closed Principle Make entities extendible

  17. Liskov Substitution Principle • use duck typing

  18. Liskov Substitution Principle def save_my_game(persistent_store) persistent_store.save(@game_data) if game_changed? end

  19. Single Responsibility Principle “A class should have one, and only

    one, reason to change.” ! Robert C. Martin Principles of Object Oriented Design
  20. Single Responsibility Principle • classes should be small • classes

    should do one thing • we should have lots of classes
  21. SOLID • hide concretions behind abstractions • make entities extendible

    • use duck typing • classes should do one thing
  22. message.author # => User @JesseKatsopolis ! message.body # => "@DannyTanner,

    @JoeyGladstone, Watch the hair!" ! message.mentioned_users # => [ User @DannyTanner, User @JoeyGladstone ]
  23. # ... ! if @message.save ! Notifier.notify(@message) ! redirect_to @message

    else ! # ...
  24. describe Notifier, ".notify" do ! it "emails the message's mentioned

    users who prefer to be emailed" it "SMS's the message's mentioned users who prefer to be SMS'd" it "FB's the message's mentioned users who prefer to be FB'd" ! end
  25. Details • what it does • what is knows

  26. Responsibilities • email users • SMS users • FB users

    ! Concretions • the message • the mentioned users • how to determine if someone wants to be emailed • how to determine if someone wants to be SMS'd • how to determine if someone wants to be FB'd

  27. Responsibilities Described it "emails the message's mentioned users who prefer

    to be emailed" it "SMS's the message's mentioned users who prefer to be SMS'd" it "FB's the message's mentioned users who prefer to be FB'd"
  28. Strained Language It emails the messages' mentioned users who prefer

    to be emailed.
  29. it "FB's the message's mentioned users who prefer to be

    FB'd" do ! notifiable_user1 = create(:facebook_connected_user, fb_me_on_mention: true) notifiable_user2 = create(:facebook_connected_user, fb_me_on_mention: true) unnotifiable_user = create(:facebook_connected_user, fb_me_on_mention: false) ! message_body = "Hi @#{notifiable_user1.name} @#{notifiable_user2.name}\ @#{ unnotifiable_user.name }" ! message = create(:message, body: message_body) ! notifiable_user1.facebook_connection.stub(put_connections: true) notifiable_user2.facebook_connection.stub(put_connections: true) unnotifiable_user.facebook_connection.stub(put_connections: true) ! Notifier.notify(message) ! expected_message = "#{message.author.name} said #{message_body}" ! expect(notifiable_user1.facebook_connection). to have_received(:put_connections). with("me", "feed", message: expected_message) ! expect(notifiable_user2.facebook_connection). to have_received(:put_connections). with("me", "feed", message: expected_message) ! expect(unnotifiable_user.facebook_connection). not_to have_received(:put_connections) ! end
  30. too many details == hard to test

  31. describe Notifier, "#notify" do let(:message_body) do "Hi @#{notifiable_user1.name} @#{notifiable_user2.name}\ @#{

    unnotifiable_user.name }" end ! let(:message) { create(:message, body: message_body) } ! it "FB's the message's mentioned users who prefer to be FB'd" do notifiable_user1 = create(:facebook_connected_user, fb_me_on_mention: true) notifiable_user2 = create(:facebook_connected_user, fb_me_on_mention: true) unnotifiable_user = create(:facebook_connected_user, fb_me_on_mention: false) ! notifiable_user1.facebook_connection.stub(put_connections: true) notifiable_user2.facebook_connection.stub(put_connections: true) unnotifiable_user.facebook_connection.stub(put_connections: true) ! Notifier.notify(message) ! expected_message = "#{message.author.name} said #{message_body}" ! expect(notifiable_user1.facebook_connection).to have_received(:put_connections).with("me", "feed", message: expected_message) expect(notifiable_user2.facebook_connection).to have_received(:put_connections).with("me", "feed", message: expected_message) expect(unnotifiable_user.facebook_connection).not_to have_received(:put_connections) end ! it "emails the message's mentioned users who prefer to be emailed" do notifiable_user1 = create(:user, email_me_on_mention: true) notifiable_user2 = create(:user, email_me_on_mention: true) unnotifiable_user = create(:user, email_me_on_mention: false) ! MessageMailer.stub(:deliver_message) ! Notifier.notify(message) ! expected(MessageMailer).to have_received(:deliver_message).with(message, [notifiable_user1, notifiable_user2]) end ! it "SMS's the message's mentioned users who prefer to be SMS'd" do notifiable_user1 = create(:sms_user, sms_me_on_mention: true) notifiable_user2 = create(:sms_user, sms_me_on_mention: true) unnotifiable_user = create(:sms_user, sms_me_on_mention: false) ! SMSGateway.stub(:deliver) ! Notifier.notify(message) ! expected_message = "#{message.author.name} said #{message_body}" ! expect(SMSGateway).to have_received(:deliver).with(expected_message, notifiable_user1.sms_number) expect(SMSGateway).to have_received(:deliver).with(expected_message, notifiable_user2.sms_number) end end
  32. class Notifier def self.notify(message) message.mentioned_users. select(&:fb_me_on_mention?). select(&:facebook_connection). each do |user|

    user.facebook_connection.put_connections( "me", "feed", message: "#{message.author.name} said: #{message.body}" ) end ! email_users = message.mentioned_users. select(&:email_me_on_mention?). select{|u| u.email.present? } MessageMailer.deliver_message(message, email_users) ! message.mentioned_users. select(&:sms_me_on_mention?). select{|u| u.sms_number.present? }. each { |user| SMSGateway.deliver(message, user.sms_number) } end end
  33. Guidelines for Pain-Free Testing ! • defer responsibilities to other

    classes • assume good abstractions
  34. it "emails the message's mentioned users who prefer to be

    emailed" it "SMS's the message's mentioned users who prefer to be SMS'd" it "FB's the message's mentioned users who prefer to be FB'd"
  35. it "______ the message's mentioned users who prefer to be

    ______" it "______ the message's mentioned users who prefer to be ______" it "______ the message's mentioned users who prefer to be ______"
  36. it "______ the message's mentioned users who prefer to be

    ______"
  37. it "notifies via SMS/FB/Email the message's mentioned users who prefer

    to be notified via SMS/FB/Email, respectively"
  38. it "notifies the message's mentioned users via their preferred notification

    channels"
  39. it "notifies the message's mentioned users via notification channels"

  40. it "notifies mentioned users via notification channels"

  41. it "notifies mentioned users via notification channels" do ! message

    = double('message') channels = [ double(notify_about_message: nil), double(notify_about_message: nil) ] notifier = Notifier.new(channels) ! notifier.notify(message) ! channel[0].should have_received(:notify_about_message).with message channel[1].should have_received(:notify_about_message).with message ! end
  42. class Notifier ! def initialize(channels) @channels = channels end !

    def notify(message) @channels.each do |channel| channel.notify_about_message(message) end end ! end ! notifier = Notifier.new(channels) notifier.notify(message)
  43. channels = case Rails.env when 'production' [ EmailChannel.new, SMSChannel.new, NSAChannel.new

    ] when 'staging' [ TestEmailChannel.new, TestSMSChannel.new ] else [ LoggingChannel.new ] end ! config.notifier = Notifier.new( channels )
  44. it "emails the message's mentioned users who prefer email" do

    ! message = build(:message, mentioned_users: [ user_who_likes_email1 = build(:user, email_on_mention: true), user_who_likes_email2 = build(:user, email_on_mention: true), user_who_dislikes_email = build(:user, email_on_mention: false) ]) MessageMailer.stub(:deliver_message) ! EmailChannel.new.notify(message) ! MessageMailer.should have_received(:deliver_message) .with(message, [user_who_likes_email1, user_who_likes_email2]) ! end
  45. - notify via channels Notifier - how to notify by

    email - who prefers to be emailed Email Channel - how to notify by SMS - who prefers to be SMS'd SMS Channel - who prefers to be Facebooked - how to notify by Facebook Facebook Channel - which channels < Configuration > - which users are mentioned Message - email address - Facebook Credentials - SMS number User - notify by email - notify by SMS - notify by Facebook - how to notify by email - who prefers to be emailed - how to notify by SMS - who prefers to be SMS'd - who prefers to be Facebooked - how to notify by Facebook Notifier - which users are mentioned Message - email address - Facebook Credentials - SMS number User
  46. @nicholaides mike@promptworks.com