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
. این انعطافپذیری و گسترشپذیری است که چندشکلی به کد میآورد.