Mucha gente está aprendiendo Ruby empezando con el framework Rails y, por desgracia, esta es la peor forma posible de aprender este lenguaje. No me malinterpretes: Rails es genial, te ayuda a construir aplicaciones web de forma rápida y eficiente sin tener que entrar en muchos detalles técnicos.
Encantado de conocerte.
Mucha gente está aprendiendo Ruby empezando con el framework Rails y, por desgracia, esta es la peor forma posible de aprender este lenguaje. No me malinterpretes: Rails es genial, te ayuda a construir aplicaciones web de forma rápida y eficiente sin tener que entrar en muchos detalles técnicos. Proporcionan un montón de "magia Rails" que hace que las cosas simplemente funcionen. Y para un programador novato esto es realmente genial, porque el momento más agradable del proceso es cuando puedes decir "¡está vivo!", y ver que todas las partes encajan y la gente usa tu aplicación. Nos gusta ser "creadores" 🙂 Pero hay algo que distingue a los buenos programadores de la media: los buenos entienden cómo funcionan las herramientas que utilizan. Y por "entender sus herramientas" no me refiero a conocer todos los métodos y módulos que proporciona un framework, sino a entender cómo funciona, a entender cómo se produce la "magia Rails". Sólo entonces podremos sentirnos cómodos utilizando objetos y programando con Rails. La base de la programación orientada a objetos, y el arma secreta que hace más fácil la complicada aplicación Rails, es el ya mencionado en el título PORO, que es Plain Old Ruby Object (Objeto Ruby Plano y Viejo).
¿Qué se esconde realmente bajo este nombre? ¿Cuál es esta gran arma secreta? Es una simple clase Ruby que no hereda de nada. Sí, sólo eso, y mucho más.
clase AwesomePoro
fin
¿En qué puedo ayudarle?
Estás continuamente desarrollando tu aplicación y añadiendo nuevas funcionalidades a medida que crece el número de usuarios y sus expectativas. Llega un momento en el que se encuentra con lugares cada vez más oscuros de lógica extremadamente retorcida, lugares que incluso los desarrolladores más valientes evitan como la peste. Cuantos más sean estos lugares, más difícil será gestionar y desarrollar la aplicación. Un ejemplo estándar es la acción de registrar un nuevo usuario, que desencadena todo un grupo de otras acciones asociadas a este evento:
- comprobando la dirección IP en una base de datos de spam,
- enviando un correo electrónico al nuevo usuario,
- añadir una bonificación a la cuenta de un usuario recomendante,
- crear cuentas en servicios relacionados,
- y muchos más...
Una muestra código responsable del registro de usuarios podría tener este aspecto:
clase RegistrationController < ApplicationController
def crear
user = User.new(parámetros_registro)
if user.valid? && ip_valid?(registration_ip)
¡user.save!
user.add_bonuses
user.synchronize_related_accounts
user.send_email
end
end
end
Vale, ya lo tienes codificado, todo funciona, pero... ¿realmente está bien todo este código? ¿Quizás podríamos escribirlo mejor? En primer lugar, rompe el principio básico de la programación - Responsabilidad Única, así que seguramente podríamos escribirlo mejor. ¿Pero cómo? Aquí es donde el ya mencionado PORO viene a ayudarte. Basta con separar una clase RegistrationService, que será responsable de una sola cosa: notificar a todos los servicios relacionados. Por servicios consideraremos las acciones individuales que ya hemos señalado anteriormente. En el mismo controlador todo lo que necesitas hacer es crear un objeto RegistrationService y llamar sobre él al método "fire!". El código ha quedado mucho más claro, nuestro controlador ocupa menos espacio, y cada una de las clases recién creadas es ahora responsable de una sola acción, por lo que podemos reemplazarlas fácilmente en caso de necesidad.
clase RegistrationService
def disparar!(parámetros)
user = User.new(parámetros)
if user.valid? && ip_validator.valid?(registration_ip)
¡user.save!
after_registered_events(usuario)
fin
usuario
end
privado
def after_registered_events(usuario)
BonusesCreator.new.fire!(usuario)
AccountsSynchronizator.fire!(usuario)
EmailSender.fire!(usuario)
fin
def ip_validator
@ip_validator ||= IpValidator.new
end
end
clase RegistrationController < ApplicationController
def create
user = RegistrationService.new.fire!(registration_params)
end
end
Sin embargo Plain Old Ruby Object puede resultar útil no sólo para los controladores. Imagina que la aplicación que estás creando utiliza un sistema de facturación mensual. El día exacto de creación de dicha facturación no nos importa, sólo necesitamos saber que se refiere a un mes y año concretos. Por supuesto, puede establecer el día para el primer día de cada mes y almacenar esta información en el objeto de la clase "Fecha", pero ni es una información verdadera, ni la necesita en su aplicación. Utilizando PORO puede crear una clase "MonthOfYear", cuyos objetos almacenarán exactamente la información que necesita. Además, al aplicar en ella el módulo "Comparable", será posible iterar y comparar sus objetos, igual que cuando se utiliza la clase Date.
class MesDeAño
include Comparable
attr_reader :año, :mes
def initialize(mes, año)
raise ArgumentError unless mes.¿entre?(1, 12)
@año, @mes = año, mes
end
def (otro)
[año, mes] [otro.año, otro.mes]
end
end
Preséntame a Rails.
En el mundo Rails estamos acostumbrados a que cada clase sea un modelo, una vista o un controlador. También tienen su ubicación precisa en la estructura de directorios, así que ¿dónde podemos colocar nuestro pequeño ejército PORO? Consideremos algunas opciones. La primera que se nos ocurre es: si las clases creadas no son ni modelos, ni vistas ni controladores, deberíamos ponerlas todas en el directorio "/lib". En teoría, es una buena idea, sin embargo, si todos sus archivos PORO van a aterrizar en un directorio, y la aplicación será grande, este directorio se convertirá rápidamente en un lugar oscuro que temerá abrir. Por lo tanto, sin duda, no es una buena idea.
AwesomeProject
├──app
│├─controladores
│├─modelos
│└─vistas
│
└─lib
└─servicios
#all poro aquí
También puedes nombrar a algunas de tus clases como Modelos no-ActiveRecord y ponerlas en el directorio "app/models", y nombrar a las que son responsables de manejar otras clases como servicios y ponerlas en el directorio "app/services". Esta es una solución bastante buena, pero tiene un inconveniente: al crear un nuevo PORO, cada vez tendrás que decidir si es más un modelo o un servicio. De esta manera, puede llegar a una situación en la que tenga dos POROs en su aplicación, sólo que más pequeños. Todavía hay un tercer enfoque, a saber: el uso de clases y módulos namespaced. Todo lo que necesitas hacer es crear un directorio que tiene el mismo nombre que un controlador o un modelo, y poner todos los archivos PORO utilizados por el controlador o modelo dado dentro.
AwesomeProject
├──app
│├─controladores
││├─registration_controller
││𥀔─registration_service.rb
│ │ └─registration_controller.rb
│ ├─models
│ │ ├─settlement
│ │ │ └─month_of_year.rb
│ │ └─settlement.rb
│ └─views
│
└─lib
Gracias a esta disposición, cuando se utiliza, no es necesario preceder el nombre de una clase con un espacio de nombres. Has ganado código más corto y una estructura de directorios más lógicamente organizada.
¡Mírame!
Es una grata sorpresa que, al utilizar PORO, las pruebas unitarias de su aplicación sean más rápidas y fáciles de escribir y, posteriormente, más susceptibles de ser comprendidas por los demás. Como cada clase es ahora responsable de una sola cosa, puede reconocer antes las condiciones límite y añadirles fácilmente escenarios de prueba adecuados.
describe MesAño do
sujeto { MonthOfYear.new(11, 2015) }
it { debe ser_tipo_de Comparable }
describe "crear nueva instancia" do
it "inicializa con el año y mes correctos" do
esperar { clase_descrita.new(10, 2015) }.que_no_produzca_error
end
it "genera error cuando el mes dado es incorrecto" do
expect { clase_descrita.nueva(0, 2015) }.to raise_error(ArgumentError)
expect { clase_descrita.new(13, 2015) }.to raise_error(ArgumentError)
end
end
end
Espero que volvamos a vernos.
Los ejemplos que presentamos muestran claramente que el uso de PORO mejora la legibilidad de las aplicaciones y las hace más modulares y, en consecuencia, más fáciles de gestionar y ampliar. Adoptar el principio de la responsabilidad única facilita el intercambio de determinadas clases en caso necesario, y hacerlo sin interferir con otros elementos. También hace que probarlos sea un procedimiento más sencillo y rápido. Además, de esta forma es mucho más fácil mantener cortos los modelos y controladores de Rails, que todos sabemos que tienden a hacerse innecesariamente grandes en el proceso de desarrollo.