Refererend aan de definitie is DSL (Domain Specific Language) een computertaal die gespecialiseerd is in een bepaald toepassingsdomein. Dit betekent dat het ontwikkeld is om aan specifieke behoeften te voldoen.
Door dit artikel te lezen zul je leren wat de DSL is en wat het gemeen heeft met Ruby.
DSL, zeg welkom!
Refererend aan de definitie is DSL (Domain Specific Language) een computertaal die is gespecialiseerd in een bepaald toepassingsdomein. Dit betekent dat het ontwikkeld is om aan specifieke behoeften te voldoen. Er zijn twee soorten DSL:
-
Een extern DSL die zijn eigen syntaxis-parser nodig heeft. Een goed bekend voorbeeld is de SQL-taal - het maakt interactie met een database mogelijk in een taal waarin de database niet is gemaakt.
-
Een intern DSL die zelf geen eigen syntaxis heeft, maar in plaats daarvan een syntaxis van een bepaalde programmeertaal gebruikt.
Zoals je waarschijnlijk al kunt raden, blijven we gefocust op het tweede DSL-type.
Wat doet het?
Door gebruik te maken van Ruby metaprogrammering kun je je eigen mini-taal maken. Metaprogrammeren is een programmeertechniek waarmee je een code dynamisch tijdens runtime (on the fly). Je bent je hier misschien niet van bewust, maar je gebruikt waarschijnlijk elke dag veel verschillende DSL's. Om te begrijpen wat een DSL kan doen, laten we eens kijken naar een paar voorbeelden hieronder - ze hebben allemaal een gemeenschappelijk element, maar kun je het aanwijzen?
Rails routing
Rails.application.routes.draw doen
root naar: 'home#index'
bronnen :gebruikers doen
get :search, op: :verzameling
einde
einde
```
Iedereen die ooit Rails heeft gebruikt, kent een config/routes.rb bestand waarin we applicatieroutes definiëren (mapping tussen HTTP-verbs en URL's naar controlleracties). Maar heb je je ooit afgevraagd hoe het werkt? In feite is het gewoon Ruby code.
Fabriek Bot
FactoryBot.define do
fabriek :gebruiker doen
bedrijf
sequentie(:email) { |i| "user_#{i}@test.com" }
sequentie(:voornaam) { |i| "gebruiker_#{i}" }
achternaam 'Test
rol 'manager
einde
einde
Het schrijven van tests vereist vaak het maken van objecten. Om tijdverspilling te voorkomen, zou het dus een goed idee zijn om het proces zoveel mogelijk te vereenvoudigen. Dat is wat de FactoryBot doet - gemakkelijk te onthouden trefwoorden en een manier om een object te beschrijven.
Sinatra
Vereis 'sinatra/base'.
klasse WebApplicatie < Sinatra::Base
get '/' do
Hallo wereld
einde
einde
```
Sinatra is een framework waarmee je webapplicaties vanaf nul kunt maken. Kan het eenvoudiger zijn om request method, path en response te definiëren?
Andere DSL-voorbeelden kunnen zijn Hark, RSpec of Actief record. Het sleutelelement van elke DSL is het gebruik van blokken.
Bouwtijd
Tijd om te begrijpen wat er onder de motorkap schuilgaat en hoe de implementatie eruit kan zien.
Laten we aannemen dat we een applicatie hebben die gegevens over verschillende producten opslaat. We willen deze uitbreiden met de mogelijkheid om gegevens te importeren uit een door de gebruiker gedefinieerd bestand. Het bestand moet het ook mogelijk maken om waar nodig dynamisch waarden te berekenen. Om dat te bereiken, besluiten we om een DSL te maken.
Een eenvoudig product weergave kan de volgende attributen hebben (product.rb):
klasse Product
attr_accessor :naam, :beschrijving, :prijs
einde
In plaats van een echte database te gebruiken, zullen we alleen het werk ervan simuleren (nep_producten_database.rb):
klasse ValseProductenDatabase
def self.store(product)
puts [product.naam, product.omschrijving, product.prijs].join(' - ')
einde
einde
Nu maken we een klasse die verantwoordelijk is voor het lezen en verwerken van bestanden met productgegevens (dsl/data_importer.rb):
module Dsl
klasse DataImporter
module Syntax
def toevoegen_product(&block)
FakeProductsDatabase.store product(&block)
einde
privé
def product(&block)
ProductBuilder.new.tap { |b| b.instance_eval(&block) }.product
einde
einde
include Syntaxis
def self.import_data(bestand_pad)
new.instance_eval Bestand.lezen(bestand_pad)
einde
einde
einde
```
De klasse heeft een methode met de naam importgegevens die een bestandspad als argument verwacht. Het bestand wordt gelezen en het resultaat wordt doorgegeven aan de instantie_eval methode die wordt aangeroepen op de instantie van de klasse. Wat doet deze methode? Het evalueert de string als een Ruby code binnen de instantie context. Dit betekent zelf zal de instantie van Gegevensimporteur klasse. Dankzij het feit dat we de gewenste syntaxis/trefwoorden kunnen definiëren (voor een betere leesbaarheid is de syntaxis gedefinieerd als een module). Wanneer de product toevoegen methode wordt aangeroepen, wordt het voor de methode gegeven blok geëvalueerd door ProductBuilder instantie die Product voorbeeld. ProductBuilder wordt hieronder beschreven (dsl/product_builder.rb):
module Dsl
klasse productbouwer
ATTRIBUTES = %i[naam beschrijving prijs].freeze
attr_lezer :product
def initialiseren
@product = Product.new
einde
ATTRIBUTES.each do |attribute|
define_method(attribute) do |arg = nil, &block|
waarde = block.is_a?(Proc) ? block.call : arg
product.public_send("#{attribute}=", waarde)
einde
einde
einde
einde
```
De klasse definieert de syntaxis die is toegestaan binnen product toevoegen blok. Met een beetje metaprogrammeren voegt het methoden toe die waarden toewijzen aan productattributen. Deze methoden ondersteunen ook het doorgeven van een blok in plaats van een directe waarde, zodat een waarde tijdens runtime kan worden berekend. Met behulp van attribuutlezer kunnen we aan het einde een gebouwd product verkrijgen.
Laten we nu het importscript toevoegen (import_job.rb):
verplicht "dsl/dataimporter
verplicht "dsl/productbuilder
Ik heb "fakeproductdatabase" nodig
verplicht "product
Dsl::DataImporter.import_data(ARGV[0])
```
En tot slot - met behulp van onze DSL - een bestand met productgegevens (dataset.rb):
``ruby
toevoegen_product doen
naam 'Oplader
beschrijving 'Levensreddend
prijs 19,99
einde
add_product do
naam 'Autowrak
description { "Autowrak op #{Time.now.strftime('%F %T')}" }
prijs 0.01
einde
add_product do
naam 'Lockpick
beschrijving "Deuren zullen niet sluiten
prijs 7.50
einde
```
Om de gegevens te importeren hoeven we maar één commando uit te voeren:
ruby import_job.rb dataset.rb
En het resultaat is...
Oplader - Levens redden - 19,99
Autowrak - Wrak op 2018-12-09 09:47:42 - 0.01
Lockpick - Deuren gaan niet dicht - 7.5
succes!
Conclusie
Als je alle bovenstaande voorbeelden bekijkt, is het niet moeilijk om de mogelijkheden van DSL op te merken. DSL maakt het mogelijk om bepaalde routinehandelingen te vereenvoudigen door alle vereiste logica te verbergen en alleen de belangrijkste sleutelwoorden aan de gebruiker bloot te stellen. Het geeft je een hoger abstractieniveau en biedt flexibele gebruiksmogelijkheden (wat vooral waardevol is in termen van herbruikbaarheid). Aan de andere kant is het toevoegen van DSL aan je project moet altijd goed overwogen worden - een implementatie die gebruik maakt van metaprogrammering is zeker veel moeilijker te begrijpen en te onderhouden. Bovendien vereist het een solide testsuite vanwege de dynamiek. Het documenteren van DSL bevordert het begrip ervan, dus het is zeker de moeite waard om te doen. Hoewel het implementeren van je eigen DSL lonend kan zijn, is het goed om te onthouden dat het moet lonen.
Ben je geïnteresseerd geraakt in het onderwerp? Zo ja, laat het ons weten - dan vertellen we je over DSL die we onlangs hebben gemaakt om te voldoen aan de vereisten in een van onze projecten.