1 مقدمه‌ای برای رابط‌ها

1.1 چیستی یک رابط

در زبان Go، یک رابط یک نوع است، یک نوع انتزاعی. رابط جزئیات اجرای خاص را مخفی می‌کند و تنها رفتار شیء را به کاربر نشان می‌دهد. رابط مجموعه‌ای از متدها را تعریف می‌کند، اما این متدها هیچ کارایی را پیاده‌سازی نمی‌کنند؛ به جای آن، توسط نوع خاص فراهم می‌شوند. ویژگی رابط‌های زبان Go غیرمزاحمتی است، به این معنی که یک نوع نیازی نیست به صورت صریح اعلام کند که کدام رابط را پیاده‌سازی می‌کند؛ تنها نیاز دارد تا متدهای مورد نیاز رابط را فراهم کند.

// تعریف یک رابط
type Reader interface {
    Read(p []byte) (n int, err error)
}

در این رابط Reader، هر نوعی که متد Read(p []byte) (n int, err error) را پیاده‌سازی کند، می‌تواند گفته شود که رابط Reader را پیاده‌سازی می‌کند.

2 تعریف رابط

2.1 ساختار نحو رابط‌ها

در زبان Go، تعریف یک رابط به صورت زیر است:

type interfaceName interface {
    methodName(parameterList) returnTypeList
}
  • interfaceName: نام رابط با قاعده‌ی نام‌گذاری Go، با شروع با یک حرف بزرگ.
  • methodName: نام متد مورد نیاز توسط رابط.
  • parameterList: لیست پارامترهای متد، با کاما جدا شده‌اند.
  • returnTypeList: لیست نوع برگشتی متد.

اگر یک نوع تمامی متدهای رابط را پیاده‌سازی کند، آنگاه این نوع رابط را پیاده‌سازی می‌کند.

type Worker interface {
    Work()
    Rest()

در رابط Worker فوق، هر نوعی با متدهای Work() و Rest() شرط رابط Worker را برآورده می‌کند.

3 مکانیزم پیاده‌سازی رابط

3.1 قوانین پیاده‌سازی رابط‌ها

در زبان Go، یک نوع تنها نیاز دارد که تمامی متدهای رابط را پیاده‌سازی کند تا به عنوان پیاده‌سازی آن رابط در نظر گرفته شود. این پیاده‌سازی به صورت ضمنی انجام می‌شود و نیازی به اعلان صریح به عنوان برخی زبان‌های دیگر ندارد. قوانین پیاده‌سازی رابط‌ها به صورت زیر است:

  • نوعی که رابط را پیاده‌سازی می‌کند می‌تواند یک ساختار یا هر نوع سفارشی دیگری باشد.
  • یک نوع برای در نظر گرفته شدن به عنوان پیاده‌سازی یک رابط، باید تمامی متدهای رابط را پیاده‌سازی کند.
  • متدهای رابط باید دقیقاً همان امضای متد متدهای رابط را که در حال پیاده‌سازی است، شامل نام، لیست پارامتر و مقادیر برگشتی یکسان داشته باشد.
  • یک نوع می‌تواند به زمانهای مختلف، چندین رابط را به یک‌بار پیاده‌سازی کند.

3.2 مثال: پیاده‌سازی یک رابط

حالا بیایید از طریق یک مثال خاص، فرآیند و روش‌های پیاده‌سازی رابط‌ها را نشان دهیم. به Speaker رابط توجه کنید:

type Speaker interface {
    Speak() string
}

برای داشتن نوع Human به عنوان پیاده‌سازی رابط Speaker، باید یک متد Speak را برای نوع Human تعریف کنیم:

type Human struct {
    Name string
}

// متد Speak اجازه می‌دهد که Human رابط Speaker را پیاده‌سازی کند.
func (h Human) Speak() string {
    return "سلام، من " + h.Name + " هستم"
}

func main() {
    var speaker Speaker
    james := Human{"جیمز"}
    speaker = james
    fmt.Println(speaker.Speak()) // خروجی: سلام، من جیمز هستم
}

در کد فوق، ساختار Human با پیاده‌سازی رابط Speaker از طریق پیاده‌سازی متد Speak() را می‌بینیم. در تابع main می‌توانیم ببینیم که متغیر نوع Human به نام james به متغیر نوع Speaker به نام speaker اختصاص داده می‌شود زیرا که james شرط رابط Speaker را برآورده می‌کند.

4 مزایا و موارد استفاده از رابط‌ها

4.1 مزایای استفاده از رابط‌ها

استفاده از رابط‌ها مزایای زیادی دارد:

  • جداسازی: رابط‌ها به کد ما اجازه می‌دهند که از جزئیات اجرای خاص جدا شود و این امر انعطاف‌پذیری و قابلیت نگهداری کد را بهبود می‌بخشد.
  • قابلیت جایگزینی: رابط‌ها به ما امکان می‌دهند که پیاده‌سازی‌های داخلی را به راحتی جایگزین کنیم، تا زمانی که پیاده‌سازی جدید شرایط همان رابط را برآورده کند.
  • گسترش‌پذیری: رابط‌ها به ما امکان می‌دهند تا قابلیت‌های یک برنامه را بدون تغییر کدهای موجود گسترش دهیم.
  • آسانی آزمون: رابط‌ها آزمون واحد را ساده می‌کنند. می‌توانیم از اشیاء موک برای پیاده‌سازی رابط‌ها برای آزمون کدها استفاده کنیم.
  • چندریختی: رابط‌ها چندریختی را پیاده‌سازی می‌کنند، اجازه می‌دهند تا اشیاء مختلف به یک پیام در شرایط مختلف به شیء در روند مختلف پاسخ دهند.

4.2 سناریوهای کاربردی واسط‌ها

واسط‌ها به طور گسترده ای در زبان Go استفاده می‌شوند. در زیر، تعدادی از سناریوهای کاربردی رایج آورده شده است:

  • واسط‌ها در کتابخانه استاندارد: به عنوان مثال، واسط io.Reader و io.Writer برای پردازش فایل و برنامه نویسی شبکه به طور گسترده استفاده می‌شوند.
  • مرتب‌سازی: پیاده‌سازی متدهای Len()، Less(i, j int) bool و Swap(i, j int) در واسط sort.Interface امکان مرتب‌سازی هر آرایه سفارشی را فراهم می‌کند.
  • دستگاه‌های HTTP: پیاده‌سازی متد ServeHTTP(ResponseWriter, *Request) در واسط http.Handler ایجاد کننده دستگاه‌های HTTP سفارشی را امکان‌پذیر می‌سازد.

در زیر یک مثال برای استفاده از واسط برای مرتب‌سازی آمده است:

package main

import (
    "fmt"
    "sort"
)

type AgeSlice []int

func (a AgeSlice) Len() int           { return len(a) }
func (a AgeSlice) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
func (a AgeSlice) Less(i, j int) bool { return a[i] < a[j] }

func main() {
    ages := AgeSlice{45, 26, 74, 23, 46, 12, 39}
    sort.Sort(ages)
    fmt.Println(ages) // خروجی: [12 23 26 39 45 46 74]
}

در این مثال، با پیاده‌سازی سه متد واسط sort.Interface، می‌توانیم آرایه AgeSlice را مرتب کنیم و قابلیت واسط‌ها برای گسترش رفتار انواع موجود را نشان می‌دهیم.

5 ویژگی‌های پیشرفته واسط‌ها

5.1 واسط خالی و کاربردهای آن

در زبان Go، واسط خالی یک نوع ویژگی ویژه است که هیچ متدی را شامل نمی‌شود. بنابراین، تقریبا هر نوع مقداری را می‌توان به عنوان یک واسط خالی در نظر گرفت. واسط خالی با استفاده از interface{} نمایش داده می‌شود و نقش‌های مهمی را به عنوان یک نوع بسیار انعطاف‌پذیر در Go ایفا می‌کند.

// تعریف یک واسط خالی
var any interface{}

پردازش نوع پویا:

واسط خالی می‌تواند مقادیر هر نوعی را ذخیره کند، که این امر بسیار مفید برای کنترل انواع نامعلوم است. به عنوان مثال، زمانی که شما یک تابعی را ایجاد می‌کنید که پارامترهای انواع مختلف را می‌پذیرد، واسط خالی می‌تواند به عنوان نوع پارامتر برای پذیرش هر نوع داده استفاده شود.

func PrintAnything(v interface{}) {
    fmt.Println(v)
}

func main() {
    PrintAnything(123)
    PrintAnything("hello")
    PrintAnything(struct{ name string }{name: "Gopher"})
}

در مثال بالا، تابع PrintAnything پارامتری از نوع واسط خالی v می‌پذیرد و آن را چاپ می‌کند. PrintAnything می‌تواند برای پردازش انواع مختلف اعداد صحیح، رشته‌ها یا ساختارها مورد استفاده قرار گیرد.

5.2 جاسازی واسط

جاسازی واسط به معنای حاوی کردن همه متدهای یک واسط دیگر است و ممکن است چندین متد جدید را هم اضافه کند. این امر با جاسازی دیگر واسط‌ها در تعریف واسط انجام می‌شود.

type Reader interface {
    Read(p []byte) (n int, err error)
}

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

// واسط ReadWriter واسط Reader و Writer را جاسازی می‌کند
type ReadWriter interface {
    Reader
    Writer
}

با استفاده از جاسازی واسط، ما می‌توانیم یک ساختار واسطی سلسله مراتبی و ماژولارتر ایجاد کنیم. در این مثال، واسط ReadWriter متدهای واسط‌های Reader و Writer را یکی دیگر جاسازی کرده و ترکیب قابلیت‌های خواندن و نوشتن را دست‌یافته می‌کند.

5.3 ادعا نوع واسط

ادعا نوع عملیاتی است که برای بررسی و تبدیل نوع مقادیر واسط استفاده می‌شود. زمانی که می‌خواهیم یک نوع خاص از مقدار را از نوع واسط استخراج کنیم، ادعا نوع بسیار مفید است.

سینتکس پایه ادعا:

value, ok := interfaceValue.(Type)

اگر ادعا موفق باشد، value مقدار نوع زیرین Type خواهد بود و ok true خواهد بود. اگر ادعا ناموفق باشد، value مقدار صفر نوع Type و ok false خواهد بود.

var i interface{} = "hello"

// ادعا نوع
s, ok := i.(string)
if ok {
    fmt.Println(s) // خروجی: hello
}

// ادعای نوع غیر واقعی
f, ok := i.(float64)
if !ok {
    fmt.Println("ادعا ناموفق بود!") // خروجی: ادعا ناموفق بود!
}

سناریوهای کاربردی:

ادعا نوع به طور رایج برای تعیین و تبدیل نوع مقادیر در واسط‌های خالی interface{}، یا در صورت پیاده‌سازی چندین واسط، برای استخراج نوعی که یک واسط خاص را پیاده‌سازی کرده است، استفاده می‌شود.

5.4 رابط و چندشکلی

چندشکلی یک مفهوم اساسی در برنامه‌نویسی شی‌گرا است که امکان پردازش انواع داده‌های مختلف را به یک شکل یکپارچه، فقط از طریق رابط‌ها، بدون نیاز به مراقبت از انواع خاص فراهم می‌کند. در زبان Go، رابط‌ها کلیدی برای دستیابی به چندشکلی هستند.

پیاده‌سازی چندشکلی از طریق رابط‌ها

type Shape interface {
    Area() float64
}

type Rectangle struct {
    Width, Height float64
}

type Circle struct {
    Radius float64
}

// Rectangle زا Shape رابط را پیاده‌سازی می‌کند
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

// Circle زا Shape رابط را پیاده‌سازی می‌کند
func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

// محاسبه مساحت اشکال مختلف
func CalculateArea(s Shape) float64 {
    return s.Area()
}

func main() {
    r := Rectangle{Width: 3, Height: 4}
    c := Circle{Radius: 5}
    
    fmt.Println(CalculateArea(r)) // خروجی: مساحت مستطیل
    fmt.Println(CalculateArea(c)) // خروجی: مساحت دایره
}

در این مثال، رابط Shape یک متد Area را برای اشکال مختلف تعریف می‌کند. هر دو نوع محسابه‌ی Rectangle و Circle این رابط را پیاده‌سازی کرده و به این معنی است که این انواع دارای قابلیت محاسبه مساحت هستند. تابع CalculateArea یک پارامتر از نوع رابط Shape می‌گیرد و می‌تواند مساحت هر شکلی که رابط Shape را پیاده‌سازی کرده است، محاسبه کند.

به این ترتیب، ما می‌توانیم به راحتی انواع جدیدی از اشکال را اضافه کنیم بدون نیاز به اصلاح پیاده‌سازی تابع CalculateArea. این انعطاف‌پذیری و گسترش‌پذیری است که چندشکلی به کد می‌آورد.