طرح‌چه

اینترفیس (interface) |‌ گولنگ به زبان ساده

۲۰ اردیبهشت ۱۴۰۴

اینترفیس (interface) |‌ گولنگ به زبان ساده
با مفهوم اینترفیس آشنا میشیم و یاد میگیریم چطور از اینترفیس در زبان GO استفاده کنیم.

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

اینترفیس در اصل رفتار (behavior) یا قابلیت‌هایی که یک شی باید داشته باشد را توصیف می‌کند.

در زبان Go، اینترفیس‌ها نوعی داده (type) هستند که فقط امضای متدها را تعریف می‌کنند و هیچ پیاده‌سازی ندارند.

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

در گولنگ پیاده سازی اینترفیس ها به صورت ضمنی انجام میشود بدین معنی که اگر یک type تمام متدهای تعریف‌ شده در یک interface رو داشته باشه، اون interface رو پیاده‌سازی کرده است. 

در مثال زیر File به‌طور خودکار Writer را پیاده‌سازی کرده است زیرا تمامی متدهای آن را دارد:

type Writer interface {
	Write(data []byte) error
}

type File struct{}

func (f File) Write(data []byte) error {
	fmt.Println("writing to file:", string(data))
}

Type Assertion یا Interface Assertion

type assertion یا interface assertion مکانیزمی است که به توسعه‌دهنده امکان می‌دهد تا از یک مقدار با نوع interface، نوع واقعی (concrete type) آن را استخراج کنند.

سینتکس کلی type assertion به صورت زیر است:

// will panic if i is not an instance of T
value := i.(T)
  • i یک مقدار از نوع interface است.

  • T نوعی است که انتظار داریم مقدار i واقعاً از آن نوع باشد.

  • value متغیری است که مقدار استخراج‌شده با نوع T در آن ذخیره می‌شود.

اگر مقدار i واقعاً از نوع T باشد، عملیات موفقیت‌آمیز خواهد بود. در غیر این صورت، برنامه دچار panic خواهد شد.

برای جلوگیری از بروز panic و بررسی ایمن نوع مقدار، می‌توان از فرم دوم type assertion استفاده کرد:

// the ok will be false if i is not an instance of T
value, ok := i.(T)
  • اگر مقدار i از نوع T باشد، متغیر ok مقدار true خواهد داشت.

  • در غیر این صورت، ok برابر false بوده و value مقدار صفر نوع T را خواهد داشت.

این الگو در Go به عنوان comma-ok idiom شناخته می‌شود و راهکاری رایج برای انجام type assertion به صورت ایمن است.

حالا بیاید یکسری مثال واقعی رو بررسی کنیم:

package main

import "fmt"

func main() {
	var data interface{} = "Hello world"

	str := data.(string)
	fmt.Println(str)

	num, ok := data.(int)
	if ok {
		fmt.Println(num)
	} else {
		fmt.Println("the value is not a number!")
	}
}

همچنین اگر interface سفارشی خودتون رو درست کرده باشید میتونید به مشابه عمل کنید:

package main

import (
	"fmt"
	"math"
)

// با این اینترفیس میتونیم مساحت رو حساب کنیم
type Shape interface {
	Area() float64
}

// نوع دایره
type Circle struct{
	 Radius float64
}

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

// نوع مستطیل
type Rectangle struct {
	Width float64
	Height float64
}

func (c Rectangle) Area() float64 {
	return math.Pi * c.Width * c.Height
}

// این تابع یکسری اطلاعات رو بر حسب دیتا تایپ نشون میده
func DescribeShape(s Shape) {
	if c, ok := s.(Circle); ok {
		fmt.Printf("این یک دایره با شعال %f است\n", c.Radius)
	}

	if r, ok := s.(Rectangle); ok {
		fmt.Printf("این یک مستطیل با طول %f و عرض %f است\n", r.Height, r.Width)	
	}
}

func main() {
	var shapes = []Shape {
		Circle{
			Radius: 3,
		},
		Rectangle{
			Width: 4,
			Height: 5,
		},
	}

	for _, s := range shapes {
		DescribeShape(s)
		fmt.Println("مساحت:", s.Area())
	}
}

در مثال بالا در تابع DescribeShape از type assertion استفاده کردیم.

Type assertion همچنین برای بررسی اینکه آیا یک مقدار، پیاده‌ساز یک اینترفیس خاص است یا خیر نیز کاربرد دارد. در این روش باید نوع متغیر خود را از نوع interface تعریف کنیم یا اینکه با type conversion آن را به نوع interface تبدیل کنیم و سپس مقدار مورد نظر خودمون رو درون این متغیر قرار دهیم و در نهایت با type assertion بررسی کنیم که آیا این دیتا تایپ اینترفیس مورد نظر ما را پیاده سازی میکند یا نه. به مثال زیر دقت کنید:

package main

import "fmt"

type Speaker interface {
	Speak()
}

type Human struct{
	Name string
}

func (h Human) Speak() {
	fmt.Println("Hi, my name is ", h.Name)
}

func main() {
	var h interface{}

	h = Human{
		Name: "Mahdi",
	}
	
	if i, ok := interface{}(h).(Speaker); ok {
		i.Speak()
	} else {
		fmt.Println("This is not an speaker")
	}
}

استفاده از Type Switch

برای بررسی نوع واقعی یک مقدار که از نوع interface است، علاوه بر type assertion، می‌توان از type switch استفاده کرد.

package main

import "fmt"

func main() {
	w := 10
	printType(w)

	x := 3.2
	printType(x)

	y := "hello"
	printType(y)

	z := struct { Name string } {
		Name: "Mahdi",
	}
	printType(z)
}

func printType(data interface{}) {
	switch v := data.(type) {
	case int:
		fmt.Println("integer number:", v)
	case float32, float64:
		fmt.Println("float number:", v)
	case string:
		fmt.Println("string:", v)
	case bool:
		fmt.Println("boolean:", v)
	default:
		fmt.Printf("unknown %#v\n", v)
	}
}

اینترفیس خالی (empty interface)

اینترفیس خالی interface{} به معنای "هر نوعی" است. چون هیچ متدی ندارد، تمام انواع در Go به‌طور پیش‌فرض آن را پیاده‌سازی می‌کنند.

اینترفیس خالی میتواند هر نوع دیتایی را در خود جای دهد به همین دلیل در جایی که نیاز دارید تا هر نوع دیتایی را بتوانید دریافت کنید میتوانید از اینترفیس خالی استفاده کنید.

package main

import "fmt"

func main() {
	PrintAny(10)
	PrintAny("Hello, world")		
}

func PrintAny(data interface{}) {
	fmt.Println(data)
}

در گولنگ 1.18 نوع any به زبان اضافه شده و در واقع فقط یک نام مستعار (alias) برای interface{} است، اما باعث می‌شود کد خواناتر به نظر برسد.

var x interface{} = 10
var y any = 10

any مانند یک نام بهتر برای interface{} است که کد را خوانا تر میکند.

معایب استفاده از اینترفیس خالی

  1. اینترفیس خالی interface{} نوعی از نوع‌زدایی (type erasure) است، یعنی نوع واقعی مخفی می‌شود و برای استفاده دوباره باید از type assertion یا type switch استفاده کرد.

  2. استفاده بیش از حد یا بی‌دلیل از interface{} باعث می‌شود کد خوانایی و ایمنی نوع خود را از دست بدهد زیرا در هنگام توسعه کد نیاز داریم تا کدهای قدیمی تر چک شود تا مطمئن شویم چه نوع داده ای به اینترفیس پاس داده شده است.

ترکیب اینترفیس ها (interface composition)

در Go، به جای ارث‌بری (inheritance)، از کامپوزیشن (Composition) برای ساختن ساختارها و اینترفیس‌های پیچیده‌تر استفاده می‌شود. بدین صورت میتوان چندین اینترفیس را با هم ترکیب کرد و یک اینترفیس بزرگتر ساخت.

فرض کنید دو اینترفیس مجزا داریم:

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

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

حالا می‌خواهیم اینترفیس جدیدی بسازیم که هم Read و هم Write داشته باشد:

type ReadWriter interface {
	Reader
	Writer
}

در اینجا ReadWriter یک کامپوزیشن از Reader و Writer است. هر تایپی (type) که هم Reader و هم Writer رو پیاده‌سازی کنه، ReadWriter رو هم پیاده‌سازی کرده است.

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

Design small interfaces. Compose them when needed

 


قسمت قبل: نوع داده struct | گولنگ به زبان ساده

قسمت بعد: جنریک ها (Generics) | گولنگ به زبان ساده


دیدگاه ها