یک مهندس نرمافزار در دیوار.
ارثبری در Go؟ آشنایی با Composition در Golang.
یک مثال ساده رو در نظر بگیریم. ما در زبانهای شیگرا احتمالا یک دانشآموزی داریم که از کلاس انسان ارث بری میکنه.
class Person {
String name
}
class Student extends Person{
String grade
}
در کد بالا کلاس دانشآموز ما از کلاس Person ارث بری ( Inheritance ) کرده. یعنی دانشآموز ما تمام خصوصیات ( صفات و متدها ) کلاس Person رو به ارث برده. ما میتونیم اگر جایی ورودی تابعی Person بود یک آبجکت از Student ورودی بدیم (اصل Liskov Substitution - جلوتر بیشتر در موردش صحبت میکنیم)
حالا این اتفاق توی Go چطور انجام میشه؟
توی Go ما از Inheritance استفاده نمیکنیم، بلکه از Composition با استفاده از Embedding تایپ ها استفاده میکنیم. مثال دانشآموز و انسان رو در نظر بگیرید. در Inheritance ما از کلاس Person استفاده میکردیم و یک کلاس جدید با تمام خصوصیات Person استفاده میساختیم و در ادامه صفات بیشتری که مد نظرمون رو هم به کلاس Student اضافه میکردیم.
اما در Composition ما تایپ جدیدی که میسازیم تایپ مادر رو شامل میشه. یعنی چی؟ مثال قبلی رو اگر بخوایم با Composition بنویسیم اینطور میشه:
class Person {
String name
}
class Student{
Person person
}
همونطوری که میبینید ما تمام خصوصیاتی که از Person میخوایم رو در صفتی به نام person ذخیره کردیم و برای مثال از طریق person میتونیم به نام دانشآموز دسترسی داشته باشیم. حالا مثالش توی Go چطور میشه؟
type Person struct {
Name string
}
type Student struct {
Person
Grade int
}
اینطور هم میتونیم یک دانشآموز بسازیم:
Ali := &Student{
Person: Person{
Name: "Ali",
},
Grade: 12,
}
fmt.Println(Ali.Person.Name)
fmt.Println(Ali.Grade)
به لطف Go وقتی Composition ایی انجام میدیم که صفاتی با نام های متفاوت داشته باشیم میتونیم اسم متغیری که کامپوزیشن رو نگه میداره رو هم صدا نزنیم و مستقیم متغیری که میخوایم رو صدا بزنیم:
fmt.Println(Ali.Person.Name) // Ali
fmt.Println(Ali.Name) // Ali
اما برای مثال اگر تایپ Student ما علاوه بر Person شامل Class هم بود که مشخص کنه دانشآموز در چه کلاسی درس میخونه کد ما اینطور میشد:
type Class struct {
Name string
}
type Student struct {
Person
Class
Grade int
}
حالا اگر Ali.Name بزنیم کامپایلر به ما ارور میده که Name مبهم هست (منظور اسم Class هست یا اسم Person؟؟). در چنین شرایطی اسم کامپوزیشن رو هم باید صدا بزنیم: Ali.Person.Name یا Ali.Class.Name.
خب بریم سراغ اصل Liskov. اگر از یک زبان شی گرا سمت Go اومده باشید مثل من این اصل اینطور توی ذهنتون مونده که اگر کلاس A فرزند کلاس B باشه هرجا نیاز به کلاس B بود میتونیم کلاس A رو به عنوان ورودی بدیم. مثلا اگر تابعی داشتیم ورودی Person میگرفت میتونیم جاش Student رو بدیم. اما توی Go ما که Inheritance نداریم. حالا چیکار کنیم؟ فرض کنید چنین تابعی داریم:
func Greeting(person Person) {
fmt.Println("Welcome ", person.Name)
}
ورودی این تابع از نوع Person هست. اگر بخوایم Student بهش بدیم چه اتفاقی میوفته؟ احتمالا IDE تون این ارور رو میده اما اگرم نداد و بخواید کامپایل کنید اینطور اروری میگیرید:
Ali := Student{
Person: Person{
Name: "Ali",
},
Grade: 12,
}
Greeting(Ali)
...
cannot use Ali (type Student) as type Person in argument to Greeting
خب. برای این کار باید چیکار کنیم؟ اول بیایم ببینیم اصل Liskov چی میگه؟ فرض کنید کلاینت (کلاس، متد، تابع و... کلا هر چیزی) A از کلاس B میخواد استفاده کنه. حالا کلاس C از کلاس B نشئت گرفته. ما باید بتونیم در صورت نیاز کلاس C رو جای کلاس B به A بدیم. یعنی B نه چیزی رو کمتر برای C پیاده سازی کرده باشه نه چیزی رو بیشتر.
حالا برای اینکه این موضوع رو در Go داشته باشیم از اینترفیس ها استفاده میکنیم. اول برای تایپ Person یک متد میسازیم به اسم GetName() که نام رو خروجی بده:
type Person struct {
Name string
}
func (p Person) GetName() string {
return p.Name
}
ما میتونیم به این متد از طریق student.GetName() دسترسی داشته باشیم. حالا میایم یک اینترفیس به اسم Namer تعریف میکنیم که متد GetName() داشته باشه:
type Namer interface {
GetName() string
}
حالا میتونیم جای اینکه یک تایپ رو به عنوان ورودی بگیریم از اینترفیس استفاده کنیم. یعنی تابع Greeting ما اینطور میشه:
func Greeting(person Namer) {
fmt.Println("Welcome ", person.GetName())
}
و راحت میتونیم Ali رو به عنوان ورودی بهش بدیم:
func main() {
Ali := Student{
Person: Person{
Name: "Ali",
},
Grade: 12,
}
Greeting(Ali)
}
func Greeting(person Namer) {
fmt.Println("Welcome", person.GetName()) // Welcome Ali
}
حالا میتونیم راحت تایپ Teacher رو هم با استفاده از Person بسازیم و به عنوان ورودی Greeting بدیم:
type Teacher struct {
Person
Age int
}
func main() {
Saeed := Teacher{
Person: Person{Name: "Saeed"},
Age: 24,
}
Greeting(Saeed)
}
func Greeting(person Namer) {
fmt.Println("Welcome ", person.GetName()) // Welcome Saeed
}
با این کار ما مجبور میشیم اصل Dependency inversion رو هم رعایت کنیم.
مطلبی دیگر از این انتشارات
پوینتر ها در گو | آشنایی و درک بهتر آنها
مطلبی دیگر از این انتشارات
کتابخانه های استاندارد در Go (بخش اول)
مطلبی دیگر از این انتشارات
جنریک ها در گولنگ