اگر قسمتهای قبلی این مجموعه مطالب را خوانده باشید، احتمالاً میتوانید در مورد کارکرد این اصل حدسهایی بزنید. و البته حق با شماست، به این معنا که اصل Dependency Inversion کم و بیش تکرار همان چیزی است که تا به حال در اصول قبلی SOLID در موردشان بحث کردیم. تعریف رسمی اصل پنجم خیلی ترسناک نیست، بنابراین بیایید به آن نگاهی بیندازیم:
ماژول های سطح بالاتر نباید به ماژولهای سطح پایین وابسته باشند. هر دو باید به انتزاعات بستگی داشته باشند.
کلاس های سطح پایین عملیات های پایهای مانند کار با دیسک، انتقال داده ها از طریق شبکه، اتصال به پایگاه داده و غیره را انجام میدهند. کلاس های سطح بالا حاوی bussiness logicهای پیچیده ای هستند که کلاس های سطح پایین را به انجام اهداف خودبه کار میگیرند. بله منطقی است اگر من یک کلاس سطح بالا داشته باشم و این کلاس برای انجام کار خود مستقیما وابستهی یک کلاس سطح پایین باشد، بهتر است چنین کلاسی را بازنویسی کنم. در هنگام بازنویسی، باید کلاسهای جدید طوری کدنویسی شوند که بر انتزاعات، مانند abstract class ها یا interface ها و موارد این چنینی تکیه کنند.
چرا؟
ما قبلاً یک نمونه عالی از این وضعیت را در اصول Single Responsibility، Open-Closed و Liskov Substitution بررسی کردیم. جایی که فرض کردیم اگر از یک سرویس تولید PDF استفاده کنیم(کلاس سطح پایین) و کدمان مملو از از ارجاع به این کلاس باشد، روزی که کسبوکار تصمیم بگیرد از سرویس دیگری استفاده کند، روزی خواهد بود که برای همیشه به یاد میماند. به دلایل اشتباه ما!
برای حل مشکل، ما یک شکل کلی برای این وابستگی تعریف کردیم. یعنی یک interface برای سرویسهای PDF ایجاد کردیم و اجازه دادیم ابزار دیگری نمونه سازی از آن را مدیریت کند و آن را به ما ارسال کند (در لاراول، ما دیدیم که چگونه سرویس کانتینر به ما در انجام این کار کمک کرد).
در مجموع، کلاس سطح بالای ما، که قبلاً کنترل ایجاد نمونه های کلاس سطح پایین برای تولید فایل PDF را بر عهده داشت، اکنون باید به دنبال هدف و وظیفهی اصلی خود باشد. میبینید که شرایط کاملا تغییر کرده و به اصطلاح ورق برگشته است، و به همین دلیل است که ما اصل پنجم را وارونگی وابستگی ها می نامیم.
اگر به دنبال یک مثال از دنیای اطرافمان هستید، تصور کنید تلفن همراه شما باید شارژ شود، آیا تلفن همراه شما باید از نحوه عملکرد اتصال برق پشت پریز و نحوهی تغییر آن به حدی که برای شارژ موبایل مناسب باشد، مطلع شود؟ مسلما نه. تلفن همراه شما فقط به یک رابط مانند سوکت شارژ برای اتصال نیاز دارد. این همان چیزی است که در مورد اصل Dependency Inversion باید بدانیم.
حال بیایید با یک مثال ساده، این موضوع را در زمان کدنویسی بررسی کنیم. فرض کنید در قسمتی از پروژه نیاز داریم تا پیغامی را به کاربر ایمیل کنیم. در نتیجه ممکن است در نگاه اول چنین کدی بنویسیم:
class Mailer
{
public function send ($string)
{
// sending mail
}
}
class SendWelcomeMessage
{
private $mailer;
public function __construct (Mailer $mailer)
{
$this->mailer = $mailer;
}
public function send_string ($user_id)
{
$string = 'welcome user ' . $user_id;
$this->mailer->send ($string);
}
}
در مثال بالا، کلاس سطح پایین ما Mailer و کلاس سطح بالای ما SendWelcomeMessage است. در کد بالا به وضوح میبینید که کلاس سطح بالا بصورت مستقیم نیازمند کلاس سطح پایین است(در داخل constructor مستقیما نمونهای از کلاس Mailer دریافت میکند. حال فرض کنید روش ارسال پیام به کاربر بنا به نیاز تغییر کند و مثلا نیاز باشد که پیام خوشآمد گویی را به کاربر SMS کنیم. در این شرایط، برای اعمال تغییرات باید کلاس سطح بالای خود را تغییر دهیم و این مسئله قانون دوم SOLID که اصل Open-Closed است را نقض میکند. برای حل این مشکل میتوانیم راهکار سادهی زیر را در نظر بگیریم. ابتدا یک interface به نام Sender تعریف میکنیم که تنها الزام آن پیادهسازی متد send باشد:
interface Sender
{
public function send ($string);
}
حال کلاسهای سطح پایین خود که Mailer و SMSer هستند را به این رابط وابسته میکنیم:
class SMSer implements Sender
{
public function send ($string)
{
// sending SMS
}
}
class Mailer implements Sender
{
public function send ($string)
{
// sending email
}
}
در مرحلهی آخر، کلاس سطح بالای خود را بجای وابستگی مستقیم به کلاسهای سطح پایین، به رابط Sender وابسته میکنیم:
class SendWelcomeMessage
{
private $sender;
public function __construct (Sender $sender)
{
$this->sender = $sender;
}
public function send_string ($user_id)
{
$string = 'welcome user ' . $user_id;
$this->sender->send ($string);
}
}
اکنون که کلاس سطح بالایمان میتواند نمونهای از رابط Interface دریافت کند، این اطمینان وجود دارد که نمونه دریافت شده در کلاس سطح بالا، حتما متد send را پیادهسازی کرده است. پس کلاس سطح بالا نیز تنها با وابستگی به رابط Sender و بدون اطلاع از نوع نمونه دریافتی در constructor میتواند کار خود را انجام دهد. حال اگر مجدد تصمیم بگیریم که پیام را بجای ارسال SMS با ایمیل ارسال کنیم، دیگر نیازی به اعمال تغییر در کلاس سطح بالا نداریم. فقط باید نمونهای از کلاس Mailer را در اختیار کلاس SendWelcomeMessage قرار دهیم.
به همین سادگی ما موفق شدیم که اصل پنجم SOLID یعنی اصل Dependency Inversion را در کدنویسی رعایت کنیم.
موفق و پیروز باشید.