تعتبر برمجة الميتابرومجة في روبي أكثر برودة مما تبدو عليه
نشرت: 2022-03-11غالبًا ما تسمع أن البرمجة الوصفية هي شيء يستخدمه روبي النينجا فقط ، وأنه ببساطة ليس للبشر العاديين. لكن الحقيقة هي أن ما وراء البرمجة ليس شيئًا مخيفًا على الإطلاق. ستعمل مشاركة المدونة هذه على تحدي هذا النوع من التفكير وتقريب البرمجة الوصفية من مطور Ruby العادي حتى يتمكنوا أيضًا من جني فوائدها.
وتجدر الإشارة إلى أن البرمجة الوصفية قد تعني الكثير ويمكن إساءة استخدامها في كثير من الأحيان وتذهب إلى أقصى الحدود عندما يتعلق الأمر بالاستخدام ، لذا سأحاول إلقاء بعض الأمثلة من العالم الحقيقي التي يمكن للجميع استخدامها في البرمجة اليومية.
ميتابروغرام
Metaprogramming هي تقنية يمكنك من خلالها كتابة التعليمات البرمجية التي تكتب التعليمات البرمجية من تلقاء نفسها ديناميكيًا في وقت التشغيل. هذا يعني أنه يمكنك تحديد الأساليب والفئات أثناء وقت التشغيل. مجنون ، أليس كذلك؟ باختصار ، باستخدام البرمجة الوصفية ، يمكنك إعادة فتح الفئات وتعديلها ، وطرق الالتقاط غير الموجودة وإنشائها بسرعة ، وإنشاء كود جاف عن طريق تجنب التكرار ، والمزيد.
أساسيات
قبل أن نتعمق في البرمجة الميتابولوجية الجادة ، يجب علينا استكشاف الأساسيات. وأفضل طريقة للقيام بذلك هي بالقدوة. لنبدأ بواحد ونفهم برمجة روبي خطوة بخطوة. ربما يمكنك تخمين ما يفعله هذا الرمز:
class Developer def self.backend "I am backend developer" end def frontend "I am frontend developer" end end
لقد حددنا فئة بطريقتين. الطريقة الأولى في هذه الفئة هي طريقة الفئة والثانية هي طريقة المثيل. هذه هي الأشياء الأساسية في Ruby ، ولكن هناك الكثير مما يحدث وراء هذا الرمز الذي نحتاج إلى فهمه قبل المضي قدمًا. وتجدر الإشارة إلى أن فئة Developer
نفسها هي في الواقع كائن. في Ruby كل شيء هو كائن ، بما في ذلك الفئات. نظرًا لأن Developer
هو مثيل ، فهو مثيل لفئة Class
. إليك كيف يبدو نموذج كائن Ruby:
p Developer.class # Class p Class.superclass # Module p Module.superclass # Object p Object.superclass # BasicObject
شيء واحد مهم يجب فهمه هنا هو معنى self
. طريقة frontend
هي طريقة عادية متوفرة في مثيلات فئة Developer
، ولكن لماذا تعتبر طريقة backend
طريقة فئة؟ يتم تنفيذ كل جزء من التعليمات البرمجية المنفذة في Ruby ضد ذات معينة. عندما ينفذ مترجم روبي أي رمز ، فإنه يتتبع دائمًا القيمة self
لأي سطر معين. يشير self
دائمًا إلى كائن ما ولكن يمكن أن يتغير هذا الكائن بناءً على الكود الذي تم تنفيذه. على سبيل المثال ، داخل تعريف فئة ، تشير self
إلى الفئة نفسها التي تعد مثيلًا Class
.
class Developer p self end # Developer
عمليات المثال الداخلية ، تشير self
إلى مثيل من الفئة.
class Developer def frontend self end end p Developer.new.frontend # #<Developer:0x2c8a148>
داخل عمليات الصنف ، تشير self
إلى الصنف نفسه بطريقة ما (والتي ستتم مناقشتها بمزيد من التفصيل لاحقًا في هذه المقالة):
class Developer def self.backend self end end p Developer.backend # Developer
هذا جيد ، لكن ما هي طريقة الفصل بعد كل شيء؟ قبل الإجابة على هذا السؤال ، نحتاج أن نذكر وجود شيء يسمى metaclass ، يُعرف أيضًا باسم single class and eigenclass. frontend
لطريقة الفئة التي حددناها سابقًا ليست سوى طريقة مثيل تم تحديدها في metaclass Developer
الكائن! الفوقية هي في الأساس فئة أنشأها روبي وإدراجها في التسلسل الهرمي للميراث للاحتفاظ بوسائل الصنف ، وبالتالي لا تتداخل مع الحالات التي تم إنشاؤها من الفصل.
الفوقية
كل كائن في روبي له طبقة خاصة به. إنه غير مرئي إلى حد ما للمطور ، لكنه موجود ويمكنك استخدامه بسهولة شديدة. نظرًا لأن Developer
الفصل الخاص بنا هو كائن في الأساس ، فإنه يحتوي على فئة metaclass الخاصة به. كمثال ، دعونا ننشئ كائنًا من فئة String
ونتعامل مع metaclass الخاص به:
example = "I'm a string object" def example.something self.upcase end p example.something # I'M A STRING OBJECT
ما فعلناه هنا هو أننا أضفنا طريقة مفردة something
إلى كائن. يتمثل الاختلاف بين طرق الصنف وطرق الفردي في أن طرق الصنف متاحة لجميع مثيلات كائن الفئة بينما لا تتوفر طرق الفردي إلا لهذا المثال الفردي. تُستخدم طرق الفصل على نطاق واسع بينما لا يتم استخدام الأساليب الفردية كثيرًا ، ولكن يتم إضافة كلا النوعين من الطرق إلى فئة metaclass لهذا الكائن.
يمكن إعادة كتابة المثال السابق على النحو التالي:
example = "I'm a string object" class << example def something self.upcase end end
بناء الجملة مختلف لكنه يفعل الشيء نفسه بشكل فعال. الآن دعنا نعود إلى المثال السابق حيث أنشأنا فئة Developer
واستكشف بعض الصيغ الأخرى لتحديد طريقة الفصل:
class Developer def self.backend "I am backend developer" end end
هذا تعريف أساسي يستخدمه الجميع تقريبًا.
def Developer.backend "I am backend developer" end
هذا هو نفس الشيء ، نحن نحدد طريقة فئة backend
Developer
. لم نستخدم self
ولكن تحديد طريقة كهذه بشكل فعال يجعلها طريقة فئة.
class Developer class << self def backend "I am backend developer" end end end
مرة أخرى ، نحن نحدد طريقة الصنف ، ولكن باستخدام بناء جملة مشابه لتلك التي استخدمناها لتعريف طريقة مفردة لكائن String
. قد تلاحظ أننا استخدمنا self
هنا والتي تشير إلى كائن Developer
نفسه. أولاً ، افتتحنا فئة Developer
، مما جعل أنفسنا مساوية لفئة Developer
. بعد ذلك ، نقوم بعمل class << self
، مما يجعل الذات مساوية للفئة الوصفية Developer
. ثم نحدد backend
للطريقة في الفئة الوصفية Developer
.
class << Developer def backend "I am backend developer" end end
من خلال تعريف كتلة مثل هذه ، نقوم بتعيين self
على metaclass Developer
طوال مدة الكتلة. نتيجة لذلك ، تتم إضافة طريقة backend
إلى الفئة الوصفية Developer
، بدلاً من الفئة نفسها.
دعونا نرى كيف تتصرف metaclass في شجرة الوراثة:
كما رأيت في الأمثلة السابقة ، لا يوجد دليل حقيقي على وجود metaclass. لكن يمكننا استخدام القليل من الاختراق الذي يمكن أن يوضح لنا وجود هذه الفئة غير المرئية:
class Object def metaclass_example class << self self end end end
إذا حددنا طريقة مثيل في فئة Object
(نعم ، يمكننا إعادة فتح أي فئة في أي وقت ، وهذا جمال آخر من البرمجة الوصفية) ، سيكون لدينا إشارة self
إلى Object
الكائن بداخلها. يمكننا بعد ذلك استخدام class << self
لتغيير الذات الحالية للإشارة إلى metaclass للكائن الحالي. نظرًا لأن الكائن الحالي هو فئة Object
نفسها ، فسيكون هذا هو الطبقة الوصفية للمثيل. تقوم الطريقة بإرجاع self
والتي هي في هذه المرحلة metaclass نفسها. لذلك من خلال استدعاء طريقة المثيل هذه على أي كائن ، يمكننا الحصول على metaclass لهذا الكائن. دعنا نحدد فئة Developer
لدينا مرة أخرى ونبدأ في الاستكشاف قليلاً:

class Developer def frontend p "inside instance method, self is: " + self.to_s end class << self def backend p "inside class method, self is: " + self.to_s end end end developer = Developer.new developer.frontend # "inside instance method, self is: #<Developer:0x2ced3b8>" Developer.backend # "inside class method, self is: Developer" p "inside metaclass, self is: " + developer.metaclass_example.to_s # "inside metaclass, self is: #<Class:#<Developer:0x2ced3b8>>"
وللتصعيد ، دعنا نرى الدليل على أن frontend
هي طريقة مثيل لفئة وأن backend
هي طريقة مثيل للفئة الوصفية:
p developer.class.instance_methods false # [:frontend] p developer.class.metaclass_example.instance_methods false # [:backend]
على الرغم من ذلك ، للحصول على metaclass لا تحتاج إلى إعادة فتح Object
وإضافة هذا الاختراق. يمكنك استخدام singleton_class
الذي توفره Ruby. إنه مماثل metaclass_example
الذي أضفناه ولكن مع هذا الاختراق يمكنك في الواقع رؤية كيفية عمل Ruby تحت الغطاء:
p developer.class.singleton_class.instance_methods false # [:backend]
تحديد الطرق باستخدام "class_eval" و "example_eval"
هناك طريقة أخرى لإنشاء طريقة فئة ، وذلك باستخدام instance_eval
:
class Developer end Developer.instance_eval do p "instance_eval - self is: " + self.to_s def backend p "inside a method self is: " + self.to_s end end # "instance_eval - self is: Developer" Developer.backend # "inside a method self is: Developer"
يتم تقييم هذا الجزء من التعليمات البرمجية لمترجم Ruby في سياق مثيل ، والذي يكون في هذه الحالة كائن Developer
. وعندما تقوم بتعريف طريقة على كائن ما ، فإنك تنشئ إما طريقة صنفية أو طريقة مفردة. في هذه الحالة ، إنها طريقة صنف - على وجه الدقة ، طرق الصنف هي طرق مفردة ولكنها طرق فردية للفصل ، في حين أن الطرق الأخرى عبارة عن طرق فردية للكائن.
من ناحية أخرى ، تقيم class_eval
الكود في سياق فئة بدلاً من مثيل. يعيد الفصل عمليا. إليك كيفية استخدام class_eval
لإنشاء طريقة مثيل:
Developer.class_eval do p "class_eval - self is: " + self.to_s def frontend p "inside a method self is: " + self.to_s end end # "class_eval - self is: Developer" p developer = Developer.new # #<Developer:0x2c5d640> developer.frontend # "inside a method self is: #<Developer:0x2c5d640>"
للتلخيص ، عندما تستدعي طريقة class_eval
، فإنك تغير self
للإشارة إلى الفئة الأصلية وعندما تستدعي instance_eval
، فإن التغييرات self
تشير إلى الفئة الأصلية 'metaclass.
تحديد الأساليب المفقودة على الطاير
هناك قطعة أخرى من أحجية البرمجة الوصفية وهي method_missing
. عندما تستدعي طريقة على كائن ما ، يدخل روبي أولاً في الفصل ويستعرض طرق المثيل الخاصة به. إذا لم يعثر على الطريقة هناك ، فسيواصل البحث في سلسلة الأسلاف. إذا لم يعثر روبي بعد على الطريقة ، فإنه يستدعي طريقة أخرى تسمى method_missing
وهي طريقة مثيل لـ Kernel
يرثها كل كائن. نظرًا لأننا على يقين من أن روبي سوف يطلق على هذه الطريقة في النهاية للطرق المفقودة ، يمكننا استخدام هذا لتنفيذ بعض الحيل.
define_method
الطريقة هي طريقة محددة في فئة Module
والتي يمكنك استخدامها لإنشاء طرق ديناميكيًا. لاستخدام define_method
، يمكنك تسميتها باسم الطريقة الجديدة والكتلة حيث تصبح معلمات الكتلة هي معلمات الطريقة الجديدة. ما الفرق بين استخدام def
لتكوين طريقة و define_method
؟ لا يوجد فرق كبير إلا أنه يمكنك استخدام طريقة method_missing
define_method
كود DRY. لكي تكون دقيقًا ، يمكنك استخدام define_method
بدلاً من def
للتعامل مع النطاقات عند تحديد فئة ، ولكن هذه قصة أخرى كاملة. دعنا نلقي نظرة على مثال بسيط:
class Developer define_method :frontend do |*my_arg| my_arg.inject(1, :*) end class << self def create_backend singleton_class.send(:define_method, "backend") do "Born from the ashes!" end end end end developer = Developer.new p developer.frontend(2, 5, 10) # => 100 p Developer.backend # undefined method 'backend' for Developer:Class (NoMethodError) Developer.create_backend p Developer.backend # "Born from the ashes!"
يوضح هذا كيف تم استخدام define_method
لتكوين طريقة مثيل بدون استخدام def
. ومع ذلك ، هناك الكثير الذي يمكننا فعله معهم. دعنا نلقي نظرة على مقتطف الشفرة هذا:
class Developer def coding_frontend p "writing frontend" end def coding_backend p "writing backend" end end developer = Developer.new developer.coding_frontend # "writing frontend" developer.coding_backend # "writing backend"
هذا الرمز ليس جافًا ، ولكن باستخدام define_method
يمكننا جعله جافًا:
class Developer ["frontend", "backend"].each do |method| define_method "coding_#{method}" do p "writing " + method.to_s end end end developer = Developer.new developer.coding_frontend # "writing frontend" developer.coding_backend # "writing backend"
هذا أفضل بكثير ، لكنه لا يزال غير مثالي. لماذا ا؟ إذا أردنا إضافة طريقة جديدة coding_debug
على سبيل المثال ، فنحن بحاجة لوضع هذا "debug"
في المصفوفة. لكن باستخدام method_missing
يمكننا إصلاح هذا:
class Developer def method_missing method, *args, &block return super method, *args, &block unless method.to_s =~ /^coding_\w+/ self.class.send(:define_method, method) do p "writing " + method.to_s.gsub(/^coding_/, '').to_s end self.send method, *args, &block end end developer = Developer.new developer.coding_frontend developer.coding_backend developer.coding_debug
هذا الجزء من الكود معقد بعض الشيء ، لذا دعنا نقسمه. استدعاء طريقة غير موجودة سيؤدي إلى إطلاق method_missing
. هنا ، نريد إنشاء طريقة جديدة فقط عندما يبدأ اسم الطريقة بـ "coding_"
. وإلا فإننا فقط نسمي super للقيام بعمل الإبلاغ عن طريقة مفقودة بالفعل. ونحن ببساطة نستخدم define_method
لإنشاء تلك الطريقة الجديدة. هذا هو! باستخدام هذا الجزء من الكود ، يمكننا إنشاء آلاف الطرق الجديدة فعليًا بدءًا من "coding_"
، وهذه الحقيقة هي التي تجعل الكود الخاص بنا جافًا. نظرًا لأن define_method
يكون خاصًا بالوحدة Module
، فنحن بحاجة إلى استخدام send
لاستدعاءها.
تغليف
هذه ليست سوى غيض من فيض. لتصبح روبي جدي ، هذه هي نقطة البداية. بعد إتقان هذه اللبنات الأساسية للبرمجة الوصفية وفهم جوهرها حقًا ، يمكنك المتابعة إلى شيء أكثر تعقيدًا ، على سبيل المثال إنشاء لغة خاصة بالمجال (DSL). DSL هو موضوع في حد ذاته ولكن هذه المفاهيم الأساسية هي شرط أساسي لفهم الموضوعات المتقدمة. تم بناء بعض الجواهر الأكثر استخدامًا في ريلز بهذه الطريقة ، وربما استخدمت DSL الخاص بها دون معرفة ذلك ، مثل RSpec و ActiveRecord.
نأمل أن تقربك هذه المقالة خطوة واحدة من فهم البرمجة الوصفية وربما حتى بناء DSL الخاص بك ، والذي يمكنك استخدامه للتشفير بشكل أكثر كفاءة.