Odnosząc się do definicji, DSL (Domain Specific Language) to język komputerowy wyspecjalizowany do konkretnej domeny aplikacji. Oznacza to, że został opracowany w celu zaspokojenia określonych potrzeb.
Czytając ten artykuł dowiesz się czym jest DSL i co ma wspólnego z Ruby.
DSL, witamy!
Odnosząc się do definicji, DSL (Domain Specific Language) to język komputerowy wyspecjalizowany do określonej domeny aplikacji. Oznacza to, że został opracowany w celu zaspokojenia określonych potrzeb. Istnieją dwa rodzaje DSL:
-
An zewnętrzny DSL, który wymaga własnego parsera składni. Dobrym znanym przykładem może być język SQL - pozwala on na interakcję z bazą danych w języku, w którym baza danych nie została utworzona.
-
An wewnętrzny DSL, który sam nie ma własnej składni, ale zamiast tego używa składni danego języka programowania.
Jak można się domyślić, skupimy się na drugim typie DSL.
Co to robi?
Zasadniczo, korzystając z metaprogramowania Ruby, można stworzyć własny mini-język. Metaprogramowanie jest techniką programowania, która pozwala na napisanie kod dynamicznie w czasie wykonywania (w locie). Możesz być tego nieświadomy, ale prawdopodobnie używasz wielu różnych DSL każdego dnia. Aby zrozumieć, co może zrobić DSL, spójrzmy na kilka poniższych przykładów - wszystkie mają jeden wspólny element, ale czy potrafisz go wskazać?
Routing w Railsach
Rails.application.routes.draw do
root to: 'home#index'
resources :users do
get :search, on: :collection
end
end
```
Każda osoba, która kiedykolwiek korzystała z Railsów zna config/routes.rb gdzie definiujemy trasy aplikacji (mapowanie między czasownikami HTTP i adresami URL do akcji kontrolera). Ale czy kiedykolwiek zastanawiałeś się, jak to działa? W rzeczywistości jest to po prostu kod Ruby.
Factory Bot
FactoryBot.define do
factory :user do
firma
sequence(:email) { |i| "user_#{i}@test.com" }
sequence(:first_name) { |i| "User #{i}" }
last_name 'Test'
role 'manager'
end
end
Pisanie testów często wymaga tworzenia obiektów. Dlatego też, aby uniknąć straty czasu, naprawdę dobrym pomysłem byłoby maksymalne uproszczenie tego procesu. To jest właśnie to FactoryBot to łatwe do zapamiętania słowa kluczowe i sposób opisu obiektu.
Sinatra
require 'sinatra/base'
class WebApplication < Sinatra::Base
get '/' do
'Hello world'
end
end
```
Sinatra to framework, który umożliwia tworzenie aplikacji internetowych od podstaw. Czy może być łatwiej zdefiniować metodę żądania, ścieżkę i odpowiedź?
Innymi przykładami DSL mogą być Grabie, RSpec lub Aktywny rekord. Kluczowym elementem każdego DSL jest użycie bloków.
Czas budowy
Czas zrozumieć, co kryje się pod maską i jak może wyglądać implementacja.
Załóżmy, że mamy aplikację, która przechowuje dane o różnych produktach. Chcemy rozszerzyć ją o możliwość importowania danych z pliku zdefiniowanego przez użytkownika. Ponadto plik powinien umożliwiać dynamiczne obliczanie wartości w razie potrzeby. Aby to osiągnąć, decydujemy się na stworzenie DSL.
Prosty produkt Reprezentacja może mieć następujące atrybuty (product.rb):
class Product
attr_accessor :name, :description, :price
end
Zamiast korzystać z prawdziwej bazy danych, będziemy po prostu symulować jej działanie (fake_products_database.rb):
class FakeProductsDatabase
def self.store(product)
puts [product.name, product.description, product.price].join(' - ')
end
end
Teraz stworzymy klasę, która będzie odpowiedzialna za odczyt i obsługę pliku zawierającego dane produktów (dsl/data_importer.rb):
moduł Dsl
klasa DataImporter
moduł Składnia
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 Składnia
def self.import_data(file_path)
new.instance_eval File.read(file_path)
end
end
end
```
Klasa posiada metodę o nazwie import_data która oczekuje ścieżki pliku jako argumentu. Plik jest odczytywany, a wynik jest przekazywany do funkcji instance_eval która jest wywoływana na instancji klasy. Co ona robi? Ocenia ciąg znaków jako kod Ruby w kontekście instancji. Oznacza to. jaźń będzie instancją DataImporter class. Dzięki temu jesteśmy w stanie zdefiniować pożądaną składnię / słowa kluczowe (dla lepszej czytelności składnia jest zdefiniowana jako moduł). Kiedy add_product metoda jest wywoływana, blok podany dla metody jest obliczany przez ProductBuilder instancja, która buduje Produkt instancja. ProductBuilder została opisana poniżej (dsl/product_builder.rb):
moduł Dsl
class ProductBuilder
ATTRIBUTES = %i[nazwa opis cena].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("#{atrybut}=", wartość)
end
end
end
end
```
Klasa definiuje składnię dozwoloną w ramach add_product block. Z odrobiną metaprogramowania dodaje metody, które przypisują wartości do atrybutów produktu. Metody te obsługują również przekazywanie bloku zamiast bezpośredniej wartości, dzięki czemu wartość może być obliczana w czasie wykonywania. Używając czytnika atrybutów, jesteśmy w stanie uzyskać zbudowany produkt na końcu.
Teraz dodajmy skrypt importu (import_job.rb):
requirelative 'dsl/dataimporter'
requirerelative 'dsl/productbuilder'
requirelative 'fakeproductsdatabase'
requirerelative "product
Dsl::DataImporter.import_data(ARGV[0])
```
I wreszcie - przy użyciu naszego DSL - plik z danymi produktów (dataset.rb):
``ruby
add_product do
name 'Ładowarka'
description 'Life saving'
cena 19.99
end
add_product do
name "Wrak samochodu
description { "Wrak samochodu w #{Time.now.strftime('%F %T')}" }
price 0.01
end
add_product do
name 'Lockpick'
description "Drzwi nie będą się zamykać
cena 7.50
end
```
Aby zaimportować dane, wystarczy wykonać jedno polecenie:
ruby import_job.rb dataset.rb
Rezultatem jest.
Ładowarka - ratowanie życia - 19,99
Wrak samochodu - rozbity o 2018-12-09 09:47:42 - 0,01
Lockpick - Drzwi nie będą się zamykać - 7.5
..sukces!
Wnioski
Patrząc na wszystkie powyższe przykłady, nietrudno zauważyć możliwości oferowane przez DSL. DSL pozwala uprościć niektóre rutynowe operacje, ukrywając całą wymaganą logikę i udostępniając użytkownikowi tylko najważniejsze słowa kluczowe. Pozwala to uzyskać wyższy poziom abstrakcji i oferuje elastyczne możliwości użytkowania (co jest szczególnie cenne pod względem możliwości ponownego wykorzystania). Z drugiej strony, dodanie DSL do swojego oprogramowania projekt powinna być zawsze dobrze przemyślana - implementacja wykorzystująca metaprogramowanie jest zdecydowanie trudniejsza do zrozumienia i utrzymania. Ponadto wymaga solidnego zestawu testów ze względu na swoją dynamikę. Dokumentowanie DSL ułatwia jego zrozumienie, więc zdecydowanie warto to robić. Choć implementacja własnego DSL może być satysfakcjonująca, warto pamiętać, że musi się opłacać.
Zainteresował Cię ten temat? Jeśli tak, daj nam znać - opowiemy Ci o DSL, które niedawno stworzyliśmy, aby spełnić wymagania w jednym z naszych projektów.