إنشاء Ruby DSL: دليل إلى Metaprogramming المتقدمة

نشرت: 2022-03-11

تعد اللغات المحددة للمجال (DSL) أداة قوية بشكل لا يصدق لتسهيل برمجة أو تكوين أنظمة معقدة. هم أيضًا موجودون في كل مكان - بصفتك مهندس برمجيات ، فمن المرجح أن تستخدم العديد من DSLs المختلفة على أساس يومي.

في هذه المقالة ، ستتعرف على اللغات المحددة للمجال ، ومتى يجب استخدامها ، وأخيرًا كيف يمكنك إنشاء DSL الخاص بك في Ruby باستخدام تقنيات metaprogramming المتقدمة.

تستند هذه المقالة إلى مقدمة نيكولا تودوروفيتش لبرمجة روبي metaprogramming ، والتي نُشرت أيضًا على مدونة Toptal. لذا إذا كنت جديدًا في مجال البرمجة الوصفية ، فتأكد من قراءتها أولاً.

ما هي لغة مجال معين؟

التعريف العام لـ DSLs هو أنها لغات متخصصة في مجال تطبيق معين أو حالة استخدام. هذا يعني أنه لا يمكنك استخدامها إلا لأشياء محددة - فهي ليست مناسبة لتطوير البرامج ذات الأغراض العامة. إذا كان هذا يبدو واسعًا ، فهذا لأنه كذلك — تأتي DSL بأشكال وأحجام مختلفة. فيما يلي بعض الفئات المهمة:

  • تم تصميم لغات الترميز مثل HTML و CSS لوصف أشياء محددة مثل البنية والمحتوى وأنماط صفحات الويب. لا يمكن كتابة خوارزميات عشوائية معهم ، لذا فهي تناسب وصف DSL.
  • تقع لغات الماكرو والاستعلام (على سبيل المثال ، SQL) فوق نظام معين أو لغة برمجة أخرى وعادة ما تكون محدودة فيما يمكن القيام به. لذلك من الواضح أنها مؤهلة كلغات مجال معين.
  • لا تمتلك العديد من DSLs بناء الجملة الخاص بها - وبدلاً من ذلك ، فإنها تستخدم بناء جملة لغة برمجة معترف بها بطريقة ذكية تشبه استخدام لغة مصغرة منفصلة.

تسمى هذه الفئة الأخيرة DSL داخليًا ، وهي واحدة من هذه الفئة التي سننشئها كمثال قريبًا جدًا. ولكن قبل أن ندخل في ذلك ، دعنا نلقي نظرة على بعض الأمثلة المعروفة من DSLs الداخلية. تعد صيغة تعريف المسار في ريلز واحدة منها:

 Rails.application.routes.draw do root to: "pages#main" resources :posts do get :preview resources :comments, only: [:new, :create, :destroy] end end

هذا هو كود Ruby ، ​​لكنه يبدو وكأنه لغة تعريف مسار مخصصة ، وذلك بفضل تقنيات metaprogramming المختلفة التي تجعل مثل هذه الواجهة النظيفة وسهلة الاستخدام ممكنة. لاحظ أن بنية DSL يتم تنفيذها باستخدام كتل Ruby ، ​​ويتم استخدام استدعاءات الطرق مثل get resources لتحديد الكلمات الأساسية لهذه اللغة المصغرة.

يتم استخدام Metaprogramming بكثرة في مكتبة اختبار RSpec:

 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 end

يحتوي هذا الجزء من الكود أيضًا على أمثلة للواجهات بطلاقة ، والتي تسمح بقراءة الإعلانات بصوت عالٍ على أنها جمل إنجليزية بسيطة ، مما يجعل فهم ما يفعله الكود أسهل كثيرًا:

 # 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_success

مثال آخر للواجهة الطلاقة هو واجهة الاستعلام الخاصة بـ ActiveRecord و Arel ، والتي تستخدم شجرة بناء جملة مجردة داخليًا لبناء استعلامات SQL المعقدة:

 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 جنبًا إلى جنب مع إمكانيات البرمجة الوصفية تجعلها مناسبة بشكل فريد لبناء لغات خاصة بالمجال ، فإن DSLs موجودة في لغات أخرى أيضًا. فيما يلي مثال على اختبار JavaScript باستخدام إطار عمل Jasmine:

 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!"); }); }); });

ربما لا يكون بناء الجملة هذا نظيفًا مثل أمثلة روبي ، لكنه يوضح أنه من خلال التسمية الذكية والاستخدام الإبداعي لبناء الجملة ، يمكن إنشاء DSLs الداخلية باستخدام أي لغة تقريبًا.

تكمن فائدة DSLs الداخلية في أنها لا تتطلب محللًا منفصلاً ، والذي قد يكون من الصعب تنفيذه بشكل صحيح. ولأنهم يستخدمون بناء جملة اللغة التي يتم تنفيذها بها ، فإنهم يتكاملون أيضًا بسلاسة مع بقية قاعدة التعليمات البرمجية.

ما يجب أن نتخلى عنه في المقابل هو الحرية النحوية - يجب أن تكون DSLs الداخلية صالحة من الناحية التركيبية في لغة التنفيذ الخاصة بها. يعتمد المقدار الذي يتعين عليك التنازل عنه في هذا الصدد إلى حد كبير على اللغة المحددة ، مع وجود لغات مكتوبة بشكل مطول وثابت مثل Java و VB.NET في أحد طرفي النطاق ، واللغات الديناميكية ذات إمكانات البرمجة الوصفية الشاملة مثل Ruby من جهة أخرى نهاية.

بناء منطقتنا — Ruby DSL لتكوين الفئة

مثال DSL الذي سنقوم ببنائه في Ruby هو محرك تكوين قابل لإعادة الاستخدام لتحديد سمات التكوين لفئة Ruby باستخدام بنية بسيطة للغاية. تعد إضافة إمكانات التكوين إلى فئة من المتطلبات الشائعة جدًا في عالم Ruby ، ​​خاصةً عندما يتعلق الأمر بتكوين الأحجار الكريمة الخارجية وعملاء API. الحل المعتاد هو واجهة مثل هذه:

 MyApp.configure do |config| config.app_ config.title = "My App" config.cookie_name = "my_app_session" end

دعنا ننفذ هذه الواجهة أولاً - وبعد ذلك ، باستخدامها كنقطة بداية ، يمكننا تحسينها خطوة بخطوة عن طريق إضافة المزيد من الميزات ، وتنظيف البنية ، وجعل عملنا قابلاً لإعادة الاستخدام.

ما الذي نحتاجه لجعل هذه الواجهة تعمل؟ يجب أن تحتوي فئة MyApp على طريقة configure فئة تأخذ كتلة ثم تنفذ تلك الكتلة من خلال التنازل عنها ، وتمرير كائن التكوين الذي يحتوي على طرق وصول لقراءة قيم التكوين وكتابتها:

 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 end

بمجرد تشغيل كتلة التكوين ، يمكننا الوصول بسهولة إلى القيم وتعديلها:

 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"

حتى الآن ، لا يبدو هذا التطبيق وكأنه لغة مخصصة بما يكفي ليتم اعتباره DSL. لكن دعونا نأخذ الأمور خطوة واحدة في كل مرة. بعد ذلك ، سنقوم بفصل وظيفة التكوين عن فئة MyApp وجعلها عامة بما يكفي لتكون قابلة للاستخدام في العديد من حالات الاستخدام المختلفة.

جعلها قابلة لإعادة الاستخدام

في الوقت الحالي ، إذا أردنا إضافة إمكانات تكوين مماثلة إلى فئة مختلفة ، فسيتعين علينا نسخ كل من فئة Configuration وطرق الإعداد ذات الصلة إلى تلك الفئة الأخرى ، وكذلك تحرير قائمة attr_accessor لتغيير سمات التكوين المقبولة. لتجنب الاضطرار إلى القيام بذلك ، دعنا ننقل ميزات التكوين إلى وحدة منفصلة تسمى Configurable . مع ذلك ، سيبدو فصل MyApp بنا كما يلي:

 class MyApp #BOLD include Configurable #BOLDEND # ... end

تم نقل كل ما يتعلق بالتكوين إلى الوحدة القابلة Configurable :

 #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

لم يتغير الكثير هنا ، باستثناء الطريقة self.included الجديدة. نحتاج إلى هذه الطريقة لأن تضمين وحدة نمطية يخلط فقط في طرق المثيل الخاصة بها ، لذلك لن تتم إضافة طرق config configure الخاصة بنا إلى فئة المضيف افتراضيًا. ومع ذلك ، إذا حددنا طريقة خاصة تسمى included في وحدة نمطية ، فإن روبي سوف تسميها كلما تم تضمين هذه الوحدة في الفصل. هناك يمكننا تمديد فئة المضيف يدويًا بالطرق الموجودة في ClassMethods :

 def self.included(host_class) # called when we include the module in `MyApp` host_class.extend ClassMethods # adds our class methods to `MyApp` end

لم ننتهي بعد - خطوتنا التالية هي أن نجعل من الممكن تحديد السمات المدعومة في فئة المضيف التي تتضمن الوحدة القابلة Configurable . قد يبدو حلًا كهذا رائعًا:

 class MyApp #BOLD include Configurable.with(:app_id, :title, :cookie_name) #BOLDEND # ... end

ربما يكون من المدهش إلى حد ما أن يكون الرمز أعلاه صحيحًا من الناحية التركيبية - include ليس كلمة رئيسية ولكنه مجرد طريقة عادية تتوقع كائن Module كمعامل لها. طالما قمنا بتمريره تعبيرا يقوم بإرجاع Module ، فسيكون من دواعي سرورنا تضمينه. لذلك ، بدلاً من تضمين قابل Configurable مباشرةً ، نحتاج إلى طريقة بالاسم with والتي تنشئ وحدة نمطية جديدة يتم تخصيصها بالسمات المحددة:

 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

هناك الكثير لتفريغه هنا. تتكون الوحدة Configurable بأكملها الآن من طريقة واحدة فقط ، with حدوث كل شيء في هذه الطريقة. أولاً ، نقوم بإنشاء فئة مجهولة جديدة باستخدام Class.new لعقد طرق الوصول إلى السمة الخاصة بنا. نظرًا لأن Class.new يأخذ تعريف الفئة ككتلة ولديه وصول للكتل إلى المتغيرات الخارجية ، فإننا قادرون على تمرير متغير attrs إلى attr_accessor دون مشاكل.

 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 لها حق الوصول إلى المتغيرات الخارجية هي أيضًا سبب تسميتها أحيانًا بالإغلاق ، لأنها تتضمن أو "تقترب" من البيئة الخارجية التي تم تحديدها فيها. لاحظ أنني استخدمت العبارة "محددة في" وليس "أعدم في". هذا صحيح - بغض النظر عن متى وأين سيتم تنفيذ كتل define_method في النهاية ، ستكون دائمًا قادرة على الوصول إلى المتغيرات config_class و class_methods ، حتى بعد انتهاء تشغيل التابع with وإعادته. يوضح المثال التالي هذا السلوك:

 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

الآن بعد أن علمنا بهذا السلوك الأنيق للكتل ، يمكننا المضي قدمًا وتحديد وحدة نمطية مجهولة في class_methods لطرق الفصل التي ستتم إضافتها إلى فئة المضيف عند تضمين الوحدة النمطية التي تم إنشاؤها. هنا يتعين علينا استخدام طريقة تعريف لتعريف طريقة config ، لأننا نحتاج إلى الوصول إلى متغير define_method config_class من داخل الطريقة. تحديد الطريقة باستخدام الكلمة الأساسية def لن يمنحنا هذا الوصول لأن تعريفات الطريقة العادية مع تعريف ليست عمليات إغلاق - ومع ذلك ، فإن define_method def كتلة ، لذلك سيعمل هذا:

 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`

أخيرًا ، نسمي Module.new لإنشاء الوحدة التي سنعيدها. نحتاج هنا إلى تحديد أسلوبنا self.included ، لكن لسوء الحظ لا يمكننا فعل ذلك باستخدام الكلمة الأساسية def ، حيث تحتاج الطريقة إلى الوصول إلى المتغير class_methods الخارجي. لذلك، يتعين علينا استخدام define_method مع كتلة مرة أخرى، ولكن هذه المرة على فئة Singleton من الوحدة النمطية، كما نحدد طريقة على مثيل الوحدة النمطية نفسها. أوه ، ونظرًا لأن define_method هي طريقة خاصة للفئة المنفردة ، علينا استخدام send لاستدعاءها بدلًا من استدعائها مباشرة:

 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 end

Phew ، لقد كان هذا بالفعل ميتابروغرام متشدد جدا. لكن هل كان التعقيد الإضافي يستحق كل هذا العناء؟ ألقِ نظرة على مدى سهولة استخدامه وقرر بنفسك:

 class SomeClass include Configurable.with(:foo, :bar) # ... end SomeClass.configure do |config| config.foo = "wat" config.bar = "huh" end SomeClass.config.foo => "wat"

ولكن يمكننا أن نفعل ما هو أفضل. في الخطوة التالية ، سنقوم بتنظيف بنية كتلة configure قليلاً لجعل الوحدة الخاصة بنا أكثر ملاءمة للاستخدام.

تنظيف النحو

هناك شيء أخير لا يزال يزعجني بتطبيقنا الحالي - علينا تكرار config في كل سطر في كتلة التكوين. يعرف DSL المناسب أن كل شيء داخل كتلة configure يجب أن يتم تنفيذه في سياق كائن التكوين الخاص بنا ويمكّننا من تحقيق نفس الشيء من خلال هذا فقط:

 MyApp.configure do app_id "my_app" title "My App" cookie_name "my_app_session" end

دعونا ننفذها ، أليس كذلك؟ من مظهره ، سنحتاج إلى شيئين. أولاً ، نحتاج إلى طريقة لتنفيذ الكتلة التي تم تمريرها configure في سياق كائن التكوين بحيث تنتقل استدعاءات الطريقة داخل الكتلة إلى هذا الكائن. ثانيًا ، علينا تغيير توابع الموصّل بحيث يكتبون القيمة إذا تم توفير وسيطة لهم ويقرؤونها مرة أخرى عند استدعائهم بدون وسيطة. يبدو التنفيذ المحتمل كما يلي:

 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

التغيير الأبسط هنا هو تشغيل كتلة configure في سياق كائن التكوين. يتيح لك استدعاء طريقة instance_eval الخاصة بـ Ruby على كائن ما تنفيذ كتلة عشوائية من التعليمات البرمجية كما لو كانت تعمل داخل هذا الكائن ، مما يعني أنه عندما تستدعي كتلة التكوين طريقة app_id في السطر الأول ، فإن هذا الاستدعاء سينتقل إلى مثيل فئة التكوين لدينا.

يعد التغيير في طرق الوصول إلى السمة في config_class أكثر تعقيدًا بعض الشيء. لفهم ذلك ، نحتاج أولاً إلى فهم ما كان يفعله attr_accessor بالضبط خلف الكواليس. خذ استدعاء attr_accessor التالي على سبيل المثال:

 class SomeClass attr_accessor :foo, :bar end

هذا يعادل تحديد أسلوب القارئ والكاتب لكل سمة محددة:

 class SomeClass def foo @foo end def foo=(value) @foo = value end # and the same with `bar` end

لذلك عندما كتبنا attr_accessor *attrs في الكود الأصلي ، حدد روبي أساليب قارئ السمات والكاتب لنا لكل سمة في attrs - أي أننا حصلنا على طرق الوصول القياسية التالية: app_id ، app_id= ، title ، title= وهكذا على. في نسختنا الجديدة ، نريد الاحتفاظ بأساليب الكاتب القياسية حتى تظل مهام مثل هذه تعمل بشكل صحيح:

 MyApp.config.app_ => "not_my_app"

يمكننا الاستمرار في إنشاء طرق الكاتب تلقائيًا عن طريق استدعاء attr_writer *attrs . ومع ذلك ، لم يعد بإمكاننا استخدام طرق القارئ القياسية ، حيث يتعين عليهم أيضًا أن يكونوا قادرين على كتابة السمة لدعم بناء الجملة الجديد:

 MyApp.configure do app_id "my_app" # assigns a new value app_id # reads the stored value end

لإنشاء طرق القارئ بأنفسنا ، نقوم بعمل حلقة فوق مصفوفة attrs وتحديد طريقة لكل سمة تُرجع القيمة الحالية لمتغير المثيل المطابق إذا لم يتم توفير قيمة جديدة وكتابة القيمة الجديدة إذا تم تحديدها:

 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

هنا نستخدم طريقة instance_variable_get من Ruby لقراءة متغير حالة باسم عشوائي ، و instance_variable_set لتعيين قيمة جديدة له. لسوء الحظ ، يجب أن يكون اسم المتغير مسبوقًا بعلامة "@" في كلتا الحالتين - ومن هنا جاء استيفاء السلسلة.

قد تتساءل لماذا يتعين علينا استخدام كائن فارغ كقيمة افتراضية لـ "غير متوفر" ولماذا لا يمكننا ببساطة استخدام " nil " لهذا الغرض. السبب بسيط - nil توجد قيمة صالحة قد يرغب شخص ما في تعيينها لسمة التكوين. إذا اختبرنا عدم وجود nil ، فلن نتمكن من التمييز بين هذين السيناريوهين:

 MyApp.configure do app_id nil # expectation: assigns nil app_id # expectation: returns current value end

هذا الكائن الفارغ المخزن في not_provided سيكون مساويًا لنفسه فقط ، وبهذه الطريقة يمكننا التأكد من أنه لن يمرره أحد إلى طريقتنا ويسبب قراءة غير مقصودة بدلاً من الكتابة.

إضافة دعم المراجع

هناك ميزة أخرى يمكننا إضافتها لجعل وحدتنا أكثر تنوعًا - القدرة على الرجوع إلى سمة تكوين من سمة أخرى:

 MyApp.configure do app_id "my_app" title "My App" cookie_name { "#{app_id}_session" } End MyApp.config.cookie_name => "my_app_session"

أضفنا هنا مرجعًا من cookie_name إلى سمة app_id . لاحظ أن التعبير الذي يحتوي على المرجع يتم تمريره ككتلة - وهذا ضروري لدعم التقييم المتأخر لقيمة السمة. تكمن الفكرة في تقييم الكتلة لاحقًا فقط عند قراءة السمة وليس عند تعريفها - وإلا فقد تحدث أشياء مضحكة إذا حددنا السمات بترتيب "خاطئ":

 SomeClass.configure do foo "#{bar}_baz" # expression evaluated here bar "hello" end SomeClass.config.foo => "_baz" # not actually funny

إذا تم تغليف التعبير في كتلة ، فسيؤدي ذلك إلى منع تقييمه على الفور. بدلاً من ذلك ، يمكننا حفظ الكتلة ليتم تنفيذها لاحقًا عند استرداد قيمة السمة:

 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!

لا يتعين علينا إجراء تغييرات كبيرة على الوحدة Configurable لإضافة دعم للتقييم المتأخر باستخدام الكتل. في الواقع ، علينا فقط تغيير تعريف طريقة السمة:

 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

عند تعيين سمة ، فإن block || value يحفظ تعبير block || value الكتلة إذا تم تمرير أحدها ، أو يحفظ القيمة بطريقة أخرى. بعد ذلك ، عندما تتم قراءة السمة لاحقًا ، نتحقق مما إذا كانت كتلة ونقيمها باستخدام instance_eval إذا كانت كذلك ، أو إذا لم تكن كتلة ، فإننا نعيدها كما فعلنا من قبل.

تأتي المراجع الداعمة مع تحذيراتها وحالاتها الخاصة بالطبع. على سبيل المثال ، يمكنك على الأرجح اكتشاف ما يحدث إذا قرأت أيًا من السمات في هذا التكوين:

 SomeClass.configure do foo { bar } bar { foo } end

الوحدة المنتهية

في النهاية ، حصلنا على وحدة نمطية أنيقة جدًا لجعل فئة عشوائية قابلة للتكوين ثم تحديد قيم التكوين هذه باستخدام DSL نظيف وبسيط يتيح لنا أيضًا الرجوع إلى سمة تكوين واحدة من أخرى:

 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" } end

هذا هو الإصدار الأخير من الوحدة النمطية التي تطبق DSL - بإجمالي 36 سطرًا من التعليمات البرمجية:

 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 end

بالنظر إلى كل سحر روبي في جزء من التعليمات البرمجية غير المقروء تقريبًا وبالتالي يصعب الحفاظ عليه ، قد تتساءل عما إذا كان كل هذا الجهد يستحق كل هذا العناء فقط لجعل لغة مجالنا المحددة أجمل قليلاً. الإجابة المختصرة هي أن الأمر يعتمد - وهو ما يقودنا إلى الموضوع الأخير من هذه المقالة.

Ruby DSLs - متى يجب استخدامها ومتى لا تستخدمها

ربما لاحظت أثناء قراءة خطوات تنفيذ DSL أنه ، نظرًا لأننا جعلنا بناء الجملة الخارجية للغة أكثر نظافة وأسهل في الاستخدام ، كان علينا استخدام عدد متزايد من حيل البرمجة الوصفية تحت الغطاء لتحقيق ذلك. نتج عن ذلك تطبيق سيكون من الصعب للغاية فهمه وتعديله في المستقبل. مثل العديد من الأشياء الأخرى في تطوير البرمجيات ، فهذه أيضًا مقايضة يجب فحصها بعناية.

لكي تكون لغة معينة في المجال تستحق تكاليف التنفيذ والصيانة الخاصة بها ، يجب أن تحقق قدرًا أكبر من الفوائد على الجدول. يتم تحقيق ذلك عادةً بجعل اللغة قابلة لإعادة الاستخدام في أكبر عدد ممكن من السيناريوهات المختلفة ، وبالتالي استهلاك التكلفة الإجمالية بين العديد من حالات الاستخدام المختلفة. من المرجح أن تحتوي الأطر والمكتبات على DSLs الخاصة بهم بالضبط لأن الكثير من المطورين يستخدمونها ، ويمكن لكل منهم الاستمتاع بفوائد الإنتاجية لتلك اللغات المضمنة.

لذلك ، كمبدأ عام ، لا تبني سوى DSLs إذا كنت أنت أو المطورين الآخرين أو المستخدمين النهائيين لتطبيقك سوف يستفيدون منها كثيرًا. إذا قمت بإنشاء DSL ، فتأكد من تضمين مجموعة اختبار شاملة معه ، بالإضافة إلى توثيق تركيبه بشكل صحيح حيث قد يكون من الصعب جدًا اكتشافه من التنفيذ وحده. المستقبل سوف يشكرك أنت وزملاؤك المطورين على ذلك.


مزيد من القراءة على مدونة Toptal Engineering:

  • كيفية الاقتراب من كتابة المترجم الفوري من الصفر