اذهب إلى المحتوى

كيفية استخدام الواجهات Interfaces في لغة جو Go


هدى جبور

أهم الصفات التي يجب أن تتمتع بها البرامج أو التعليمات البرمجية التي نكتبها، هي المرونة وإمكانية إعادة الاستخدام، إضافةً إلى الصفة التركيبية 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.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...