Facendo riferimento alla definizione, il DSL (Domain Specific Language) è un linguaggio informatico specializzato per un particolare dominio applicativo. Ciò significa che viene sviluppato per soddisfare esigenze specifiche.
Leggendo questo articolo imparerete cos'è il DSL e cosa ha in comune con Ruby.
DSL, date il benvenuto!
Facendo riferimento alla definizione, il DSL (Domain Specific Language) è un linguaggio informatico specializzato per un particolare dominio applicativo. Ciò significa che viene sviluppato per soddisfare esigenze specifiche. Esistono due tipi di DSL:
-
Un esterno DSL che richiede un proprio parser di sintassi. Un esempio noto è il linguaggio SQL, che permette di interagire con il database in un linguaggio in cui il database non è stato creato.
-
Un interno DSL che non ha una propria sintassi, ma utilizza la sintassi di un determinato linguaggio di programmazione.
Come si può intuire, ci concentreremo sul secondo tipo di DSL.
Che cosa fa?
In pratica, utilizzando la metaprogrammazione di Ruby, è possibile creare un proprio mini-linguaggio. La metaprogrammazione è una tecnica di programmazione che permette di scrivere un linguaggio codice dinamicamente in fase di esecuzione (al volo). Forse non ne siete consapevoli, ma probabilmente utilizzate molti DSL diversi ogni giorno. Per capire cosa può fare un DSL, diamo un'occhiata ad alcuni esempi qui sotto: tutti hanno un elemento in comune, ma sapete indicarlo?
Rails routing
Rails.application.routes.draw do
radice a: 'home#index'
risorse :utenti do
get :search, on: :collection
fine
fine
```
Ogni persona che abbia mai usato Rails conosce un config/routes.rb in cui si definiscono le rotte dell'applicazione (mappatura tra verbi HTTP e URL e azioni del controllore). Ma vi siete mai chiesti come funziona? In realtà, si tratta solo di codice Ruby.
Fabbrica Bot
FactoryBot.define do
fabbrica :utente do
azienda
sequence(:email) { |i| "user_#{i}@test.com" }
sequence(:first_name) { |i| "Utente #{i}" }
cognome 'Test'
ruolo 'manager'
fine
fine
La scrittura di test richiede spesso la creazione di oggetti. Quindi, per evitare una perdita di tempo, sarebbe una buona idea semplificare il più possibile il processo. Questo è l'obiettivo di FabbricaBot fa - parole chiave facili da ricordare e un modo per descrivere un oggetto.
Sinatra
richiedere 'sinatra/base'
classe WebApplication < Sinatra::Base
ottenere '/' fare
'Ciao mondo'
fine
fine
```
Sinatra è un framework che consente di creare applicazioni web da zero. Potrebbe essere più semplice definire il metodo di richiesta, il percorso e la risposta?
Altri esempi di DSL potrebbero essere Rastrello, RSpec o Record attivo. L'elemento chiave di ogni DSL è l'uso dei blocchi.
Tempo di costruzione
È ora di capire cosa si nasconde sotto il cofano e come può apparire l'implementazione.
Supponiamo di avere un'applicazione che memorizza dati su diversi prodotti. Vogliamo estenderla dando la possibilità di importare i dati da un file definito dall'utente. Inoltre, il file dovrebbe consentire di calcolare i valori dinamicamente, se necessario. Per ottenere questo risultato, decidiamo di creare un DSL.
Un semplice prodotto La rappresentazione può avere i seguenti attributi (prodotto.rb):
classe Prodotto
attr_accessor :name, :description, :price
fine
Invece di utilizzare un database reale, ci limiteremo a simulare il suo funzionamento (fake_products_database.rb):
classe FakeProductsDatabase
def self.store(prodotto)
mette [nome.prodotto, descrizione.prodotto, prezzo.prodotto].join(' - ')
fine
fine
Ora, creeremo una classe che sarà responsabile della lettura e della gestione dei file contenenti i dati dei prodotti (dsl/data_importer.rb):
modulo Dsl
classe DataImporter
modulo Sintassi
def add_product(&block)
FakeProductsDatabase.store prodotto(&blocco)
fine
privato
def prodotto(&blocco)
ProductBuilder.new.tap { |b| b.instance_eval(&block) }.product
fine
fine
includere Sintassi
def self.import_data(file_path)
new.instance_eval File.read(file_path)
fine
fine
fine
```
La classe ha un metodo chiamato importazione_dati che si aspetta un percorso di file come argomento. Il file viene letto e il risultato viene passato al metodo istanza_eval che viene richiamato sull'istanza della classe. Cosa fa? Valuta la stringa come codice Ruby nel contesto dell'istanza. Ciò significa sé sarà l'istanza di Importatore di dati classe. Grazie al fatto che siamo in grado di definire la sintassi e le parole chiave desiderate (per una migliore leggibilità, la sintassi è definita come modulo). Quando la classe aggiungi_prodotto il blocco dato per il metodo viene valutato da Costruttore di prodotti che costruisce Prodotto istanza. Costruttore di prodotti è descritta di seguito (dsl/product_builder.rb):
modulo Dsl
classe ProductBuilder
ATTRIBUTI = %i[nome descrizione prezzo].freeze
attr_reader :product
def inizializzare
@prodotto = Prodotto.nuovo
fine
ATTRIBUTI.each do |attributo|
define_method(attributo) do |arg = nil, &block|
value = block.is_a?(Proc) ? block.call : arg
product.public_send("#{attributo}=", valore)
fine
fine
fine
fine
```
La classe definisce la sintassi consentita all'interno di aggiungi_prodotto blocco. Con un po' di metaprogrammazione, aggiunge metodi che assegnano valori agli attributi del prodotto. Questi metodi supportano anche il passaggio di un blocco invece che di un valore diretto, in modo da poter calcolare un valore in fase di esecuzione. Utilizzando il lettore di attributi, siamo in grado di ottenere un prodotto costruito alla fine.
Ora, aggiungiamo lo script di importazione (import_job.rb):
requirerelativo 'dsl/dataimporter'
requirerelativo "dsl/productbuilder".
requirerelativo 'fakeproductsdatabase'
requirerelativo 'prodotto'
Dsl::DataImporter.import_data(ARGV[0])
```
E infine, usando il nostro DSL, un file con i dati dei prodotti (dataset.rb):
```ruby
add_product do
nome 'Caricabatterie'
descrizione 'Salvagente'
prezzo 19.99
fine
aggiungere_prodotto fare
nome 'Relitto d'auto'
description { "Relitto al #{Time.now.strftime('%F %T')}" }
prezzo 0.01
fine
add_product do
nome 'Lockpick
descrizione 'Le porte non si chiudono'
prezzo 7,50
fine
```
Per importare i dati è sufficiente eseguire un comando:
ruby import_job.rb dataset.rb
E il risultato è..
Caricabatterie - Salvavita - 19,99
Auto distrutta - Distrutta al 2018-12-09 09:47:42 - 0,01
Lockpick - Le porte non si chiudono - 7,5
...successo!
Conclusione
Osservando tutti gli esempi precedenti, non è difficile notare le possibilità offerte dal DSL. Il DSL permette di semplificare alcune operazioni di routine, nascondendo tutta la logica necessaria ed esponendo all'utente solo le parole chiave più importanti. Permette di ottenere un livello di astrazione più elevato e offre possibilità di utilizzo flessibili (il che è particolarmente prezioso in termini di riusabilità). D'altra parte, l'aggiunta di un DSL al vostro progetto dovrebbe essere sempre ben considerato: un'implementazione che utilizza la metaprogrammazione è sicuramente molto più difficile da comprendere e mantenere. Inoltre, richiede una solida suite di test a causa della sua dinamicità. Documentare il DSL ne facilita la comprensione, quindi vale sicuramente la pena farlo. Anche se l'implementazione del proprio DSL può essere gratificante, è bene ricordare che deve dare i suoi frutti.
Vi siete interessati all'argomento? Se sì, fatecelo sapere: vi parleremo del DSL che abbiamo creato di recente per soddisfare i requisiti in uno dei nostri progetti.