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

قواعد استيفاء الـ Interface

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

بينما تستخدم لغة Go أسلوب duck typing لتحقيق الواجهات (interfaces) بشكل ضمني، هناك قواعد محددة تحدد ما إذا كان النوع يحقق الواجهة بالفعل. يساعدك فهم هذه القواعد على تجنب الأخطاء البرمجية الخفية.

تتضمن القاعدة الأكثر أهمية مستقبلات المؤشر (pointer receivers). إذا تم تعريف method باستخدام pointer receiver، فإن المؤشر لهذا النوع فقط هو الذي يحقق الـ interface — وليس القيمة نفسها:

type Saver interface {
    Save() string
}

type Document struct{ Name string }

func (d *Document) Save() string {  // مستقبل مؤشر (pointer receiver)
    return "Saved: " + d.Name
}

func Process(s Saver) {
    fmt.Println(s.Save())
}

لأن Save() يمتلك مستقبل مؤشر (pointer receiver)، فإن *Document فقط هو ما يحقق Saver:

func main() {
    doc := Document{Name: "report.txt"}
    
    Process(&doc)  // يعمل - المؤشر يحقق الواجهة
    // Process(doc)  // خطأ في التجميع - القيمة لا تحقق الواجهة
}

ومع ذلك، فإن العكس أكثر مرونة. إذا كان للمنهج (method) مستقبل قيمة (value receiver)، فإن كلاً من نوع القيمة ونوع المؤشر يحققان الواجهة (interface). تقوم Go تلقائيًا بفك إسناد المؤشرات (dereferences pointers) عند استدعاء مناهج مستقبل القيمة:

func (d Document) Info() string {  // مستقبل القيمة (value receiver)
    return d.Name
}

// كل من Document و *Document يحققان واجهة تتطلب Info()

يوجد هذا التباين لأن لغة Go يمكنها دائمًا الحصول على قيمة من مؤشر (pointer) (عن طريق dereferencing)، لكنها لا تستطيع دائمًا الحصول على مؤشر من قيمة (فقد لا تكون القيمة قابلة للعنونة/addressable). إن وضع هذه القاعدة في الاعتبار يمنع أخطاء المترجم (compiler) المربكة عند التعامل مع الواجهات (interfaces).

challenge icon

التحدي

سهل

لنقم ببناء نظام تكوين (configuration system) يوضح كيف تؤثر مستقبلات المؤشر (pointer receivers) ومستقبلات القيمة (value receivers) على تحقيق الواجهة (interface satisfaction). ستقوم بإنشاء أنواع حيث يحدد اختيار المستقبل ما إذا كان يمكن استخدام القيم، أو المؤشرات، أو كليهما مع الواجهة.

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

  • config.go: قم بتعريف واجهة Configurable تتطلب طريقتين: GetValue() string و SetValue(string). ثم قم بإنشاء نوعين من التكوين:
    • ReadOnlyConfig مع حقل Value—استخدم مستقبل قيمة (value receiver) لـ GetValue() (يعيد القيمة) و مستقبل مؤشر (pointer receiver) لـ SetValue() (يحدث القيمة).
    • Setting مع حقل Data—استخدم مستقبلات قيمة (value receivers) لكلتا الطريقتين (GetValue تعيد Data، و SetValue تطبع فقط "Cannot modify" دون تغيير أي شيء).
  • processor.go: قم بإنشاء دالة تسمى ProcessConfig تقبل Configurable وسلسلة نصية للقيمة الجديدة. يجب أن تطبع القيمة الحالية باستخدام GetValue()، وتستدعي SetValue() بالقيمة الجديدة، ثم تطبع القيمة مرة أخرى لإظهار أي تغييرات.
  • main.go: اقرأ تفاصيل التكوين من المدخلات ووضح قواعد تحقيق الواجهة:
    1. قم بإنشاء ReadOnlyConfig ومرر مؤشرًا (pointer) إلى ProcessConfig (مطلوب لأن SetValue لديها مستقبل مؤشر).
    2. قم بإنشاء Setting ومرر القيمة مباشرة إلى ProcessConfig (يعمل لأن كلتا الطريقتين لديهما مستقبلات قيمة).

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

  • السطر 1: القيمة الأولية لـ ReadOnlyConfig
  • السطر 2: القيمة الجديدة لتعيينها لـ ReadOnlyConfig
  • السطر 3: القيمة الأولية لـ Setting
  • السطر 4: القيمة الجديدة لمحاولة تعيينها لـ Setting

يجب أن تطبع دالة ProcessConfig بهذا التنسيق:

Current: [value]
Current: [value after SetValue]

على سبيل المثال، إذا كانت المدخلات debug، production، localhost، و remote، يجب أن يكون مخرجك:

Current: debug
Current: production
Current: localhost
Cannot modify
Current: localhost

لاحظ كيف يغير ReadOnlyConfig قيمته بالفعل (لأننا مررنا مؤشرًا)، بينما يظل Setting دون تغيير (لأن SetValue الخاصة به مع مستقبل قيمة لا يمكنها تعديل الأصل). الفكرة الأساسية هي أن قيم ReadOnlyConfig وحدها لن تحقق واجهة Configurable—المؤشرات فقط هي التي تفعل ذلك—بينما تعمل قيم Setting مباشرة لأن جميع طرقها تستخدم مستقبلات قيمة.

ورقة مرجعية

تستخدم لغة Go أسلوب "duck typing" لتحقيق الواجهات (interfaces) بشكل ضمني، ولكن هناك قواعد محددة تتعلق بمستقبلات المؤشرات (pointer receivers) ومستقبلات القيم (value receivers):

قاعدة مستقبل المؤشر (Pointer Receiver Rule)

إذا كانت الطريقة (method) تحتوي على مستقبل مؤشر (pointer receiver)، فإن المؤشر إلى ذلك النوع هو الوحيد الذي يحقق الواجهة:

type Saver interface {
    Save() string
}

type Document struct{ Name string }

func (d *Document) Save() string {  // pointer receiver
    return "Saved: " + d.Name
}

func main() {
    doc := Document{Name: "report.txt"}
    
    Process(&doc)  // works - pointer satisfies interface
    // Process(doc)  // compile error - value doesn't satisfy interface
}

قاعدة مستقبل القيمة (Value Receiver Rule)

إذا كانت الطريقة تحتوي على مستقبل قيمة (value receiver)، فإن كلاً من أنواع القيم والمؤشرات تحقق الواجهة:

func (d Document) Info() string {  // value receiver
    return d.Name
}

// Both Document and *Document satisfy an interface requiring Info()

لماذا يوجد هذا التباين

تستطيع Go دائماً الحصول على قيمة من مؤشر (عن طريق dereferencing)، ولكن لا يمكنها دائماً الحصول على مؤشر من قيمة (لأن القيمة قد لا تكون قابلة للعنونة).

جرّب بنفسك

package main

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

func main() {
	scanner := bufio.NewScanner(os.Stdin)
	
	// قراءة المدخلات
	scanner.Scan()
	rocInitial := scanner.Text()
	scanner.Scan()
	rocNew := scanner.Text()
	scanner.Scan()
	settingInitial := scanner.Text()
	scanner.Scan()
	settingNew := scanner.Text()
	
	// TODO: إنشاء ReadOnlyConfig بالقيمة rocInitial
	// TODO: تمرير POINTER إلى ProcessConfig (مطلوب لأن SetValue لديها pointer receiver)
	
	// TODO: إنشاء Setting بالقيمة settingInitial
	// TODO: تمرير VALUE مباشرة إلى ProcessConfig (يعمل لأن كلا الطريقتين لديهما value receivers)
	
	// استخدم هذه المتغيرات لتجنب أخطاء المتغيرات غير المستخدمة
	_ = rocInitial
	_ = rocNew
	_ = settingInitial
	_ = settingNew
}
quiz iconاختبر نفسك

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

جميع دروس Object Oriented Programming