¿Un poco de DSL con su ensalada?

Visita este artí­culo en http://www.estadobeta.com/2007/10/22/un-poco-de-dsl-con-su-ensalada/

Por Ismael en Ruby & Rails, artículos

Una red social (web) en que trabajo actualmente requiere que las páginas muestren distintos artículos segun el perfil y red particular del usuario registrado. Para levantar un prototipo rapidamente (no por nada el Agile Development), definimos esa lógica directamente en nuestros modelos ActiveRecord, evaluando las condiciones y cargando los articulos seleccionados en el mismo lugar*.

Code (ruby)
  1.  
  2. def lista_de_articulos( usuario )
  3.   articulos = []
  4.   if usuario.is_a? Admin #Admin puede ver todo
  5.     articulos += find(:all)
  6.   elsif usuario.is_a? User #Articulos publicos,  escritos por el usuario o sus amigos
  7.     articulos += find(:all, :conditions=>{:public=>true})
  8.     articulos += find(:all, :conditions=>{:author=>usuario})
  9.     articulos += find(:all, :conditions=>["author_id IN (?)", usuario.friends.collect(&:id)])
  10.   else #Usuario anonimo solo ve articulos publicos
  11.     articulos += find(:all, :conditions=>{:public=>true})
  12.   end
  13.  
  14.   articulos.uniq.sort_by(&:published_at)
  15. end
  16.  

Es un prototipo. Nadie dijo que tenía que ser escalable. Aun cuando podríamos optimizar el método y reducir el numero de accesos a la base de datos, lo verdaderamente grave de este enfoque es que estamos resolviendo nuestras reglas de negocio (quien ve qué) y nuestra estrategia de almacenamiento (acceso a la base de datos) en el mismo lugar. Si las reglas cambian, debemos modificar la lógica de condiciones tanto en los if/else como en los parámetros pasados a ActiveRecord#find. Si, por otro lado, nuestro sitio es un exito y debemos usar Memcached para quitarle trabajo a la base de datos, debemos tener sumo cuidado en no romper las condiciones que soportan nuestras reglas.

Ni hablar de optimizar las queries escribiendolas directamente en SQL.

Luego de analizar un poco el proyecto las cosas son claras. Las reglas de privacidad de los artículos pueden cambiar, o pueden agregarse nuevos perfiles de usuarios con otros derechos. El negocio de nuestro cliente se basa en esas reglas. El cómo y dónde almacenamos esos datos es un detalle de implementación que no tiene que ver con lo primero. Debieramos ser capaces de definir reglas y permisos de lectura libremente y preocuparnos del levantamiento de datos en otra parte.

Con un poco de refactoring y la transparencia de Ruby llegamos a algo así:

Code (ruby)
  1.  
  2. Class Usuario < ActiveRecord::Base
  3.  
  4.   def mis_articulos
  5.     prepara_articulos do |articulos|
  6.       articulos.add :from=>self, :to=>:all, :status => :all
  7.       articulos.add :from=>:all, :to=>:self, :status => :public
  8.       articulos.add :from=>friends, :to=>:all, :status => :all
  9.     end
  10.   end
  11. end
  12.  

Con un pequeño DSL en Ruby separamos la definición de reglas de negocio del acceso a datos, que manejamos en otra capa de nuestra aplicación, y que podemos cambiar fácilmente de una fuente de datos a otra. Ademas nos acercamos más al dominio del negocio de nuestro cliente, que podemos manejar de forma más limpia y mantenible. ¿Dónde estan las condiciones? Usando apropiadamente herencia de clases podemos hacer que cada tipo de usuario defina sus propias reglas sin necesidad de lógica condicional. Sabemos bien que cuando empezamos a ver if y else nadando por nuestro código es hora de repensar la arquitectura.

Rails es en sí un DSL que facilita el desarrollo web al acercarnos a la semántica y al flujo habitual de un proyecto web promedio. Pero Rails no intenta resolver el problema específico de nuestro negocio, sino que más bien nos deja el camino libre para hacerlo nosotros mismos. Si bien se puede usar ActiveRecord “out of the box” y acomodar nuestro problema a sus particularidades, haríamos mejor en aprovechar la plataforma de Rails por un lado, y la flexibilidad de Ruby por otro para modelar el problema o negocio lde a forma más transparente y comoda.

Es uno de los problemas que aborda el uso de Presentadores en proyectos Rails complejos.

¿Se puede hacer en otros lenguajes? Por supuesto que se puede. Existen cientos de miles de aplicaciones escritas en Java, .NET o PHP que así lo prueban. Ese nunca fue el punto de la discusión. El punto es si lo pasas bien haciendolo o no. Y sabemos que un producto hecho con cariño suele ser mejor que uno donde desearías estar muerto cada vez que el cliente menciona la palabra “mantención”.

*.
Los detalles del proyecto y el código de los ejemplos han sido simplificados para efectos de ilustración.

La versión en inglés de este artículo está en el blog de New Bamboo.

5 comentarios para “¿Un poco de DSL con su ensalada?”

  1. GravatarAntoine Dice:

    Hola Ismael,
    Es verdad que el DSL es mucho más legible y abstrae las reglas pero, ¿podrías indicar cómo lo integras en tu clase User desde la otra capa? Por tu artículo creo entender que cada clase (User, Admin, etc) implementa su propio prepara_articulos, pero no termino de entender cómo separa la lógica de negocio (que sería ese método prepara_articulos) de la capa de acceso a datos.

  2. GravatarIsmael Dice:

    Antoine, el ejemplo esta bastante simplificado, pero el esquema que tengo es algo asi: Tengo una supeclase Persona. Usuario y Admin son subclases de Persona. Persona tiene una serie de utilidades y metodos protegidos que hacen el levantamiento de datos. Usuario y Admin solo definen las reglas con prepara_articulos. Esas reglas son procesadas por Persona (la superclase) para traducirlas a SQl, llamadas a ActiveRecord o el metodo que prefieras.

    Por el momento prepara_articulos en realidad es llamado dentro de metodos de instancia. Eso es bueno porque reuso la misma interface en distintos puntos:

    def mis_articulos
        prepara_articulos do |articulos|
        articulos.add :from=>self, :to=>:all, :status => :all
        articulos.add :from=>:all, :to=>:self, :status => :public
        articulos.add :from=>friends, :to=>:all, :status => :all
      end
    
    end
    

    prepara_articulos retorna un objeto ConfiguradorDeArticulos, que no es mas que un Array al que agregue un par de utilidades (ConfiguradorDeArticulo#add limpia y normaliza los parametros).

    def prepara_articulos
      config = ConfiguradorDeArticulos.new
      yield config if block_given?
      config
    end
    

    El array de opciones se lo paso a un metodo que construye el SQL necesario para cargar los articulos indicados, pero bien podria pasarselo a otro adaptador que use Memcache, etc.

    La arquitectura completa es más complicada, aunque el proyecto es tambien mucho mas complejo de lo que cuento aquí Espero que se entienda en todo caso el punto: este tipo de abstracciones valen la pena si hacen más manejable el dominio del proyecto y separan las responsabilidades de cada módulo.

  3. GravatarAntoine Dice:

    Aja, entonces la parte de acceso a datos quedaría solamente en Persona y la parte de reglas de negocio en sus herederas Usuario y Admin, ¿es así? Mi confusión vino porque entendí que el control de acceso quedaba fuera por completo de las clases AR (inclusive de las que heredaban de Persona, en el caso que comentas). Gracias por la aclaración, ahora entiendo y comparto tu punto de vista :)

  4. GravatarEstadoBeta » Archivo » Usando bloques Ruby en lugar de métodos en Rails, parte I Dice:

    […] un tiempo contaba sobre un proyecto cuya complejidad justificaba la creación de un simple DSL para estructurar de mejor forma la […]

  5. GravatarAbunza Dice:

    Hola caballeros les gustaria analizar esta pagina esta muy ..

Deja un comentario

XHTML: puedes usar estas etiquetas: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <code> <em> <i> <strike> <strong>