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.
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
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
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
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
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
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
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
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
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
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
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
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
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