الواجهة كعقد
جزء من قسم 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) دون الحاجة إلى قواعد بيانات حقيقية أو اتصالات شبكة. لا تدرك الدالة الخاضعة للاختبار الفرق لأن العقد قد تم الوفاء به.
التحدي
سهللنقم ببناء نظام إشعارات يوضح كيف تعمل الواجهات (interfaces) كعقود. ستقوم بإنشاء واجهة Notifier تحدد ما يعنيه إرسال إشعار، ثم تقوم بتنفيذ قنوات إشعارات متعددة تلتزم جميعها بهذا العقد.
ستقوم بتنظيم الكود الخاص بك عبر ملفين:
notifiers.go: قم بتعريف واجهةNotifierمع طريقة واحدةNotify(message string) string. ثم قم بإنشاء ثلاثة هياكل (structs) مختلفة تستوفي هذا العقد:ConsoleNotifierمع حقلPrefixFileNotifierمع حقلFilenameWebhookNotifierمع حقل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")
}
يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.
جميع دروس Object Oriented Programming
1أساسيات الـ OOP في Go
الملفات الخارجيةمساحة العمل والـ Modules في Goالـ Packages والـ Importsالأسماء المصدرة مقابل غير المصدرةمقدمة إلى الـ OOP في Goالـ Structs كـ Classesتعريف الـ Methods في الـ Structsالـ Pointer Receivers مقابل الـ Value Receiversتهيئة الـ Structدوال الـ Constructorمراجعة - آلة حاسبة بسيطة4الواجهات
مقدمة في الواجهاتالتنفيذ الضمنيالواجهة كعقدالواجهة الفارغة (any)تأكيد النوع (Type Assertion)تبديل النوع (Type Switch)تركيب الواجهاتواجهات Stringer و Errorمراجعة - حاسبة الأشكال7التغليف
الحقول المصدرة وغير المصدرةالتغليف على مستوى الحزمةدوال الـ Getter والـ Setterإخفاء المعلومات في Goملخص - سجلات الطلاب10الأنواع العامة (Go 1.18+)
مقدمة في الأنواع العامةمعاملات الأنواعقيود الأنواعالـ Structs العامةحل بديل للـ Methods العامةملخص - المجموعات العامة13أنماط التصميم - الجزء الأول
مقدمة في أنماط التصميمنمط Singletonنمط Factoryنمط Abstract Factoryنمط Observerنمط Strategy2تعمق في الأنواع و Structs
الأنواع الأساسية والمركبةتعريفات الأنواع المخصصةStruct TagsStructs مجهولةStructs متداخلةالقيم الصفرية والافتراضيةمراجعة - دفتر العناوين5التركيب بدلاً من الوراثة
لماذا لا تدعم Go الوراثةأساسيات تضمين الـ Structترقية الـ Methodتضمين عدة Structsالتضمين مقابل التجميعحجب الـ Methods المضمنةملخص - الهيكل الهرمي للموظفين8معالجة الأخطاء و OOP
واجهة errorأنواع الأخطاء المخصصةتغليف الأخطاء (fmt.Errorf)أخطاء Sentinelerrors.Is() و errors.As()Panic و Defer و Recoverملخص - File Parser11المكتبة القياسية والبرمجة كائنية التوجه (OOP)
io.Reader و io.Writerواجهة sort.Interfaceواجهة fmt.Stringerencoding/json مع Structsواجهة http.Handlerمراجعة - نماذج REST API14أنماط التصميم - الجزء الثاني
نمط الأمرنمط المحولنمط المزيننمط طريقة القالبنمط الحالةنمط التركيبMiddleware كنمط مزين3المؤشرات والذاكرة
أساسيات المؤشرات في Goالمؤشرات إلى الـ Structsالتمرير بالقيمة مقابل التمرير بالمرجعدالة ()newGarbage Collection في Goملخص - بناء Linked List6تعدد الأشكال في Go
تعدد الأشكال عبر InterfacesDuck Typing في Goقواعد استيفاء الـ Interfaceمجموعات متعددة الأشكالحقن التبعيةمراجعة - معالج الدفع9التزامن والبرمجة كائنية التوجه (OOP)
أساسيات الـ Goroutinesالقنوات (Channels) والاتصالالقنوات المخزنة (Buffered) مقابل غير المخزنةجملة Selectsync.Mutex و sync.RWMutexsync.WaitGroupتصميم الـ Structs الآمنة للخيوط (Thread-Safe)مراجعة - Worker Pool