Object Calisthenics

El objetivo de este artículo, Object Calisthenics, no es otro que repasar una pequeña lista de reglas a seguir en nuestro día a día como programadores, con el fin de mejorar nuestro código y facilitarnos un poco más la vida. Suena bien, ¿verdad?

  1. Un nivel de indentación por método
  2. No uses la palabra clave ELSE
  3. Envuelve primitivos
  4. Colecciones como clases de primer orden
  5. Un punto por línea
  6. No abrevies
  7. Mantén las entidades pequeñas
  8. Evita tener más de dos atributos de instancia
  9. Evita getters/setters o atributos públicos
Object_Calisthenics

1. Un nivel de indentación por método

Caso muy común, nos encontramos con un código con miles de líneas de indentación, el cual tenemos que depurar, lo que convierte esa actividad en un infierno.

La regla sugiere tener únicamente un nivel de indentación, que se traduce en no tener métodos con mucha lógica y muy complejos.

En este caso aplicaremos la técnica Extract Method, para así conseguir código más limpio, más legible y muchísimo más fácil de depurar, sin olvidarnos de una de las ventajas mas importantes que nos aporta: estamos siguiendo el Principio de Responsabilidad Única (SOLID)

Aquí vemos un ejemplo de lo que podría ser un método que NO sigue esta regla:

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

Y aqui su refactorización:

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

A simple vista parece que hay más código, pero lo principal es aportar estas ventajas que hemos mencionado anteriormente.

2. No uses la palabra clave ELSE

Conocemos la estructura if/else… pues bien, en este caso lo que queremos conseguir aplicando esta regla son estructuras más sencillas (aún más sencillas).

Veamos un ejemplo de lo más basico:

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

En este caso solo usaremos la condición if. En el caso de que la condición se cumpla, ejecutaremos una serie de acciones y, en el caso contrario, no será necesario utilizar la cláusula else, ya que se ejecutará el código restante. De esta manera, conseguimos un método más legible y más sencillo de entender. Así, resulta menos complejo ver que el propósito principal es lanzar un error en caso de que el mail no sea válido.

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

Esta regla nos sugiere utilizar objetos mas complejos para envolver primitivos.

Un buen caso de uso podría ser el de Value Objects en Ruby. Siguiendo este patrón, este objeto creado será el que contenga toda su lógica de negocio asociada, consiguiendo así encapsular y abstraer toda esta funcionalidad.

4. Colecciones como clases de primer orden

Esta es sencilla. Una clase que pudiera contener una colección, no deberá tener más atributos. Cada colección se envolverá en su propia clase, conteniendo así los métodos necesarios para gestionarla. Esta implementación es muy parecida a la mencionada anteriormente, por lo que conseguiremos encapsular toda esta lógica del resto de la aplicación.

En este ejemplo tenemos una clase Transfer que contiene una colección, y además contiene el método users_canceled el cual contiene lógica de negocio asociada a la colección.

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

Eliminaremos los métodos asociados y acoplados a dicha colección:

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

Y crearemos una nueva clase Users, la cual contendrá esta colección de usuarios y además todos los métodos asociados ésta.

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. Un punto por línea

Como bien dice el título de esta regla, no deberemos encadenar llamadas sobre objetos que se han sido proporcionado por otros y, por consiguiente, no deberemos tener más de un punto por línea.

Si vemos todos los puntos conectados, es que probablemente un objeto este profundizando demasiado en otro, por lo que estaremos violando la encapsulación.

Una buena base de la que partir para seguir esta regla es La Ley Demeter. Con ella conseguimos evitar acoplamiento entre objetos, con todo lo que eso conlleva: código más flexible, eliminar dependencias, fácil de reutilizar etc.

6. No abrevies

El objetivo de esta regla es muy simple: no abreviar el nombre de clases, objetos, variables o funciones. Obtendremos código mas autoexplicativo y mas fácil de entender.

Por ejemplo: en el caso de los métodos, podrían tener al menos 1 o 2 palabras. Debemos evitar repetir el contexto. Por ejemplo, si tenemos una clase User, con un método cuya nomenclatura sea canceled_ids en vez de users_canceled_ids, al llamar a este método escribiremos users.canceled_ids() en vez de users.users_canceled_ids().

7. Mantén las entidades pequeñas

Esta regla nos invita a no tener archivos de más de 50 líneas.

Las clases que suelen tener mas de 50 lineas, por lo general, suelen hacer más de una cosa, por lo que estaríamos violando el Principio de Responsabilidad Única (SOLID).

Al tener un máximo de 50 lineas por archivo nos provee de clases mas fáciles de entender y manejar.

8. Evita tener más de dos atributos de instancia

Cada clase debe ser responsable de gestionar una única variable de instancia, como mucho dos.

Esta es una de las reglas que suele causar mas controversia entre desarrolladores: ¿cuándo una entidad no necesita más de dos variables de instancia?

Veamos un ejemplo:

Tenemos una clase User, la cual contiene los datos personales del usuario.

class User
  def initialize(name:, surname:, address:, postal_code:)
    @name = name
    @surname = surname
    @address = address
    @postal_code = postal_code
  end
end

Podemos refactorizar esta clase para que solo contenga los datos name y surname.

class User
  def initialize(name:, surname:)
    @name = name
    @surname = surname
  end 
end

Y por otro lado tendríamos una clase Address la cual gestionará los datos relativos a una dirección.

class Address 
  def initialize(address:, postal_code:)
    @address = address
    @postal_code = postal_code
  end 
end

Con esto conseguimos cumplir con la regla, abstraer a la clase User de los datos de dirección y también conseguimos una clase Address, la cual podremos reutilizar con cualquier otra clase que también necesite de datos de dirección.

9. Evita getters/setters o atributos públicos

El objetivo de esta regla es implementar el principio Tell don’t ask, lo que sostiene que a un objeto no le pediremos información sobre su estado, le pediremos que haga cosas con su estado. Los objetos solo deberán exponer su comportamiento, por lo que no tendremos getters o setters, sino que tendremos métodos que nos modifiquen el estado del objeto.

Ejemplo: tenemos una clase Account, con un getter/setter amount.

class Account
  def initialize(amount:)
    @amount = amount
  end

  attr_accessor :amount 
end

Nuestro objetivo es poder aumentar su amount. Pero de esta manera estamos violando el principio 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

Para conseguir esto, podemos añadir un nuevo método add_amount que sera el que se encargue de modificar la información del objeto 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

Como podéis ver, estas son una serie de reglas que siempre te ayudaran a conseguir código adaptable, claro y funcional. No obstante, algunas de las reglas aquí mencionadas no hay por qué seguirlas de forma estricta, pueden servir a modo de guía y a modo de buenas practicas, siempre y cuando se apliquen con coherencia.

Full-Stack

Marta Gutiérrez

Marta Gutiérrez

Desarrolladora web Backend que intenta, de alguna manera, facilitarle la vida a la gente.
Marta Gutiérrez

Marta Gutiérrez

Desarrolladora web Backend que intenta, de alguna manera, facilitarle la vida a la gente.
2023 ©Secture Labs, S.L. Created by Madrid x Secture