Si nos remitimos a la definición, DSL (Domain Specific Language) es un lenguaje informático especializado en un dominio de aplicación concreto. Esto significa que se desarrolla para satisfacer necesidades específicas.
Leyendo este artículo aprenderás qué es el DSL y qué tiene en común con Ruby.
DSL, ¡bienvenido!
Si nos remitimos a la definición, DSL (Domain Specific Language) es un lenguaje informático especializado en un dominio de aplicación concreto. Esto significa que se desarrolla para satisfacer necesidades específicas. Existen dos tipos de DSL:
-
En externo DSL que requiere su propio analizador sintáctico. Un buen ejemplo conocido puede ser el lenguaje SQL: permite interactuar con bases de datos en un lenguaje en el que la base de datos no fue creada.
-
En interno DSL que no tiene sintaxis propia, sino que utiliza la sintaxis de un lenguaje de programación determinado.
Como ya habrás adivinado, vamos a centrarnos en el segundo tipo de DSL.
¿Para qué sirve?
Básicamente, haciendo uso de una metaprogramación Ruby permite crear su propio mini-lenguaje. La metaprogramación es una técnica de programación que permite escribir un código dinámicamente en tiempo de ejecución (sobre la marcha). Puede que no seas consciente de ello, pero probablemente utilices muchos DSL diferentes cada día. Para entender lo que puede hacer un DSL, veamos algunos ejemplos a continuación - todos ellos tienen un elemento común, pero ¿puedes señalarlo?
Enrutamiento Rails
Rails.application.routes.draw do
raíz a: 'home#index'
recursos :usuarios do
obtener :búsqueda, en: :colección
end
end
```
Toda persona que haya utilizado alguna vez Rails conoce un config/rutas.rb donde definimos las rutas de la aplicación (mapeo entre verbos HTTP y URLs a acciones del controlador). Pero, ¿te has preguntado alguna vez cómo funciona? De hecho, es sólo código Ruby.
Fábrica Bot
FactoryBot.define do
fábrica :usuario do
empresa
secuencia(:email) { |i| "usuario_#{i}@prueba.com" }
secuencia(:nombre) { |i| "Usuario #{i}" }
last_name 'Prueba
rol 'manager'
end
end
Escribir pruebas a menudo requiere fabricar objetos. Por lo tanto, para evitar una pérdida de tiempo, sería muy buena idea simplificar el proceso todo lo posible. Eso es lo que hace el FactoryBot hace: palabras clave fáciles de recordar y una forma de describir un objeto.
Sinatra
require 'sinatra/base'
clase WebApplication < Sinatra::Base
get '/' do
'Hola mundo'
end
end
```
Sinatra es un framework que permite crear aplicaciones web desde cero. ¿Podría ser más fácil definir el método de solicitud, la ruta y la respuesta?
Otros ejemplos de DSL podrían ser Rastrillo, RSpec o Registro activo. El elemento clave de cada DSL es el uso de bloques.
Tiempo de construcción
Es hora de entender qué se esconde bajo el capó y cómo puede ser la aplicación.
Supongamos que tenemos una aplicación que almacena datos sobre diferentes productos. Queremos ampliarla dando la posibilidad de importar datos desde un fichero definido por el usuario. Además, el fichero debe permitir calcular valores dinámicamente si es necesario. Para lograrlo, decidimos crear DSL.
Una simple producto puede tener los siguientes atributos (product.rb):
class Producto
attr_accessor :nombre, :descripción, :precio
end
En lugar de utilizar una base de datos real, nos limitaremos a simular su funcionamiento (fake_products_database.rb):
class BaseDatosProductosFalsos
def self.store(producto)
puts [nombre.producto, descripción.producto, precio.producto].join(' - ')
end
end
Ahora, crearemos una clase que se encargará de leer y manejar el fichero que contiene los datos de los productos (dsl/data_importer.rb):
módulo Dsl
clase DataImporter
módulo Syntax
def añadir_producto(&bloque)
FakeProductsDatabase.store producto(&bloque)
fin
privado
def producto(&bloque)
ProductBuilder.new.tap { |b| b.instance_eval(&block) }.product
end
end
incluir Sintaxis
def self.importar_datos(ruta_archivo)
new.instance_eval Archivo.read(ruta_archivo)
fin
fin
fin
```
La clase tiene un método llamado importar_datos que espera una ruta de archivo como argumento. El archivo se lee y el resultado se pasa a la función instancia_eval que se ejecuta en la instancia de la clase. ¿Qué es lo que hace? Evalúa la cadena como un código Ruby dentro del contexto de la instancia. Es decir auto será la instancia de Importador de datos clase. Gracias a ello podemos definir la sintaxis/palabras clave deseadas (para una mejor legibilidad la sintaxis se define como un módulo). Cuando el añadir_producto el bloque dado para el método es evaluado por ProductBuilder que construye Producto instancia. ProductBuilder se describe a continuación (dsl/product_builder.rb):
módulo Dsl
clase ProductBuilder
ATRIBUTOS = %i[nombre descripción precio].freeze
attr_reader :producto
def inicializar
@producto = Producto.nuevo
end
ATTRIBUTES.each do |atributo|
define_method(atributo) do |arg = nil, &block|
value = block.is_a?(Proc) ? block.call : arg
product.public_send("#{atributo}=", valor)
end
end
end
end
```
La clase define la sintaxis permitida dentro de añadir_producto bloque. Con un poco de metaprogramación añade métodos que asignan valores a los atributos del producto. Estos métodos también admiten pasar un bloque en lugar de un valor directo, por lo que se puede calcular un valor en tiempo de ejecución. Usando el lector de atributos, podemos obtener un producto construido al final.
Ahora, vamos a añadir el script de importación (import_job.rb):
requirelative 'dsl/dataimporter'
requirelative 'dsl/productbuilder'
requirelative 'basede datosdeproductosfalsos'
requirelative 'producto
Dsl::DataImporter.import_data(ARGV[0])
```
Y finalmente - usando nuestro DSL - un fichero con los datos de los productos (dataset.rb):
```ruby
add_product do
nombre 'Cargador'
descripción 'Life saving'
precio 19.99
end
add_product do
name 'Naufragio'
description { "Naufragio en #{Time.now.strftime('%F %T')}" }
precio 0.01
end
add_product do
nombre 'Ganzúa'
descripción 'Las puertas no se cierran'
precio 7,50
end
```
Para importar los datos sólo tenemos que ejecutar un comando:
ruby import_job.rb dataset.rb
Y el resultado es..
Cargador - Salvavidas - 19.99
Naufragio - Naufragado en 2018-12-09 09:47:42 - 0.01
Ganzúa - Las puertas no se cierran - 7.5
...¡éxito!
Conclusión
Viendo todos los ejemplos anteriores, no es difícil darse cuenta de las posibilidades que ofrece DSL. DSL permite simplificar algunas operaciones rutinarias ocultando toda la lógica necesaria y exponiendo al usuario sólo las palabras clave más importantes. Permite obtener un mayor nivel de abstracción y ofrece posibilidades de uso flexibles (lo que es especialmente valioso en términos de reutilización). Por otro lado, añadir DSL a su proyecto debe ser siempre bien considerado - una implementación usando metaprogramación es definitivamente mucho más difícil de entender y mantener. Además, requiere un conjunto de pruebas sólido debido a su dinamismo. Documentar el DSL facilita su comprensión, por lo que merece la pena hacerlo. Aunque implementar tu propio DSL puede ser gratificante, es bueno recordar que debe ser rentable.
¿Te ha interesado el tema? Si es así, háganoslo saber: le hablaremos de DSL que hemos creado recientemente para satisfacer los requisitos de uno de nuestros proyectos.