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 the primary purpose of
Ruby language.
— Yukihiro Matsumoto
@seemaisms
seemaullal@gmail.com
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 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
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 = 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
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
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
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 |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
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
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 "(#{phone[0..2]}) #{phone[3..5]}-#{phone[6..9]}"
end
end
@seemaisms
seemaullal@gmail.com
class PayEmployee
def self.call(employee)
payment = PaymentCreator.new(employee: employee).create_payment
DebitCompanyForPayment.call(payment, employee.company)
end
end
@seemaisms
seemaullal@gmail.com
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.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
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
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
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
)
@seemaisms
seemaullal@gmail.com
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_double(User, first_name: 'Cookie', middle_initial: nil, last_name: 'Monster')
@seemaisms
seemaullal@gmail.com
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 :user_invoice_email
UserInvoice.create
end
end
@seemaisms
seemaullal@gmail.com
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
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 right path’ do
expect(File).to receive(:open).with("/tmp/documents/1/test")
end
@seemaisms
seemaullal@gmail.com
it ‘writes to the correct path’ do
expect(File).to receive(
:open
).with(
“/tmp/documents/#{document.id}/artifact”
)
end
@seemaisms
seemaullal@gmail.com
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 "(#{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
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
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
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.calculate(user_id)
user_discounts = UserDiscount.where(user_id: user_id)
user_discounts.sum { |ud| ud.discount.amount }
end
end
@seemaisms
seemaullal@gmail.com
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
@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
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
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
Slides on www.seemaullal.com under Talks
✉
seemaullal@gmail.com
@seemaisms
Thank You!
@seemaisms
seemaullal@gmail.com

Writing Loveable Code