الواجهات

الدرس 9: الواجهات

تشبيه من الواقع

تخيل أنك تذهب إلى مطعم لطلب طعام. لا تحتاج لمعرفة من الطاهي، أي مقلاة يستخدم، أو كيف يطبخ — تنظر فقط إلى القائمة وتطلب "دجاج كونغ باو." القائمة هي واجهة: تُعرّف "ما يمكن فعله" دون الاهتمام بـ "من يفعله" أو "كيف يُفعل."

في Go، الواجهات تعمل بنفس الطريقة. تُعرّف مجموعة من توقيعات الأساليب، وأي نوع يُنفذ هذه الأساليب يُلبي الواجهة تلقائيًا — دون حاجة لإعلان صريح.


المفاهيم الأساسية

المفهوم الوصف
الواجهة مجموعة من توقيعات الأساليب تُحدد عقد سلوكي
التطبيق الضمني نوع يُلبي الواجهة تلقائيًا إذا نفّذ جميع أساليب الواجهة
typing البطة "إذا مشت كبطة وصرّت كبطة، فهي بطة"
الواجهة الفارغة interface{} لا تحتوي أساليب؛ أي نوع يُلبيها
تأكيد النوع استخراج نوع محدد من قيمة واجهة
تركيب الواجهات بناء واجهات أكبر عبر تضمين عدة واجهات

الصيغة الأساسية والاستخدام

تعريف واجهة

GO
// تعريف واجهة Speaker
type Speaker interface {
    Speak() string
}

التطبيق الضمني

GO
// نوع Dog يُنفذ واجهة Speaker (لا حاجة لإعلان)
type Dog struct {
    Name string
}

func (d Dog) Speak() string {
    return "Woof! I'm " + d.Name
}

// نوع Cat يُنفذ أيضًا واجهة Speaker
type Cat struct {
    Name string
}

func (c Cat) Speak() string {
    return "Meow! I'm " + c.Name
}
💡 نصيحة: ليس في Go كلمة implements. طالما أن نوعًا لديه جميع الأساليب المطلوبة من واجهة، يُنفذ تلك الواجهة تلقائيًا.

استخدام الواجهات

GO
func makeItSpeak(s Speaker) {
    fmt.Println(s.Speak())
}

func main() {
    dog := Dog{Name: "Rex"}
    cat := Cat{Name: "Whiskers"}

    makeItSpeak(dog) // الناتج: Woof! I'm Rex
    makeItSpeak(cat) // الناتج: Meow! I'm Whiskers
}

الواجهة الفارغة interface{}

GO
// الواجهة الفارغة يمكنها احتواء قيم أي نوع
func printAnything(v interface{}) {
    fmt.Printf("Value: %v, Type: %T\n", v, v)
}

func main() {
    printAnything(42)         // Value: 42, Type: int
    printAnything("hello")   // Value: hello, Type: string
    printAnything(3.14)      // Value: 3.14, Type: float64
}
💡 نصيحة: في Go 1.18+، يمكن اختصار interface{} إلى any. هما متكافئان.

تأكيدات النوع ومحولات النوع

GO
func describe(v interface{}) {
    // تأكيد النوع: محاولة تحويل قيمة الواجهة إلى نوع محدد
    str, ok := v.(string)
    if ok {
        fmt.Println("This is a string:", str)
        return
    }

    // محول النوع: التعامل الأنيق مع عدة أنواع
    switch val := v.(type) {
    case int:
        fmt.Println("This is an integer:", val)
    case float64:
        fmt.Println("This is a float:", val)
    case bool:
        fmt.Println("This is a boolean:", val)
    default:
        fmt.Printf("Unknown type: %T\n", val)
    }
}
💡 نصيحة: استخدام نمط "فاصلة موافق" مع تأكيدات النوع يتجنب panic. v.(Type) يُسبب panic إذا فشل التأكيد، بينما v, ok := v.(Type) يُرجع قيمة صفرية و false بأمان.

تركيب الواجهات

GO
// واجهات أساسية
type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

// واجهة مركبة: تضم عدة واجهات
type ReadWriter interface {
    Reader
    Writer
}

// ReadWriter تتطلب تنفيذ كلتا الدالتين Read و Write
💡 نصيحة: تركيب الواجهات يتبع مبدأ "الواجهة الصغيرة". العديد من الواجهات في المكتبة القياسية لـ Go لها فقط 1-2 أسلوب، مثل io.Reader، io.Writer، fmt.Stringer، إلخ.


الأمثلة

مثال: حساب مساحة الأشكال (الصعوبة ⭐)

GO
package main

import (
    "fmt"
    "math"
)

// Shape واجهة تُعرّف سلوك "الأشكال"
type Shape interface {
    Area() float64
    Perimeter() float64
}

// مستطيل
type Rectangle struct {
    Width  float64
    Height float64
}

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

func (r Rectangle) Perimeter() float64 {
    return 2 * (r.Width + r.Height)
}

// دائرة
type Circle struct {
    Radius float64
}

func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

func (c Circle) Perimeter() float64 {
    return 2 * math.Pi * c.Radius
}

// printShapeInfo يقبل أي تنفيذ لواجهة Shape
func printShapeInfo(s Shape) {
    fmt.Printf("Area: %.2f, Perimeter: %.2f\n", s.Area(), s.Perimeter())
}

func main() {
    rect := Rectangle{Width: 10, Height: 5}
    circle := Circle{Radius: 7}

    fmt.Print("Rectangle -> ")
    printShapeInfo(rect)

    fmt.Print("Circle -> ")
    printShapeInfo(circle)
}
▶ جرّب الكود
Rectangle -> Area: 50.00, Perimeter: 30.00
Circle -> Area: 153.94, Perimeter: 43.98

مثال: شرائح الواجهات والترتيب (الصعوبة ⭐⭐)

GO
package main

import (
    "fmt"
    "sort"
)

// Employee واجهة
type Employee interface {
    Name() string
    Salary() float64
}

// FullTime موظف بدوام كامل
type FullTime struct {
    name   string
    annual float64 // الراتب السنوي
}

func (f FullTime) Name() string    { return f.name }
func (f FullTime) Salary() float64 { return f.annual }

// Contractor عامل بعقد
type Contractor struct {
    name    string
    hourly  float64 // الأجر بالساعة
    hours   float64 // ساعات العمل
}

func (c Contractor) Name() string    { return c.name }
func (c Contractor) Salary() float64 { return c.hourly * c.hours }

// BySalary يُنفذ sort.Interface، يرتب حسب الراتب
type BySalary []Employee

func (s BySalary) Len() int           { return len(s) }
func (s BySalary) Less(i, j int) bool { return s[i].Salary() < s[j].Salary() }
func (s BySalary) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }

// totalCost يحسب إجمالي تكلفة العمالة
func totalCost(employees []Employee) float64 {
    total := 0.0
    for _, e := range employees {
        total += e.Salary()
    }
    return total
}

func main() {
    team := []Employee{
        FullTime{name: "Zhang San", annual: 120000},
        Contractor{name: "Li Si", hourly: 200, hours: 1000},
        FullTime{name: "Wang Wu", annual: 150000},
        Contractor{name: "Zhao Liu", hourly: 180, hours: 800},
    }

    fmt.Println("=== Before Salary Sort ===")
    for _, e := range team {
        fmt.Printf("  %s: $%.0f\n", e.Name(), e.Salary())
    }

    sort.Sort(BySalary(team))

    fmt.Println("\n=== After Salary Sort ===")
    for _, e := range team {
        fmt.Printf("  %s: $%.0f\n", e.Name(), e.Salary())
    }

    fmt.Printf("\nTotal labor cost: $%.0f\n", totalCost(team))
}
▶ جرّب الكود
=== Before Salary Sort ===
  Zhang San: $120000
  Li Si: $200000
  Wang Wu: $150000
  Zhao Liu: $144000

=== After Salary Sort ===
  Zhang San: $120000
  Zhao Liu: $144000
  Wang Wu: $150000
  Li Si: $200000

Total labor cost: $614000

مثال: تنفيذ واجهات io.Reader/Writer (الصعوبة ⭐⭐⭐)

GO
package main

import (
    "fmt"
    "io"
    "strings"
)

// UpperReader يُحوّل المحتوى المقروء إلى أحرف كبيرة
type UpperReader struct {
    source io.Reader
}

// يُنفذ واجهة io.Reader
func (u *UpperReader) Read(p []byte) (n int, err error) {
    n, err = u.source.Read(p)
    // تحويل جميع البايتات المقروءة إلى أحرف كبيرة
    for i := 0; i < n; i++ {
        if p[i] >= 'a' && p[i] <= 'z' {
            p[i] = p[i] - 32 // ASCII: حرف صغير إلى كبير
        }
    }
    return
}

// مُنشئ UpperReader
func NewUpperReader(r io.Reader) *UpperReader {
    return &UpperReader{source: r}
}

// PrefixWriter يُضيف بادئة قبل كل كتابة
type PrefixWriter struct {
    prefix string
    target io.Writer
}

// يُنفذ واجهة io.Writer
func (p *PrefixWriter) Write(data []byte) (n int, err error) {
    // كتابة البادئة أولاً
    _, err = p.target.Write([]byte(p.prefix))
    if err != nil {
        return 0, err
    }
    // ثم كتابة البيانات الفعلية
    return p.target.Write(data)
}

// مُنشئ PrefixWriter
func NewPrefixWriter(prefix string, w io.Writer) *PrefixWriter {
    return &PrefixWriter{prefix: prefix, target: w}
}

// TeeReader يقرأ ويكتب في نفس الوقت (مشابه لأمر tee)
func TeeReader(r io.Reader, w io.Writer) io.Reader {
    return &teeReader{r: r, w: w}
}

type teeReader struct {
    r io.Reader
    w io.Writer
}

func (t *teeReader) Read(p []byte) (n int, err error) {
    n, err = t.r.Read(p)
    if n > 0 {
        // الكتابة إلى w أثناء القراءة
        t.w.Write(p[:n])
    }
    return
}

func main() {
    fmt.Println("=== UpperReader Example ===")
    // إنشاء Reader من سلسلة
    source := strings.NewReader("hello, go interfaces!")
    upper := NewUpperReader(source)

    // استخدام io.ReadAll لقراءة كل المحتوى
    buf := make([]byte, 64)
    n, _ := upper.Read(buf)
    fmt.Printf("Uppercase result: %s\n", string(buf[:n]))

    fmt.Println("\n=== PrefixWriter Example ===")
    // الكتابة إلى stdout مع بادئة
    writer := NewPrefixWriter("[LOG] ", &strings.Builder{})
    writer.Write([]byte("System started\n"))
    // استخدام strings.Builder لالتقاط الإخراج
    var builder strings.Builder
    pw := NewPrefixWriter("[DEBUG] ", &builder)
    pw.Write([]byte("Interface initialized"))
    fmt.Println(builder.String())

    fmt.Println("\n=== TeeReader Example ===")
    // القراءة والكتابة في نفس الوقت إلى Writer آخر
    input := strings.NewReader("Go is powerful")
    var capture strings.Builder
    tee := TeeReader(input, &capture)

    buf2 := make([]byte, 1024)
    n2, _ := tee.Read(buf2)
    fmt.Printf("Read: %s\n", string(buf2[:n2]))
    fmt.Printf("Also captured: %s\n", capture.String())
}
▶ جرّب الكود
=== UpperReader Example ===
Uppercase result: HELLO, GO INTERFACES!

=== PrefixWriter Example ===
[DEBUG] Interface initialized

=== TeeReader Example ===
Read: Go is powerful
Also captured: Go is powerful

سيناريوهات التطبيق الواقعية

السيناريو 1: نظام التسجيل (نمط الاستراتيجية)

GO
package main

import (
    "fmt"
    "os"
    "time"
)

// Logger واجهة التسجيل
type Logger interface {
    Log(message string)
}

// ConsoleLogger مُسجل وحدة التحكم
type ConsoleLogger struct{}

func (c ConsoleLogger) Log(message string) {
    timestamp := time.Now().Format("2006-01-02 15:04:05")
    fmt.Printf("[%s] %s\n", timestamp, message)
}

// FileLogger مُسجل الملف
type FileLogger struct {
    file *os.File
}

func NewFileLogger(filename string) (*FileLogger, error) {
    f, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
    if err != nil {
        return nil, err
    }
    return &FileLogger{file: f}, nil
}

func (f *FileLogger) Log(message string) {
    timestamp := time.Now().Format("2006-01-02 15:04:05")
    fmt.Fprintf(f.file, "[%s] %s\n", timestamp, message)
}

// MultiLogger يُخرج إلى عدة مسجلات في نفس الوقت
type MultiLogger struct {
    loggers []Logger
}

func (m *MultiLogger) Add(l Logger) {
    m.loggers = append(m.loggers, l)
}

func (m *MultiLogger) Log(message string) {
    for _, l := range m.loggers {
        l.Log(message)
    }
}

// App تستخدم واجهة Logger، لا تهتم بالتنفيذ المحدد
type App struct {
    logger Logger
}

func (a *App) Run() {
    a.logger.Log("Application started")
    a.logger.Log("Processing request...")
    a.logger.Log("Request processing complete")
}

func main() {
    // دمج عدة مخارج تسجيل
    multi := &MultiLogger{}
    multi.Add(ConsoleLogger{})

    // يمكن التبديل بسهولة أو إضافة طرق إخراج السجل
    app := &App{logger: multi}
    app.Run()
}
[2026-06-26 10:30:00] Application started
[2026-06-26 10:30:00] Processing request...
[2026-06-26 10:30:00] Request processing complete

السيناريو 2: طبقة تجريد تخزين البيانات

GO
package main

import "fmt"

// Store واجهة التخزين
type Store interface {
    Get(key string) (string, bool)
    Set(key string, value string)
    Delete(key string)
    Keys() []string
}

// MemoryStore تنفيذ التخزين في الذاكرة
type MemoryStore struct {
    data map[string]string
}

func NewMemoryStore() *MemoryStore {
    return &MemoryStore{data: make(map[string]string)}
}

func (m *MemoryStore) Get(key string) (string, bool) {
    val, ok := m.data[key]
    return val, ok
}

func (m *MemoryStore) Set(key string, value string) {
    m.data[key] = value
}

func (m *MemoryStore) Delete(key string) {
    delete(m.data, key)
}

func (m *MemoryStore) Keys() []string {
    keys := make([]string, 0, len(m.data))
    for k := range m.data {
        keys = append(keys, k)
    }
    return keys
}

// CacheService تستخدم واجهة Store، منفصلة عن التخزين المحدد
type CacheService struct {
    store Store
}

func (c *CacheService) GetOrSet(key, defaultValue string) string {
    if val, ok := c.store.Get(key); ok {
        return val
    }
    c.store.Set(key, defaultValue)
    return defaultValue
}

func (c *CacheService) GetAll() map[string]string {
    result := make(map[string]string)
    for _, key := range c.store.Keys() {
        if val, ok := c.store.Get(key); ok {
            result[key] = val
        }
    }
    return result
}

func main() {
    // استخدام التخزين في الذاكرة
    store := NewMemoryStore()
    cache := &CacheService{store: store}

    // كتابة البيانات
    cache.GetOrSet("user:1", "Alice")
    cache.GetOrSet("user:2", "Bob")
    cache.GetOrSet("config:theme", "dark")

    // قراءة البيانات
    fmt.Println("All cached data:")
    for k, v := range cache.GetAll() {
        fmt.Printf("  %s = %s\n", k, v)
    }

    // اختبار GetOrSet: المفتاح الموجود يُرجع القيمة القديمة
    result := cache.GetOrSet("user:1", "Charlie")
    fmt.Printf("\nuser:1 value: %s\n", result)
}
All cached data:
  user:1 = Alice
  user:2 = Bob
  config:theme = dark

user:1 value: Alice

📖 ملخص

النقطة الرئيسية الوصف
الواجهات تُعرّف السلوك تهتم فقط بـ "ماذا يمكنه فعل" وليس "ما هو"
التطبيق الضمني لا حاجة لإعلان؛ تنفيذ الأساليب يُلبي الواجهة
الواجهة الفارغة any يمكنها احتواء قيم أي نوع
تأكيد النوع استخراج أنواع محددة من قيم الواجهة؛ استخدم نمط comma ok للأمان
تركيب الواجهات بناء واجهات كبيرة عبر تضمين صغيرة
البرمجة نحو الواجهات اعتمد على الواجهات بدل التنفيذات المحددة للمرونة
💡 أفضل الممارسات: Go تُفضّل الواجهات الصغيرة. الواجهات الأكثر استخدامًا في المكتبة القياسية عادةً لها فقط 1-2 أسلوب. عند تعريف الواجهات، ابدأ من احتياجات المستخدم (المُستدعي) وليس المُنفّذ.


❓ أسئلة شائعة

س1: ما الفرق بين الواجهة والتراكيب؟

التراكيب هو نوع بيانات محدد يُعرّف "ما هو"؛ الواجهة هي عقد سلوكي يُعرّف "ماذا يمكنه فعل." التراكيب يمكن تشييدها؛ الواجهات لا يمكن تشييدها مباشرة، لكن يمكنها احتواء قيم أي نوع يُنفذ الواجهة.

س2: لماذا لا يحتاج Go لكلمة implements؟

Go تستخدم تصميم typing البطة. المترجم يتحقق تلقائيًا وقت الترجمة مما إذا كان نوع يُلبي واجهة. هذا التصميم يفصل الواجهات عن التنفيذات تمامًا — يمكنك تعريف واجهات جديدة لأنواع مكتبات طرف ثالث دون تعديل الكود الموجود.

س3: متى أُعرّف واجهة؟

💡 قاعدة عامة: اكتب التنفيذات المحددة أولاً، ثم عرّف الواجهات عند اكتشاف الحاجة للتجريد. لا تُفرط في التصميم.

س4: ما الفرق بين interface{} و any؟

لا يوجد فرق. any هو بديل نوعي لـ interface{} مُقدَّم في Go 1.18. هما متكافئان تمامًا. يُنصح بـ any لأنه أكثر إيجازًا.


📝 تمارين

تمرين 1: الأساسيات — تنفيذ واجهة Stringer

واجهة fmt.Stringer لها أسلوب واحد فقط: String() string. عند استخدام fmt.Println أو تنسيق %v، تستدعي Go هذا الأسلوب تلقائيًا.

GO
// نفّذ واجهة fmt.Stringer للأنواع التالية
type Temperature struct {
    Celsius float64
}

type Money struct {
    Amount   float64
    Currency string
}

// السلوك المتوقع:
// fmt.Println(Temperature{36.5})  -> "36.5°C"
// fmt.Println(Money{99.9, "USD"}) -> "$99.90"

تمرين 2: متوسط — تصميم نظام إشعارات

صمم نظام إشعارات يدعم عدة طرق إخطار:

GO
// 1. عرّف واجهة Notifier
// 2. نفّذ EmailNotifier و SMSNotifier و WechatNotifier
// 3. نفذ دالة يمكنها الإرسال إلى عدة Notifiers في نفس الوقت
// 4. استخدم شريحة واجهات لتخزين Notifiers مختلفة

تمرين 3: تحدي — تنفيذ نظام إضافات بسيط

GO
// عرّف واجهة Plugin بأساليب Name() و Version() و Execute()
// نفّذ 3 إضافات مختلفة على الأقل
// أنشئ PluginManager يمكنه تسجيل وإيجاد وتنفيذ الإضافات
// تلميح: استخدم map[string]Plugin لتخزين الإضافات

الدرس التالي

الواجهات هي حجر الأساس للتعددية الشكلية في Go. بإتقان الواجهات، يمكنك كتابة كود مرن وقابل للاختبار وقابل للتوسيع. تاليًا، سنتعلّم عن آليات معالجة الأخطاء في Go — واحدة من أهم فلسفة التصميم في لغة Go.

👉 الدرس التالي: معالجة الأخطاء

Web-Tutorial.com

فريق Web-Tutorial التقني

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

100%