secture & code

Object Calisthenics

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?

  1. One indentation level per method
  2. Do not use the keyword ELSE
  3. Wraps primitives
  4. Collections as first-order classes
  5. One dot per line
  6. Do not abbreviate
  7. Keep entities small
  8. Avoid having more than two instance attributes
  9. Avoid getters/setters or public attributes
Object_Calisthenics

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
end

And 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 
end

At 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
end

In 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'
end

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

We 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 
end

And 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 
end

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

We 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 
end

And 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 
end

With 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 
end

Our 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
=> 3000

To 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 
end
irb(main):010:0> account = Account.new(amount: 20000)
=> #Account: 0x000000014d01668 @amount=20000>
irb(main):011:0> account.add_amount(10000)
=> 30000

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

Full-Stack

Picture of Marta Gutiérrez

Marta Gutierrez

Programming always gives you the opportunity to improve
Picture of Marta Gutiérrez

Marta Gutierrez

Programming always gives you the opportunity to improve

We are HIRING!

What Can We Do