لماذا لا تدعم Go الوراثة
جزء من قسم Object Oriented Programming في رحلة GO على Coddy — الدرس 34 من 107.
تستخدم لغات البرمجة كائنية التوجه التقليدية مثل Java و C++ الوراثة لمشاركة الكود بين الأنواع. حيث تقوم الفئة الابنة (child class) بتوسيع الفئة الأب (parent class)، لترث جميع حقولها ودوالها. وتتجاهل لغة Go هذه الميزة عمدًا.
تخلق الوراثة ارتباطاً وثيقاً بين الأنواع. عندما تتغير الفئة الأب (parent class)، تتأثر جميع الفئات الأبناء (child classes).
تصبح التسلسلات الهرمية العميقة للوراثة صعبة الفهم والصيانة. تحدث "مشكلة الفئة الأساسية الهشة" (fragile base class problem) عندما تؤدي التعديلات على الفئة الأساسية إلى كسر الفئات المشتقة بشكل غير متوقع.
اختار مصممو Go مساراً مختلفاً: التركيب بدلاً من الوراثة (composition over inheritance). بدلاً من قول "إن Dog هو Animal"، تشجعك Go على قول "إن Dog لديه سلوكيات تشبه Animal". يؤدي هذا التحول البسيط إلى كود أكثر مرونة وقابلية للصيانة.
يحقق Go إعادة استخدام الكود من خلال آليتين تعلمتهما بالفعل:
- الواجهات (Interfaces) تُحدد عقود السلوك بدون تفاصيل التنفيذ
- تضمين الـ Struct يسمح للأنواع بتضمين أنواع أخرى وإعادة استخدام الـ methods الخاصة بها
تأمل هذه المقارنة. في البرمجة كائنية التوجه (OOP) التقليدية، قد تكتب class Dog extends Animal. في لغة Go، تقوم بتضمين هيكل Animal داخل Dog وتنفذ واجهات (interfaces) مشتركة. والنتيجة هي وظائف مماثلة مع اقتران أقل بين الأنواع.
يستكشف هذا الفصل مفهوم تضمين الهياكل (struct embedding) بعمق، موضحاً كيف تحقق لغة Go فوائد الوراثة (inheritance) دون عيوبها.
التحدي
سهللنقم ببناء نظام إشعارات يوضح نهج التركيب (composition) في لغة Go بدلاً من الوراثة (inheritance). ستقوم بإنشاء أنواع تشترك في السلوك من خلال الواجهات (interfaces) وتضمين الهياكل (struct embedding) بدلاً من التسلسلات الهرمية للفئات.
ستقوم بتنظيم الكود الخاص بك عبر ثلاثة ملفات:
notifier.go: قم بتعريف واجهةNotifierمع طريقة واحدةNotify(message string) string. قم أيضاً بإنشاء هيكلBaseNotifierمع حقلNameسيتم تضمينه بواسطة أنواع أخرى. امنحBaseNotifierطريقة تسمىFormat(message string) stringتعيد الرسالة مسبوقة باسم المنبه بين قوسين مربعيين.channels.go: قم بإنشاء نوعين من قنوات الإشعارات يقومان بتضمينBaseNotifierوينفذان واجهةNotifier:EmailNotifierمع حقل إضافيAddressSMSNotifierمع حقل إضافيPhone
Notifyلكل نوع طريقةFormatالمضمنة وتتضمن معلومات القناة الخاصة بها في المخرجات.main.go: قم بإنشاء دالة تسمىSendAlertتقبل أيNotifierورسالة، ثم تعيد نتيجة استدعاءNotify. اقرأ تفاصيل الإشعارات من المدخلات، وأنشئ كلا النوعين من المنبهات، ووضح كيف يمكن استخدامهما بالتبادل من خلال الواجهة.
سيتم توفير المدخلات التالية:
- السطر 1: اسم المنبه للبريد الإلكتروني
- السطر 2: عنوان البريد الإلكتروني
- السطر 3: اسم المنبه للرسائل القصيرة (SMS)
- السطر 4: رقم الهاتف
- السطر 5: رسالة التنبيه
يجب أن تعيد طريقة Format في BaseNotifier ما يلي:
[Name] messageيجب أن تعيد طرق Notify ما يلي:
- EmailNotifier:
Email to [Address]: [formatted message] - SMSNotifier:
SMS to [Phone]: [formatted message]
على سبيل المثال، إذا كانت المدخلات Alerts، و user@mail.com، و Urgent، و 555-1234، و Server down، يجب أن تكون مخرجاتك:
Email to user@mail.com: [Alerts] Server down
SMS to 555-1234: [Urgent] Server downلاحظ كيف يعيد كلا نوعي المنبهات استخدام طريقة Format من BaseNotifier من خلال التضمين، بينما يوفر كل منهما تنفيذ Notify الخاص به. تعمل دالة SendAlert مع أي Notifier دون معرفة النوع الملموس—هذا هو مبدأ التركيب بدلاً من الوراثة في الواقع العملي.
ورقة مرجعية
تستخدم لغة Go التركيب بدلاً من الوراثة (composition over inheritance) لمشاركة الكود بين الأنواع، متجنبةً بذلك مشاكل الارتباط الوثيق والفئة الأساسية الهشة التي تظهر في الوراثة التقليدية الموجهة للكائنات.
بدلاً من التسلسلات الهرمية للفئات، تحقق Go إعادة استخدام الكود من خلال:
- الواجهات (Interfaces) - تحدد عقود السلوك دون تفاصيل التنفيذ
- تضمين الهياكل (Struct embedding) - يسمح للأنواع بتضمين أنواع أخرى وإعادة استخدام دوالها (methods)
بدلاً من قول "الكلب هو حيوان" (الوراثة)، تشجع Go على قول "الكلب لديه سلوكيات تشبه الحيوانات" (التركيب).
مثال على تضمين الهياكل (Struct Embedding)
تعريف هيكل أساسي سيتم تضمينه:
type BaseNotifier struct {
Name string
}
func (b BaseNotifier) Format(message string) string {
return "[" + b.Name + "] " + message
}تضمين الهيكل الأساسي في أنواع أخرى لإعادة استخدام حقوله ودواله:
type EmailNotifier struct {
BaseNotifier // هيكل مضمن
Address string
}
func (e EmailNotifier) Notify(message string) string {
// يمكن استدعاء دالة Format المضمنة مباشرة
return "Email to " + e.Address + ": " + e.Format(message)
}تعريف واجهة (interface) يمكن لعدة أنواع تنفيذها:
type Notifier interface {
Notify(message string) string
}استخدام الواجهة للعمل مع أنواع ملموسة مختلفة بشكل تبادلي:
func SendAlert(n Notifier, message string) string {
return n.Notify(message)
}يوفر هذا النهج وظائف مشابهة للوراثة ولكن مع ارتباط أقل وثوقاً بين الأنواع.
جرّب بنفسك
package main
import (
"bufio"
"fmt"
"os"
)
// SendAlert تقبل أي Notifier ورسالة، وتعيد نتيجة استدعاء Notify
// TODO: قم بتنفيذ دالة SendAlert
func main() {
scanner := bufio.NewScanner(os.Stdin)
// قراءة اسم مرسل البريد الإلكتروني
scanner.Scan()
emailName := scanner.Text()
// قراءة عنوان البريد الإلكتروني
scanner.Scan()
emailAddress := scanner.Text()
// قراءة اسم مرسل الرسائل النصية SMS
scanner.Scan()
smsName := scanner.Text()
// قراءة رقم الهاتف
scanner.Scan()
phoneNumber := scanner.Text()
// قراءة رسالة التنبيه
scanner.Scan()
alertMessage := scanner.Text()
// TODO: قم بإنشاء EmailNotifier باستخدام emailName و emailAddress
// TODO: قم بإنشاء SMSNotifier باستخدام smsName و phoneNumber
// TODO: استخدم SendAlert لإرسال alertMessage عبر كلا المرسلين
// وقم بطباعة النتائج
// كتم تحذيرات المتغيرات غير المستخدمة (قم بإزالتها عند استخدام المتغيرات)
_ = emailName
_ = emailAddress
_ = smsName
_ = phoneNumber
_ = alertMessage
}
يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.
جميع دروس 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