الواجهات
في لغة Go، الواجهة هي نوع مجرد، يُستخدم لتعريف مجموعة من تواقيع الدوال دون توفير تنفيذ الدوال. الفكرة الأساسية للواجهات هي وصف السلوك، بينما يتم توفير التنفيذ المحدد للسلوك من قبل الأنواع التي تنفذ الواجهة. تستخدم الواجهات على نطاق واسع في لغة Go لتحقيق تعدد الأشكال، الاقتران المفكك وإعادة استخدام الكود.
المفهوم
لتاريخ تطور الواجهات في Go نقطة تحول، في Go1.17 وما قبله، عرّف الدليل الرسمي الواجهة بأنها: مجموعة من الدوال.
An interface type specifies a method set called its interface.
وكان تعريف تنفيذ الواجهة:
A variable of interface type can store a value of any type with a method set that is any superset of the interface. Such a type is said to implement the interface
ترجمته: عندما تكون مجموعة دوال نوع ما مجموعة عليا لمجموعة دوال الواجهة، ويمكن تخزين قيمة هذا النوع في متغير من نوع الواجهة، يُقال أن هذا النوع ينفذ الواجهة.
لكن في Go1.18، تغير تعريف الواجهة لتصبح: مجموعة من الأنواع.
An interface type defines a type set.
وتعريف تنفيذ الواجهة:
A variable of interface type can store a value of any type that is in the type set of the interface. Such a type is said to implement the interface
ترجمته: عندما يكون نوع ما ضمن مجموعة أنواع الواجهة، ويمكن تخزين قيمة هذا النوع في متغير من نوع الواجهة، يُقال أن هذا النوع ينفذ الواجهة. كما تم تقديم التعريفات الإضافية التالية.
عندما تتحقق الحالات التالية، يمكن القول أن النوع T ينفذ الواجهة I
- T ليس واجهة، وهو عنصر في مجموعة أنواع الواجهة I
- T هو واجهة، ومجموعة أنواع T هي مجموعة فرعية من مجموعة أنواع الواجهة I
إذا نفّذ T واجهة، فإن قيم T أيضاً تنفذ الواجهة.
أكبر تغيير في Go 1.18 هو إضافة الأنواع العامة، والتعريف الجديد للواجهات يخدم الأنواع العامة، لكنه لا يؤثر إطلاقاً على استخدام الواجهات السابق. في الوقت نفسه، انقسمت الواجهات إلى فئتين:
- الواجهات الأساسية (
Basic Interface): الواجهات التي تحتوي فقط على مجموعة دوال هي واجهات أساسية - الواجهات العامة (
General Interface): الواجهات التي تحتوي على مجموعة أنواع هي واجهات عامة
ما هي مجموعة الدوال؟ مجموعة الدوال هي مجموعة من الدوال. وبالمثل، مجموعة الأنواع هي مجموعة من الأنواع.
TIP
قد تجد هذا المفهوم صعب الفهم، لكنك في الواقع لا تحتاج لفهم كل ما سبق.
الواجهات الأساسية
ذكرنا سابقاً أن الواجهات الأساسية هي مجموعات دوال، أي مجموعة من الدوال.
التعريف
لنرى كيف تبدو الواجهة.
type Person interface {
Say(string) string
Walk(int)
}هذه واجهة Person، لها دالتان مكشوفتان Walk و Say، في الواجهة تصبح أسماء المعاملات غير مهمة، بالطبع يمكن إضافة أسماء المعاملات وأسماء القيم المُعادة إذا رغبت.
التهيئة
الواجهة وحدها لا يمكن تهيئتها لأنها مجرد مواصفات وليس لها تنفيذ محدد، لكن يمكن تعريفها.
func main() {
var person Person
fmt.Println(person)
}الإخراج
<nil>التنفيذ
لنأخذ مثالاً، شركة بناء تريد رافعة بمواصفات خاصة، فتعطي مواصفات ورسومات خاصة للرافعة، وتحدد أن الرافعة يجب أن تكون قادرة على الرفع والشحن، شركة البناء لا تصنع الرافعات، بل تعطي فقط مواصفات، هذا هو الواجهة، فتتولى الشركة A الطلب، وتصنع رافعة خارقة باستخدام تقنياتها الخاصة وتسلمها لشركة البناء، شركة البناء لا تهتم بالتقنية المستخدمة في التنفيذ، ولا تهتم بالرافعة الخارقة، طالما تستطيع الرفع والشحن، توفير وظائف محددة حسب المواصفات، هذا هو التنفيذ. استخدام الوظائف حسب مواصفات الواجهة فقط، وإخفاء التنفيذ الداخلي، هذا هو البرمجة الموجهة للواجهات. بعد فترة، تعطلت الرافعة الخارقة وهربت الشركة A، فتصنع الشركة B رافعة عملاقة أقوى حسب المواصفات، وبما أن لديها نفس وظائف الرفع والشحن، يمكن استبدالها بالرافعة الخارقة بسلاسة، دون التأثير على进度 البناء، تغيير التنفيذ الداخلي مع بقاء الوظائف كما هي، لا يؤثر على الاستخدام السابق، يمكن الاستبدال بحرية، هذه هي فائدة البرمجة الموجهة للواجهات.
سنستخدم Go لوصف الحالة أعلاه
// واجهة الرافعة
type Crane interface {
JackUp() string
Hoist() string
}
// الرافعة A
type CraneA struct {
work int // الحقول الداخلية المختلفة تمثل تفاصيل داخلية مختلفة
}
func (c CraneA) Work() {
fmt.Println("استخدام تقنية A")
}
func (c CraneA) JackUp() string {
c.Work()
return "jackup"
}
func (c CraneA) Hoist() string {
c.Work()
return "hoist"
}
// الرافعة B
type CraneB struct {
boot string
}
func (c CraneB) Boot() {
fmt.Println("استخدام تقنية B")
}
func (c CraneB) JackUp() string {
c.Boot()
return "jackup"
}
func (c CraneB) Hoist() string {
c.Boot()
return "hoist"
}
type ConstructionCompany struct {
Crane Crane // تخزين الرافعة حسب نوع Crane فقط
}
func (c *ConstructionCompany) Build() {
fmt.Println(c.Crane.JackUp())
fmt.Println(c.Crane.Hoist())
fmt.Println("اكتمل البناء")
}
func main() {
// استخدام الرافعة A
company := ConstructionCompany{CraneA{}}
company.Build()
fmt.Println()
// استبدال بالرافعة B
company.Crane = CraneB{}
company.Build()
}الإخراج
استخدام تقنية A
jackup
استخدام تقنية A
hoist
اكتمل البناء
استخدام تقنية B
jackup
استخدام تقنية B
hoist
اكتمل البناءفي المثال أعلاه، يمكن ملاحظة أن تنفيذ الواجهة ضمني، ويتوافق مع التعريف الرسمي لتنفيذ الواجهة الأساسية: مجموعة الدوال هي مجموعة عليا لمجموعة دوال الواجهة، لذلك في Go، تنفيذ واجهة لا يحتاج إلى كلمة implements لتحديد الواجهة المراد تنفيذها بشكل صريح، طالما تم تنفيذ جميع دوال الواجهة، فقد تم تنفيذ الواجهة. بعد التنفيذ، يمكن تهيئة الواجهة، شركة البناء لديها متغير عضو من نوع Crane، يمكنه حفظ جميع القيم التي نفذت واجهة Crane. بما أنه متغير من نوع Crane، فالدوال المتاحة للوصول هي فقط JackUp و Hoist، والدوال الداخلية الأخرى مثل Work و Boot لا يمكن الوصول إليها.
ذكرنا سابقاً أن أي نوع مخصص يمكن أن يكون له دوال، لذلك حسب تعريف التنفيذ، أي نوع مخصص يمكنه تنفيذ الواجهات، إليك بعض الأمثلة الخاصة.
type Person interface {
Say(string) string
Walk(int)
}
type Man interface {
Exercise()
Person
}مجموعة دوال واجهة Man هي مجموعة عليا من Person، لذلك Man أيضاً ينفذ واجهة Person، لكن هذا أشبه بـ "الوراثة".
type Number int
func (n Number) Say(s string) string {
return "bibibibibi"
}
func (n Number) Walk(i int) {
fmt.Println("cannot walk")
}النوع Number نوعه الأساسي هو int، رغم أن هذا يبدو غريباً في لغات أخرى، لكن مجموعة دوال Number هي بالفعل مجموعة عليا من Person، لذلك يعتبر تنفيذاً.
type Func func()
func (f Func) Say(s string) string {
f()
return "bibibibibi"
}
func (f Func) Walk(i int) {
f()
fmt.Println("cannot walk")
}
func main() {
var function Func
function = func() {
fmt.Println("do somthing")
}
function()
}وبالمثل، نوع الدالة يمكنه تنفيذ الواجهات.
الواجهة الفارغة
type Any interface{
}واجهة Any لا تحتوي على مجموعة دوال، حسب تعريف التنفيذ، جميع الأنواع هي تنفيذ لواجهة Any، لأن مجموعة دوال أي نوع هي مجموعة عليا من المجموعة الفارغة، لذلك واجهة Any يمكنها حفظ قيم أي نوع.
func main() {
var anything Any
anything = 1
println(anything)
fmt.Println(anything)
anything = "something"
println(anything)
fmt.Println(anything)
anything = complex(1, 2)
println(anything)
fmt.Println(anything)
anything = 1.2
println(anything)
fmt.Println(anything)
anything = []int{}
println(anything)
fmt.Println(anything)
anything = map[string]int{}
println(anything)
fmt.Println(anything)
}الإخراج
(0xe63580,0xeb8b08)
1
(0xe63d80,0xeb8c48)
something
(0xe62ac0,0xeb8c58)
(1+2i)
(0xe62e00,0xeb8b00)
1.2
(0xe61a00,0xc0000080d8)
[]
(0xe69720,0xc00007a7b0)
map[]من خلال الإخراج ستكتشف أن نتائج الإخراجين غير متطابقة، في الواقع يمكن اعتبار الواجهة داخلياً كمجموعة من (val,type)، type هو النوع المحدد، عند استدعاء الدوال يتم استدعاء القيمة المحددة للنوع المحدد.
interface{}هذه أيضاً واجهة فارغة، لكنها واجهة فارغة مجهولة، في التطوير عادةً ما تُستخدم الواجهة الفارغة المجهولة لتمثيل قبول أي نوع من القيم، مثال:
func main() {
DoSomething(map[int]string{})
}
func DoSomething(anything interface{}) interface{} {
return anything
}في التحديثات اللاحقة، اقترح المسؤول حلاً آخر، للتسهيل، يمكن استخدام any كبديل لـ interface{}، كلاهما متكافئ تماماً لأن الأول مجرد اسم مستعار للنوع، كالتالي:
type any = interface{}عند مقارنة الواجهات الفارغة، يتم مقارنة نوعها الأساسي، إذا لم يتطابق النوع فالنتيجة false، ثم تتم مقارنة القيم، مثال:
func main() {
var a interface{}
var b interface{}
a = 1
b = "1"
fmt.Println(a == b)
a = 1
b = 1
fmt.Println(a == b)
}الإخراج:
false
trueإذا كان النوع الأساسي غير قابل للمقارنة، فسيحدث panic، في Go، حالة الأنواع المدمجة القابلة للمقارنة كالتالي:
| النوع | قابل للمقارنة | الأساس |
|---|---|---|
| الأنواع الرقمية | نعم | تساوي القيم |
| أنواع السلاسل | نعم | تساوي القيم |
| أنواع المصفوفات | نعم | جميع عناصر المصفوفة متساوية |
| أنواع الشرائح | لا | غير قابل للمقارنة |
| البنى | نعم | جميع قيم الحقول متساوية |
| أنواع map | لا | غير قابل للمقارنة |
| القنوات | نعم | تساوي العناوين |
| المؤشرات | نعم | تساوي العناوين المخزنة في المؤشر |
| الواجهات | نعم | تساوي البيانات المخزنة داخلياً |
في Go هناك نوع واجهة خاص لتمثيل جميع الأنواع القابلة للمقارنة، وهو comparable
type comparable interface{ comparable }TIP
إذا حاولت مقارنة نوع غير قابل للمقارنة، سيحدث panic
الواجهات العامة
الواجهات العامة موجودة لخدمة الأنواع العامة، إتقان الأنواع العامة يعني إتقان الواجهات العامة، راجع الأنواع العامة
