أهم الصفات التي يجب أن تتمتع بها البرامج أو التعليمات البرمجية التي نكتبها، هي المرونة وإمكانية إعادة الاستخدام، إضافةً إلى الصفة التركيبية modular، إذ تُعد هذه الصفات الثلاثة أمرًا ضروريًّا لتطوير برامج متعددة الاستخدامات، كما أنها تُسهّل عمليات التعديل والصيانة على البرامج، فمثلًا إذا احتجنا لتعديل بسيط على البرنامج، سيكون بالإمكان إجراء هذا التعديل في مكانٍ واحد بدلًا من إجراء نفس التعديل في أماكن متعددة من البرنامج.
تختلف كيفية تحقيق تلك الصفات من لغة إلى أخرى، فعلى سبيل المثال، يٌستخدم مفهوم الوراثة في لغات مثل جافا Java و ++C و #C لتحقيق ذلك. يمكن للمطورين أيضًا تحقيق أهداف التصميم هذه من خلال التركيب Composition، وهي طريقة لدمج الكائنات أو أنواع البيانات في أنواع أكثر تعقيدًا، وهو النهج المُستخدم في لغة جو لتحقيق الصفات السابقة. توفِّر الواجهات في لغة جو توابعًا لتنظيم التراكيب المعقدة، وسيتيح لك تعلم كيفية استخدامها إنشاء شيفرة مشتركة وقابلة لإعادة الاستخدام.
ستتعلم في هذا المقال كيفية تركيب أنواع مخصصة لها سلوكيات مشتركة، مما سيتيح لنا إعادة استخدام الشيفرة التي نكتبها، وسنتعلم أيضًا كيفية تحقيق الواجهات للأنواع المخصصة التي تتوافق مع الواجهات المُعرّفة في حزم أخرى.
تعريف سلوك Behavior
تُعد الواجهات من العناصر الأساسية للتركيب، فهي تعرّف سلوك نوعٍ ما، وتُعد الواجهة fmt.Stringer
من أكثر الواجهات شيوعًا في مكتبة جو القياسية:
type Stringer interface { String() string }
نُعرّف في السطر الأول من الشيفرة السابقة نوعًا جديدًا يُدعى Stringer
، ونحدد أنه واجهة. بعد ذلك، نكتب محتويات هذه البنية بين قوسين {}
، إذ ستُعرِّف هذه المحتويات سلوك الواجهة، أي ما الذي تفعله الواجهة؛ فبالنسبة للواجهة السابقة Stringer
، من الواضح أن السلوك الوحيد فيها هو تابع ()String
لا يأخذ أي وسطاء ويعيد سلسلةً نصية.
سنرى الآن بعض المقتطفات البرمجية التي تمتلك سلوك الواجهة fmt.Stringer
:
package main import "fmt" type Article struct { Title string Author string } func (a Article) String() string { return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author) } func main() { a := Article{ Title: "Understanding Interfaces in Go", Author: "Sammy Shark", } fmt.Println(a.String()) }
هنا نُنشئ نوعًا جديدًا اسمه Article
، ويمتلك حقلين هما Title
و Author
، وكلاهما من نوع سلاسل نصية.
... type Article struct { Title string Author string } …
نُعرّف بعد ذلك تابعًا يسمى String
على النوع Article
، بحيث يعيد هذا التابع سلسلةً تمثل هذا النوع، أي محتوياته عمليًّا:
... func (a Article) String() string { return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author) } ...
نُعرّف بعد ذلك في الدالة main
متغيرًا من النوع Article
ونسميه a
، ونسند السلسلة "Understanding Interfaces in Go" إلى الحقل Title
والسلسلة "Sammy Shark" للحقل Author
:
... a := Article{ Title: "Understanding Interfaces in Go", Author: "Sammy Shark", } ...
نطبع بعد ذلك نتيجة التابع String
من خلال استدعاء الدالة fmt.Println
وتمرير نتيجة استدعاء التابع ()a.String
إليها:
... fmt.Println(a.String())
ستحصل عند تشغيل البرنامج على:
The "Understanding Interfaces in Go" article was written by Sammy Shark.
لم نستخدم واجهةً حتى الآن، لكننا أنشأنا نوعًا يمتلك سلوكًا يطابق سلوك الواجهة fmt.Stringer
. دعنا نرى كيف يمكننا استخدام هذا السلوك لجعل الشيفرة الخاصة بنا أكثر قابلية لإعادة الاستخدام.
تعريف واجهة Interface
بعد أن عرّفنا نوعًا جديدًا مع سلوك مُحدد، سنرى كيف يمكننا استخدام هذا السلوك، لكن قبل ذلك سنلقي نظرةً على ما سنحتاج إلى فعله إذا أردنا استدعاء التابع String
من نوع Article
داخل دالة:
package main import "fmt" type Article struct { Title string Author string } func (a Article) String() string { return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author) } func main() { a := Article{ Title: "Understanding Interfaces in Go", Author: "Sammy Shark", } Print(a) } func Print(a Article) { fmt.Println(a.String()) }
أضفنا في هذه الشيفرة دالةً جديدةً تسمى Print
، تأخذ وسيطًا من النوع Article
. لاحظ أن كل ما تفعله هذه الدالة هو أنها تستدعي التابع String
، لذا يمكننا بدلًا من ذلك تعريف واجهة للتمرير إلى الدالة:
package main import "fmt" type Article struct { Title string Author string } func (a Article) String() string { return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author) } type Stringer interface { String() string } func main() { a := Article{ Title: "Understanding Interfaces in Go", Author: "Sammy Shark", } Print(a) } func Print(s Stringer) { fmt.Println(s.String()) }
ننشئ هنا واجهةً اسمها Stringer
:
... type Stringer interface { String() string } ...
تتضمّن هذه الواجهة تابعًا وحيدًا يسمى ()String
يعيد سلسلةً، ويُعرّف هذا التابع على نوع بيانات مُحدد، وعلى عكس الدوال فلا يمكن له أن يُستدعى إلا من متغير من هذا النوع.
نعدّل بعد ذلك بصمة الدالة Print
بحيث تستقبل وسيطًا من النوع Stringer
الذي يُمثّل واجهةً، وليس نوعًا مُحددًا مثل Article
. بما أن المصرّف يعرف أن Stringer
هي واجهة تمتلك التابع String
، فلن يقبل إلا الأنواع التي تُحقق هذا التابع.
يمكننا الآن استخدام الدالة Print
مع أي نوع يتوافق مع الواجهة Stringer
. دعنا ننشئ نوعًا آخر لتوضيح ذلك:
package main import "fmt" type Article struct { Title string Author string } func (a Article) String() string { return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author) } type Book struct { Title string Author string Pages int } func (b Book) String() string { return fmt.Sprintf("The %q book was written by %s.", b.Title, b.Author) } type Stringer interface { String() string } func main() { a := Article{ Title: "Understanding Interfaces in Go", Author: "Sammy Shark", } Print(a) b := Book{ Title: "All About Go", Author: "Jenny Dolphin", Pages: 25, } Print(b) } func Print(s Stringer) { fmt.Println(s.String()) }
عرّفنا هنا نوع بيانات جديد يُسمى Book
يمتلك التابع String
، وبالتالي يُحقق الواجهة Stringer
، وبالتالي يمكننا استخدامه مثل وسيط للدالة Print
.
The "Understanding Interfaces in Go" article was written by Sammy Shark. The "All About Go" book was written by Jenny Dolphin. It has 25 pages.
أوضحنا إلى الآن كيفية استخدام واجهة واحدة فقط، ويمكن عمومًا أن يكون للواجهة أكثر من سلوك معرّف. سنرى فيما يلي كيف يمكننا جعل واجهاتنا أكثر تنوعًا من خلال التصريح عن المزيد من التوابع.
تعدد السلوكيات في الواجهة
تُعد كتابة أنواع صغيرة وموجزة وتركيبها في أنواع أكبر وأكثر تعقيدًا من الأمور الجيدة عند كتابة الشيفرات في لغة جو، وينطبق الشيء نفسه عند إنشاء واجهات. لمعرفة كيفية إنشاء الواجهة، سنبدأ أولًا بتعريف واجهة واحدة فقط. سنحدد شكلين؛ دائرة Circle
ومربع Square
، وسيُعرّف كلاهما تابعًا يُسمى المساحة Area
يعيد المساحة الهندسية للشكل:
package main import ( "fmt" "math" ) type Circle struct { Radius float64 } func (c Circle) Area() float64 { return math.Pi * math.Pow(c.Radius, 2) } type Square struct { Width float64 Height float64 } func (s Square) Area() float64 { return s.Width * s.Height } type Sizer interface { Area() float64 } func main() { c := Circle{Radius: 10} s := Square{Height: 10, Width: 5} l := Less(c, s) fmt.Printf("%+v is the smallest\n", l) } func Less(s1, s2 Sizer) Sizer { if s1.Area() < s2.Area() { return s1 } return s2 }
بما أن النوعين يُصرحان عن تابع Area
، يمكننا إنشاء واجهة تحدد هذا السلوك. نُنشئ واجهة Sizer
التالية:
... type Sizer interface { Area() float64 } …
نعرّف بعد ذلك دالةً تسمى Less
تأخذ واجهتين Sizer
مثل وسيطين وتعيد أصغر واحدة:
... func Less(s1, s2 Sizer) Sizer { if s1.Area() < s2.Area() { return s1 } return s2 } ...
لاحظ أن معاملات الدالة وكذلك القيمة المُعادة هي من النوع Sizer
، وهذا يعني أننا لا نعيد مربعًا أو دائرة، بل نعيد واجهة Sizer
.
والآن نطبع الوسيط الذي لديه أصغر مساحة:
{Width:5 Height:10} is the smallest
سنضيف الآن سلوكًا آخر لكل نوع، وسنضيف التابع ()String
الذي يعيد سلسلةً نصيةً، وهذا بدوره سيؤدي إلى تحقيق الواجهة fmt.Stringer
:
package main import ( "fmt" "math" ) type Circle struct { Radius float64 } func (c Circle) Area() float64 { return math.Pi * math.Pow(c.Radius, 2) } func (c Circle) String() string { return fmt.Sprintf("Circle {Radius: %.2f}", c.Radius) } type Square struct { Width float64 Height float64 } func (s Square) Area() float64 { return s.Width * s.Height } func (s Square) String() string { return fmt.Sprintf("Square {Width: %.2f, Height: %.2f}", s.Width, s.Height) } type Sizer interface { Area() float64 } type Shaper interface { Sizer fmt.Stringer } func main() { c := Circle{Radius: 10} PrintArea(c) s := Square{Height: 10, Width: 5} PrintArea(s) l := Less(c, s) fmt.Printf("%v is the smallest\n", l) } func Less(s1, s2 Sizer) Sizer { if s1.Area() < s2.Area() { return s1 } return s2 } func PrintArea(s Shaper) { fmt.Printf("area of %s is %.2f\n", s.String(), s.Area()) }
بما أن النوعين Circle
و Square
ينفذّان التابعين Area
و String
، سيكون بالإمكان إنشاء واجهة أخرى لوصف تلك المجموعة الأوسع من السلوك. لأجل ذلك سننشئ واجهةً تسمى Shaper
من واجهة Sizer
وواجهة fmt.Stringer
:
... type Shaper interface { Sizer fmt.Stringer } ...
ملاحظة: حبذا أن ينتهي اسم الواجهة بالحرفين er
مثل fmt.Stringer
و io.Writer
، إلخ. ولهذا السبب أطلقنا على واجهتنا اسم Shaper
، وليس Shape
.
يمكننا الآن إنشاء دالة تسمى PrintArea
تأخذ وسيطًا من النوع Shaper
. هذا يعني أنه يمكننا استدعاء التابعين على القيمة التي تُمرّر لكل من Area
و String
:
... func PrintArea(s Shaper) { fmt.Printf("area of %s is %.2f\n", s.String(), s.Area()) }
ستحصل عند تشغيل البرنامج على الخرج التالي:
area of Circle {Radius: 10.00} is 314.16 area of Square {Width: 5.00, Height: 10.00} is 50.00 Square {Width: 5.00, Height: 10.00} is the smallest
رأينا كيف يمكننا إنشاء واجهات أصغر وبناء واجهات أكبر حسب الحاجة. كان بإمكاننا البدء بالواجهة الأكبر وتمريرها إلى جميع الدوال، إلا أن الممارسات الجيدة تقتضي إرسال أصغر واجهة فقط إلى الدالة المطلوبة، إذ أن ذلك ضروري لجعل الشيفرة واضحة أكثر، فإذا كانت الدالة تهدف لإجراء سلوك محدد، وهذا السلوك مُعرّف في واجهة أصغر، فحبذا أن تُمرر الواجهة الأصغر وليس الواجهة الأكبر (التي تحتوي الواجهة الأصغر).
إذا مررنا مثلًا الواجهة Shaper
إلى الدالة Less
، فهنا نفترض أنها ستستدعي كلًا من التابعين Area
و String
، لكنها لا تستدعي إلا التابع Area
، وهذا سيجعل الدالة أقل وضوحًا، كما أننا نعلم أنه يمكننا فقط استدعاء التابع Area
لأي وسيط يُمرّر إليه.
خاتمة
تعلمنا في هذا المقال كيفية إنشاء واجهات صغيرة وكيفية توسيعها لتصبح واجهات أكبر، وكيفية مشاركة الأشياء التي نريدها فقط مع دالة أو تابع من خلال تلك الواجهات. تعلمنا أيضًا كيفية تركيب واجهة من واجهات أصغر أو من واجهات موجودة في حزم أخرى وليس فقط الحزمة التي نعمل ضمنها.
ترجمة -وبتصرف- للمقال How To Use Interfaces in Go لصاحبه Gopher Guides.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.