Menu
Coddy logo textTech
flag Ar iconالعربيةdown icon

واجهة مشتركة

جزء من قسم 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(). وهذا يجعل كودك مرناً، حيث يمكنك إضافة أنواع مخرجات جديدة لاحقاً دون تغيير الوظائف الموجودة.

challenge icon

التحدي

سهل

لنقم ببناء نظام لمعالجة المدفوعات حيث يمكن استخدام طرق دفع مختلفة بالتبادل! ستقوم بإنشاء فئتين (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().

ستتلقى ثلاثة مدخلات:

  1. رقم بطاقة ائتمان (مثلاً، 4532-1234-5678)
  2. معرف حساب بنكي (مثلاً، ACC-9876)
  3. مبلغ الدفع (مثلاً، 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 لمعالجة المبلغ من خلال كلا طريقتي الدفع
-- أولاً بطاقة الائتمان، ثم التحويل البنكي
quiz iconاختبر نفسك

يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.

جميع دروس Object Oriented Programming