Advertisement
Advertisement

More Related Content

Advertisement

Writing Loveable Code

  1. Writing Loveable Code @seemaisms seemaullal@gmail.com
  2. !"# @seemaisms seemaullal@gmail.com
  3. !" @seemaisms seemaullal@gmail.com
  4. Seema Ullal @seemaisms @seemaisms seemaullal@gmail.com
  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. How did we get there? @seemaisms seemaullal@gmail.com
  7. ✨ Let's build a new feature! @seemaisms seemaullal@gmail.com
  8. ! Spend some time thinking about how it should work @seemaisms seemaullal@gmail.com
  9. ! Write the code @seemaisms seemaullal@gmail.com
  10. It works! Everything is great! @seemaisms seemaullal@gmail.com
  11. Sometime later... @seemaisms seemaullal@gmail.com
  12. ! : "Let's make this even better" ! : "Sounds great, I'll start implementing those changes" @seemaisms seemaullal@gmail.com
  13. Hmm, let me take a look at what the code is doing now @seemaisms seemaullal@gmail.com
  14. ! How does it work? @seemaisms seemaullal@gmail.com
  15. ...does it work? @seemaisms seemaullal@gmail.com
  16. ! How do I even start to change this? @seemaisms seemaullal@gmail.com
  17. Can I make changes without accidentally breaking things? @seemaisms seemaullal@gmail.com
  18. @seemaisms seemaullal@gmail.com
  19. We have the most context on the code we write @seemaisms seemaullal@gmail.com
  20. But other people will read your code @seemaisms seemaullal@gmail.com
  21. They may even need to change it @seemaisms seemaullal@gmail.com
  22. How can we write code that is easy for people to understand and modify? @seemaisms seemaullal@gmail.com
  23. Code That Is Clear, Testable, Changeable @seemaisms seemaullal@gmail.com
  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. Give context to your code @seemaisms seemaullal@gmail.com
  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. 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. 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. Functions and classes should do one thing well @seemaisms seemaullal@gmail.com
  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. 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. Use service objects to isolate complexity @seemaisms seemaullal@gmail.com
  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. 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. Validate assumptions and fail fast @seemaisms seemaullal@gmail.com
  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. Code That Is Clear, Testable, Changeable @seemaisms seemaullal@gmail.com
  38. Write tests @seemaisms seemaullal@gmail.com
  39. Let your tests document how the code should behave @seemaisms seemaullal@gmail.com
  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. expect(company_tax_total).to eq(472.67) @seemaisms seemaullal@gmail.com
  42. expect(company_tax_total).to eq( 116.55 + 62.59 + 267.63 + 25.9 ) @seemaisms seemaullal@gmail.com
  43. expect(company_tax_total).to eq( state_unemployment_tax + medicare_tax + social_security_tax + federal_unemployment_tax ) @seemaisms seemaullal@gmail.com
  44. Don't hit the database when you don't need to @seemaisms seemaullal@gmail.com
  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. Avoid flaky tests @seemaisms seemaullal@gmail.com
  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. 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. before do Timecop.freeze(Date.new(2018, 1, 10)) end after { Timecop.return } @seemaisms seemaullal@gmail.com
  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. it ‘writes to the correct path’ do expect(File).to receive( :open ).with( “/tmp/documents/#{document.id}/artifact” ) end @seemaisms seemaullal@gmail.com
  52. Prevent unit tests from testing more than they should @seemaisms seemaullal@gmail.com
  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. 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. 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. Code That Is Clear, Testable, Changeable @seemaisms seemaullal@gmail.com
  57. Eliminate surprises and side effects @seemaisms seemaullal@gmail.com
  58. class Discount attr_reader :amount def initialize(amount) @amount = amount end end @seemaisms seemaullal@gmail.com
  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. 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. @seemaisms seemaullal@gmail.com
  62. Consciously uncouple the different parts of your code @seemaisms seemaullal@gmail.com
  63. Delete unneeded comments @seemaisms seemaullal@gmail.com
  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. 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. Slides on www.seemaullal.com under Talks ✉ seemaullal@gmail.com @seemaisms Thank You! @seemaisms seemaullal@gmail.com
Advertisement