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.
Muchas personas están 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
However Plain Old Ruby Object may prove useful not only for controllers. Imagine that the application you are creating uses a monthly billing system. The exact day of creating such a billing is not important to us, we only need to know that it concerns a specific month and year. Of course you can set the day for the first day of each month and store this information in the object of the “Date” class, but neither is it a true information, nor do you need it in your application. By using PORO you can create a class “MonthOfYear”, the objects of which will store the exact information you need. Moreover, when applying in it the module “Comparable”, it will be possible to iterate and to compare its objects, just like when you are using the Date class.
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.