2.4 اینترفیس (Interface)

2.4 اینترفیس (Interface)

اینترفیس در زبان گو مجموعه‌ای از متدها است. این مجموعه متدها با توجه به ورودی و خروجی که دارند دارای رفتارهای خاصی هستند. زمانیکه شما یک اینترفیس به همراه یکسری از متدها تعریف می‌کنید باید در جایی این متدها را پیاده سازی کنید.

اینترفیس‌ها به شما اجازه می‌دهد تا از Duck typing استفاده کنید. حالا این duck typing چیست؟

duck typing روشی در برنامه‌نویسی کامپیوتری است که به شما امکان می‌دهد تست اردک را انجام دهید، جایی که ما نوع را بررسی نمی‌کنیم، بلکه تنها وجود برخی ویژگی‌ها یا روش‌ها را بررسی می‌کنیم. بنابراین آنچه واقعاً اهمیت دارد این است که آیا یک شی دارای ویژگی‌ها و روش‌های خاصی است و نه نوع آن.

برگردیم به بحث اینترفیس, در زیر ما یک نمونه اینترفیس را قرار دادیم:

type name_of_interface interface{
//Method signature 1
//Method signature 2
}

برای درک بهتر مفهوم ارائه شده، بیایید از یک مثال ساده استفاده کنیم. فرض کنید ما یک شی به نام «animal» داریم که شامل یکسری رفتارها است، مانند نفس کشیدن و راه رفتن. این رفتارها باید به یک حیوان خاص اختصاص یابند تا بتوانیم ویژگی‌ها و رفتارهای دقیق آن حیوان را مشخص و تعریف کنیم.

type animal interface {
    breathe()
    walk()
}

در بالا ما یک اینترفیس تعریف کردیم ۲ تا متد دارد حالا بیاید یک متغیر از نوع اینترفیس animal درست کنیم و چاپ کنیم.

package main

import "fmt"

type animal interface {
    breathe()
    walk()
}

func main() {
    var a animal
    fmt.Println(a)
}

در بالا وقتی اینترفیس را چاپ کردیم، خروجی nil بود. توجه کنید اینترفیس مقدار پیش‌فرض یا خالی بودنش nil هست.

2.4.1 پیاده‌سازی اینترفیس #

در بالا ما یک اینترفیس animal تعریف کردیم که ۲ متد داشت حالا قصد داریم یک شی (منظور ساختار در گو) به نام lion تعریف کنیم و متدهای اینترفیس animal را پیاده‌سازی کنیم.

package main

import "fmt"

type animal interface {
    breathe()
    walk()
}

type lion struct {
    age int
}

func (l lion) breathe() {
    fmt.Println("Lion breathes")
}

func (l lion) walk() {
    fmt.Println("Lion walk")
}

func main() {
    var a animal
    a = lion{age: 10}
    a.breathe()
    a.walk()
}

در بالا ما یک متغیر با تایپ animal تعریف کردیم:

var a animal

سپس ما یک نمونه از ساختار lion را بهش اختصاص دادیم:

a = lion{}

اختصاص یک نمونه از ساختار lion به متغیر a که با تایپ lion بود موفقیت آمیز بود زیرا ما برای lion متدهای مربوط به animal را که breathe و walk بود، پیاده سازی کردیم. این مفهوم کاملاً شبیه به ducking typing هست که در بالا گفتیم. یک شیر می‌تواند نفس بکشد و راه برود از این رو او یک حیوان است.

توجه کنید اگر شما متد جدیدی را اضافه یا کم کنید و همچنین اگر تغییر ایجاد کنید باید این تغییرات بر روی اشیایی که با اینترفیس شما در ارتباط هستند صورت بگیرید.

به عنوان مثال شما اگر به اینترفیس animal یک متد جدیدی اضافه کنید حتما باید برای ساختار lion هم پیاده سازی کنید.

2.4.2 اینترفیس‌ها بطور ضمنی (implicitly) پیاده سازی می‌شود #

برای اینترفیس هیچ حالت صریح (explicit) هنگام تعریف وجود ندارد و همه چی بصورت ضمنی است تا زمانیکه یک اینترفیس برای یک شی (ساختار) متدهایش پیاده سازی نشود هیچ کاربردی نخواهد داشت.

توجه کنید هیچ حالت صریحی وجود ندارد که بگوید شما تمامی متدهای اینترفیس animal را برای ساختار lion پیاده سازی کردید یا خیر و فقط در زمان کامپایل اگر ایرادی وجود داشته باشد کامپایلر به شما خطا می‌دهد و البته IDE هایی مانند: Goland , Vscode به شما هنگام نوشتن کد در خصوص این مورد کمک می‌کنند قبل از کامپایل متوجه خطاهای مرتبط با پیاده سازی اینترفیس شوید.

خب بزارید یک مثال پیچیده برای اینترفیس animal بزنیم و یک شی (ساختار) دیگر به نام dog اضافه کنیم و متدهای اینترفیس animal را برای این شی پیاده سازی کنیم.

package main

import "fmt"

type animal interface {
    breathe()
    walk()
}

type lion struct {
     age int
}

func (l lion) breathe() {
    fmt.Println("Lion breathes")
}

func (l lion) walk() {
    fmt.Println("Lion walk")
}

type dog struct {
     age int
}

func (l dog) breathe() {
    fmt.Println("Dog breathes")
}

func (l dog) walk() {
    fmt.Println("Dog walk")
}

func main() {
    var a animal

    a = lion{age: 10}
    a.breathe()
    a.walk()

    a = dog{age: 5}
    a.breathe()
    a.walk()
}

در مثال بالا ما یک ساختار با نام dog تعریف کردیم و سپس متدهای animal را برای ساختار dog پیاده سازی کردیم و در نهایت ساختار dog را به متغیر اینترفیس a اختصاص دادیم. همانطور که می‌بینیم dog هم همانند lion نفس می‌کشد و راه می‌رود.

توجه کنید در بالا ما برای ۲ تا شی lion و dog یک وجه مشترک به نام animal به همراه رفتار مشترک تعریف کردیم که به اینکار پلی مورفیسم می‌گویند و یکی از عناوین پر کاربرد در شی‌گرایی می باشد که در بخش شی گرایی زبان گو بیشتر می‌پردازیم.

دو نکته مهم در خصوص اینترفیس‌:

  1. اینترفیس‌ها فقط زمان کامپایل مشخص می‌شود که برای اشیا به درستی پیاده سازی شده‌اند یا خیر و اگر فرضاً ما برای ساختار lion در کد بالا متد walk را حذف کنیم با خطای زیر رو به رو خواهیم شد:
cannot use lion literal (type lion) as type animal in assignment:
  1. ورود و خروجی‌های هر متدی که پیاده سازی می‌کنید برای اشیا (ساختارها) بستگی به تعریف ضمنی متد داخل اینترفیس دارد و اگر شما متدی را داخل اینترفیس تغییر دهید حتما باید آن متد در اشیایی که قبلا پیاده سازی شده تغییر یابد.

حالا فرض کنید ما برای اینترفیس animal یک متد جدیدی به نام speed تعریف کردیم که این متد به عنوان خروجی مقداری با تایپ int بر می‌گرداند:

type animal interface {
    breathe()
    walk()
    speed() int
}

حالا ساختار lion باید متد speed را مانند کد زیر پیاده سازی کرده باشد :

func (l lion) speed()

اگر دقت کنید ما داخل اینترفیس animal گفتیم متد speed یک مقدار خروجی از نوع int دارد ولی ما برای ساختار lion متد speed را بدون خروجی نوشتیم. اتفاقی که می‌افتد هنگام کامپایل با خطای زیر مواجه خواهیم شد :

cannot use lion literal (type lion) as type animal in assignment:
        lion does not implement animal (wrong type for speed method)
                have speed()
                want speed() int

با توجه به اتفاقی که افتاد ما نتیجه میگریم متدی که داخل اینترفیس به همراه ورودی و خروجی اضافه می‌شود باید به همان شکل برای ساختارهامون پیاده سازی کنیم.

2.4.3 استفاده از اینترفیس به عنوان پارامتر ورودی تابع #

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

به عنوان مثال ما در کد زیر ۲ تا تابع داریم به نام های callBreathe و callWalk که به عنوان ورودی اینترفیس animal را قبول می‌کند و ما ۲ نمونه از ساختارهای lion و dog را که متدهای اینترفیس animal را پیاده سازی کرده‌اند را به این ۲ تابع پاس دادیم.

package main

import "fmt"

type animal interface {
	breathe()
	walk()
}

type lion struct {
     age int
}

func (l lion) breathe() {
	fmt.Println("Lion breathes")
}

func (l lion) walk() {
	fmt.Println("Lion walk")
}

type dog struct {
     age int
}

func (l dog) breathe() {
	fmt.Println("Dog breathes")
}

func (l dog) walk() {
	fmt.Println("Dog walk")
}

func main() {
	l := lion{age: 10}
	callBreathe(l)
	callWalk(l)

	d := dog{age: 5}
	callBreathe(d)
	callWalk(d)
}

func callBreathe(a animal) {
	a.breathe()
}

func callWalk(a animal) {
	a.breathe()
}

2.4.4 چرا اینترفیس؟ #

شاید برای شما این سوال پیش بیاد چرا باید از اینترفیس استفاده کنیم و مزایای آن چیست؟ ما در زیر مزایای استفاده از اینترفیس و علت اینکه چرا باید از اینترفیس استفاده کنیم را توضیح خواهیم داد.

  1. اینترفیس به ما در نوشتن کدهای ماژولارتر و جدا شده‌‌تر بین بخش‌های مختلف کد کمک می‌کند و همچنین می‌تواند باعث کاهش وابستگی بین بخش‌های مختلف کد شود.

کد باید برای تغییر بسته، و برای توسعه باز باشد. #

اصل باز و بسته بودن یا اصل Open/Closed به نظر بسیاری، اساس برنامه نویسی شی گرا را تشکیل می‌دهد. رابرت مارتین (Robert C. Martin) که در بین برنامه نویسان به عمو باب (Uncle Bob) مشهور است با عبارت: “مهم‌ترین اصل طراحی شی گرا” از این اصل یاد کرده است. ما با استفاده از اینترفیس ها میتونیم این اصل مهم رو پیاده سازی کنیم.

بزارید چند مثال کاربردی بزنیم: فرض کنید ما چند تا سرویس اس ام اس داریم و در آینده هم ممکنه که سرویس های اس ام اس تغییر کنند و از یک ارائه دهنده دیگه خدمات بگیریم. خب در این صورت ما باید چیکار کنیم که با حذف و اضافه کردن سرویس جدید کد های ما تغییر نکنند؟ میایم یک اینترفیس به اسم مثلا Sms می نویسیم و مشخص میکنیم هر کی که میخواد از این اینترفیس استفاده کنه باید متد send_sms و هر چیزی که نیاز هستش رو پیاده سازیش کنه.

فرض کنید شما یک برنامه نوشتید که یک لایه دیتابیس دارد و داده‌ها، با توجه به کانفیگ، در یکی از دو دیتابیس mongodb یا arangodb ذخیره ‌می‌شود. حالا اگر ما بیایم در لایه دیتابیس یک اینترفیس قرار دهیم و متدهای مربوط به تعاملات با دیتابیس را ایجاد کنیم، در برنامه‌ای که نوشتیم فقط کافیست متدهای ایترفیس استفاده شود تا با توجه به نوع کانفینگ دیتابیس، پیاده سازی متود اجرا شود. یعنی اگر ما بیایم داخل کانفیگ پروژه تنظیمات arangodb را به mongodb تغییر دهیم بدون هیچ تغییری در لایه برنامه می‌توانیم به واسطه اینترفیسی که قرار دادیم با دیتابیس mongodb تعامل داشته باشیم.

  1. از اینترفیس‌ها می‌توان برای پیاده‌سازی مفهوم پلی مورفیسم در زمان اجرا استفاده کرد. که به این مفهوم RunTime Polymorphism می‌گویند.

بزارید یک مثال برای توضیح فوق بزنیم:

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

type taxCalculator interface{
    calculateTax()
}

در بالا ما یک اینترفیس با نام taxCalculator داریم که یک متد به نام calculateTax برای محاسبه مالیات دارد. حالا ما باید به ازای هر کشور یک ساختار داشته باشیم که این ساختارها باید متد calculateTax را با توجه شیوه محاسباتی خود پیاده سازی کرده باشند.

package main

import "fmt"

type taxSystem interface {
    calculateTax() int
}
type indianTax struct {
    taxPercentage int
    income        int
}
func (i *indianTax) calculateTax() int {
    tax := i.income * i.taxPercentage / 100
    return tax
}
type singaporeTax struct {
    taxPercentage int
    income        int
}
func (i *singaporeTax) calculateTax() int {
    tax := i.income * i.taxPercentage / 100
    return tax
}
type usaTax struct {
    taxPercentage int
    income        int
}
func (i *usaTax) calculateTax() int {
    tax := i.income * i.taxPercentage / 100
    return tax
}
func main() {
    indianTax := &indianTax{
        taxPercentage: 30,
        income:        1000,
    }
    singaporeTax := &singaporeTax{
        taxPercentage: 10,
        income:        2000,
    }


    taxSystems := []taxSystem{indianTax, singaporeTax}
    totalTax := calculateTotalTax(taxSystems)


    fmt.Printf("Total Tax is %d\n", totalTax)
}

func calculateTotalTax(taxSystems []taxSystem) int {
    totalTax := 0
    for _, t := range taxSystems {
        totalTax += t.calculateTax() // در اینجا runtime polymorphism رخ می دهد
    }
    return totalTax
}

در خط زیر RunTime Polymorphism رخ داده است.

 totalTax += t.calculateTax() //This is where runtime polymorphism happens

2.4.5 استفاده از اشاره‌گر هنگام پیاده‌سازی اینترفیس #

متدها تایپ‌های گیرنده خود را به دو صورت اشاره‌گر یا مقدار می‌توانند دریافت کنند. در بالا مثال animal را داشتیم که با حالت گیرنده مقدار بود. حالا می‌خواهیم بصورت گیرنده اشاره‌گر تعریف کنیم.

2 نکته با توجه مثالی که خواهیم زد وجود دارد:

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

  • اگر شما برای یک تایپی تمامی متدهای اینترفیس را بصورت گیرنده اشاره‌گر تعریف کرده باشید فقط متغیری که یک نمونه از تایپ که با اشاره‌گر تعریف کرده باشد می‌تواند به اینترفیس انتصاب یابد.

مثال با حالت اولی که توضیح دادیم:

package main

import "fmt"

type animal interface {
	breathe()
	walk()
}

type lion struct {
	age int
}

func (l lion) breathe() {
	fmt.Println("Lion breathes", l)
}

func (l lion) walk() {
	fmt.Println("Lion walk", l)
}

func main() {
	var a animal

	a = lion{age: 10}
	a.breathe()
	a.walk()

	a = &lion{age: 5}
	a.breathe()
	a.walk()
}

در بالا ما یک نمونه از ساختار lion با اشاره‌گر ایجاد کردیم و مقدار age را ۵ قرار دادیم و به اینترفیس animal انتصابش کردیم و بدون هیچ مشکلی کار کرد.

حالا برای حالت دوم به مثال زیر توجه کنید:

package main

import "fmt"

type animal interface {
	breathe()
	walk()
}

type lion struct {
	age int
}

func (l *lion) breathe() {
	fmt.Println("Lion breathes")
}

func (l *lion) walk() {
	fmt.Println("Lion walk")
}

func main() {
	var a animal

	a = lion{age: 10}
	a.breathe()
	a.walk()

	a = &lion{age: 5}
	a.breathe()
	a.walk()
}

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

2.4.6 پیاده سازی اینترفیس برای تایپ‌های غیر ساختار #

همانطور که قبلاً گفتیم شما می‌توانید برای هر تایپی متد تعریف کنید و در اینجا هم می‌توانید متدهای یک اینترفیس را برای هر تایپی پیاده سازی کنید.

package main

import "fmt"

type animal interface {
	breathe()
	walk()
}

type cat string

func (c cat) breathe() {
	fmt.Println("Cat breathes")
}

func (c cat) walk() {
	fmt.Println("Cat walk")
}

func main() {
	var a animal

	a = cat("smokey")
	a.breathe()
	a.walk()
}

در بالا ما یک تایپ با نام cat از نوع رشته تعریف کردیم و سپس متدهای اینترفیس animal را برای این تایپ پیاده‌سازی کردیم.

2.4.7 پیاده‌سازی چندتایی اینترفیس برای تایپ #

شما می‌توانید برای تایپ‌های خود چندین اینترفیس مختلف استفاده کنید و متدهای این اینترفیس‌ها را پیاده سازی کنید.

در کد زیر ما ۲ تا اینترفیس animal و mammal داریم که داخل اینترفیس mammal یک متد با نام feed وجود دارد حالا می‌خواهیم برای ساختار lion از این اینترفیس استفاده کنیم.

package main

import "fmt"

type animal interface {
    breathe()
    walk()
}

type mammal interface {
    feed()
}

type lion struct {
     age int
}
func (l lion) breathe() {
    fmt.Println("Lion breathes")
}
func (l lion) walk() {
    fmt.Println("Lion walk")
}
func (l lion) feed() {
    fmt.Println("Lion feeds young")
}
func main() {
    var a animal
    l := lion{}
    a = l
    a.breathe()
    a.walk()
    var m mammal
    m = l
    m.feed()
}

2.4.8 مقدار صفر یا پیش‌فرض اینترفیس #

اینترفیس هم همانند سایر تایپ‌ها یک مقدار پیش‌فرض دارد که این مقدار پیش‌فرض nil هست.

package main

import "fmt"
type animal interface {
    breathe()
    walk()
}

func main() {
    var a animal
    fmt.Println(a)
}

2.4.9 بدنه اینترفیس #

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

graph TD A[Interface Variable] --> B(Interface Type) & C(Interface Value) C --> D(تایپ داخلی) & E(مقدار داخلی)

اگر بخواهیم با توجه به مثال ساختار lion توجه کنیم به شکل زیر می‌شود:

graph TD A[Interface Variable] --> B(Interface Type) & C(Interface Value) C --> D(lion) & E("{age: 10}")

حالا در زیر مثالی زدیم با استفاده از T% و v% نوع و مقدار را می‌توانید چاپ کنیم.

package main

import "fmt"

type animal interface {
    breathe()
    walk()
}

type lion struct {
    age int
}

func (l lion) breathe() {
    fmt.Println("Lion breathes")
}

func (l lion) walk() {
    fmt.Println("Lion walk")
}

func main() {
    var a animal
    a = lion{age: 10}
    fmt.Printf("Underlying Type: %T\n", a)
    fmt.Printf("Underlying Value: %v\n", a)
}

2.4.10 دسترسی به مقادیر داخلی اینترفیس #

برای اینکه بتوانید به مقادیر داخلی اینترفیس دسترسی پیدا کنید ۲ تا روش وجود دارد‌:

  • با استفاده از Type Assertion
  • با استفاده از Switch

2.4.10.1 با استفاده از Type Assertion #

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

val, ok := i.({type})

در بالا زمانیکه Type Assertion انجام می‌دهید ۲ تا متغیر دارید که اولیش مقدار است و دومیش تایید می‌کند تایپی که به اینترفیس دادید همان است (منظور متغیر ok است که مقدار آن از نوع bool است)

اگر هنگام Type Assertion شما وضعیت متغیر ok را بررسی نکنید با خطای panic مواجه خواهید شد.
package main

import "fmt"

type animal interface {
	breathe()
	walk()
}

type lion struct {
	age int
}

func (l lion) breathe() {
	fmt.Println("Lion breathes")
}

func (l lion) walk() {
	fmt.Println("Lion walk")
}

type dog struct {
	age int
}

func (d dog) breathe() {
	fmt.Println("Dog breathes")
}

func (d dog) walk() {
	fmt.Println("Dog walk")
}

func main() {
	var a animal

	a = lion{age: 10}
	print(a)

}

func print(a animal) {
	l, ok := a.(lion)
	if ok {
		fmt.Printf("Age: %d\n", l.age)
	}
}

در بالا ما تایپ lion را به اینترفیس animal پاس دادیم و بررسی کردیم آیا تایپ lion از نوع تایپ داخلی اینترفیس animal هست یا خیر.

l := a.(lion)

2.4.10.2 با استفاده از Switch #

شما با استفاده از switch می‌توانید تایپ اینترفیس را تشخیص دهید.

package main

import "fmt"

type animal interface {
	breathe()
	walk()
}

type lion struct {
	age int
}

func (l lion) breathe() {
	fmt.Println("Lion breathes")
}

func (l lion) walk() {
	fmt.Println("Lion walk")
}

type dog struct {
	age int
}

func (d dog) breathe() {
	fmt.Println("Dog breathes")
}

func (d dog) walk() {
	fmt.Println("Dog walk")
}

func main() {
	var a animal

	a = lion{age: 10}
	print(a)

}

func print(a animal) {
	switch v := a.(type) {
	case lion:
		fmt.Println("Type: lion")
	case dog:
		fmt.Println("Type: dog")
	default:
		fmt.Printf("Unknown Type %T", v)
	}
}

2.4.11 اینترفیس خالی #

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

package main

import "fmt"

func main() {
    test("thisisstring")
    test("10")
    test(true)
}

func test(a interface{}) {
    fmt.Printf("(%v, %T)\n", a, a)
}

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