Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Writing Loveable Code

68 views

Published on

Join me on my journey from thinking that the best code is clever and elegant to realizing that code that makes your fellow developers happy should be your goal.We will start with code that has a high barrier of entry and make changes to make it more easily testable, understandable, and extendable.

Published in: Software
  • Be the first to comment

  • Be the first to like this

Writing Loveable Code

  1. 1. Writing Loveable Code @seemaisms seemaullal@gmail.com
  2. 2. !"# @seemaisms seemaullal@gmail.com
  3. 3. !" @seemaisms seemaullal@gmail.com
  4. 4. Seema Ullal @seemaisms @seemaisms seemaullal@gmail.com
  5. 5. I hope to see Ruby help every programmer in the world to be productive, and to enjoy programming, and to be happy. That is the primary purpose of Ruby language. — Yukihiro Matsumoto @seemaisms seemaullal@gmail.com
  6. 6. How did we get there? @seemaisms seemaullal@gmail.com
  7. 7. ✨ Let's build a new feature! @seemaisms seemaullal@gmail.com
  8. 8. ! Spend some time thinking about how it should work @seemaisms seemaullal@gmail.com
  9. 9. ! Write the code @seemaisms seemaullal@gmail.com
  10. 10. It works! Everything is great! @seemaisms seemaullal@gmail.com
  11. 11. Sometime later... @seemaisms seemaullal@gmail.com
  12. 12. ! : "Let's make this even better" ! : "Sounds great, I'll start implementing those changes" @seemaisms seemaullal@gmail.com
  13. 13. Hmm, let me take a look at what the code is doing now @seemaisms seemaullal@gmail.com
  14. 14. ! How does it work? @seemaisms seemaullal@gmail.com
  15. 15. ...does it work? @seemaisms seemaullal@gmail.com
  16. 16. ! How do I even start to change this? @seemaisms seemaullal@gmail.com
  17. 17. Can I make changes without accidentally breaking things? @seemaisms seemaullal@gmail.com
  18. 18. @seemaisms seemaullal@gmail.com
  19. 19. We have the most context on the code we write @seemaisms seemaullal@gmail.com
  20. 20. But other people will read your code @seemaisms seemaullal@gmail.com
  21. 21. They may even need to change it @seemaisms seemaullal@gmail.com
  22. 22. How can we write code that is easy for people to understand and modify? @seemaisms seemaullal@gmail.com
  23. 23. Code That Is Clear, Testable, Changeable @seemaisms seemaullal@gmail.com
  24. 24. class Connect4 def initialize @board = ("......n"*7).chomp @player = "2" @done = false end def play col return "Game has finished!" if @done begin @board[%r/A((......n){#{col}}d*)(.)/, 3] = "#@player" rescue return "Column full!" end @player.tr! "12", "21" if [0,5,6,7].any?{|off| @board =~ /(d)([sS]{#{off}}1){3}/} @done = true "Player #@player wins!" else "Player #@player has a turn" end end end @seemaisms seemaullal@gmail.com
  25. 25. Give context to your code @seemaisms seemaullal@gmail.com
  26. 26. class Connect4 def initialize @board = initialize_board @turn = 1 @finished = false end def play(row_index) @row_index = row_index return 'Game has finished!' if @finished return "Column full!" if column_full? @board[@row_index] << @turn if player_won? @finished = true return "Player #{@turn} wins!" else toggle_turn return "Player #{@turn == 1 ? 2 : 1} has a turn" end end @seemaisms seemaullal@gmail.com
  27. 27. def toggle_turn @turn = @turn == 1 ? 2 : 1 end def player_won? four_in_a_row?(vertical) || four_in_a_row?(horizontal) || four_in_a_row?(diagonal_up) || four_in_a_row?(diagonal_down) end def four_in_a_row?(array) !!array.chunk_while { |i,j| i == j }.find { |arr| arr.length >= 4 && arr[0] } end def column_full? @board[@row_index].length == 6 end def initialize_board 7.times.with_object([]) { |i, board| board << [] } end @seemaisms seemaullal@gmail.com
  28. 28. class InvoiceCreator def initialize(user_id, invoice_date) @user_id = user_id @invoice_date = invoice_date end def create_invoice user_subscriptions = UserSubscription.where( user_id: @user_id ).select do |subcription| subscription.invoice_date == invoice_date end total_cost = user_subscriptions.sum do |user_subscription| if user_subscription.monthly_subscription? user_subscription.cost else user_subscription.cost /12 end end UserInvoice.create!(user_id: @user_id, amount: total_cost, date: @invoice_date) end end @seemaisms seemaullal@gmail.com
  29. 29. Functions and classes should do one thing well @seemaisms seemaullal@gmail.com
  30. 30. class UserSubscriptionFetcher def fetch(user_id, invoice_date) UserSubscription.where(user_id: @user_id).select do |subcription| subscription.invoice_date == invoice_date end end end class UserSubscription def monthly_cost monthly_subscription? cost : cost /12 end end class InvoiceCreator def initialize(user_id, invoice_amount, invoice_date) @user_id = user_id @invoice_amount = invoice_amount @invoice_date = invoice_date end def create_invoice UserInvoice.create!(user_id: @user_id, amount: @invoice_amount, date: @invoice_date) end end @seemaisms seemaullal@gmail.com
  31. 31. class Employee # ... def formatted_phone_number return '' if phone_number.blank? return "(#{phone[0..2]}) #{phone[3..5]}-#{phone[6..9]}" end end @seemaisms seemaullal@gmail.com
  32. 32. Use service objects to isolate complexity @seemaisms seemaullal@gmail.com
  33. 33. class PhoneNumberFormatter def self.number_with_parentheses(phone_number) return '' if phone_number.blank? return "(#{phone[0..2]}) #{phone[3..5]}-#{phone[6..9]}" end end @seemaisms seemaullal@gmail.com
  34. 34. class PayEmployee def self.call(employee) payment = PaymentCreator.new(employee: employee).create_payment DebitCompanyForPayment.call(payment, employee.company) end end @seemaisms seemaullal@gmail.com
  35. 35. Validate assumptions and fail fast @seemaisms seemaullal@gmail.com
  36. 36. class PayEmployee def self.call(employee) raise "Employee id #{employee.id} does not have a bank account" unless employee.bank_account raise "Company id #{company.id} does not have a bank account" unless company.bank_account payment = PaymentCreator.new(employee: employee).create_payment DebitCompanyForPayment.call(payment, employee.company) end end @seemaisms seemaullal@gmail.com
  37. 37. Code That Is Clear, Testable, Changeable @seemaisms seemaullal@gmail.com
  38. 38. Write tests @seemaisms seemaullal@gmail.com
  39. 39. Let your tests document how the code should behave @seemaisms seemaullal@gmail.com
  40. 40. context 'when the person lives in Tennessee' do context 'and also works there' do it 'includes taxes for Tennessee' do end end context 'when they work in a different state' do it 'includes taxes for that state also' do end end end @seemaisms seemaullal@gmail.com
  41. 41. expect(company_tax_total).to eq(472.67) @seemaisms seemaullal@gmail.com
  42. 42. expect(company_tax_total).to eq( 116.55 + 62.59 + 267.63 + 25.9 ) @seemaisms seemaullal@gmail.com
  43. 43. expect(company_tax_total).to eq( state_unemployment_tax + medicare_tax + social_security_tax + federal_unemployment_tax ) @seemaisms seemaullal@gmail.com
  44. 44. Don't hit the database when you don't need to @seemaisms seemaullal@gmail.com
  45. 45. class User def full_name [ first_name, middle_initial, last_name].compact.join(' ') end end # in the test user = instance_double(User, first_name: 'Cookie', middle_initial: nil, last_name: 'Monster') @seemaisms seemaullal@gmail.com
  46. 46. Avoid flaky tests @seemaisms seemaullal@gmail.com
  47. 47. describe('user invoice callbacks') do it 'sends an email after the invoice is created' do expect(UserMailer).to receive :user_invoice_email UserInvoice.create end end @seemaisms seemaullal@gmail.com
  48. 48. describe('user invoice callbacks') do it 'sends an email after the invoice is created' do expect(UserMailer).to receive :user_invoice_email UserInvoice.create end end This usually works. But, what if we only send emails on business days. @seemaisms seemaullal@gmail.com
  49. 49. before do Timecop.freeze(Date.new(2018, 1, 10)) end after { Timecop.return } @seemaisms seemaullal@gmail.com
  50. 50. context ‘saving a file’ do let(:document) { create(:file, val: ‘test’) } subject { document.save_file! } it ‘writes to the right path’ do expect(File).to receive(:open).with("/tmp/documents/1/test") end @seemaisms seemaullal@gmail.com
  51. 51. it ‘writes to the correct path’ do expect(File).to receive( :open ).with( “/tmp/documents/#{document.id}/artifact” ) end @seemaisms seemaullal@gmail.com
  52. 52. Prevent unit tests from testing more than they should @seemaisms seemaullal@gmail.com
  53. 53. class PhoneNumberFormatter def self.number_with_parentheses(phone_number) return '' if phone_number.blank? return "(#{phone[0..2]}) #{phone[3..5]}-#{phone[6..9]}" end end class Employee def formatted_phone_number PhoneNumberFormatter.number_with_parentheses(phone) end end @seemaisms seemaullal@gmail.com
  54. 54. class Employee def formatted_phone_number PhoneNumberFormatter.number_with_parentheses(phone) end end describe Employee do describe '#formatted_phone_number' do let(:phone_number) { '8001234567'} it 'returns the formatted version of the phone number' do expect(employee.formatted_phone_number).to eq('(800) 123-4567') end end end @seemaisms seemaullal@gmail.com
  55. 55. describe Employee do describe '#formatted_phone_number' do it 'calls the formatter class' do expect(PhoneNumberFormatter).to receive(:number_with_parentheses). with(employee.phone) end end end @seemaisms seemaullal@gmail.com
  56. 56. Code That Is Clear, Testable, Changeable @seemaisms seemaullal@gmail.com
  57. 57. Eliminate surprises and side effects @seemaisms seemaullal@gmail.com
  58. 58. class Discount attr_reader :amount def initialize(amount) @amount = amount end end @seemaisms seemaullal@gmail.com
  59. 59. class Discount attr_reader :amount def initialize(amount) @amount = amount end end class DiscountCalculator def self.calculate(user_id) user_discounts = UserDiscount.where(user_id: user_id) user_discounts.sum { |ud| ud.discount.amount } end end @seemaisms seemaullal@gmail.com
  60. 60. class Discount attr_reader :amount def initialize(amount) @amount = amount end end class DiscountCalculator def self.calculate(user_id) user_discounts = UserDiscount.where(user_id: user_id) user_discounts.sum { |ud| ud.discount.amount } end end Amount is sometimes the amount of the discount in cents but sometimes it is a percentage... @seemaisms seemaullal@gmail.com
  61. 61. @seemaisms seemaullal@gmail.com
  62. 62. Consciously uncouple the different parts of your code @seemaisms seemaullal@gmail.com
  63. 63. Delete unneeded comments @seemaisms seemaullal@gmail.com
  64. 64. class PhoneNumberFormatter # this is used to format employee phone numbers, # user phone numbers, and admin phone numbers def self.number_with_parentheses(phone_number) return '' if phone_number.blank? return "(#{phone[0..2]}) #{phone[3..5]}-#{phone[6..9]}" end end @seemaisms seemaullal@gmail.com
  65. 65. Any fool can write code that a computer can understand. Good programmers write code that humans can understand. — Refactoring: Improving the Design of Existing Code @seemaisms seemaullal@gmail.com
  66. 66. Slides on www.seemaullal.com under Talks ✉ seemaullal@gmail.com @seemaisms Thank You! @seemaisms seemaullal@gmail.com

×