Skip to content

الواجهات

في لغة 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

قد تجد هذا المفهوم صعب الفهم، لكنك في الواقع لا تحتاج لفهم كل ما سبق.

الواجهات الأساسية

ذكرنا سابقاً أن الواجهات الأساسية هي مجموعات دوال، أي مجموعة من الدوال.

التعريف

لنرى كيف تبدو الواجهة.

go
type Person interface {
  Say(string) string
  Walk(int)
}

هذه واجهة Person، لها دالتان مكشوفتان Walk و Say، في الواجهة تصبح أسماء المعاملات غير مهمة، بالطبع يمكن إضافة أسماء المعاملات وأسماء القيم المُعادة إذا رغبت.

التهيئة

الواجهة وحدها لا يمكن تهيئتها لأنها مجرد مواصفات وليس لها تنفيذ محدد، لكن يمكن تعريفها.

go
func main() {
   var person Person
   fmt.Println(person)
}

الإخراج

 <nil>

التنفيذ

لنأخذ مثالاً، شركة بناء تريد رافعة بمواصفات خاصة، فتعطي مواصفات ورسومات خاصة للرافعة، وتحدد أن الرافعة يجب أن تكون قادرة على الرفع والشحن، شركة البناء لا تصنع الرافعات، بل تعطي فقط مواصفات، هذا هو الواجهة، فتتولى الشركة A الطلب، وتصنع رافعة خارقة باستخدام تقنياتها الخاصة وتسلمها لشركة البناء، شركة البناء لا تهتم بالتقنية المستخدمة في التنفيذ، ولا تهتم بالرافعة الخارقة، طالما تستطيع الرفع والشحن، توفير وظائف محددة حسب المواصفات، هذا هو التنفيذ. استخدام الوظائف حسب مواصفات الواجهة فقط، وإخفاء التنفيذ الداخلي، هذا هو البرمجة الموجهة للواجهات. بعد فترة، تعطلت الرافعة الخارقة وهربت الشركة A، فتصنع الشركة B رافعة عملاقة أقوى حسب المواصفات، وبما أن لديها نفس وظائف الرفع والشحن، يمكن استبدالها بالرافعة الخارقة بسلاسة، دون التأثير على进度 البناء، تغيير التنفيذ الداخلي مع بقاء الوظائف كما هي، لا يؤثر على الاستخدام السابق، يمكن الاستبدال بحرية، هذه هي فائدة البرمجة الموجهة للواجهات.

سنستخدم Go لوصف الحالة أعلاه

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 لا يمكن الوصول إليها.

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

go
type Person interface {
   Say(string) string
   Walk(int)
}

type Man interface {
   Exercise()
   Person
}

مجموعة دوال واجهة Man هي مجموعة عليا من Person، لذلك Man أيضاً ينفذ واجهة Person، لكن هذا أشبه بـ "الوراثة".

go
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، لذلك يعتبر تنفيذاً.

go
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()
}

وبالمثل، نوع الدالة يمكنه تنفيذ الواجهات.

الواجهة الفارغة

go
type Any interface{

}

واجهة Any لا تحتوي على مجموعة دوال، حسب تعريف التنفيذ، جميع الأنواع هي تنفيذ لواجهة Any، لأن مجموعة دوال أي نوع هي مجموعة عليا من المجموعة الفارغة، لذلك واجهة Any يمكنها حفظ قيم أي نوع.

go
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 هو النوع المحدد، عند استدعاء الدوال يتم استدعاء القيمة المحددة للنوع المحدد.

go
interface{}

هذه أيضاً واجهة فارغة، لكنها واجهة فارغة مجهولة، في التطوير عادةً ما تُستخدم الواجهة الفارغة المجهولة لتمثيل قبول أي نوع من القيم، مثال:

go
func main() {
   DoSomething(map[int]string{})
}

func DoSomething(anything interface{}) interface{} {
   return anything
}

في التحديثات اللاحقة، اقترح المسؤول حلاً آخر، للتسهيل، يمكن استخدام any كبديل لـ interface{}، كلاهما متكافئ تماماً لأن الأول مجرد اسم مستعار للنوع، كالتالي:

go
type any = interface{}

عند مقارنة الواجهات الفارغة، يتم مقارنة نوعها الأساسي، إذا لم يتطابق النوع فالنتيجة false، ثم تتم مقارنة القيم، مثال:

go
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

go
type comparable interface{ comparable }

TIP

إذا حاولت مقارنة نوع غير قابل للمقارنة، سيحدث panic

الواجهات العامة

الواجهات العامة موجودة لخدمة الأنواع العامة، إتقان الأنواع العامة يعني إتقان الواجهات العامة، راجع الأنواع العامة

Golang تم تحريره بواسطة www.golangdev.cn