واجهة مشتركة
جزء من قسم Object Oriented Programming في رحلة Lua على Coddy — الدرس 40 من 70.
لقد أظهر لنا أسلوب Duck typing أن الأصناف (classes) غير المرتبطة يمكنها العمل معاً إذا كانت تشترك في أسماء الدوال (method names). وتؤدي هذه الفكرة إلى مفهوم تصميم قوي: الواجهة المشتركة (common interface). فعندما تقوم بتصميم أصناف متعددة عمداً لتمتلك نفس تواقيع الدوال (method signatures)، فإنك تنشئ مكونات قابلة للتبديل.
تخيل أنك تقوم ببناء نظام تسجيل (logging system). قد ترغب في إرسال الرسائل إلى وجهات مختلفة — ملف، أو الشاشة، أو حتى شبكة. بدلاً من كتابة كود منفصل لكل منها، تقوم بتصميم جميع فئات الإخراج (output classes) لديك لتنفيذ نفس الطريقة :write():
local LogFile = {}
LogFile.__index = LogFile
function LogFile:new(filename)
local obj = {filename = filename}
setmetatable(obj, LogFile)
return obj
end
function LogFile:write(message)
print("[FILE:" .. self.filename .. "] " .. message)
end
local Screen = {}
Screen.__index = Screen
function Screen:new()
local obj = {}
setmetatable(obj, Screen)
return obj
end
function Screen:write(message)
print("[SCREEN] " .. message)
endالآن يمكن لأي كود يحتاج إلى إخراج رسائل قبول أي من النوعين:
local function logMessage(output, msg)
output:write(msg)
end
local file = LogFile:new("app.log")
local screen = Screen:new()
logMessage(file, "Starting up") -- [FILE:app.log] بدء التشغيل
logMessage(screen, "Starting up") -- [SCREEN] بدء التشغيللا تعرف الوظيفة logMessage ولا تهتم بأي فئة تستقبلها. فهي تتوقع فقط وجود طريقة :write(). وهذا يجعل كودك مرناً، حيث يمكنك إضافة أنواع مخرجات جديدة لاحقاً دون تغيير الوظائف الموجودة.
التحدي
سهللنقم ببناء نظام لمعالجة المدفوعات حيث يمكن استخدام طرق دفع مختلفة بالتبادل! ستقوم بإنشاء فئتين (classes) للدفع غير مرتبطتين تماماً تشتركان في واجهة مشتركة—وهي الطريقة :process(amount)—مما يسمح لأي كود يتعامل مع المدفوعات بالعمل مع أي من النوعين دون معرفة الفرق بينهما.
ستقوم بتنظيم الكود الخاص بك عبر ثلاثة ملفات:
CreditCard.lua: فئة دفع تحتوي على منشئ:new(cardNumber)يقوم بتخزين رقم البطاقة. يجب أن تقوم طريقة:process(amount)الخاصة بها بطباعةCharging {amount} to card {cardNumber}.BankTransfer.lua: فئة دفع منفصلة تماماً مع منشئ:new(accountId). يجب أن تقوم طريقة:process(amount)الخاصة بها بطباعةTransferring {amount} from account {accountId}.main.lua: قم بإنشاء دالة تسمىhandlePayment(paymentMethod, amount)تقبل أي كائن دفع ومبلغاً، ثم تستدعي:process(amount)عليه. لا تحتاج هذه الدالة لمعرفة ما إذا كانت تتعامل مع بطاقة ائتمان أو تحويل بنكي—فهي تتوقع فقط أن يحتوي الكائن على طريقة:process().
ستتلقى ثلاثة مدخلات:
- رقم بطاقة ائتمان (مثلاً،
4532-1234-5678) - معرف حساب بنكي (مثلاً،
ACC-9876) - مبلغ الدفع (مثلاً،
150)
في ملفك الرئيسي، قم بإنشاء مثيل (instance) واحد من كل نوع دفع باستخدام أول مدخلين. ثم استخدم دالة handlePayment لمعالجة المبلغ المعطى من خلال كلتا طريقتي الدفع—أولاً بطاقة الائتمان، ثم التحويل البنكي.
على سبيل المثال، إذا كانت المدخلات هي 1111-2222-3333، و ACC-5555، و 75، يجب أن يكون المخرج:
Charging 75 to card 1111-2222-3333
Transferring 75 from account ACC-5555يكمن جمال هذا التصميم في أن دالة handlePayment تعمل مع أي كائن ينفذ :process(). يمكنك إضافة فئة PayPal غداً، وطالما أنها تحتوي على طريقة :process(amount)، فستعمل مع دالتك الحالية دون أي تغييرات!
ورقة مرجعية
تعد الواجهة المشتركة (common interface) نمط تصميم تقوم فيه فئات متعددة غير مرتبطة بتنفيذ نفس تواقيع الدوال (method signatures) عن قصد، مما يجعلها قابلة للتبادل.
من خلال تصميم الفئات لتتشارك في أسماء الدوال، يمكنك كتابة دوال تعمل مع أي كائن يقوم بتنفيذ تلك الواجهة:
local LogFile = {}
LogFile.__index = LogFile
function LogFile:new(filename)
local obj = {filename = filename}
setmetatable(obj, LogFile)
return obj
end
function LogFile:write(message)
print("[FILE:" .. self.filename .. "] " .. message)
end
local Screen = {}
Screen.__index = Screen
function Screen:new()
local obj = {}
setmetatable(obj, Screen)
return obj
end
function Screen:write(message)
print("[SCREEN] " .. message)
endيمكن للدوال قبول أي كائن يقوم بتنفيذ الدالة المتوقعة:
local function logMessage(output, msg)
output:write(msg)
end
local file = LogFile:new("app.log")
local screen = Screen:new()
logMessage(file, "Starting up") -- [FILE:app.log] Starting up
logMessage(screen, "Starting up") -- [SCREEN] Starting upلا تحتاج الدالة logMessage إلى معرفة الفئة التي تستقبلها—فهي تتوقع فقط وجود دالة :write(). هذا يجعل الكود مرنًا ويسمح بإضافة أنواع جديدة دون تعديل الدوال الموجودة.
جرّب بنفسك
-- استيراد فئات الدفع
local CreditCard = require('CreditCard')
local BankTransfer = require('BankTransfer')
-- قراءة المدخلات
local cardNumber = io.read()
local accountId = io.read()
local amount = tonumber(io.read())
-- TODO: إنشاء دالة handlePayment التي تقبل أي كائن دفع
-- ومبلغاً، ثم تستدعي process(amount): عليه
-- TODO: إنشاء مثيلات من CreditCard و BankTransfer باستخدام المدخلات
-- TODO: استخدام handlePayment لمعالجة المبلغ من خلال كلا طريقتي الدفع
-- أولاً بطاقة الائتمان، ثم التحويل البنكي
يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.
جميع دروس Object Oriented Programming
4مشروع: بنك رقمي
إعداد المشروعطريقة الإيداع7تعدد الأشكال وإعادة التعريف
إعادة تعريف الدوالاستدعاء دوال الفئة الأبDuck Typingواجهة مشتركةالتحقق من النوعمراجعة - أدوار الموظفين2نمط النموذج الأولي للفئات
مفهوم النموذج الأوليالربط باستخدام __indexالمنشئ :new()تهيئة الخصائصنسخ مستقلةملخص - مصنع السيارات5تحميل المعاملات في OOP
جمع الكائناتطرح الكائناتدمج الكائناتمقارنة الكائنات (<, >)مراجعة - رياضيات المحفظة8التغليف
اصطلاحات التسميةاستخدام Closures للخصوصيةالوصول عبر Closuresجداول للقراءة فقطمنطق التحقق من الصحةمراجعة - الخزنة الآمنة11أنماط التصميم (مبسط)
دوال المصنعجدول Singletonنمط الـ Iteratorالمراقب (المستمع)مراجعة - مصنع الـ Logger3حالة الكائن وسلوكه
متغيرات النسخةدالات الجلب (Getter Methods)دالات التعيين (Setter Methods)الخصائص المحسوبةتنسيق النصوصالتحقق من المساواةمراجعة - درجة الطالب6أساسيات الوراثة
إعداد الوراثةوراثة الـ Methodsتوسيع الـ Constructorإضافة Methods للابنالمشترك مقابل الفريدملخص - التسلسل الهرمي للأشكال