الواجهات
الدرس 9: الواجهات
تشبيه من الواقع
تخيل أنك تذهب إلى مطعم لطلب طعام. لا تحتاج لمعرفة من الطاهي، أي مقلاة يستخدم، أو كيف يطبخ — تنظر فقط إلى القائمة وتطلب "دجاج كونغ باو." القائمة هي واجهة: تُعرّف "ما يمكن فعله" دون الاهتمام بـ "من يفعله" أو "كيف يُفعل."
في Go، الواجهات تعمل بنفس الطريقة. تُعرّف مجموعة من توقيعات الأساليب، وأي نوع يُنفذ هذه الأساليب يُلبي الواجهة تلقائيًا — دون حاجة لإعلان صريح.
المفاهيم الأساسية
| المفهوم | الوصف |
|---|---|
| الواجهة | مجموعة من توقيعات الأساليب تُحدد عقد سلوكي |
| التطبيق الضمني | نوع يُلبي الواجهة تلقائيًا إذا نفّذ جميع أساليب الواجهة |
| typing البطة | "إذا مشت كبطة وصرّت كبطة، فهي بطة" |
الواجهة الفارغة interface{} |
لا تحتوي أساليب؛ أي نوع يُلبيها |
| تأكيد النوع | استخراج نوع محدد من قيمة واجهة |
| تركيب الواجهات | بناء واجهات أكبر عبر تضمين عدة واجهات |
الصيغة الأساسية والاستخدام
تعريف واجهة
// تعريف واجهة Speaker
type Speaker interface {
Speak() string
}
التطبيق الضمني
// نوع 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
}
implements. طالما أن نوعًا لديه جميع الأساليب المطلوبة من واجهة، يُنفذ تلك الواجهة تلقائيًا.
استخدام الواجهات
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{}
// الواجهة الفارغة يمكنها احتواء قيم أي نوع
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
}
interface{} إلى any. هما متكافئان.
تأكيدات النوع ومحولات النوع
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)
}
}
v.(Type) يُسبب panic إذا فشل التأكيد، بينما v, ok := v.(Type) يُرجع قيمة صفرية و false بأمان.
تركيب الواجهات
// واجهات أساسية
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
io.Reader، io.Writer، fmt.Stringer، إلخ.
الأمثلة
مثال: حساب مساحة الأشكال (الصعوبة ⭐)
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
مثال: شرائح الواجهات والترتيب (الصعوبة ⭐⭐)
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 (الصعوبة ⭐⭐⭐)
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: نظام التسجيل (نمط الاستراتيجية)
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: طبقة تجريد تخزين البيانات
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 للأمان |
| تركيب الواجهات | بناء واجهات كبيرة عبر تضمين صغيرة |
| البرمجة نحو الواجهات | اعتمد على الواجهات بدل التنفيذات المحددة للمرونة |
❓ أسئلة شائعة
س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 هذا الأسلوب تلقائيًا.
// نفّذ واجهة 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: متوسط — تصميم نظام إشعارات
صمم نظام إشعارات يدعم عدة طرق إخطار:
// 1. عرّف واجهة Notifier
// 2. نفّذ EmailNotifier و SMSNotifier و WechatNotifier
// 3. نفذ دالة يمكنها الإرسال إلى عدة Notifiers في نفس الوقت
// 4. استخدم شريحة واجهات لتخزين Notifiers مختلفة
تمرين 3: تحدي — تنفيذ نظام إضافات بسيط
// عرّف واجهة Plugin بأساليب Name() و Version() و Execute()
// نفّذ 3 إضافات مختلفة على الأقل
// أنشئ PluginManager يمكنه تسجيل وإيجاد وتنفيذ الإضافات
// تلميح: استخدم map[string]Plugin لتخزين الإضافات
الدرس التالي
الواجهات هي حجر الأساس للتعددية الشكلية في Go. بإتقان الواجهات، يمكنك كتابة كود مرن وقابل للاختبار وقابل للتوسيع. تاليًا، سنتعلّم عن آليات معالجة الأخطاء في Go — واحدة من أهم فلسفة التصميم في لغة Go.



