التحسينات في روبي
إن ميّزة الأصناف المفتوحة في لغة روبي تسمح لك بإعادة تعريف أو إضافة وظائف إلى أصناف معرّفة مسبقًا. وهذا ما يسمى بمصطلح "ترقيع القرد" (monkey patch). المشكلة هنا أنَّ تعديلات من هذا النوع تكون مرئيّة على المستوى العام (global)، وبالتالي جميع مستخدمي الصنف المرقّع قادرون على رؤية هذه التغييرات، ممّا قد يسبّب تأثيرات جانبيّة غير محسوبة أو حتى عطب في البرامج.
تأتي التحسينات هنا لتقلّل أثر ترقيع القرد على مستخدمي الصنف الآخرين، إذ تقدّم طريقة لتوسيع الصنف محليًّا. وإليك مثال على تحسين بسيط:
class C
def foo
puts "C#foo"
end
end
module M
refine C do
def foo
puts "C#foo in M"
end
end
end
في البداية يُعرّف الصنف C
، ثم يُجرى له تحسينًا باستخدام التابع Module.refine
. وبما أنّ التحسينات تعدّل أصنافًا فقط ولا تعدّل وحدات، فيجب أن يكون الوسيط الممرر إليه صنفًا.
ينشئ Module.refine
بعدئذٍ وحدة مجهولة (anonymous) تحوي التعديلات أو التحسينات المطبّقة على هذا الصنف (في مثالنا الصنف C). واستخدام self
أو Module.module_eval
سيشير إلى هذه الوحدة الجديدة.
أمّا تفعيل هذا التحسين فيكون باستخدام using
:
using M
c = C.new
c.foo # "C#foo in M" تطبع
النطاق
يمكنك تفعيل التحسينات في المستوى الأعلى من البرنامج، أو ضمن الأصناف والوحدات، لكن لا يمكنك حصرها ضمن نطاق تابع معيّن، إذ أنها تبقى مفعّلة حتى نهاية تعريف الصنف أو الوحدة الحاليّة. و في حال تفعيلها في المستوى الأعلى من البرنامج، تبقى مفعّلة حتى نهاية الملف الحالي.
كما بإمكانك تفعيل التحسينات في سلسلة أوامر ممرّرة إلى التابع Kernel.eval
، وحينها تبقى فعالة حتى نهاية هذه السلسلة.
تبدأ صلاحية التحسينات ضمن نطاق معيّن عند استدعاء using
، وبالتالي فإنّ أيّ سطر برمجيّ يسبق هذا الاستدعاء لن يكون للتحسينات أثر عليه.
وحين ينتقل التحكّم إلى خارج النّطاق، يُلغى تفعيل التحسينات تلقائيًّا؛ وهذا يعني أنّك إن قمت بطلب أو تحميل ملفّ، أو أجريت استدعاءً لتابع معرّف خارج النّطاق الحاليّ، فسيلغى تفعيل التحسينات تلقائيًّا:
class C
end
module M
refine C do
def foo
puts "C#foo in M"
end
end
end
def call_foo(x)
x.foo
end
using M
x = C.new
x.foo # "C#foo in M" يطبع
call_foo(x) #=> NoMethodError يسبب
إذا عُرّف تابع في نطاق كانت فيه التحسينات مفعّلة، فستبقى هذه التحسينات على حالة تفعيلها عند استدعاء هذا التابع. إليك المثال التالي الذي يمتدّ على ملفّات عدّة: الملف c.rb:
class C
end
الملف m.rb:
require "c"
module M
refine C do
def foo
puts "C#foo in M"
end
end
end
الملف m_user.rb:
require "m"
using M
class MUser
def call_foo(x)
x.foo
end
end
الملف main.rb:
require "m_user"
x = C.new
m_user = MUser.new
m_user.call_foo(x) # "C#foo in M" يطبع
x.foo #=> NoMethodError يسبب
بما أنّ التحسين M
فعال في الملف m_user.rb حيث تعريف التابع MUser.call_foo
، فسيبقى هذا التحسين فعالًا عندما يُستدعى التابع call_foo
في الملف main.rb.
وبما أنّ using
هو تابع، فستفعّل التحسينات فقط عند استدعائه، وإليك أمثلة حيث يكون التحسين M
مفعلًا أو غير مفعّل.
في ملف:
# غير فعال هنا
using M
# فعال هنا
class Foo
# فعال هنا
def foo
# فعال هنا
end
# فعال هنا
end
# فعال هنا
في صنف:
# غير فعال هنا
class Foo
# غير فعال هنا
def foo
# غير فعال هنا
end
using M
# فعال هنا
def bar
# فعال هنا
end
# فعال هنا
end
# غير فعال هنا
لاحظ أنّ التحسينات M
لا تفعّل تلقائيًّا في حال أُعيد فتح الصنف لاحقًا.
في eval
:
# غير فعال هنا
eval <<EOF
# غير فعال هنا
using M
# فعال هنا
EOF
# غير فعال هنا
عند عدم تحقّق التنفيذ:
# غير فعال هنا
if false
using M
end
# غير فعال هنا
عند تعريف عدة تحسينات في نفس الوحدة داخل عدّة كتل برمجيّة محسِّنة، فكلّ التحسينات في هذه الوحدة تصبح فعالة عند استدعاء تابع محسَّن (أيّ من توابع to_json
في المثال أدناه) :
module ToJSON
refine Integer do
def to_json
to_s
end
end
refine Array do
def to_json
"[" + map { |i| i.to_json }.join(",") + "]"
end
end
refine Hash do
def to_json
"{" + map { |k, v| k.to_s.dump + ":" + v.to_json }.join(",") + "}"
end
end
end
using ToJSON
p [{1=>2}, {3=>4}].to_json # "[{\"1\":2},{\"3\":4}]" تطبع
البحث عن التوابع
إذا أراد مفسر لغة روبي البحث عن تابع لكائن من الصنف C
فإنّه يسير وفق الترتيب التالي:
- إذا وُجدت تحسينات فعالة للصنف
C
فيتمّ البحث بعكس ترتيب تفعيلها: - ثم الوحدات المضمّنة في الصنف باستخدام
prepend
- ثم الصنف نفسه
- ثم الوحدات المضمّنة في الصنف باستخدام
include
وإذا لم يجد المفسّر أيّ تابع مطابق في أيّ مرحلة من هذه المراحل، فإنّ عملية البحث تكرّر بنفس الترتيب لكن مع الصنف الأب للصنف C
.
لاحظ أنّ أولوية التوابع في الصنف الابن أعلى من التحسينات في الصنف الأب؛ فعلى سبيل المثال، لو عرّفنا التابع /
في تحسين للصنف Numeric
، فإنّ العملية 1/2
تستدعي التابع المقابل في الصنف Integer
بسبب كون الأخير صنفًا فرعيًا عن Numeric
فيسبقه في عمليّة البحث، وبما أنّ التابع موجود في الصنف الابن فسيتوقف البحث بمجرّد العثور على هذا التابع فيه.
وبالمقابل لو عرّفنا التابع foo
في الصنف Numeric
ضمن تحسين ما، فإنّ كتابة 1.foo
ستستدعي هذا التابع من الصنف Numeric
لكونه غير معرّف في الصنف Integer
.
استعمال super
إليك الترتيب الذي يسير وفقه المفسر عند استدعاء super
:
- الوحدات المضمّنة في الصنف الحاليّ، مع ملاحظة أنّ الصنف الحاليّ يمكن أن يكون تحسينًا.
- في حال كان الصنف الحاليّ تحسينًا، فإنّ عملية البحث تسير وفق الترتيب المذكور في الفقرة السابقة "البحث عن التوابع".
- وفي حال كان للصنف الحاليّ أب مباشر، فستسير عمليّة البحث كما في الفقرة السابقة وإنّما على الصنف الأب.
لاحظ أنّ استدعاء super
في تابع ضمن تحسين صنف ما سيستدعي هذا التابع من الصنف الذي يُحسَّن، حتى ولو سبق ذلك تفعيل تحسين آخر ضمن نفس السياق.
الاستدعاء غير المباشر للتوابع
إذا استدعي تابع بشكل غير مباشر كما هو الحال عند استخدام Kernel.send
أو Kernel.method
أو ?Kernel.respond_to
، فحينها لا تُعطى التحسينات أولويّة أثناء عمليّة البحث. إلّا أنّ هذا السّلوك قد يتغيّر في المستقبل.
وراثة التحسينات باستخدام Module.include
إذا ضُمّنت الوحدة X
في الوحدة Y
، فإنّ الثانية ترث التحسينات المطبّقة في الأولى. ففي المثال التالي ترث C
تحسينات من A
و B
:
module A
refine X do ... end
refine Y do ... end
end
module B
refine Z do ... end
end
module C
include A
include B
end
using C
# فعالة هنا B و A التحسينات من
وتحسينات الأبناء لها أولوية أعلى من تحسينات الآباء.