قواعد استيفاء الـ 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).
التحدي
سهللنقم ببناء نظام تكوين (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: اقرأ تفاصيل التكوين من المدخلات ووضح قواعد تحقيق الواجهة:- قم بإنشاء
ReadOnlyConfigومرر مؤشرًا (pointer) إلىProcessConfig(مطلوب لأنSetValueلديها مستقبل مؤشر). - قم بإنشاء
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
}
يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.
جميع دروس 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