Si l'on se réfère à la définition, le DSL (Domain Specific Language) est un langage informatique spécialisé dans un domaine d'application particulier. Cela signifie qu'il est développé pour répondre à des besoins spécifiques.
En lisant cet article, vous apprendrez ce qu'est le DSL et ce qu'il a en commun avec Ruby.
DSL, dites bienvenue !
Si l'on se réfère à la définition, le DSL (Domain Specific Language) est un langage informatique spécialisé dans un domaine d'application particulier. Cela signifie qu'il est développé pour répondre à des besoins spécifiques. Il existe deux types de DSL :
-
Un externe DSL qui nécessite son propre analyseur syntaxique. Un bon exemple connu est le langage SQL, qui permet d'interagir avec une base de données dans un langage dans lequel la base de données n'a pas été créée.
-
Un interne DSL qui n'a pas sa propre syntaxe mais utilise celle d'un langage de programmation donné.
Comme vous pouvez le deviner, nous allons nous concentrer sur le deuxième type de LIS.
Que fait-il ?
En fait, en utilisant la métaprogrammation Ruby, il est possible de créer son propre mini-langage. La métaprogrammation est une technique de programmation qui permet d'écrire un code dynamiquement au moment de l'exécution (à la volée). Vous ne le savez peut-être pas, mais vous utilisez probablement plusieurs DSL différents chaque jour. Pour comprendre ce que peut faire un DSL, examinons quelques exemples ci-dessous - ils ont tous un élément commun, mais pouvez-vous le pointer ?
Rails routing
Rails.application.routes.draw do
root to : 'home#index'
ressources :users do
get :search, on : :collection
end
end
```
Tous ceux qui ont déjà utilisé Rails connaissent un config/routes.rb dans lequel nous définissons les routes d'application (correspondance entre les verbes et URL HTTP et les actions du contrôleur). Mais vous êtes-vous déjà demandé comment cela fonctionnait ? En fait, il s'agit simplement de code Ruby.
Bot d'usine
FactoryBot.define do
factory :user do
entreprise
sequence(:email) { |i| "user_#{i}@test.com" }
sequence(:first_name) { |i| "Utilisateur #{i}" }
last_name 'Test'
role 'manager'
fin
fin
L'écriture de tests nécessite souvent la fabrication d'objets. Pour éviter une perte de temps, il serait donc judicieux de simplifier le processus autant que possible. C'est la raison d'être de l'outil FactoryBot fait - des mots-clés faciles à retenir et une façon de décrire un objet.
Sinatra
nécessite 'sinatra/base'
classe WebApplication < Sinatra::Base
get '/' do
'Hello world' (Bonjour le monde)
end
end
```
Sinatra est un cadre qui vous permet de créer des applications web à partir de zéro. Pourrait-il être plus facile de définir une méthode de requête, un chemin et une réponse ?
Voici d'autres exemples de LIS Râteau, RSpec ou Registre actif. L'élément clé de chaque LIS est l'utilisation de blocs.
Temps de construction
Il est temps de comprendre ce qui se cache sous le capot et comment la mise en œuvre peut se présenter.
Supposons que nous ayons une application qui stocke des données sur différents produits. Nous voulons l'étendre en donnant la possibilité d'importer des données à partir d'un fichier défini par l'utilisateur. De plus, le fichier doit permettre de calculer des valeurs dynamiquement si nécessaire. Pour ce faire, nous décidons de créer un DSL.
Un simple produit La représentation peut avoir les attributs suivants (product.rb):
classe Produit
attr_accessor :nom, :description, :prix
fin
Au lieu d'utiliser une véritable base de données, nous nous contenterons de simuler son fonctionnement (fake_products_database.rb):
classe FakeProductsDatabase
def self.store(product)
puts [product.name, product.description, product.price].join(' - ')
end
end
Nous allons maintenant créer une classe qui sera chargée de lire et de traiter les fichiers contenant des données sur les produits (dsl/data_importer.rb):
module Dsl
classe DataImporter
module Syntaxe
def add_product(&block)
FakeProductsDatabase.store product(&block)
end
private
def product(&block)
ProductBuilder.new.tap { |b| b.instance_eval(&block) }.product
end
end
include Syntaxe
def self.import_data(chemin_du_fichier)
new.instance_eval File.read(file_path)
end
end
end
```
La classe possède une méthode nommée import_data qui attend un chemin d'accès à un fichier comme argument. Le fichier est lu et le résultat est transmis à la fonction instance_eval qui est appelée sur l'instance de la classe. Que fait-elle ? Elle évalue la chaîne comme un code Ruby dans le contexte de l'instance. Cela signifie que soi sera l'instance de DataImporter classe. Grâce à cela, nous pouvons définir la syntaxe et les mots-clés souhaités (pour une meilleure lisibilité, la syntaxe est définie en tant que module). Lorsque le module ajouter_produit est appelée, le bloc donné pour la méthode est évalué par ProductBuilder qui construit Produit instance. ProductBuilder est décrite ci-dessous (dsl/product_builder.rb):
module Dsl
classe ProductBuilder
ATTRIBUTES = %i[nom description prix].freeze
attr_reader :product
def initialize
@product = Product.new
end
ATTRIBUTES.each do |attribute|
define_method(attribute) do |arg = nil, &block|
value = block.is_a ?(Proc) ? block.call : arg
product.public_send("#{attribut}=", valeur)
fin
fin
fin
end
```
La classe définit la syntaxe autorisée dans les ajouter_produit bloc. Avec un peu de métaprogrammation, il ajoute des méthodes qui attribuent des valeurs aux attributs du produit. Ces méthodes permettent également de passer un bloc au lieu d'une valeur directe, de sorte qu'une valeur peut être calculée au moment de l'exécution. En utilisant le lecteur d'attributs, nous sommes en mesure d'obtenir un produit construit à la fin.
Ajoutons maintenant le script d'importation (import_job.rb):
requérant relatif 'dsl/dataimporter'
requérant relatif à "dsl/productbuilder" (en anglais)
requérant relatif "fakeproductsdatabase" (base de données de produits fictifs)
requérant relatif "product
Dsl::DataImporter.import_data(ARGV[0])
```
Et enfin - en utilisant notre DSL - un fichier avec les données des produits (dataset.rb) :
``Ruby
add_product do
name 'Chargeur'
description 'Life saving' (sauver la vie)
prix 19.99
end
add_product do
name 'Épave de voiture'
description { "Épave à #{time.now.strftime('%F %T')}" }
prix 0.01
end
add_product do
name 'Lockpick'
description 'Les portes ne doivent pas se fermer'
prix 7,50
fin
```
Pour importer les données, il suffit d'exécuter une commande :
<ruby import_job.rb dataset.rb
Et le résultat est...
Chargeur - Sauvetage - 19.99
Épave de voiture - Épuisée à 2018-12-09 09:47:42 - 0.01
Lockpick - Les portes ne se ferment pas - 7.5
... succès !
Conclusion
En examinant tous les exemples ci-dessus, il n'est pas difficile de constater les possibilités offertes par le DSL. Le DSL permet de simplifier certaines opérations de routine en cachant toute la logique requise et en n'exposant à l'utilisateur que les mots-clés les plus importants. Il permet d'atteindre un niveau d'abstraction plus élevé et offre des possibilités d'utilisation flexibles (ce qui est particulièrement précieux en termes de réutilisation). D'autre part, l'ajout d'un DSL à votre projet doit toujours être bien considéré - une implémentation utilisant la métaprogrammation est certainement beaucoup plus difficile à comprendre et à maintenir. En outre, elle nécessite une solide suite de tests en raison de son dynamisme. Documenter le DSL facilite sa compréhension et vaut donc la peine d'être fait. Bien que l'implémentation de votre propre DSL puisse être gratifiante, il est bon de se rappeler qu'elle doit être payante.
Le sujet vous a-t-il intéressé ? Si c'est le cas, faites-le nous savoir - nous vous parlerons de la DSL que nous avons récemment créée pour répondre aux besoins de l'un de nos projets.