1 مقدمة حول الواجهات
1.1 ما هي الواجهة
في لغة Go، الواجهة هي نوع من الأنواع، وهو نوع مجرد. تخفي الواجهة تفاصيل التنفيذ الخاصة وتعرض سلوك الكائن للمستخدم فقط. تحدد الواجهة مجموعة من الأساليب، لكن هذه الأساليب لا تنفذ أي وظيفة؛ بدلاً من ذلك، يتم توفيرها من قبل النوع المحدد. ميزة واجهات لغة Go هي عدم التداخل، مما يعني أن النوع لا يحتاج إلى إعلان بشكل صريح عن الواجهة التي ينفذها؛ بل يحتاج فقط إلى توفير الأساليب المطلوبة بواسطة الواجهة.
// تعريف واجهة
type Reader interface {
Read(p []byte) (n int, err error)
}
في هذه الواجهة Reader
، يمكن القول إن أي نوع ينفذ الطريقة Read(p []byte) (n int, err error)
يمكن اعتبارها تنفيذ الواجهة Reader
.
2 تعريف الواجهة
2.1 بنية الجملة للواجهات
في لغة Go، تعريف الواجهة كالتالي:
type interfaceName interface {
methodName(parameterList) returnTypeList
}
-
interfaceName
: اسم الواجهة يتبع التقليد في لغة Go، حيث يبدأ بحرف كبير. -
methodName
: اسم الطريقة المطلوبة من الواجهة. -
parameterList
: قائمة المعاملات للطريقة، مع فواصل تفصل بين المعاملات. -
returnTypeList
: قائمة أنواع الإرجاع للطريقة.
إذا نفذ نوعٌ جميع الأساليب في الواجهة، فإن هذا النوع ينفذ الواجهة.
type Worker interface {
Work()
Rest()
في الواجهة Worker
أعلاه، أي نوع يحتوي على طرق Work()
و Rest()
يُرضي الواجهة Worker
.
آلية تنفيذ الواجهة
3.1 قواعد تنفيذ الواجهات
في لغة Go، يحتاج النوع إلى تنفيذ كافة الأساليب في الواجهة فقط ليُعتبر كتنفيذ لتلك الواجهة. هذا التنفيذ ضمني ولا يحتاج إلى إعلان بشكل صريح كما هو الحال في بعض اللغات الأخرى. القواعد لتنفيذ الواجهات على النحو التالي:
- يمكن أن يكون النوع الذي ينفذ الواجهة هيكل بياني أو أي نوع مخصص آخر.
- يجب على النوع أن ينفذ كافة الأساليب في الواجهة ليُعتبر تنفيذاً لتلك الواجهة.
- يجب أن تحتوي الأساليب في الواجهة على نفس توقيع الأساليب التي يتم تنفيذها، بما في ذلك الاسم وقائمة المعاملات وقيم الإرجاع.
- يمكن للنوع أن ينفذ العديد من الواجهات في نفس الوقت.
3.2 مثال: تنفيذ واجهة
الآن دعونا نوضح عملية وطرق تنفيذ الواجهات من خلال مثال محدد. لنأخذ في اعتبارنا واجهة Speaker
:
type Speaker interface {
Speak() string
}
لنجعل النوع Human
ينفذ الواجهة Speaker
، نحتاج إلى تعريف طريقة Speak
للنوع Human
:
type Human struct {
Name string
}
// طريقة Speak تتيح للنوع Human تنفيذ الواجهة Speaker
func (h Human) Speak() string {
return "مرحبًا، اسمي " + h.Name
}
func main() {
var speaker Speaker
james := Human{"جيمس"}
speaker = james
fmt.Println(speaker.Speak()) // الناتج: مرحبًا، اسمي جيمس
}
في الكود أعلاه، ينفذ هيكلبياني Human
واجهة Speaker
بتنفيذ طريقة Speak()
، يمكن رؤية في الوظيفة main
أن متغير النوع Human
james
يتم تعيينه لمتغير الواجهة Speaker
بسبب إرضاء james
لواجهة Speaker
.
4 فوائد وحالات الاستخدام لاستخدام الواجهات
4.1 فوائد استخدام الواجهات
هناك العديد من الفوائد لاستخدام الواجهات:
- فصل الترابط: تتيح الواجهات لكتابتنا الفصل عن تفاصيل التنفيذ الخاصة، مما يحسن من مرونة الكود وسهولة صيانته.
- الاستبدالية: تجعل الواجهات من السهل بالنسبة لنا استبدال التنفيذات الداخلية، طالما أن التنفيذ الجديد يرضي نفس الواجهة.
- القابلية للتوسعة: تتيح الواجهات لنا توسيع وظائف البرنامج دون تعديل الكود الحالي.
- سهولة الاختبار: تجعل الواجهات اختبار وحدة الكود بسيطاً. يمكننا استخدام الكائنات المزيفة لتنفيذ الواجهات في اختبار الكود.
- التعددية الشكلية: تنفذ الواجهات التعددية الشكلية، مما يتيح للكائنات المختلفة الاستجابة لنفس الرسالة بطرق مختلفة في سيناريوهات مختلفة.
4.2 سيناريوهات تطبيق الواجهات
تُستخدم الواجهات على نطاق واسع في لغة Go. وفيما يلي بعض السيناريوهات التطبيقية النموذجية:
-
الواجهات في المكتبة القياسية: على سبيل المثال، تُستخدم واجهات
io.Reader
وio.Writer
على نطاق واسع لمعالجة الملفات وبرمجة الشبكات. -
الفرز: يُمكن القيام بفرز أي شريحة مخصصة عن طريق تنفيذ أساليب
Len()
،Less(i, j int) bool
، وSwap(i, j int)
في واجهةsort.Interface
. -
معالجات HTTP: من خلال تنفيذ أسلوب
ServeHTTP(ResponseWriter, *Request)
في واجهةhttp.Handler
، يُمكن إنشاء معالجات HTTP مخصصة.
وفيما يلي مثال على استخدام الواجهات للفرز:
package main
import (
"fmt"
"sort"
)
type AgeSlice []int
func (a AgeSlice) Len() int { return len(a) }
func (a AgeSlice) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a AgeSlice) Less(i, j int) bool { return a[i] < a[j] }
func main() {
ages := AgeSlice{45, 26, 74, 23, 46, 12, 39}
sort.Sort(ages)
fmt.Println(ages) // Output: [12 23 26 39 45 46 74]
}
في هذا المثال، من خلال تنفيذ ثلاثة أساليب في sort.Interface
، يُمكننا فرز شريحة AgeSlice
، مما يوضح قدرة الواجهات على توسيع سلوك أنواع موجودة.
5 ميزات متقدمة للواجهات
5.1 الواجهة الفارغة وتطبيقاتها
في لغة Go، الواجهة الفارغة هي نوع واجهة خاص لا يحتوي على أي أساليب. لذلك، يُمكن اعتبار أي نوع تقريبًا لقيمة كواجهة فارغة. ويُمثل الواجهة الفارغة باستخدام interface{}
وتلعب أدوارًا مهمة في لغة Go كنوع مرن للغاية.
// تحديد واجهة فارغة
var any interface{}
معالجة الأنواع الديناميكية:
يُمكن أن تخزن الواجهة الفارغة قيم أي نوع، مما يجعلها مفيدة جدًا لمعالجة الأنواع غير المؤكدة. على سبيل المثال، عند بناء دالة تقبل معاملات من أنواع مختلفة، يُمكن استخدام الواجهة الفارغة كنوع للمعلمة لقبول أي نوع من البيانات.
func PrintAnything(v interface{}) {
fmt.Println(v)
}
func main() {
PrintAnything(123)
PrintAnything("hello")
PrintAnything(struct{ name string }{name: "Gopher"})
}
في المثال أعلاه، تأخذ الدالة PrintAnything
معلمة من نوع واجهة فارغة v
وتطبعها. يُمكن لـ PrintAnything
التعامل سواء تم تمرير عدد صحيح أو سلسلة أو هيكل بيانات.
5.2 تضمين الواجهات
تتضمن الواجهة تحتوي على جميع أساليب واجهة أخرى، وربما إضافة بعض الأساليب الجديدة. يُتحقق ذلك من خلال تضمين الواجهات الأخرى في تعريف الواجهة.
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
// تضمين الواجهة ReadWriter الواجهة Reader والواجهة Writer
type ReadWriter interface {
Reader
Writer
}
باستخدام تضمين الواجهات، يُمكننا بناء هيكل واجهة أكثر تنظيمًا وتسلسلية. في هذا المثال، تدمج الواجهة ReadWriter
أساليب الواجهات Reader
و Writer
، وتحقق انصهارًا لوظائف القراءة والكتابة.
5.3 تأكيد نوع الواجهة
تأكيد النوع هو عملية لفحص وتحويل قيم النوع الواجهة. عندما نحتاج إلى استخراج نوع محدد من قيمة النوع الواجهة، يُصبح تأكيد النوع مفيدًا جدًا.
الصيغة الأساسية للتأكيد:
value, ok := interfaceValue.(Type)
إذا نجح التأكيد، ستكون value
قيمة النوع الأساسي Type
، وستكون ok
تساوي true
؛ إذا فشل التأكيد، ستكون value
صفر القيمة للنوع Type
، وستكون ok
تساوي false
.
var i interface{} = "hello"
// تأكيد النوع
s, ok := i.(string)
if ok {
fmt.Println(s) // الناتج: hello
}
// تأكيد نوع غير فعلي
f, ok := i.(float64)
if !ok {
fmt.Println("فشل التأكيد!") // الناتج: فشل التأكيد!
سيناريوهات التطبيق:
يُستخدم تأكيد النوع بشكل شائع لتحديد وتحويل نوع القيم في الواجهة الفارغة interface{}
، أو في حالة تنفيذ العديد من الواجهات، لاستخراج النوع الذي ينفذ واجهة محددة.
5.4 الواجهة والتعددية
التعددية هي مفهوم أساسي في البرمجة الشيئية، حيث تسمح بمعالجة أنواع بيانات مختلفة بطريقة موحدة، وذلك من خلال الواجهات فقط، دون الاهتمام بالأنواع المحددة. في لغة Go، تعتبر الواجهات المفتاح لتحقيق التعددية.
تنفيذ التعددية من خلال الواجهات
type Shape interface {
Area() float64
}
type Rectangle struct {
Width, Height float64
}
type Circle struct {
Radius float64
}
// المستطيل ينفذ واجهة الشكل
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
// الدائرة تنفذ واجهة الشكل
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
// حساب مساحة أشكال مختلفة
func CalculateArea(s Shape) float64 {
return s.Area()
}
func main() {
r := Rectangle{Width: 3, Height: 4}
c := Circle{Radius: 5}
fmt.Println(CalculateArea(r)) // الناتج: مساحة المستطيل
fmt.Println(CalculateArea(c)) // الناتج: مساحة الدائرة
}
في هذا المثال، تعرف الواجهة Shape
طريقة Area
لأشكال مختلفة. كل من أنواع Rectangle
و Circle
المحددة تنفذ هذه الواجهة، مما يعني أن لديها القدرة على حساب المساحة. يقوم الدالة CalculateArea
بأخذ معلمة من نوع واجهة Shape
ويمكنها حساب مساحة أي شكل ينفذ الواجهة Shape
.
بهذه الطريقة، يمكننا بسهولة إضافة أنواع أشكال جديدة دون الحاجة إلى تعديل تنفيذ دالة CalculateArea
. هذه هي المرونة والقابلية للتوسيع التي يجلبها التعددية إلى الكود.