Writing readable code
I have recently been applying for a number of jobs, and one of the reoccurring themes of IT job applications seems to be the technical test.
Given problem X solve it using technology A, B or C.
I quite like completing technical tests. I find it is an opportunity for me to practice writing “readable code”, implementing all the good practices I know and posting to my public Github account. Basically, doing the things that can be a struggle in your day job given the pressure of looming deadlines.
What do I mean by “Readable code”?
I am talking about something that I used to believe was a black art. Something that “those” people could do. What I am talking about here is writing code that is logical, easy to follow and most importantly easy to extend.
In actual fact this is not as hard as it seems, the key is to simply write your code as you normally would. Then tidy it up using simple refactoring techniques.
I will admit this is not always the case especially if your algorithm is fundamentally wrong. But this rule does hold try in most other cases.
An example?
NOTE : The code extract I am talking about comes from a larger problem that I have been asked not to blog about specific details.
The problem
Calculate the total cost of a group of items, given:
- There are different rates, which apply depending on the number of items you buy.
- There will always be a valid rate.
- A base cost can be added to the calculated price.
The initial code
def initialize(data)
@rates = data['rates']
@daily_charge = data['daily_charge']
end
def price(items)
cost = 0
remaining = items.to_i
@rates.each do |rate|
if remaining > 0
amount_at_rate = [remaining, rate['limit']].compact.min
cost += amount_at_rate * rate['price']
remaining -= amount_at_rate
end
end
cost += DAYS_IN_YEAR * @daily_charge if @daily_charge
cost
end
So it works and that is a bonus. But what does it do, and why and how could I ever extend it.
Simple refactorings
NOTE : This refactoring was only possible due to the use of functional tests over implementation tests. This meant that I had to make only a few modifications to the test cases as I changed the codebase.
Create a rate object to ensure you are not using Hash object everywhere. This will allow future refactorings to extract the logic out of the price method.
def initialize(data)
@rates = data['rates'].map { |rate| Rate.new(rate) }
@daily_charge = data['daily_charge']
end
class Rate
attr_reader :limit, :price
def initialize(data)
@limit = data['limit']
@price = data['price']
end
end
Extract the daily rate calculation
cost += DAYS_IN_YEAR * @daily_charge if @daily_charge
becomes:
cost += base_charge
and add
def base_change
DAYS_IN_YEAR * (@daily_charge || 0)
end
Optimize the algorithm
I need to simplify the loop. I have chosen to do this by storing the cost of previous rating on each rating object.
def initialize(data)
@rates = data['rates'].map {|rate| Rate.new(rate) }
@daily_charge = data['daily_charge']
end
becomes:
def initialize(data)
@rates = RateBuilder.build(data['rates'])
@daily_charge = data['daily_charge']
end
And then implement the RatesBuilder class. This class creates an accumulator for existing rates and creates each new rate object with a min_limit, min_cost and price field:
class RatesBuilder
def self.build(rates_hash)
builder = new
rates_hash.each do |rate_hash|
builder.add(rate_hash['price'], rate_hash['limit'])
end
builder.rates
end
attr_reader :rates
def initialize
@min_limit = 0
@min_cost = 0
@rates = []
end
def add(price, limit)
@rates << Rate.new(@min_limit, @min_cost, price)
@min_limit += limit
@min_cost += limit * price
end
end
The price method
I would like to point out that each of the previous refactorings was done in multiple stages, not all of which were shown. I kept the tests passing as I went, this gave me confidence in the changes I was making. It also meant that I could detect when things weren’t working and were able to roll back to a previous good state and start again.
def price(items)
items = items.to_i
selected_rate = @rates.reverse.detect do |rate|
rate.min_limit < items
end
selected_rate.min_cost +
(item - selected_rate.min_limit) * selected_rate.price +
base_change
end
Move some methods across to the “Rate” object
class Rate
def intialize(max_limit, min_cost, price)
@max_limit = max_limit
@min_cost = min_cost
@price = price
end
def cost(items)
min_cost + (item - min_limit) * price
end
def used_for_count?(items)
min_limit < items
end
end
def initialize(data)
@rates = RateBuilder.build(data['rates']).reverse
@daily_charge = data['daily_charge']
end
def price(items)
items = items.to_i
@rates.detect { |rate| rate.used_for_count?(items) }.cost +
base_change
end
What did I do?
- I simplified the rate selection. I did this by using an accumulator to build an array of rate objects rather than iterating over any array of hashes.
- I reversed the order of the rate objects, this was the key to getting everything working, it allowed me to greatly simplify the selection logic.
- Store the min_limit and min_cost on the rate instead of the limit for that rate. This meant that I did need to accumulate the cost each time, this method is called. This makes sense if the object is initialized once and the price method is called multiple times, which was my expected use case.
Conclusion
I think the final version of the code is much easier to read, and would allow the next developer in to understand my intent. Added to this it proved to be easy to extend, something I was required to do when being interviewed for the position.
As such I hope that this helps understand a little more about the art of writing beautiful code.