خلاصه کتاب Pragmatic Programmer. درس 31

درس 31: مالیات وراثت Inheritance Tax

آیا با زبونای برنامه نویسی شی گرا کد میزنید؟ آیا از شی گرایی استفاده میکنید؟

اگر آره. دست نگه دارید. این احتمالا چیزی نیست که شما میخاید.

بریم ببینیم چرا؟

یک فلش بک بزنیم به گذشته

شی گرایی اولین بار توی زبون برنامه نویسی Simula 67 در سال 1969 میلادی (1347 شمسی) ایجاد شد. یک راه حل ظریف بود که در مسئله صف بندی ایونت ها با تایپ های مختلف، کارساز شد. و توی این زبان بود که از prefix classes استفاده شد. شما یک چنین چیزی باید مینوشتید:

link CLASS car;
... implementation of car
link CLASS bicycle;
... implementation of bicycle

در اینجا link یک prefix class هستش که قابلیت linked list رو اضافه می کنه. این اجازه میده که ماشین و دوچرخه رو به لیستی اضافه کنید. توی این مکانیزم link یک کلاس پدر هست و هر دو کلاس ماشین و دوچرخه پیاده سازی میکنن link رو و کدش رو به ارث بردن.

بعدتر در زبون برنامه نویسی SmallTalk 72 نوع دیگه ای شی گرایی رو داشتیم، که Alan Kay یکی از سازنده هاش توی سایت Quora سال 2019 اینجوری راجبش توضیح داد: ما دنبال راه دیگه ای بودیم، که به صورت آزمایشی روش داینامیک مثل زبون Lisb رو اعمال کردیم.

زبونهای C++ و جاوا سبک شی گرایی شون ادامه راهه Simula هست که تفکرش به این شکله که از شی گرایی برای ترکیب کردن تایپ ها استفاده میکنه. و در سبک SmallTalk شی گرایی dynamic organization of behaviors هستش که در زبونهای Ruby و JavaScript این سبک پیگیری شد.

در استفاده از شی گرایی، معمولا ما با دو دسته برنامه نویس طرف هستیم: اونهایی که typing رو دوست ندارن یا اونهایی که برعکس دوست دارند.(تایپ به منظور ساختار داده)

دسته اول که دوست ندارن از این بابته که مجبور به نوشتن تایپ های جدید یا طولانی نشن و از تایپ های بیس فانکشنالیتی ها رو ارث ببرن. دسته دومم که دوست دارند از این بابت که روابط بین داده ها رو به درستی اعمال کنند مثلا a Car is-a-kind-of Vehicle

** متاسفانه هر دو نوع شی گرایی، مشکلاتی به همراه داره.

· مشکلات شی گرایی برای share کردن کد

شی گرایی وابستگی است(coupling). نه تنها کلاس فرزند وابسته است به کلاس والدش و همچنین والدهای والدش، بلکه کدی که از اون کلاس فرزند استفاده میکنه هم درگیر کل اون مسیر والدها هستش. این مثالو ببینید:

class Vehicle

def initialize

@speed = 0

end

def stop

@speed = 0

end

def move_at(speed)

@speed = speed

end

end

class Car < Vehicle

def info

"I'm car driving at #{@speed}"

end

end

# top-level code

my_ride = Car.new

my_ride.move_at(30)

وقتی کد top-level اینو کال میکنه move_at متود در کلاس vehicle کال میشه یعنی کلاس والد car. حالا فرض کنید برنامه نویس طبق تسکی کلاس vehicle رو تغییر میده و move_at تبدیل به تابع set_velocity میشه و متغیر @speed هم به @velocity تغییر میکنه. این تغییر باعث خطا توی تمامی کلاینتهایی که از کلاس vehicle استفاده کردن، میشه. اما خوده کلاسش که خطایی نداره و صرفا تغییر کرده.

وابستگی زیاد So much coupling.

حالت بدترش ارث بری چندگانه است، که یک کلاس از چند کلاس ارث ببره، توی C++ این وجود داره و دردسرهای زیادی هم بوجود میاره، زبونهای شی گرای مدرنتر این رو پیاده سازی نکردند.

Don’t Pay Inheritance Tax

· راه های جایگزین بهترند

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

- Interfaces and protocols

- Delegation

- Mixins and traits

· Interfaces and protocols

اکثر زبونای برنامه نویسی شی گرا این امکانو میدن که کلاسها بتونن رفتارشون رو از جاهای مختلفی به ارث ببرند و موظف به پیاده سازی برای اون رفتار بشن، مثلا فرض کنید کلاس car رو که هم رفتار drivable رو پیاده سازی میکنه هم locatable. سینتکس این مثال توی زبون جاوا به شکل زیره:

public class Car implements Drivable, Locatable {
// Code for class Car. This code must include
// the functionality of both Drivable
// and Locatable
}

drivableو locatableهر دو در جاوا اینترفیس هستند، زبونهای دیگه هم اینترفیس رو به شکل های مختلف دارند و در بعضی زبون ها بهش میگن protocols. اینترفیس ها به شکل ذیل نوشته میشن:

public interface Drivable {
double getSpeed();
void stop();
}

public interface Locatable() {
Coordinate getLocation();
boolean locationIsValid();
}

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

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

List<Locatable> items = new ArrayList<>();
items.add(new Car(...));
items.add(new Phone(...));
items.add(new Car(...));
// ...

و بعد با خیال راحت میتونیم لیستو پراسس کنیم، چون مطمئنیم هر ایتم توابع getLocation و locationIsValid رو پیاده سازی کرده.

ترجیح به اینه که polymorphism (چندریختی) رو با اینترفیس ها پیاده سازی کنید و از ارث بری استفاده نکنید.

· Delegation

ارث بری برنامه نویس ها رو به سمتی میبره که کلاسهایی مینویسن که متودهای زیادی داره، اگر کلاس والد 20 متود داشته باشه، و کلاس فرزند فقط 2 تاشو لازم داشته باشه، ابجکت کلاس فرزند الکی 18 متود اضافی رو یدک میکشه. مثال زیرو در نظر بگیرید:

class Account < PersistenceBaseClass
end

کلاس اکانت الان تمامی فانکشنهای کلاس persistence رو ارث میبره، حالا فرض کنید با delegation این حرکتو بزنیم:

class Account
def initialize(. . .)
@repo = Persister.for(self)
end
def
save
@repo.save()
end
end

با این کار دیگه کل توابع پرسیستنس رو توی کلاس اکانت نمیاریم، اما اصل جداسازی(decoupling) رو نقض کردیم که! اما چیز فراتری وجود داره. با اینکار ما دیگه زیر تعهده persistence api نیستیم و میتونیم api خودمونو هرآنچه دقیقا لازم داریم، راحت پیاده سازی کنیم. شاید بگید خوب به روش اینترفیس هم میشد، اما اینجوری خیالمون راحته که اگر حتی interface هم بای پس شد، زیر چتر persistence نمیریم و کنترل کلاس خودمونو داریم.

میتونیم فراتر بریم، آیا لازمه که یک اکانت اطلاعات persist رو خودش نگه داره؟ مگه کارش نگهداری و مدیریت بیزنس رول های یک حساب نیست؟

class Account
# nothing but account stuff

end
class
AccountRecord
# wraps an account with the ability
# to be fetched and stored
end

با این مکانیزم حالا واقن جداسازی(decoupling) رو داریم، اما برامون هزینه هم داشت. کد بیشتری باید بنویسیم، و بعضیاشونم تکراری اند، مثلا این یک نیازه که همه کلاسهای رکوردمون یک تابع find داشته باشن.

خوشبختانه مشکل اینجارو هم mixins و traits برامون حل میکنن.

· Mixins, Traits, Categories, Protocol Extensions, …

به عنوان یک صنعت، ما هم عاشق اینکاریم که برای هر چیزی اسم تولید کنیم، هرچند که به یک چیز نامهای مختلف بدیم. هر چی بیشتر بهتر. افتاد؟ 😊

در مورد mixins ها همینطوره. ایده اونها خیلی ساده است: ما میخایم آبجکتها و کلاس ها رو گسترش بدیم و بهشون فانکشنالیتی اضافه کنیم، بدون ارث بری.

در نتیجه یک سری فانکشن دسته بندی شده می نویسیم، به هر دسته یک اسم میدیم، و بعد یک کلاس رو با اون دسته فانکشن اکستند میکنیم.(اینکه اکثر این جملات رو فینگیلیش مینویسم چون اگر ترجمه معادل این کلمات به درد میخورد که ترجمه های جعفرنژاد قمی یا سالخورده حقیقی هم به درد میخورد😊 . و اصولا کسی که با برنامه نویسی اشنا هست بهتر میفهمه داستانو با همین کلمات پرکاربرد تخصصی. بگذریم)

توی این حالت شما کلاسهایی میسازید که ترکیبی از امکانات خودشون و mixins ها رو دارند. ممکنه اسم و نحوه پیاده سازی این فیچر توی زبونهای مختلف فرق بکنه. نکته مهم اینه که از mixins نمیشه آبجکت ساخت بلکه فقط کارشون و ذاتشون اضافه کردن قابلیت هاست به کلاسها و آبجکت های دیگه.

برای مثال برگردیم به همون مثال account، در اونجا AccountRecord باید هم از account مطلع میبود هم از persistence framework و نیاز داشت تا متودهایی persistence layer رو دلیگیت کنه، اونایی رو ک میخاست برای بیرون خودش استفاده کنه.

با mixins ها اینجوریه که به عنوان مثال ما یک mixins مینویسیم که دو سه تا از متودهای استاندارد finder رو پیاده سازی کنه. و بعد به AccountRecord اضافه شون میکنیم به عنوان mixins.

mixin CommonFinders {
def find(id) { ... }
def findAll() { ... }
end
class AccountRecord extends BasicRecord with CommonFinders
class OrderRecord extends BasicRecord with CommonFinders

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

اگر یک account رو مثال بزنیم، لایه های مختلفی از ولیدیشن وجود داره که میتونه اعمال بشه:

- چک کردن پسورد هش شده اکانت برای اطمینان از لاگین بودن کاربر

- ولیدیشن دیتای ورودی یوزر هنگام ایجاد اکانت

- ولیدیشن سازگاری نوع داده ها

یک راه معمول(هرچند که از ضم ما ایده آل هم نیست) اینه که همه ولیدیشن ها رو بیاریم توی یک کلاس. ما فکر میکنیم راه بهترش استفاده از mixins هاست برای ساخت کلاسهایی برای شرایط خاص:

class AccountForCustomer extends Account
with AccountValidations,AccountCustomerValidations
class AccountForAdmin extends Account
with AccountValidations,AccountAdminValidations

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

- از mixins ها برای انتشار فانکشنالیتی استفاده میکنیم.

خوب ما سه روش جایگزین برای ارث بری سنتی رو گفتیم، شما باید بسنجید که کدوم روش در کجا به کارتون میاد و حداکثر کارایی و تمیزی رو بهتون میده، همچین اینو مدنظرتون داشته باشید که نگاه 0 و 1 نداشته باشید و یک روش رو کلا کنار نزارید و همیشه بالا پایین کنید تمام راه های ممکن رو.

دروس مرتبط: 8, 10, 28

منبع کانال تلگرامی: https://t.me/pragmaticprogrammer_fa