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
@seemaisms
seemaullal@gmail.com
!"#
@seemaisms
seemaullal@gmail.com
!"
@seemaisms
seemaullal@gmail.com
Seema Ullal
@seemaisms
@seemaisms
seemaullal@gmail.com
I hope to see Ruby help every
programmer in the world to be
productive, and to enjoy
programming, and to be happy.
That is...
How did we get
there?
@seemaisms
seemaullal@gmail.com
✨
Let's build a new feature!
@seemaisms
seemaullal@gmail.com
!
Spend some time thinking about how it
should work
@seemaisms
seemaullal@gmail.com
!
Write the code
@seemaisms
seemaullal@gmail.com
It works! Everything is great!
@seemaisms
seemaullal@gmail.com
Sometime later...
@seemaisms
seemaullal@gmail.com
!
: "Let's make this even better"
!
: "Sounds great, I'll start implementing
those changes"
@seemaisms
seemaullal@gmail.com
Hmm, let me take a look at what the code is
doing now
@seemaisms
seemaullal@gmail.com
!
How does it work?
@seemaisms
seemaullal@gmail.com
...does it work?
@seemaisms
seemaullal@gmail.com
!
How do I even start to change this?
@seemaisms
seemaullal@gmail.com
Can I make changes without accidentally
breaking things?
@seemaisms
seemaullal@gmail.com
@seemaisms
seemaullal@gmail.com
We have the most context on the code we
write
@seemaisms
seemaullal@gmail.com
But other people will read your code
@seemaisms
seemaullal@gmail.com
They may even need to change it
@seemaisms
seemaullal@gmail.com
How can we write code that is easy
for people to understand and modify?
@seemaisms
seemaullal@gmail.com
Code That Is
Clear, Testable, Changeable
@seemaisms
seemaullal@gmail.com
class Connect4
def initialize
@board = ("......n"*7).chomp
@player = "2"
@done = false
end
def play col
return "Game has f...
Give context to your code
@seemaisms
seemaullal@gmail.com
class Connect4
def initialize
@board = initialize_board
@turn = 1
@finished = false
end
def play(row_index)
@row_index = r...
def toggle_turn
@turn = @turn == 1 ? 2 : 1
end
def player_won?
four_in_a_row?(vertical) ||
four_in_a_row?(horizontal) ||
f...
class InvoiceCreator
def initialize(user_id, invoice_date)
@user_id = user_id
@invoice_date = invoice_date
end
def create_...
Functions and classes should do one thing
well
@seemaisms
seemaullal@gmail.com
class UserSubscriptionFetcher
def fetch(user_id, invoice_date)
UserSubscription.where(user_id: @user_id).select do |subcri...
class Employee
# ...
def formatted_phone_number
return '' if phone_number.blank?
return "(#{phone[0..2]}) #{phone[3..5]}-#...
Use service objects to isolate complexity
@seemaisms
seemaullal@gmail.com
class PhoneNumberFormatter
def self.number_with_parentheses(phone_number)
return '' if phone_number.blank?
return "(#{phon...
class PayEmployee
def self.call(employee)
payment = PaymentCreator.new(employee: employee).create_payment
DebitCompanyForP...
Validate assumptions and fail fast
@seemaisms
seemaullal@gmail.com
class PayEmployee
def self.call(employee)
raise "Employee id #{employee.id} does not have a bank account" unless employee....
Code That Is
Clear, Testable, Changeable
@seemaisms
seemaullal@gmail.com
Write tests
@seemaisms
seemaullal@gmail.com
Let your tests document how the code
should behave
@seemaisms
seemaullal@gmail.com
context 'when the person lives in Tennessee' do
context 'and also works there' do
it 'includes taxes for Tennessee' do
end...
expect(company_tax_total).to eq(472.67)
@seemaisms
seemaullal@gmail.com
expect(company_tax_total).to eq(
116.55 +
62.59 +
267.63 +
25.9
)
@seemaisms
seemaullal@gmail.com
expect(company_tax_total).to eq(
state_unemployment_tax +
medicare_tax +
social_security_tax +
federal_unemployment_tax
)
...
Don't hit the database when you don't need
to
@seemaisms
seemaullal@gmail.com
class User
def full_name
[ first_name, middle_initial, last_name].compact.join(' ')
end
end
# in the test
user = instance_...
Avoid flaky tests
@seemaisms
seemaullal@gmail.com
describe('user invoice callbacks') do
it 'sends an email after the invoice is created' do
expect(UserMailer).to receive :u...
describe('user invoice callbacks') do
it 'sends an email after the invoice is created' do
expect(UserMailer).to receive :u...
before do
Timecop.freeze(Date.new(2018, 1, 10))
end
after { Timecop.return }
@seemaisms
seemaullal@gmail.com
context ‘saving a file’ do
let(:document) { create(:file, val: ‘test’) }
subject { document.save_file! }
it ‘writes to the...
it ‘writes to the correct path’ do
expect(File).to receive(
:open
).with(
“/tmp/documents/#{document.id}/artifact”
)
end
@...
Prevent unit tests from testing more than
they should
@seemaisms
seemaullal@gmail.com
class PhoneNumberFormatter
def self.number_with_parentheses(phone_number)
return '' if phone_number.blank?
return "(#{phon...
class Employee
def formatted_phone_number
PhoneNumberFormatter.number_with_parentheses(phone)
end
end
describe Employee do...
describe Employee do
describe '#formatted_phone_number' do
it 'calls the formatter class' do
expect(PhoneNumberFormatter)....
Code That Is
Clear, Testable, Changeable
@seemaisms
seemaullal@gmail.com
Eliminate surprises and side effects
@seemaisms
seemaullal@gmail.com
class Discount
attr_reader :amount
def initialize(amount)
@amount = amount
end
end
@seemaisms
seemaullal@gmail.com
class Discount
attr_reader :amount
def initialize(amount)
@amount = amount
end
end
class DiscountCalculator
def self.calcu...
class Discount
attr_reader :amount
def initialize(amount)
@amount = amount
end
end
class DiscountCalculator
def self.calcu...
@seemaisms
seemaullal@gmail.com
Consciously uncouple the different parts of
your code
@seemaisms
seemaullal@gmail.com
Delete unneeded comments
@seemaisms
seemaullal@gmail.com
class PhoneNumberFormatter
# this is used to format employee phone numbers,
# user phone numbers, and admin phone numbers
...
Any fool can write code that a
computer can understand. Good
programmers write code that
humans can understand.
— Refactor...
Slides on www.seemaullal.com under Talks
✉
seemaullal@gmail.com
@seemaisms
Thank You!
@seemaisms
seemaullal@gmail.com
Upcoming SlideShare
Loading in …5
×

Writing Loveable Code

123 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

×