الدمج
لا توفّر Go مفهوم الوراثة المعتاد القائم على الأنواع، لكنها تملك القدرة على «استعارة» أجزاء من تنفيذ ما بتضمين الأنواع داخل بنية أو واجهة.
وتضمين الواجهات أمر بسيط جدًا. لقد ذكرنا واجهتَي io.Reader وio.Writer من قبل؛ وهذه تعريفاتهما:
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
وتصدّر الحزمة io أيضًا عدة واجهات أخرى تحدّد كائنات تستطيع تنفيذ عدة من هذه الدوال. فمثلًا هناك io.ReadWriter، وهي واجهة تحوي Read وWrite معًا. يمكننا أن نحدّد io.ReadWriter بذكر الدالتين صراحةً، لكن تضمين الواجهتين لتكوين الواجهة الجديدة أسهل وأوضح:
// ReadWriter is the interface that combines the Reader and Writer interfaces.
type ReadWriter interface {
Reader
Writer
}
وهذا يقول ما يبدو تمامًا: تستطيع ReadWriter أن تفعل ما تفعله Reader وما تفعله Writer؛ إنها اتحاد للواجهات المُدمجة. ولا يمكن تضمين سوى الواجهات داخل الواجهات.
وتنطبق الفكرة الأساسية نفسها على البنى، لكن نتائجها أبعد مدًى. فلدى الحزمة bufio نوعا بنية، هما bufio.Reader وbufio.Writer، وكلاهما ينفّذ بطبيعة الحال الواجهتين المقابلتين من الحزمة io. كما أن bufio تنفّذ قارئًا/كاتبًا مُخزَّنًا مؤقتًا، وذلك بدمج قارئ وكاتب في بنية واحدة باستخدام التضمين: فهي تذكر الأنواع داخل البنية لكنها لا تعطيها أسماء حقول.
// ReadWriter stores pointers to a Reader and a Writer.
// It implements io.ReadWriter.
type ReadWriter struct {
*Reader // *bufio.Reader
*Writer // *bufio.Writer
}
والعناصر المُدمجة هي مؤشرات إلى بنى، ويجب بالطبع تهيئتها لتشير إلى بنى صالحة قبل أن تُستعمل. ويمكن كتابة بنية ReadWriter هكذا:
type ReadWriter struct {
reader *Reader
writer *Writer
}
لكن عندها نحتاج — لترقية دوال الحقول ولإشباع واجهات io — إلى توفير دوال تمرير (forwarding methods) هكذا:
func (rw *ReadWriter) Read(p []byte) (n int, err error) {
return rw.reader.Read(p)
}
وبتضمين البنى مباشرةً، نتفادى هذه الأعمال الكتابية كلها. فدوال الأنواع المُدمجة تأتي مجّانًا، أي أن bufio.ReadWriter لا يملك دوال bufio.Reader وbufio.Writer فحسب، بل يشبع أيضًا الواجهات الثلاث: io.Reader وio.Writer وio.ReadWriter.
وهناك فرق مهم بين التضمين والوراثة. فعند تضمين نوع، تصبح دوال ذلك النوع دوال للنوع الخارجي، غير أن المُستقبِل عند نداء تلك الدوال هو النوع الداخلي لا الخارجي. ففي مثالنا، حين تُندعى دالة Read الخاصة بـ bufio.ReadWriter، يكون لها الأثر نفسه تمامًا الذي للدالة المكتوبة أعلاه؛ فالمُستقبِل هو الحقل reader داخل ReadWriter لا ReadWriter نفسها.
ويمكن أن يكون التضمين مجرّد راحة. فالمثال التالي يُظهر حقلًا مُدمجًا إلى جانب حقل عادي مُسمّى:
type Job struct {
Command string
*log.Logger
}
أصبح لدى النوع Job الآن دوال Print وPrintf وPrintln وغيرها من دوال *log.Logger. كان بإمكاننا أن نُعطي Logger اسم حقل، لكن ذلك ليس ضروريًا. والآن، بعد التهيئة، يمكننا أن نسجّل في Job:
job.Println("starting now...")
وLogger حقل عادي في بنية Job، ويمكن تهيئته على النحو المعتاد داخل مُنشئ Job، هكذا:
func NewJob(command string, logger *log.Logger) *Job {
return &Job{command, logger}
}
أو بقيمة مركّبة:
job := &Job{command, log.New(os.Stderr, "Job: ", log.Ldate)}
وإن احتجنا الإشارة إلى حقل مُدمج مباشرةً، فإن اسم نوع الحقل — متجاهلين مُحدِّد الحزمة — يعمل اسم حقل، كما كان في دالة Read الخاصة ببنية ReadWriter. وهنا، إن احتجنا الوصول إلى *log.Logger الخاص بمتغيّر job من النوع Job، لكتَبنا job.Logger، وهو ما يكون مفيدًا لو أردنا تنقيح دوال Logger:
func (job *Job) Printf(format string, args ...interface{}) {
job.Logger.Printf("%q: %s", job.Command, fmt.Sprintf(format, args...))
}
ويُقدّم تضمين الأنواع مشكلة تعارض الأسماء، لكن قواعد حلّها بسيطة. أولًا، الحقل أو الدالة X تحجب أي عنصر آخر اسمُه X في جزء أعماق تداخلًا داخل النوع. فإذا كان log.Logger يحوي حقلًا أو دالة اسمُها Command، لطغى حقل Command الخاص بـ Job عليها.
ثانيًا، إن ظهر الاسم نفسه على مستوى التداخل نفسه، فذلك عادةً خطأ؛ ومن الخطأ تضمين log.Logger إذا كانت بنية Job تحوي حقلًا أو دالة أخرى اسمُها Logger. لكن إن لم يُذكر الاسم المكرّر في البرنامج خارج تعريف النوع، فلا بأس. ويقدّم هذا التقييد قدرًا من الحماية ضد التغييرات التي تُجرى على أنواع مُدمجة من الخارج: فلا مشكلة إن أُضيف حقل يتعارض مع حقل آخر في نوع فرعي آخر، ما دام لم يُستعمل أيٌّ منهما أبدًا.