Bir Ruby DSL Oluşturma: Gelişmiş Metaprogramlamaya Yönelik Bir Kılavuz
Yayınlanan: 2022-03-11Etki alanına özgü diller (DSL), karmaşık sistemleri programlamayı veya yapılandırmayı kolaylaştırmak için inanılmaz derecede güçlü bir araçtır. Ayrıca her yerdeler; bir yazılım mühendisi olarak büyük olasılıkla günlük olarak birkaç farklı DSL kullanıyorsunuz.
Bu makalede, etki alanına özgü dillerin ne olduğunu, ne zaman kullanılması gerektiğini ve son olarak gelişmiş metaprogramlama tekniklerini kullanarak Ruby'de kendi DSL'nizi nasıl oluşturabileceğinizi öğreneceksiniz.
Bu makale, Nikola Todoroviç'in, yine Toptal Blog'da yayınlanan Ruby metaprogramming'e girişine dayanmaktadır. Bu nedenle, metaprogramlama konusunda yeniyseniz, önce bunu okuduğunuzdan emin olun.
Etki Alanına Özgü Dil Nedir?
DSL'lerin genel tanımı, belirli bir uygulama alanı veya kullanım durumu için uzmanlaşmış diller olmalarıdır. Bu, bunları yalnızca belirli şeyler için kullanabileceğiniz anlamına gelir; bunlar genel amaçlı yazılım geliştirme için uygun değildir. Kulağa geniş geliyorsa, bunun nedeni, DSL'lerin birçok farklı şekil ve boyutta gelmesidir. İşte birkaç önemli kategori:
- HTML ve CSS gibi biçimlendirme dilleri, web sayfalarının yapısı, içeriği ve stilleri gibi belirli şeyleri açıklamak için tasarlanmıştır. Onlarla rastgele algoritmalar yazmak mümkün değildir, bu nedenle bir DSL tanımına uyarlar.
- Makro ve sorgu dilleri (örn. SQL) belirli bir sistemin veya başka bir programlama dilinin üzerine oturur ve yapabilecekleri genellikle sınırlıdır. Bu nedenle, açıkça alana özgü diller olarak nitelendirilirler.
- Birçok DSL'nin kendi sözdizimi yoktur; bunun yerine, yerleşik bir programlama dilinin sözdizimini, ayrı bir mini dil kullanıyormuş gibi hissettiren akıllı bir şekilde kullanırlar.
Bu son kategoriye dahili DSL denir ve çok yakında örnek olarak oluşturacağımız bunlardan biridir. Ancak buna girmeden önce, birkaç iyi bilinen dahili DSL örneğine bir göz atalım. Rails'deki rota tanımı sözdizimi bunlardan biridir:
Rails.application.routes.draw do root to: "pages#main" resources :posts do get :preview resources :comments, only: [:new, :create, :destroy] end end Bu Ruby kodudur, ancak bu kadar temiz, kullanımı kolay bir arayüzü mümkün kılan çeşitli metaprogramlama teknikleri sayesinde daha çok özel bir rota tanımlama dili gibi hissettirir. DSL yapısının Ruby blokları kullanılarak uygulandığına ve bu mini dilin anahtar sözcüklerini tanımlamak için get ve resources gibi yöntem çağrılarının kullanıldığına dikkat edin.
Metaprogramlama, RSpec test kitaplığında daha da yoğun bir şekilde kullanılır:
describe UsersController, type: :controller do before do allow(controller).to receive(:current_user).and_return(nil) end describe "GET #new" do subject { get :new } it "returns success" do expect(subject).to be_success end end endBu kod parçası ayrıca, bildirimlerin düz İngilizce cümleler olarak yüksek sesle okunmasına izin veren ve kodun ne yaptığını anlamayı çok daha kolay hale getiren akıcı arayüzler için örnekler içerir:
# Stubs the `current_user` method on `controller` to always return `nil` allow(controller).to receive(:current_user).and_return(nil) # Asserts that `subject.success?` is truthy expect(subject).to be_successAkıcı bir arabirime başka bir örnek, karmaşık SQL sorguları oluşturmak için dahili olarak soyut bir sözdizimi ağacı kullanan ActiveRecord ve Arel'in sorgu arabirimidir:
Post. # => select([ # SELECT Post[Arel.star], # `posts`.*, Comment[:id].count. # COUNT(`comments`.`id`) as("num_comments"), # AS num_comments ]). # FROM `posts` joins(:comments). # INNER JOIN `comments` # ON `comments`.`post_id` = `posts`.`id` where.not(status: :draft). # WHERE `posts`.`status` <> 'draft' where( # AND Post[:created_at].lte(Time.now) # `posts`.`created_at` <= ). # '2017-07-01 14:52:30' group(Post[:id]) # GROUP BY `posts`.`id`Ruby'nin temiz ve etkileyici sözdizimi ile birlikte metaprogramlama yetenekleri, onu alana özgü diller oluşturmak için benzersiz bir şekilde uygun hale getirse de, DSL'ler diğer dillerde de mevcuttur. Jasmine çerçevesini kullanan bir JavaScript testi örneği:
describe("Helper functions", function() { beforeEach(function() { this.helpers = window.helpers; }); describe("log error", function() { it("logs error message to console", function() { spyOn(console, "log").and.returnValue(true); this.helpers.log_error("oops!"); expect(console.log).toHaveBeenCalledWith("ERROR: oops!"); }); }); });Bu sözdizimi belki de Ruby örneklerininki kadar temiz değil, ancak akıllıca adlandırma ve sözdiziminin yaratıcı kullanımıyla, neredeyse her dil kullanılarak dahili DSL'lerin oluşturulabileceğini gösteriyor.
Dahili DSL'lerin yararı, düzgün bir şekilde uygulanması çok zor olabilen ayrı bir ayrıştırıcı gerektirmemeleridir. Ve uygulandıkları dilin sözdizimini kullandıkları için, kod tabanının geri kalanıyla da sorunsuz bir şekilde bütünleşirler.
Karşılığında vazgeçmemiz gereken şey sözdizimsel özgürlüktür—dahili DSL'ler uygulama dillerinde sözdizimsel olarak geçerli olmalıdır. Bu konuda ne kadar ödün vermeniz gerektiği büyük ölçüde seçilen dile bağlıdır; Java ve VB.NET gibi ayrıntılı, statik olarak yazılan diller yelpazenin bir ucunda yer alır ve diğer ucunda Ruby gibi kapsamlı metaprogramlama yeteneklerine sahip dinamik diller bulunur. son.
Kendimizi Oluşturma—Sınıf Yapılandırması için Bir Ruby DSL
Ruby'de oluşturacağımız örnek DSL, çok basit bir sözdizimi kullanarak bir Ruby sınıfının konfigürasyon özniteliklerini belirtmek için yeniden kullanılabilir bir konfigürasyon motorudur. Bir sınıfa yapılandırma yetenekleri eklemek, Ruby dünyasında, özellikle de harici taşlar ve API istemcilerinin yapılandırılması söz konusu olduğunda çok yaygın bir gereksinimdir. Her zamanki çözüm, bunun gibi bir arayüzdür:
MyApp.configure do |config| config.app_ config.title = "My App" config.cookie_name = "my_app_session" endÖnce bu arayüzü uygulayalım ve sonra onu başlangıç noktası olarak kullanarak, daha fazla özellik ekleyerek, sözdizimini temizleyerek ve çalışmamızı yeniden kullanılabilir hale getirerek adım adım iyileştirebiliriz.
Bu arayüzün çalışması için neye ihtiyacımız var? MyApp sınıfı, bir blok alan ve ardından bu bloğu ona vererek, konfigürasyon değerlerini okumak ve yazmak için erişimci yöntemlerine sahip bir konfigürasyon nesnesi ileterek bu bloğu yürüten bir configure sınıf yöntemine sahip olmalıdır:
class MyApp # ... class << self def config @config ||= Configuration.new end def configure yield config end end class Configuration attr_accessor :app_id, :title, :cookie_name end endYapılandırma bloğu çalıştığında, değerlere kolayca erişebilir ve bunları değiştirebiliriz:
MyApp.config => #<MyApp::Configuration:0x2c6c5e0 @app_, @title="My App", @cookie_name="my_app_session"> MyApp.config.title => "My App" MyApp.config.app_ => "not_my_app" Şimdiye kadar, bu uygulama bir DSL olarak kabul edilecek kadar özel bir dil gibi gelmiyor. Ama her seferinde bir adım atalım. Daha sonra, yapılandırma işlevselliğini MyApp sınıfından ayıracağız ve onu birçok farklı kullanım durumunda kullanılabilecek kadar genel hale getireceğiz.
Yeniden Kullanılabilir Hale Getirmek
Şu anda, farklı bir sınıfa benzer yapılandırma yetenekleri eklemek istiyorsak, hem Configuration sınıfını hem de ilgili kurulum yöntemlerini o diğer sınıfa kopyalamamız ve kabul edilen yapılandırma niteliklerini değiştirmek için attr_accessor listesini düzenlememiz gerekir. Bunu yapmak zorunda kalmamak için, yapılandırma özelliklerini Configurable adlı ayrı bir modüle taşıyalım. Bununla, MyApp sınıfımız şöyle görünecek:
class MyApp #BOLD include Configurable #BOLDEND # ... end Yapılandırmayla ilgili her şey Configurable modüle taşındı:
#BOLD module Configurable def self.included(host_class) host_class.extend ClassMethods end module ClassMethods #BOLDEND def config @config ||= Configuration.new end def configure yield config end #BOLD end #BOLDEND class Configuration attr_accessor :app_id, :title, :cookie_name end #BOLD end #BOLDEND Yeni self.included yöntemi dışında burada pek bir şey değişmedi. Bu yönteme ihtiyacımız var, çünkü bir modül dahil etmek yalnızca örnek yöntemleriyle karışır, bu nedenle config ve configure sınıf yöntemlerimiz varsayılan olarak ana bilgisayar sınıfına eklenmez. Ancak, bir modüle included edilen özel bir yöntem tanımlarsak, o modül bir sınıfa dahil edildiğinde Ruby onu çağırır. Orada, Host sınıfını ClassMethods içindeki yöntemlerle manuel olarak genişletebiliriz:
def self.included(host_class) # called when we include the module in `MyApp` host_class.extend ClassMethods # adds our class methods to `MyApp` end Henüz işimiz bitmedi—bir sonraki adımımız, Configurable modülü içeren ana bilgisayar sınıfında desteklenen öznitelikleri belirtmeyi mümkün kılmaktır. Bunun gibi bir çözüm güzel görünecek:
class MyApp #BOLD include Configurable.with(:app_id, :title, :cookie_name) #BOLDEND # ... end Belki biraz şaşırtıcı bir şekilde, yukarıdaki kod sözdizimsel olarak doğrudur— include bir anahtar kelime değil, parametresi olarak bir Module nesnesi bekleyen normal bir yöntemdir. Module döndüren bir ifade ilettiğimiz sürece, onu mutlu bir şekilde içerecektir. Bu nedenle, doğrudan Configurable dahil etmek yerine, belirtilen niteliklerle özelleştirilmiş yeni bir modül oluşturan, üzerinde adı with bir yönteme ihtiyacımız var:
module Configurable #BOLD def self.with(*attrs) #BOLDEND # Define anonymous class with the configuration attributes #BOLD config_class = Class.new do attr_accessor *attrs end #BOLDEND # Define anonymous module for the class methods to be "mixed in" #BOLD class_methods = Module.new do define_method :config do @config ||= config_class.new end #BOLDEND def configure yield config end #BOLD end #BOLDEND # Create and return new module #BOLD Module.new do singleton_class.send :define_method, :included do |host_class| host_class.extend class_methods end end end #BOLDEND end Burada açılacak çok şey var. Configurable modülün tamamı artık yalnızca tek with yöntemden oluşuyor ve her şey bu yöntem içinde gerçekleşiyor. İlk olarak, öznitelik erişimci yöntemlerimizi tutmak için Class.new ile yeni bir anonim sınıf oluşturuyoruz. Class.new , sınıf tanımını bir blok olarak aldığından ve blokların dış değişkenlere erişimi olduğundan, attrs değişkenini attr_accessor sorunsuz bir şekilde geçirebiliyoruz.
def self.with(*attrs) # `attrs` is created here # ... config_class = Class.new do # class definition passed in as a block attr_accessor *attrs # we have access to `attrs` here end Ruby'deki blokların dış değişkenlere erişimi olması, aynı zamanda, içerdikleri gibi bazen closures olarak adlandırılmalarının veya tanımlandıkları dış ortamı "kapatmalarının" nedenidir. ve "yürütüldü" değil. Bu doğru – define_method bloklarımızın nihayetinde ne zaman ve nerede yürütüleceğinden bağımsız olarak, with yönteminin çalışması ve geri dönmesinden sonra bile, config_class ve class_methods değişkenlerine her zaman erişebilecekler. Aşağıdaki örnek, bu davranışı gösterir:
def create_block foo = "hello" # define local variable return Proc.new { foo } # return a new block that returns `foo` end block = create_block # call `create_block` to retrieve the block block.call # even though `create_block` has already returned, => "hello" # the block can still return `foo` to us Artık blokların bu düzgün davranışını bildiğimize göre, devam edip, oluşturulan modülümüz dahil edildiğinde ana bilgisayar sınıfına eklenecek sınıf yöntemleri için class_methods içinde anonim bir modül tanımlayabiliriz. Burada config yöntemini tanımlamak için define_method kullanmamız gerekiyor çünkü metodun içinden dışarıdaki config_class değişkenine erişmemiz gerekiyor. Yöntemi def anahtar sözcüğünü kullanarak tanımlamak bize bu erişimi sağlamaz çünkü def içeren normal yöntem tanımları birer kapanış değildir – ancak, define_method bir blok alır, bu nedenle bu işe yarar:

config_class = # ... # `config_class` is defined here # ... class_methods = Module.new do # define new module using a block define_method :config do # method definition with a block @config ||= config_class.new # even two blocks deep, we can still end # access `config_class` Son olarak, geri döneceğimiz modülü oluşturmak için Module.new . Burada self.included yöntemimizi tanımlamamız gerekiyor, ancak yöntemin dış class_methods değişkenine erişmesi gerektiğinden ne yazık ki bunu def anahtar sözcüğüyle yapamıyoruz. Bu nedenle, yine bir blok ile define_method kullanmamız gerekiyor, ancak bu sefer modülün singleton sınıfında, modül örneğinin kendisinde bir yöntem tanımladığımız için. Oh, ve define_method , singleton sınıfının özel bir yöntemi olduğundan, onu çağırmak için doğrudan çağırmak yerine send kullanmamız gerekir:
class_methods = # ... # ... Module.new do singleton_class.send :define_method, :included do |host_class| host_class.extend class_methods # the block has access to `class_methods` end endPhew, bu zaten oldukça sert bir metaprogramlamaydı. Ancak eklenen karmaşıklık buna değdi mi? Kullanmanın ne kadar kolay olduğuna bir göz atın ve kendiniz karar verin:
class SomeClass include Configurable.with(:foo, :bar) # ... end SomeClass.configure do |config| config.foo = "wat" config.bar = "huh" end SomeClass.config.foo => "wat" Ama daha da iyisini yapabiliriz. Bir sonraki adımda, modülümüzü daha da kullanışlı hale getirmek için configure bloğunun sözdizimini biraz temizleyeceğiz.
Sözdizimini Temizleme
Şu anki uygulamamızda beni hala rahatsız eden son bir şey daha var - konfigürasyon bloğundaki her satırda config tekrarlamamız gerekiyor. Uygun bir DSL, configure bloğundaki her şeyin yapılandırma nesnemiz bağlamında yürütülmesi gerektiğini bilir ve aynı şeyi yalnızca bununla elde etmemizi sağlar:
MyApp.configure do app_id "my_app" title "My App" cookie_name "my_app_session" end Onu uygulayalım, olur mu? Görünüşe göre, iki şeye ihtiyacımız olacak. İlk olarak, blok içindeki yöntem çağrılarının o nesneye gitmesi için yapılandırma nesnesi bağlamında configure iletilen bloğu yürütmenin bir yoluna ihtiyacımız var. İkincisi, erişimci yöntemlerini, onlara bir argüman sağlanmışsa değeri yazacak ve argüman olmadan çağrıldığında geri okuyacak şekilde değiştirmeliyiz. Olası bir uygulama şöyle görünür:
module Configurable def self.with(*attrs) #BOLD not_provided = Object.new #BOLDEND config_class = Class.new do #BOLD attrs.each do |attr| define_method attr do |value = not_provided| if value === not_provided instance_variable_get("@#{attr}") else instance_variable_set("@#{attr}", value) end end end attr_writer *attrs #BOLDEND end class_methods = Module.new do # ... def configure(&block) #BOLD config.instance_eval(&block) #BOLDEND end end # Create and return new module # ... end end Buradaki daha basit değişiklik, yapılandırma nesnesi bağlamında configure bloğunu çalıştırmaktır. Bir nesne üzerinde Ruby'nin instance_eval yöntemini çağırmak, rastgele bir kod bloğunu o nesne içinde çalışıyormuş gibi çalıştırmanıza izin verir; bu, yapılandırma bloğu ilk satırda app_id yöntemini çağırdığında, bu çağrının bizim yapılandırma sınıfı örneğimize gideceği anlamına gelir.
config_class içindeki öznitelik erişimci yöntemlerindeki değişiklik biraz daha karmaşıktır. Bunu anlamak için önce attr_accessor perde arkasında tam olarak ne yaptığını anlamamız gerekiyor. Örneğin aşağıdaki attr_accessor çağrısını yapın:
class SomeClass attr_accessor :foo, :bar endBu, belirtilen her öznitelik için bir okuyucu ve yazar yöntemi tanımlamaya eşdeğerdir:
class SomeClass def foo @foo end def foo=(value) @foo = value end # and the same with `bar` end Orijinal koda attr_accessor *attrs yazdığımızda, Ruby attrs her öznitelik için öznitelik okuyucu ve yazar yöntemlerini bizim için tanımladı - yani, şu standart erişimci yöntemlerini aldık: app_id , app_id= , title , title= ve benzeri üzerinde. Yeni sürümümüzde, bunun gibi atamaların hala düzgün çalışması için standart yazma yöntemlerini korumak istiyoruz:
MyApp.config.app_ => "not_my_app" attr_writer *attrs çağırarak yazar yöntemlerini otomatik olarak oluşturmaya devam edebiliriz. Ancak, bu yeni sözdizimini desteklemek için özniteliği yazabilmeleri gerektiğinden standart okuyucu yöntemlerini artık kullanamayız:
MyApp.configure do app_id "my_app" # assigns a new value app_id # reads the stored value end Okuyucu yöntemlerini kendimiz oluşturmak için, attrs dizisi üzerinde dolaşıyoruz ve yeni bir değer sağlanmadıysa eşleşen örnek değişkeninin geçerli değerini döndüren ve belirtilmişse yeni değeri yazan her öznitelik için bir yöntem tanımlıyoruz:
not_provided = Object.new # ... attrs.each do |attr| define_method attr do |value = not_provided| if value === not_provided instance_variable_get("@#{attr}") else instance_variable_set("@#{attr}", value) end end end Burada, rastgele bir ada sahip bir örnek değişkeni okumak için Ruby'nin instance_variable_get yöntemini ve buna yeni bir değer atamak için instance_variable_set yöntemini kullanıyoruz. Ne yazık ki, her iki durumda da değişken adının önüne bir "@" işareti konulmalıdır - bu nedenle dize enterpolasyonu.
Neden “sağlanmadı” için varsayılan değer olarak boş bir nesne kullanmamız gerektiğini ve neden bu amaç için nil basitçe kullanamadığımızı merak ediyor olabilirsiniz. Nedeni basit; nil , birinin bir yapılandırma özniteliği için ayarlamak isteyebileceği geçerli bir değerdir. nil için test yapsaydık, bu iki senaryoyu birbirinden ayıramazdık:
MyApp.configure do app_id nil # expectation: assigns nil app_id # expectation: returns current value end not_provided içinde depolanan bu boş nesne yalnızca kendisine eşit olacaktır, bu şekilde kimsenin onu bizim yöntemimize geçirmeyeceğinden ve yazma yerine istenmeyen bir okumaya neden olmayacağından emin olabiliriz.
Referanslar için Destek Ekleme
Modülümüzü daha da çok yönlü hale getirmek için ekleyebileceğimiz bir özellik daha var: bir konfigürasyon özniteliğine başka bir öznitelikten referans verme yeteneği:
MyApp.configure do app_id "my_app" title "My App" cookie_name { "#{app_id}_session" } End MyApp.config.cookie_name => "my_app_session" Burada app_id niteliğine cookie_name bir referans ekledik. Referansı içeren ifadenin bir blok olarak iletildiğine dikkat edin; bu, nitelik değerinin gecikmeli değerlendirmesini desteklemek için gereklidir. Buradaki fikir, bloğu daha sonra öznitelik tanımlandığında değil, yalnızca okunduğunda değerlendirmektir - aksi takdirde öznitelikleri "yanlış" sırada tanımlarsak komik şeyler olur:
SomeClass.configure do foo "#{bar}_baz" # expression evaluated here bar "hello" end SomeClass.config.foo => "_baz" # not actually funnyİfade bir bloğa sarılırsa, bu hemen değerlendirilmesini engeller. Bunun yerine, öznitelik değeri alındığında daha sonra yürütülecek bloğu kaydedebiliriz:
SomeClass.configure do foo { "#{bar}_baz" } # stores block, does not evaluate it yet bar "hello" end SomeClass.config.foo # `foo` evaluated here => "hello_baz" # correct! Blokları kullanarak gecikmeli değerlendirme desteği eklemek için Configurable modülde büyük değişiklikler yapmak zorunda değiliz. Aslında, yalnızca öznitelik yöntemi tanımını değiştirmemiz gerekir:
define_method attr do |value = not_provided, &block| if value === not_provided && block.nil? result = instance_variable_get("@#{attr}") result.is_a?(Proc) ? instance_eval(&result) : result else instance_variable_set("@#{attr}", block || value) end end Bir nitelik ayarlarken, block || value block || value ifadesi, eğer bir tane geçirilmişse bloğu kaydeder, aksi takdirde değeri kaydeder. Daha sonra öznitelik daha sonra okunduğunda, bir blok olup olmadığını kontrol edip, varsa instance_eval kullanarak değerlendiririz, yoksa blok değilse daha önce yaptığımız gibi geri döndürürüz.
Destekleyici referanslar elbette kendi uyarıları ve son durumları ile birlikte gelir. Örneğin, bu konfigürasyondaki özniteliklerden herhangi birini okursanız muhtemelen ne olduğunu anlayabilirsiniz:
SomeClass.configure do foo { bar } bar { foo } endBiten Modül
Sonunda, kendimize, rastgele bir sınıfı yapılandırılabilir hale getirmek ve ardından bu yapılandırma değerlerini, bir yapılandırma özniteliğine diğerinden referans almamıza izin veren temiz ve basit bir DSL kullanarak belirtmek için oldukça düzgün bir modüle sahibiz:
class MyApp include Configurable.with(:app_id, :title, :cookie_name) # ... end SomeClass.configure do app_id "my_app" title "My App" cookie_name { "#{app_id}_session" } endToplam 36 satırlık kod içeren DSL'imizi uygulayan modülün son sürümü aşağıdadır:
module Configurable def self.with(*attrs) not_provided = Object.new config_class = Class.new do attrs.each do |attr| define_method attr do |value = not_provided, &block| if value === not_provided && block.nil? result = instance_variable_get("@#{attr}") result.is_a?(Proc) ? instance_eval(&result) : result else instance_variable_set("@#{attr}", block || value) end end end attr_writer *attrs end class_methods = Module.new do define_method :config do @config ||= config_class.new end def configure(&block) config.instance_eval(&block) end end Module.new do singleton_class.send :define_method, :included do |host_class| host_class.extend class_methods end end end endTüm bu Ruby büyüsüne neredeyse okunamayan ve bu nedenle bakımı çok zor olan bir kod parçasına bakarak, alana özel dilimizi biraz daha güzel hale getirmek için tüm bu çabaya değip değmeyeceğini merak edebilirsiniz. Kısa cevap, duruma bağlı olmasıdır - bu da bizi bu makalenin son konusuna getirir.
Ruby DSL'ler—Ne Zaman Kullanılmalı ve Ne Zaman Kullanılmamalı
DSL'imizin uygulama adımlarını okurken, dilin dışa dönük sözdizimini daha temiz ve kullanımı daha kolay hale getirdiğimizden, bunu gerçekleştirmek için kaputun altında giderek artan sayıda metaprogramlama hilesi kullanmak zorunda kaldığımızı fark etmişsinizdir. Bu, gelecekte anlaşılması ve değiştirilmesi inanılmaz derecede zor olacak bir uygulama ile sonuçlandı. Yazılım geliştirmedeki diğer pek çok şey gibi, bu da dikkatle incelenmesi gereken bir ödünleşimdir.
Etki alanına özgü bir dilin uygulama ve bakım maliyetine değmesi için, masaya daha da fazla fayda sağlaması gerekir. Bu genellikle, dili mümkün olduğu kadar çok farklı senaryoda yeniden kullanılabilir hale getirerek ve böylece birçok farklı kullanım durumu arasındaki toplam maliyeti amorti ederek elde edilir. Çerçevelerin ve kitaplıkların, her biri bu gömülü dillerin üretkenlik avantajlarından yararlanabilen çok sayıda geliştirici tarafından kullanıldığından, tam olarak kendi DSL'lerini içerme olasılığı daha yüksektir.
Bu nedenle, genel bir ilke olarak, yalnızca siz, diğer geliştiriciler veya uygulamanızın son kullanıcıları bunlardan çok faydalanacaksa DSL'ler oluşturun. Bir DSL oluşturursanız, onunla kapsamlı bir test paketi eklediğinizden ve sözdizimini doğru bir şekilde belgelediğinizden emin olun, çünkü yalnızca uygulamadan anlaşılması çok zor olabilir. Gelecekte siz ve geliştirici arkadaşlarınız bunun için size teşekkür edecek.
Toptal Mühendislik Blogunda Daha Fazla Okuma:
- Sıfırdan Tercüman Yazmaya Nasıl Yaklaşılır?
