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

الواجهة كعقد

جزء من قسم Object Oriented Programming في رحلة GO على Coddy — الدرس 27 من 107.

التفكير في الـ interfaces كـ عقود يساعدك على تصميم برامج Go أفضل. عندما تقبل function نوع interface، فهي تقول: "لا يهمني الـ concrete type الذي تعطيه لي، طالما أنه يفي بهذا العقد."

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

type DataSource interface {
    FetchData() string
}

func ProcessData(src DataSource) {
    data := src.FetchData()
    fmt.Println("Processing:", data)
}

لا تعرف الدالة ProcessData ولا تهتم بما إذا كانت تتلقى اتصالاً بقاعدة بيانات، أو عميل API، أو قارئ ملفات. فهي تتطلب فقط أن يكون المدخل قادراً على FetchData(). أي نوع يلتزم بهذا العقد سيعمل.

type Database struct{ ConnectionString string }
func (d Database) FetchData() string { return "data from DB" }

type APIClient struct{ Endpoint string }
func (a APIClient) FetchData() string { return "data from API" }

func main() {
    db := Database{ConnectionString: "localhost"}
    api := APIClient{Endpoint: "https://api.example.com"}
    
    ProcessData(db)   // يعمل!
    ProcessData(api)  // يعمل أيضاً!
}

يجعل نهج العقد هذا الكود الخاص بك أكثر قابلية للاختبار أيضاً. أثناء الاختبار، يمكنك إنشاء نوع وهمي (mock type) يستوفي الواجهة (interface) دون الحاجة إلى قواعد بيانات حقيقية أو اتصالات شبكة. لا تدرك الدالة الخاضعة للاختبار الفرق لأن العقد قد تم الوفاء به.

challenge icon

التحدي

سهل

لنقم ببناء نظام إشعارات يوضح كيف تعمل الواجهات (interfaces) كعقود. ستقوم بإنشاء واجهة Notifier تحدد ما يعنيه إرسال إشعار، ثم تقوم بتنفيذ قنوات إشعارات متعددة تلتزم جميعها بهذا العقد.

ستقوم بتنظيم الكود الخاص بك عبر ملفين:

  • notifiers.go: قم بتعريف واجهة Notifier مع طريقة واحدة Notify(message string) string. ثم قم بإنشاء ثلاثة هياكل (structs) مختلفة تستوفي هذا العقد:
    • ConsoleNotifier مع حقل Prefix
    • FileNotifier مع حقل Filename
    • WebhookNotifier مع حقل URL
    يجب أن يقوم كل هيكل بتنفيذ طريقة Notify لإرجاع سلسلة نصية تصف كيفية إرسال الإشعار.
  • main.go: قم بإنشاء دالة تسمى BroadcastAlert تقبل شريحة (slice) من قيم Notifier وسلسلة نصية للرسالة. يجب أن تستدعي هذه الدالة Notify على كل مُرسل إشعارات وتطبع النتائج. اقرأ التكوين من الإدخال، وأنشئ جميع أنواع مُرسلي الإشعارات الثلاثة، وقم ببث تنبيه من خلالهم جميعاً.

سيتم توفير المدخلات التالية:

  • السطر 1: Console prefix
  • السطر 2: Filename
  • السطر 3: Webhook URL
  • السطر 4: Alert message

يجب أن تعيد طرق Notify الخاصة بك سلاسل نصية بهذه التنسيقات:

  • ConsoleNotifier: [Prefix] [message]
  • FileNotifier: Writing to [Filename]: [message]
  • WebhookNotifier: POST [URL]: [message]

على سبيل المثال، إذا تم إعطاء ALERT:، و log.txt، و https://hooks.example.com، و Server down!، يجب أن يكون مخرجك:

ALERT: Server down!
Writing to log.txt: Server down!
POST https://hooks.example.com: Server down!

تكمن قوة هذا التصميم في أن دالة BroadcastAlert الخاصة بك لا تعرف أو تهتم بأنواع مُرسلي الإشعارات المحددة — فهي تتطلب فقط أن يفي كل منها بعقد Notifier. يمكنك إضافة SlackNotifier أو EmailNotifier لاحقاً دون تغيير دالة البث على الإطلاق.

ورقة مرجعية

تعمل الواجهات (Interfaces) في لغة Go كـ عقود تحدد السلوك المطلوب دون تحديد أنواع ملموسة. عندما تقبل دالة نوع واجهة، يمكن استخدام أي نوع ينفذ طرق (methods) تلك الواجهة.

قم بتعريف واجهة من خلال تحديد تواقيع الدوال (method signatures):

type DataSource interface {
    FetchData() string
}

تعمل الدوال التي تقبل الواجهات مع أي نوع يستوفي العقد:

func ProcessData(src DataSource) {
    data := src.FetchData()
    fmt.Println("Processing:", data)
}

يمكن لعدة أنواع تنفيذ نفس الواجهة:

type Database struct{ ConnectionString string }
func (d Database) FetchData() string { return "data from DB" }

type APIClient struct{ Endpoint string }
func (a APIClient) FetchData() string { return "data from API" }

func main() {
    db := Database{ConnectionString: "localhost"}
    api := APIClient{Endpoint: "https://api.example.com"}
    
    ProcessData(db)   // يعمل!
    ProcessData(api)  // يعمل أيضاً!
}

يتيح هذا النهج القائم على العقود ما يلي:

  • الدوال التي تركز على السلوك بدلاً من أنواع محددة
  • قابلية اختبار أفضل من خلال التنفيذات الوهمية (mock implementations)
  • القابلية للتوسع دون تعديل الكود الحالي

جرّب بنفسك

package main

import (
	"bufio"
	"fmt"
	"os"
)

// تقوم BroadcastAlert بإرسال رسالة عبر جميع الـ notifiers
// TODO: قم بتنفيذ هذه الدالة لـ:
// - استقبال slice من قيم Notifier وسلسلة نصية message
// - استدعاء Notify على كل notifier وطباعة النتائج
func BroadcastAlert(notifiers []Notifier, message string) {
	// TODO: قم بالمرور عبر الـ notifiers وطباعة نتيجة كل إشعار
}

func main() {
	scanner := bufio.NewScanner(os.Stdin)
	
	// قراءة بادئة الكونسول (console prefix)
	scanner.Scan()
	prefix := scanner.Text()
	
	// قراءة اسم الملف (filename)
	scanner.Scan()
	filename := scanner.Text()
	
	// قراءة رابط الـ webhook URL
	scanner.Scan()
	url := scanner.Text()
	
	// قراءة رسالة التنبيه (alert message)
	scanner.Scan()
	message := scanner.Text()
	
	// TODO: قم بإنشاء مثيلات (instances) لجميع أنواع الـ notifier الثلاثة
	// - ConsoleNotifier مع الـ prefix
	// - FileNotifier مع الـ filename
	// - WebhookNotifier مع الـ URL
	
	// TODO: قم بإنشاء slice من Notifier تحتوي على جميع الـ notifiers الثلاثة
	
	// TODO: قم باستدعاء BroadcastAlert مع الـ notifiers والـ message
	
	_ = prefix
	_ = filename
	_ = url
	_ = message
	fmt.Println("TODO: Implement the solution")
}
quiz iconاختبر نفسك

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

جميع دروس Object Oriented Programming