The purpose of this article, Object Calisthenics, is none other than to review a small list of rules to follow in our day-to-day work as programmers, in order to improve our code and make our lives a little easier. Sounds good, doesn't it?
- One indentation level per method
- Do not use the keyword ELSE
- Wraps primitives
- Collections as first-order classes
- One dot per line
- Do not abbreviate
- Keep entities small
- Avoid having more than two instance attributes
- Avoid getters/setters or public attributes

1. One level of indentation per method
Very common case, we find ourselves with a code with thousands of lines of indentation, which we have to debug, which turns that activity into a hell.
The rule suggests having only one level of indentation, which translates into not having very logical and complex methods.
In this case we will apply the technique Extract Method, to get cleaner code, more readable and much easier to debug, without forgetting one of the most important advantages it gives us: we are following the Single Responsibility Principle (SOLID)
Here is an example of what could be a method that does NOT follow this rule:
def fill_report
DB = Sequel.sqlite
records = DB[:users].where(Sequel[:created_at] < Date.today - 7).order(:created_at, :name)
report_fields = records.as_hash(:id, :name, :last_name. :created_at)
report_fields = report_fields.map do |key, value|
if ['name', 'last_name'].include?(key.to_s)
value = value.gsub!(/[^0-9A-Za-z]/, '')
end
file_headers = ['id', 'name', 'last_name', 'created_at']
CSV.open('weekly_users_report.yml', 'w') do |csv|
csv << file_headers
report_fields.each do |field|
csv << CSV::Row.new(field.keys, field.values)
end
end
end
endAnd here its refactoring:
DB = Sequel.sqlite
FILE_HEADERS = ['id', 'name', 'last_name', 'created_at']
FILE_NAME = 'weekly_users_report.yml'
NAME_REGEX = /[^0-9A-Za-z]/
def fill_report
records = report_recordd
report_fields = format_fields(records)
CSV.open (FILE_NAME, 'w') do |csv|
csv << file_headers
report_fields.each { |field| csv << CSV::Row.new(field.keys, field.values) }
end
end
def report_records
DB[:users].where(Sequel[:created_at] < Date.today - 7).order(:created_at, :name)
end
def format_fields(records)
report_fields = records.as_hash(:id, :name, :last_name. :created_at)
report_fields = report_fields.map do |key, value|
if ['name', 'last_name'].include?(key.to_s)
value = value.gsub!(NAME_REGEX, '')
end
end
report_fields
endAt first glance it seems that there is more code, but the main thing is to provide these advantages mentioned above.
2. Do not use the keyword ELSE
We know the structure if/else... well, in this case what we want to achieve by applying this rule are simpler structures (even simpler).
Let's look at a very basic example:
EMAIL_REGEX = /^|w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3]})+$/
def validate_email(email)
if email.match(EMAIL_REGEX)
return email
else
raise StandardError.new 'Invalid email'
end
endIn this case we will only use the condition if. If the condition is met, we will execute a series of actions and, if not, it will not be necessary to use the clause else, The remaining code will be executed. In this way, we get one more method readable and more simple to understand. Thus, it is less complex to see that the main purpose is to throw an error in case the mail is invalid.
EMAIL_REGEX = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/
def validate_email(email)
return email if email.match(EMAIL _REGEX)
raise StandardError.new 'Invalid email'
end3. Wrap primitives
This rule suggests using more complex objects to wrap primitives.
A good use case could be Value Objects in Ruby. Following this pattern, this created object will be the one that will contain all its associated business logic, thus achieving encapsulate y abstract all this functionality.
4. Collections as first order classes
This one is simple. A class that could contain a collection, should not have more attributes. Each collection will be wrapped in its own class, thus containing the necessary methods to manage it. This implementation is very similar to the one mentioned above, so that we will get encapsulate all this logic from the rest of the application.
In this example we have a class Transfer which contains a collection, and also contains the method users_canceled which contains business logic associated with the collection.
class Transfer
def initialize(name:, users:)
@name = name
@users = users
end
def users_canceled
@users.select do |user|
user.status == 'canceled'
end
end
def send
users_canceled_ids = @users_canceled.map { |user| user.id }
@transfer.update(status: 'sent') unless users_canceled_ids.include?(@transfer.user_reference)
end
endWe will eliminate the methods associated and coupled to that collection:
class Transfer
def initialize(name:, users:)
@name = name
@users = users
end
def send
users_canceled_ids = @users.canceled_ids
@transfer.update(status: 'sent') unless users_canceled_ids.include?(transfer.user_reference)
end
endAnd we will create a new class Users, which will contain this collection of users and also all the methods associated with it.
class Users
def initialize(users:)
@users = users
end
def canceled_ids
@users.select do |user|
user.status == 'canceled'
end.map { |user| user.id }
end
end5. One point per line
As the title of this rule states, we should not chain calls on objects that have been provided by others and, therefore, we should not have more than one point per line.
If we see all the dots connected, it is likely that one object is going too deep into another, so we are violating encapsulation.
A good basis from which to start following this rule is Demeter Law. With it we get avoid coupling between objects, with all that that entails: code more flexible, eliminate dependencies, easy to reuser etc.
6. Do not abbreviate
The purpose of this rule is very simple: do not abbreviate the name of classes, objects, variables or functions. We will get more code self-explanatory and easier to understand.
For example: in the case of methods, they could have at least 1 or 2 words. We must avoid repeating the context. For example, if we have a class User, with a method whose nomenclature is canceled_ids instead of users_canceled_ids, When calling this method we will write users.canceled_ids() instead of users.users_canceled_ids().
7. Keep entities small
This rule invites us not to have files longer than 50 lines.
Classes that usually have more than 50 lines, usually do more than one thing, so we would be in violation of the Single Responsibility Principle (SOLID).
Having a maximum of 50 lines per file provides us with more classes. easy to understand and manage.
8. Avoid having more than two instance attributes
Each class should be responsible for managing only one instance variable, at most two.
This is one of the most controversial rules among developers: when does an entity not need more than two instance variables?
Let's look at an example:
We have a class User, which contains the user's personal data.
class User
def initialize(name:, surname:, address:, postal_code:)
@name = name
@surname = surname
@address = address
@postal_code = postal_code
end
endWe can refactor this class so that it contains only the following data name y surname.
class User
def initialize(name:, surname:)
@name = name
@surname = surname
end
endAnd on the other hand, we would have a class Address which will manage the data related to an address.
class Address
def initialize(address:, postal_code:)
@address = address
@postal_code = postal_code
end
endWith this we comply with the rule, abstract to the class User of the address data and we also get a class of Address, which we can reuse with any other class that also needs address data.
9. Avoid getters/setters or public attributes.
The purpose of this rule is to implement the principle Tell don't ask, This means that we will not ask an object for information about its state, we will ask it to do things with its state. Objects should only have to expose their behavior, so we will not have getters o setters, but we will have methods that modify the state of the object.
Example: we have a class Account, with a getter/setter amount.
class Account
def initialize(amount:)
@amount = amount
end
attr_accessor :amount
endOur goal is to be able to increase your amount. But in this way we are violating the principle Tell don't ask.
irb(main):016:0> account = Account.new( amount: 20000)
=> #Account: 0×000000014b890138 @amount=20000>
irb(main):017:0> account.amount
=> 20000
irb(main):018:0> account.amount = 3000
=' 3000
irb(main):019:0> account.amount
=> 3000To achieve this, we can add a new method add_amount which will be responsible for modifying the object's information Account.
class Account
def initialize(amount:)
@amount = amount
end
def add_amount(new_amount)
@amount += new_amount
end
endirb(main):010:0> account = Account.new(amount: 20000)
=> #Account: 0x000000014d01668 @amount=20000>
irb(main):011:0> account.add_amount(10000)
=> 30000As you can see, these are a series of rules that will always help you to get code. adaptable, of course y functional. However, some of the rules mentioned here need not be followed strictly, but can serve as guidelines and best practices, as long as they are applied consistently.
