One more article about ruby best practice
Everyone engineer should read tech literature in free time. When you are reading you learn a lot of new things. You read about someone’s experience, best practice and fails. With that useful information you can see new opportunity, can make your code better and more stable, can create new features better and faster.
In this article I want to share some good practice that I have read in tech literature.
Write readable code and think about responsibility
Ruby is oop language, do not forget it. And endeavor to write readable code. Many developers forget about responsibility principle when they are writing code. Many times I have seen additional condition where they should not be. It makes negative consequences in your code. It is very hard to support that code and create new features in it. Sometimes if you need to realize new feature, the better way is delete all code and rewrite it in scratch.
First example. Everyday one information system parses subscribers list. Subscribers list is a csv-file with next columns:
- ‘number’ — index row number.
- ‘email’ — user’s email.
- ‘first_name’ — user’s first name.
- ‘last_name’ — user’s last name.
- ‘action’ — user action. It may contains true or false. True means user want to subscribe. False means user want to unsubscribe.
- ‘subscription_ids’ — list subscription’s ids.
Like this:
number|email|first_name|last_name|action|subscription_ids
1|some_email.ru|James|Smith|true|1,3
2|some_email2.ru|Barbara|O'connor|true|1,2,3,5
3|some_email3.ru|||false|3,6
Below is example of bad code. It has many conditions and it hard to read.
Let’s make code more readable. We need add some method into User class and create SubscriberRow class. This is better.
Second example. We write software (rails app) for a restaurant company. The company makes and sells pizza everyday. We need to write module for showing weekly statistics. Company need to know data for everyday in current week:
- day of the week
- number of pizza sold
- sum of orders
- TOP 3 buyers (users who have largest amount of orders)
- TOP 3 pizza sold
For this task, we can create controller with one public method and any private method like this:
We finish our task, but code is bad. If we need to add some big features in this controller we will have problems. Or if we need to duplicate functional for another part of system (another controller, api), or send data by email, we will have problems.
What should we do? We need to define a responsibility and use DRY. Need create some classes to aggregate data and collect everyday data to week.
It looks better. In StatisticsController we have only one line that we can easily duplicate to another part of system.
Do not afraid use .fetch for hash
Sometimes we need to work with complicated, nested hash. When we are trying to access value in deeply depth, code can be like this:
var = nilif(some_hash.present? and some_hash[:key_1].present? and some_hash[:key_1][:key_2].present?)
var = some_hash[:key_1][:key_2]end
There is a long condition for accessing value. It is hard to read. We can rewrite it using .fetch() method. This hash method takes two arguments key and placeholder. If hash have not key, method will returns placeholder.
var = nil
if some_hash.present?
var = some_hash.fetch(:key_1, {}).fetch(:key_2, nil)
end
Fetch has one feature: if hash key exists and it returns nil or false method will return nil or false. But if you use activesupport you can rewrite code on rails-style.
var = some_hash.try(:fetch, :key_1, {}).try(:fetch, :key_2, nil)
Check passed arguments
Ruby does not have static typing. It means any passed arguments and returned values can be instance of any class. It can make any confuse in code.
Look at example. We created class GeoCoordinate for creating geo-positions in interactive map.
class GeoCoordinate
def initialize(latitude, longitude)
@latitude = latitude
@longitude = longitude
end
# and more code
end
When are you looking at code you see that initialize method takes two arguments ‘latitude’ and ‘longitude’. But you do not see which instance should latitude and longitude be. Should latitude be instance of Number or instance of String? Different developers can write different code.
GeoCoordinate.new(55.45, 37.37)
GeoCoordinate.new('55.45', '37.37')
GeoCoordinate.new(['55.45'], ['37.37'])
GeoCoordinate.new(magicCoordinate.new)
GeoCoordinate.new(Latitude.new('55.45'), Longitude.new('37.37'))
GeoCoordinate.new(latitude: '55.45', longitude: '37.37’)
In additions there are many others classes in different parts on system.
In this case, we can write function with the same name for checking passed arguments. The function gives us assurance that passed arguments are valid. If arguments are invalid it will raise TypeError.
Checking returned values
In static typing programing languages if some method/function returns array it will return array everytime. If some another method returns string it will return string everytime. In not static typing programing languages every method/function can return different instance every time. It can makes any troubles in our code.
For example, takes GeoCoordinate class. It has three_nearest_coordinates method that should returns maximum three instance of GeoCoordinate class. But in current realization it can returns array, an instance, false or nil. It is very hard for debugging.
We need rewrite three_nearest_coordinates. It must return array every time.
Long hash in passed attributes is bad
Any method/function can takes hash in passed attributes with 2–3 keys. It does not sound bad. After some time, system become bigger, has more features and the hash has 8–10 keys instead 2–3. It can create problems. Developer can make a spelling mistake in keys (misspelling is hard to debug), or choosing right parameter takes many times. Look at example.
The better way is rewrite method/function, it should take any instance instead hash. It really make code easily to read and debug.
That is all. I hope these advices and practice will be very useful for you.