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

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

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

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

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

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

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

برای اینکه مفهوم بالا را بهتر بفهمیم بزارید یک مثال بزنیم ساده بزنیم فرض کنید ما یک شی به نام animal که دارای یکسری رفتارها مانند: نفش کشید, راه رفتن را دارد که این رفتارهای باید به یک حیوان اختصاص دهیم تا بتوانیم رفتارهای آن حیوان را مشخص کنیم.

1type animal interface {
2    breathe()
3    walk()
4}

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

 1package main
 2
 3import "fmt"
 4
 5type animal interface {
 6    breathe()
 7    walk()
 8}
 9
10func main() {
11    var a animal
12    fmt.Println(a)
13}
1$ go run main.go
2nil

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

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

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

 1package main
 2
 3import "fmt"
 4
 5type animal interface {
 6    breathe()
 7    walk()
 8}
 9
10type lion struct {
11    age int
12}
13
14func (l lion) breathe() {
15    fmt.Println("Lion breathes")
16}
17
18func (l lion) walk() {
19    fmt.Println("Lion walk")
20}
21
22func main() {
23    var a animal
24    a = lion{age: 10}
25    a.breathe()
26    a.walk()
27}
1$ go run main.go
2Lion breathes
3Lion walk

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

1var a animal

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

1a = 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 را برای این شی پیاده سازی کنیم.

 1package main
 2
 3import "fmt"
 4
 5type animal interface {
 6    breathe()
 7    walk()
 8}
 9
10type lion struct {
11     age int
12}
13
14func (l lion) breathe() {
15    fmt.Println("Lion breathes")
16}
17
18func (l lion) walk() {
19    fmt.Println("Lion walk")
20}
21
22type dog struct {
23     age int
24}
25
26func (l dog) breathe() {
27    fmt.Println("Dog breathes")
28}
29
30func (l dog) walk() {
31    fmt.Println("Dog walk")
32}
33
34func main() {
35    var a animal
36    
37    a = lion{age: 10}
38    a.breathe()
39    a.walk()
40  
41    a = dog{age: 5}
42    a.breathe()
43    a.walk()
44}
1$ go run main.go
2Lion breathes
3Lion walk
4Dog breathes
5Dog walk

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

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

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

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

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

1type animal interface {
2    breathe()
3    walk()
4    speed() int
5}

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

1func (l lion) speed()

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

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

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

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

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

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

 1package main
 2
 3import "fmt"
 4
 5type animal interface {
 6	breathe()
 7	walk()
 8}
 9
10type lion struct {
11     age int
12}
13
14func (l lion) breathe() {
15	fmt.Println("Lion breathes")
16}
17
18func (l lion) walk() {
19	fmt.Println("Lion walk")
20}
21
22type dog struct {
23     age int
24}
25
26func (l dog) breathe() {
27	fmt.Println("Dog breathes")
28}
29
30func (l dog) walk() {
31	fmt.Println("Dog walk")
32}
33
34func main() {
35	l := lion{age: 10}
36	callBreathe(l)
37	callWalk(l)
38
39	d := dog{age: 5}
40	callBreathe(d)
41	callWalk(d)
42}
43
44func callBreathe(a animal) {
45	a.breathe()
46}
47
48func callWalk(a animal) {
49	a.breathe()
50}
1$ go run main.go
2Lion breathes
3Lion walk
4Dog breathes
5Dog walk

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

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

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

بزارید یک مثال کاربردی بزنیم :

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

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

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

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

1type taxCalculator interface{
2    calculateTax()
3}

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

 1package main
 2
 3import "fmt"
 4
 5type taxSystem interface {
 6    calculateTax() int
 7}
 8type indianTax struct {
 9    taxPercentage int
10    income        int
11}
12func (i *indianTax) calculateTax() int {
13    tax := i.income * i.taxPercentage / 100
14    return tax
15}
16type singaporeTax struct {
17    taxPercentage int
18    income        int
19}
20func (i *singaporeTax) calculateTax() int {
21    tax := i.income * i.taxPercentage / 100
22    return tax
23}
24type usaTax struct {
25    taxPercentage int
26    income        int
27}
28func (i *usaTax) calculateTax() int {
29    tax := i.income * i.taxPercentage / 100
30    return tax
31}
32func main() {
33    indianTax := &indianTax{
34        taxPercentage: 30,
35        income:        1000,
36    }
37    singaporeTax := &singaporeTax{
38        taxPercentage: 10,
39        income:        2000,
40    }
41
42
43    taxSystems := []taxSystem{indianTax, singaporeTax}
44    totalTax := calculateTotalTax(taxSystems)
45
46
47    fmt.Printf("Total Tax is %d\n", totalTax)
48}
49
50func calculateTotalTax(taxSystems []taxSystem) int {
51    totalTax := 0
52    for _, t := range taxSystems {
53        totalTax += t.calculateTax() // در اینجا runtime polymorphism رخ می دهد
54    }
55    return totalTax
56}
1$ go run main.go
2Total Tax is 300

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

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

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

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

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

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

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

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

 1package main
 2
 3import "fmt"
 4
 5type animal interface {
 6	breathe()
 7	walk()
 8}
 9
10type lion struct {
11	age int
12}
13
14func (l lion) breathe() {
15	fmt.Println("Lion breathes", l)
16}
17
18func (l lion) walk() {
19	fmt.Println("Lion walk", l)
20}
21
22func main() {
23	var a animal
24
25	a = lion{age: 10}
26	a.breathe()
27	a.walk()
28
29	a = &lion{age: 5}
30	a.breathe()
31	a.walk()
32}
1$ go run main.go
2Lion breathes {10}
3Lion walk {10}
4Lion breathes {5}
5Lion walk {5}

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

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

 1package main
 2
 3import "fmt"
 4
 5type animal interface {
 6	breathe()
 7	walk()
 8}
 9
10type lion struct {
11	age int
12}
13
14func (l *lion) breathe() {
15	fmt.Println("Lion breathes")
16}
17
18func (l *lion) walk() {
19	fmt.Println("Lion walk")
20}
21
22func main() {
23	var a animal
24
25	a = lion{age: 10}
26	a.breathe()
27	a.walk()
28
29	a = &lion{age: 5}
30	a.breathe()
31	a.walk()
32}
1$ go run main.go
2cannot use lion literal (type lion) as type animal in assignment:
3        lion does not implement animal (breathe method has pointer receiver)

در بالا خطایی که رخ داده گفته شده شما فقط می توانید یک نمونه از ساختار lion را فقط با اشاره گر استفاده کنید.

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

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

 1package main
 2
 3import "fmt"
 4
 5type animal interface {
 6	breathe()
 7	walk()
 8}
 9
10type cat string
11
12func (c cat) breathe() {
13	fmt.Println("Cat breathes")
14}
15
16func (c cat) walk() {
17	fmt.Println("Cat walk")
18}
19
20func main() {
21	var a animal
22
23	a = cat("smokey")
24	a.breathe()
25	a.walk()
26}
1$ go run main.go
2Cat breathes
3Cat walk

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

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

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

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

 1package main
 2
 3import "fmt"
 4
 5type animal interface {
 6    breathe()
 7    walk()
 8}
 9
10type mammal interface {
11    feed()
12}
13
14type lion struct {
15     age int
16}
17func (l lion) breathe() {
18    fmt.Println("Lion breathes")
19}
20func (l lion) walk() {
21    fmt.Println("Lion walk")
22}
23func (l lion) feed() {
24    fmt.Println("Lion feeds young")
25}
26func main() {
27    var a animal
28    l := lion{}
29    a = l
30    a.breathe()
31    a.walk()
32    var m mammal
33    m = l
34    m.feed()
35}
1$ go run main.go
2Lion breathes
3Lion walk
4Lion feeds young

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

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

 1package main
 2
 3import "fmt"
 4type animal interface {
 5    breathe()
 6    walk()
 7}
 8
 9func main() {
10    var a animal
11    fmt.Println(a)
12}
1$ go run main.go
2nil

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% نوع و مقدار را می توانید چاپ کنیم.

 1package main
 2
 3import "fmt"
 4
 5type animal interface {
 6    breathe()
 7    walk()
 8}
 9
10type lion struct {
11    age int
12}
13
14func (l lion) breathe() {
15    fmt.Println("Lion breathes")
16}
17
18func (l lion) walk() {
19    fmt.Println("Lion walk")
20}
21
22func main() {
23    var a animal
24    a = lion{age: 10}
25    fmt.Printf("Underlying Type: %T\n", a)
26    fmt.Printf("Underlying Value: %v\n", a)
27}
1$ go run main.go
2Concrete Type: main.lion
3Concrete Value: {10}

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

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

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

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

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

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

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

اگر هنگام Type Assertion شما وضعیت متغیر ok را بررسی نکنید با خطای panic مواجه خواهید شد.
 1package main
 2
 3import "fmt"
 4
 5type animal interface {
 6	breathe()
 7	walk()
 8}
 9
10type lion struct {
11	age int
12}
13
14func (l lion) breathe() {
15	fmt.Println("Lion breathes")
16}
17
18func (l lion) walk() {
19	fmt.Println("Lion walk")
20}
21
22type dog struct {
23	age int
24}
25
26func (d dog) breathe() {
27	fmt.Println("Dog breathes")
28}
29
30func (d dog) walk() {
31	fmt.Println("Dog walk")
32}
33
34func main() {
35	var a animal
36
37	a = lion{age: 10}
38	print(a)
39
40}
41
42func print(a animal) {
43	l, ok := a.(lion)
44	if ok {
45		fmt.Printf("Age: %d\n", l.age)
46	}
47}
1$ go run main.go
2Age: 10

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

1l := a.(lion)

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

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

 1package main
 2
 3import "fmt"
 4
 5type animal interface {
 6	breathe()
 7	walk()
 8}
 9
10type lion struct {
11	age int
12}
13
14func (l lion) breathe() {
15	fmt.Println("Lion breathes")
16}
17
18func (l lion) walk() {
19	fmt.Println("Lion walk")
20}
21
22type dog struct {
23	age int
24}
25
26func (d dog) breathe() {
27	fmt.Println("Dog breathes")
28}
29
30func (d dog) walk() {
31	fmt.Println("Dog walk")
32}
33
34func main() {
35	var a animal
36
37	x = lion{age: 10}
38	print(x)
39
40}
41
42func print(a animal) {
43	switch v := a.(type) {
44	case lion:
45		fmt.Println("Type: lion")
46	case dog:
47		fmt.Println("Type: dog")
48	default:
49		fmt.Printf("Unknown Type %T", v)
50	}
51}
1$ go run main.go
2Type: lion

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

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

 1package main
 2
 3import "fmt"
 4
 5func main() {
 6    test("thisisstring")
 7    test("10")
 8    test(true)
 9}
10
11func test(a interface{}) {
12    fmt.Printf("(%v, %T)\n", a, a)
13}
1$ go run main.go
2(thisisstring, string)
3(10, string)
4(true, bool)

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