DSL (Domain Specific Language) er et dataspråk som er spesialisert for et bestemt anvendelsesområde. Det betyr at det er utviklet for å tilfredsstille spesifikke behov.
Ved å lese denne artikkelen vil du lære hva DSL er og hva det har til felles med Ruby.
DSL, si velkommen!
DSL (Domain Specific Language) er et dataspråk som er spesialisert for et bestemt anvendelsesområde. Det betyr at det er utviklet for å tilfredsstille spesifikke behov. Det finnes to typer DSL:
-
En utvendig DSL som krever sin egen syntaksparser. Et godt kjent eksempel kan være SQL-språket - det gjør det mulig å samhandle med en database på et språk som databasen ikke ble opprettet på.
-
En internt DSL som ikke har sin egen syntaks, men i stedet bruker syntaksen til et gitt programmeringsspråk.
Som du sikkert skjønner, kommer vi til å fokusere på den andre DSL-typen.
Hva gjør den?
I utgangspunktet kan du lage ditt eget minispråk ved å bruke Ruby-metaprogrammering. Metaprogrammering er en programmeringsteknikk som gjør det mulig å skrive et kode dynamisk under kjøring (on the fly). Du er kanskje ikke klar over dette, men du bruker sannsynligvis mange forskjellige DSL-er hver dag. For å forstå hva en DSL kan gjøre, la oss ta en titt på noen eksempler nedenfor - alle disse har ett felles element, men kan du peke på det?
Rails-ruting
Rails.application.routes.draw do
root to: 'home#index'
resources :users do
get :search, on: :collection
end
end
```
Alle som noen gang har brukt Rails, kjenner en config/routes.rb filen der vi definerer applikasjonsruter (mapping mellom HTTP-verb og URL-er til kontrollerhandlinger). Men har du noen gang lurt på hvordan det fungerer? Det er faktisk bare Ruby-kode.
Factory Bot
FactoryBot.define do
factory :user do
company
sequence(:email) { |i| "user_#{i}@test.com" }
sequence(:first_name) { |i| "Bruker #{i}" }
etternavn 'Test'
rolle 'manager'
end
end
Å skrive tester krever ofte at man produserer objekter. For å unngå å kaste bort tid, er det derfor en god idé å forenkle prosessen så mye som mulig. Det er nettopp det FactoryBot gjør - nøkkelord som er enkle å huske og en måte å beskrive et objekt på.
Sinatra
krever 'sinatra/base'
class WebApplication < Sinatra::Base
get '/' do
'Hello world'
end
end
```
Sinatra er et rammeverk som lar deg lage webapplikasjoner fra bunnen av. Kan det være enklere å definere forespørselsmetode, sti og svar?
Andre DSL-eksempler kan være Rive, RSpec eller Aktiv rekord. Det viktigste elementet i hvert DSL er bruk av blokker.
Byggetid
Det er på tide å forstå hva som skjuler seg under panseret og hvordan implementeringen kan se ut.
La oss anta at vi har en applikasjon som lagrer data om ulike produkter. Vi ønsker å utvide den ved å gi mulighet til å importere data fra en brukerdefinert fil. Filen skal også gjøre det mulig å beregne verdier dynamisk ved behov. For å oppnå det, bestemmer vi oss for å lage DSL.
En enkel produkt representasjon kan ha følgende attributter (product.rb):
class Produkt
attr_accessor :name, :description, :price
end
I stedet for å bruke en ekte database vil vi bare simulere dens arbeid (fake_products_database.rb):
class FakeProductsDatabase
def self.store(produkt)
puts [produkt.navn, produkt.beskrivelse, produkt.pris].join(' - ')
end
end
Nå skal vi opprette en klasse som skal være ansvarlig for å lese og håndtere filer som inneholder produktdata (dsl/data_importer.rb):
modul Dsl
klasse DataImporter
modul Syntaks
def add_product(&blokk)
FakeProductsDatabase.store produkt(&blokk)
end
private
def produkt(&blokk)
ProductBuilder.new.tap { |b| b.instance_eval(&blokk) }.product
end
end
include Syntaks
def self.import_data(file_path)
new.instance_eval File.read(file_path)
end
end
end
```
Klassen har en metode som heter import_data som forventer en filbane som argument. Filen leses, og resultatet sendes til instance_eval metoden som kalles på klasseforekomsten. Hva gjør den? Den evaluerer strengen som en Ruby-kode innenfor instansens kontekst. Dette betyr at seg selv vil være forekomsten av DataImporter klasse. Takket være dette kan vi definere ønsket syntaks/nøkkelord (for bedre lesbarhet er syntaksen definert som en modul). Når add_product metoden kalles, evalueres blokken som er gitt for metoden av ProductBuilder instans som bygger Produkt eksempel. ProductBuilder klassen er beskrevet nedenfor (dsl/product_builder.rb):
modul Dsl
class ProductBuilder
ATTRIBUTES = %i[navn beskrivelse pris].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("#{attributt}=", verdi)
end
end
end
end
```
Klassen definerer tillatt syntaks innenfor add_product blokk. Med litt metaprogrammering legger den til metoder som tilordner verdier til produktattributter. Disse metodene støtter også overlevering av en blokk i stedet for en direkte verdi, slik at en verdi kan beregnes ved kjøretid. Ved hjelp av attributtleser kan vi få et ferdig bygget produkt til slutt.
La oss nå legge til importskriptet (import_job.rb):
requirerelative 'dsl/dataimporter'
requirerelative 'dsl/productbuilder'
requirerelative 'fakeproductsdatabase'
requirerelative 'produkt'
Dsl::DataImporter.import_data(ARGV[0])
```
Og til slutt - ved hjelp av DSL-en vår - en fil med produktdata (dataset.rb):
```ruby
add_product do
navn 'Lader'
description 'Livreddende'
price 19.99
end
add_product do
name 'Bilvrak'
description { "Vrak på #{Time.now.strftime('%F %T')}" }
price 0.01
end
add_product do
navn 'Lockpick'
description 'Dører skal ikke lukke seg'
price 7.50
end
```
For å importere dataene trenger vi bare å utføre én kommando:
ruby import_job.rb dataset.rb
Og resultatet er...
Lader - Livreddende - 19.99
Bilvrak - Ødelagt kl 2018-12-09 09:47:42 - 0.01
Lockpick - Dører skal ikke lukkes - 7.5
...suksess!
Konklusjon
Ved å se på alle eksemplene ovenfor er det ikke vanskelig å se hvilke muligheter DSL gir. DSL gjør det mulig å forenkle en del rutineoperasjoner ved å skjule all nødvendig logikk bak og bare eksponere de viktigste nøkkelordene for brukeren. Det gir et høyere abstraksjonsnivå og fleksible bruksmuligheter (noe som er spesielt verdifullt med tanke på gjenbruk). På den annen side kan det å legge til DSL i prosjekt bør alltid vurderes nøye - en implementering som bruker metaprogrammering er definitivt mye vanskeligere å forstå og vedlikeholde. Dessuten krever den en solid testpakke på grunn av sin dynamikk. Å dokumentere DSL gjør det lettere å forstå, så det er absolutt verdt å gjøre. Selv om det kan være givende å implementere din egen DSL, er det godt å huske at det må lønne seg.
Ble du interessert i temaet? I så fall gi oss beskjed - vi vil fortelle deg om DSL som vi nylig har laget for å oppfylle kravene i et av våre prosjekter.