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?
- Un nivel de indentación por método
- No uses la palabra clave ELSE
- Envuelve primitivos
- Colecciones como clases de primer orden
- Un punto por línea
- No abrevies
- Mantén las entidades pequeñas
- Evita tener más de dos atributos de instancia
- Evita getters/setters o atributos públicos
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.