التنفيذ الضمني
جزء من قسم Object Oriented Programming في رحلة GO على Coddy — الدرس 26 من 107.
في العديد من اللغات كائنية التوجه مثل Java أو C#، يجب عليك التصريح صراحةً بأن النوع ينفذ واجهة (interface) باستخدام كلمات مفتاحية مثل implements. تتبع Go نهجاً مختلفاً: تنفيذ الواجهة يكون ضمنياً.
يستوفي النوع الواجهة ببساطة عن طريق تنفيذ جميع دوالها. لا توجد حاجة للتصريح عن العلاقة في أي مكان. يكتشف المترجم ذلك تلقائيًا.
type Writer interface {
Write(data string) int
}
type FileWriter struct {
Filename string
}
// FileWriter يطبق الواجهة Writer بشكل ضمني
func (f FileWriter) Write(data string) int {
fmt.Println("Writing to", f.Filename)
return len(data)
}
لاحظ أنه لا يوجد تصريح implements Writer في FileWriter. وبما أن FileWriter يحتوي على دالة Write(data string) int تطابق توقيع الواجهة، فإنه يستوفي تلقائياً واجهة Writer.
لهذا التصميم نتيجة قوية: يمكنك تعريف الواجهات (interfaces) بعد أن تكون الأنواع (types) موجودة بالفعل. إذا كانت هناك مكتبة من جهة خارجية تحتوي على نوع يمتلك الدوال (methods) التي تحتاجها، فيمكنك إنشاء واجهة يحققها ذلك النوع بالفعل دون تعديل الكود الأصلي.
func Save(w Writer, content string) {
w.Write(content)
}
func main() {
fw := FileWriter{Filename: "data.txt"}
Save(fw, "Hello!") // يعمل لأن FileWriter يستوفي شروط Writer
}
يحافظ التنفيذ الضمني على كود Go مفككاً ومرناً. لا تحتاج الأنواع إلى معرفة الواجهات مسبقاً، مما يسهل بناء الأنظمة من أجزاء مستقلة.
التحدي
سهللنقم ببناء نظام لتوصيل الرسائل يوضح كيف تحقق الأنواع الواجهات (interfaces) بشكل ضمني في لغة Go. ستقوم بإنشاء أنواع مرسلين مختلفة تحقق جميعها نفس الواجهة — دون التصريح بتلك العلاقة بشكل صريح أبداً.
ستقوم بتنظيم الكود الخاص بك عبر ملفين:
messengers.go: قم بتعريف واجهةMessengerمع طريقة واحدةSend(message string) string. ثم أنشئ هيكلين (structs) سيحققان هذه الواجهة ضمنياً:EmailMessengerمع حقلAddress(string)SMSMessengerمع حقلPhoneNumber(string)
Sendتعيد سلسلة نصية تصف عملية التوصيل. لا يجب على أي من الهيكلين التصريح صراحةً بأنه ينفذMessenger— ستكتشف Go ذلك تلقائياً بناءً على توقيعات الطرق (method signatures).main.go: أنشئ دالة تسمىDeliverMessageتقبل أيMessengerوسلسلة رسالة نصية، ثم تعيد نتيجة استدعاءSend. اقرأ معلومات الاتصال من المدخلات، وأنشئ كلا نوعي المرسلين، واستخدم دالتك لتوصيل الرسائل من خلال كل منهما.
سيتم توفير المدخلات التالية:
- السطر 1: عنوان البريد الإلكتروني
- السطر 2: رقم الهاتف
- السطر 3: الرسالة المراد إرسالها
يجب أن تعيد طرق Send الخاصة بك سلاسل نصية بهذه التنسيقات:
- EmailMessenger:
Email to [Address]: [message] - SMSMessenger:
SMS to [PhoneNumber]: [message]
على سبيل المثال، إذا تم إعطاء alice@example.com، و 555-1234، و Hello!، يجب أن يكون مخرجك:
Email to alice@example.com: Hello!
SMS to 555-1234: Hello!الفكرة الجوهرية هنا هي أن دالة DeliverMessage الخاصة بك تقبل أي Messenger، وكل من EmailMessenger و SMSMessenger يحققان تلك الواجهة ببساطة من خلال امتلاك طريقة Send مطابقة. لا توجد كلمة مفتاحية implements في أي مكان — فالتنفيذ الضمني في Go يتولى كل شيء.
ورقة مرجعية
في لغة Go، يكون تنفيذ الواجهة (interface) ضمنيًا. يحقق النوع الواجهة ببساطة عن طريق تنفيذ جميع أساليبها (methods) - لا يلزم وجود إعلان صريح.
type Writer interface {
Write(data string) int
}
type FileWriter struct {
Filename string
}
// FileWriter ينفذ Writer بشكل ضمني
func (f FileWriter) Write(data string) int {
fmt.Println("Writing to", f.Filename)
return len(data)
}
لا يوجد إعلان implements Writer. نظرًا لأن FileWriter لديه أسلوب Write(data string) int يطابق توقيع الواجهة، فإنه يحقق واجهة Writer تلقائيًا.
يمكنك استخدام أي نوع يحقق الواجهة دون أن يعرف النوع عن الواجهة:
func Save(w Writer, content string) {
w.Write(content)
}
func main() {
fw := FileWriter{Filename: "data.txt"}
Save(fw, "Hello!") // يعمل لأن FileWriter يحقق Writer
}
يسمح لك هذا التصميم بتعريف الواجهات بعد وجود الأنواع بالفعل، مما يجعل الكود مفككًا ومرنًا.
جرّب بنفسك
package main
import (
"bufio"
"fmt"
"os"
)
// TODO: قم بإنشاء دالة DeliverMessage تقبل أي Messenger
// وسلسلة نصية للرسالة، ثم تُرجع نتيجة استدعاء Send
func main() {
reader := bufio.NewReader(os.Stdin)
// قراءة عنوان البريد الإلكتروني
email, _ := reader.ReadString('\n')
email = email[:len(email)-1]
// قراءة رقم الهاتف
phone, _ := reader.ReadString('\n')
phone = phone[:len(phone)-1]
// قراءة الرسالة
message, _ := reader.ReadString('\n')
if len(message) > 0 && message[len(message)-1] == '\n' {
message = message[:len(message)-1]
}
// TODO: قم بإنشاء EmailMessenger باستخدام عنوان البريد الإلكتروني
// TODO: قم بإنشاء SMSMessenger باستخدام رقم الهاتف
// TODO: استخدم DeliverMessage لإرسال الرسالة عبر كل messenger
// وطباعة النتائج
}
يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.
جميع دروس 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