در این آموزش میخواهیم اصل چهارم از اصول پنجگانه SOLID را بررسی کنیم. اصل Interface Segregation همانطور که از نام آن برمیآید، مربوط به جداسازی Interface هاست. چیزی که به نظرم مبهم میآید این است که کجا و چگونه این اتفاق میافتد.
اگر شما هم همین فکر به ذهنتان رسید، به من اعتماد کنید و باور داشته باشید که تقریباً درک و استفاده از این اصل را به پایان رسانده اید. اگر تمام پنج اصل SOLID ابزار سرمایهگذاری بودند، این یکی طولانیمدتترین ارزش را در یادگیری کدنویسی خوب ارائه میکرد (خوب، متوجه شدم که در مورد هر اصل این را میگویم، اما باور کنید این اصول واقعا منطق کدنویسی شما را دگرگون خواهند کرد).
اصل Interface Segregation میگوید: هرچه تعداد رابطهای تخصصیتر و متعددتری در برنامه شما وجود داشته باشد، کد شما ماژولارتر است و سایر برنامه نویسان با دیدن کد شما کمتر تعجب خواهند کرد.
بیایید به یک مثال بسیار رایج و کاربردی نگاه کنیم. هر توسعهدهندهی حرفهای لاراول در کار خود با الگوی Repository مواجه میشود، پس از آن، چند هفته با این الگو کلنجار میرود و آن را سبک و سنگین میکند، اما در نهایت اکثر افراد این الگو را کنار میگذارند. چرا؟ زیرا در تمام آموزشهایی که الگوی Repository را یاد میدهند، به شما توصیه میشود که یک رابط مشترک (به نام RepositoryInterface) ایجاد کنید که متدهای مورد نیاز برای دسترسی یا دستکاری دادهها را تعریف میکند. این رابط پایه ممکن است چیزی شبیه به این باشد:
interface RepositoryInterface {
public function getOne($id);
public function getAll();
public function create(array $data);
public function update(array $data, $id);
public function delete($id);
}
و اکنون، برای مدل کاربر خود، قرار است یک UserRepository ایجاد کنید که این رابط را پیاده سازی کند. سپس، برای مدل مشتری خود، قرار است یک CustomerRepository ایجاد کنید که این رابط را پیاده سازی کند. شما ایده این الگو را به این شکل فرا گرفتهاید.
حالا، در یکی از پروژههای من چنین اتفاقی افتاد که برخی از مدلها قرار نبود توسط کسی غیر از سیستم قابل نوشتن باشند. قبل از شروع به چرخاندن چشمان خود، در نظر بگیرید که ثبت یا حفظ یک مسیر حسابرسی یک مثال خوب و واقعی از چنین مدلهای «فقط خواندنی» است. مشکلی که من با آن روبرو شدم این بود که از آنجایی که قرار بود مخازنی ایجاد کنم که همگی رابط RepositoryInterface را پیاده سازی کنند، مثلاً LoggingRepository، حداقل دو تا از متدهای موجود در اینترفیس، update() و delete() برای من فایده ای نداشتند.
بله، یک راه حل سریع این است که به هر حال برای اینکه بتوانم در کلاس LoggingRepository رابط مخزن RepositoryInterface را پیاده سازی(implement) کنم، ناچارا متدهای اضافی update() و delete() را داخل کلاس قرار دهم ولی داخل آنها را خالی بگذارم. یا درون آنها یک Exception ایجاد کنم تا به محض اجرا شدن خطا بدهند. اما اگر تکیه بر چنین راه حل هایی برای ماله کشی و پاک کردن صورت مساله به جای حل آن، کار خوبی بود، در وهله اول نباید از الگوی Repository پیروی میکردم! حال فکر میکنید این بدان معناست که همه اینها تقصیر الگوی Repository است؟ نه اصلا! این ما هستیم که بد آموزش دیدهایم.
در واقع، یک Repository یک الگوی شناخته شده و پذیرفته شده است که سازگاری، انعطاف پذیری و انتزاع را برای روشهای دسترسی به داده های شما به ارمغان می آورد. مشکل این است که رابطی که ما ایجاد کردیم یا باید بگویم رابطی که تقریباً در هر آموزشی آن را مثال میزنند، بسیار گسترده و عمومی است. چنین رابطی نمیتواند تحت هر شرایطی پاسخگوی نیازهای ما باشد. از طرفی ما هم نباید فکر کنیم که برای استفاده از الگوی Repository فقط مجاز هستیم که یه رابط مانند RepositoryInterface داشته باشیم و با این فرض اشتباه، تمام پیشفرضهای مورد نیاز را داخل این رابط بگنجانیم. این فقط آشفتگی به بار میآورد.
گاهی اوقات این پدیده با گفتن این که رابط “fat” است بیان می شود، و واقعا هم همین معنی را میدهد. رابط چاق است، یعنی Interface فرضیات زیادی ایجاد می کند، و بنابراین متدهایی را اضافه می کند که برای برخی از کلاسها بی فایده هستند، اما همچنان همهی کلاسها مجبور به پیاده سازی آن متدها هستند، این باعث شکنندگی و سردرگمی در کد میشود. شکننده و گیج کننده بودن کدها شاید در مثال ما خیلی ملموس نباشد، زیرا نهایتا دو متد update و delete هستند که بی استفاده تعریف شدهاند. اما تصور کنید وقتی چندین کلاس متدهایی را که نمیخواستند یا آنهایی را که میخواستند در کنار دیگر متدهای غیر لازم، پیادهسازی کردهاند، چه آشفتگی بدی میتواند ایجاد شود.
راه حل ساده است، و همچنین نام اصلی است که ما در مورد آن بحث می کنیم: Interface Segregation
نکته این است که ما نباید رابط های خود را کورکورانه ایجاد کنیم. همچنین نباید فرضیات بیخود و نامرتبط در داخل Interface ها داشته باشیم. مهم نیست که چقدر با تجربه یا باهوش و درایت فکر می کنیم. به جای ایجاد یک رابط کلی و حجیم، باید چندین رابط کوچکتر و تخصصی ایجاد کنیم و به کلاس ها اجازه دهیم موارد مورد نیاز را پیاده سازی کنند و آن هایی را که ضروری نیستند کنار بگذارند.
در مثالی که بحث کردیم، بهتر است به جای یک رابط، دو رابط ایجاد کنیم: یکی برای فقط خواندن اطلاعات به نام ReadOnlyRepositoryInterface (شامل توابع getOne() و getAll()) و یکی فقط برای اعمال تغییرات به نام WriteModifyRepositoryInterface (شامل بقیه توابع). برای مخازن معمولی، مانند UserRepository میتوانم بگویم کلاس هر دو interface را implement کند و برای مخازنی که باید فقط خواندنی باشند، بگویم که کلاسهای مربوطه فقط از WriteModifyRepositoryInterface پیروی کنند. (نکته جانبی: موارد خاصی ممکن است هنوز پیش بیاید، و این خوب است زیرا هیچ طراحی ای کامل نیست. حتی ممکن است بخواهید برای هر متد یک رابط جداگانه ایجاد کنید، و این نیز خوب خواهد بود، با فرض اینکه نیازهای پروژه شما به اندازه کافی ریز باشد.)
interface WriteModifyRepositoryInterface {
public function create(array $data);
public function update(array $data, $id);
public function delete($id);
}
interface ReadOnlyRepositoryInterface {
public function getOne($id);
public function getAll();
}
class UserRepository implements ReadOnlyRepositoryInterface, WriteModifyRepositoryInterface {
public function getOne($id){} public function getAll(){} public function create(array $data){} public function update(array $data, $id){} public function delete($id){} }
اکنون رابط های بیشتری وجود دارد و برخی ممکن است بگویند که چیزهای زیادی برای به خاطر سپردن وجود دارد یا اینکه تعریف یک کلاس اکنون خیلی طولانی است (یا زشت به نظر می رسد) و غیره، اما به آنچه به دست آورده ایم نگاه کنید: هر رابط تخصصی، کوچک، و خود گویای کاریست که انجام میدهد. چنین رابطهایی می توانند در صورت نیاز ترکیب شوند و مانع یکدیگر نشوند. تا زمانی که برای امرار معاش خود نرم افزار می نویسید، به یاد داشته باشید که این حالت ایده آلی است که همهی برنامه نویسان برای رسیدن به آن تلاش می کنند.