آموزش اصل Dependency Inversion از اصول SOLID

در دنیای برنامه‌نویسی همیشه تلاش می‌کنیم تا کدی بنویسیم که هم مقیاس‌پذیر باشد و هم اعمال تغییرات در آن بدون دردسر انجام شود. اما وابستگی بین کلاس‌ها باعث سخت شدن این مسئله می‌شود. اصل وارونگی وابستگی یا Dependency Inversion به ما کمک می‌کند این مشکل را حل کنیم. همچنین باعث می‌شود کدهای انعطاف‌پذیرتری داشته باشیم.

اصل Dependency Inversion یکی از اصول SOLID در برنامه‌نویسی است. مشابه چهار اصل دیگر، این اصل، یک اصل (Principle) است و نه یک قانون! بنابراین اجباری به رعایت آن در کدهایی که می‌نویسیم نداریم.

اما اگر بخواهیم کدهای بهتری و توسعه‌پذیرتری بنویسیم، بهتر است آن را رعایت کنیم. همچنین اگر در پروژه‌ای باید از اصول SOLID پیروی کنیم، رعایت آن الزامی می‌شود.

اصل وارونگی وابستگی چیست؟

در تعریف اصل Dependency Inversion داریم که:

  • ماژول‌های سطح بالا نباید به ماژول‌های سطح پایین وابسته باشند. هر دوی آن‌ها باید به یک انتزاع (abstract) وابسته باشند.
  • جزئیات نباید تعیین‌کننده نحوه‌ی کارکرد انتزاع باشند، بلکه این انتزاع است که باید بر جزئیات حاکم باشد.

گیج شدید، مگر نه؟ 😉 حق دارید! تعریفش کمی پیچیده و حتی ناملموس است! به همین دلیل آموزش را با مثال پیش می‌برم.

در ابتدا خوب است دو مفهوم کلاس یا ماژول سطح بالا و پایین را تعریف کنم.

‎ماژول سطح بالا و سطح پایین یعنی چه؟

ماژول سطح بالا یا کلاس سطح بالا ماژول‌هایی هستند که منطق برنامه را مدیریت می‌کنند و برای تصمیم‌گیری‌ها و کنترل جریان استفاده می‌شود. این‌ها معمولاً شامل کلاس‌هایی هستند که وظیفه کنترل و پردازش داده‌ها را بر عهده دارند.

ماژول سطح پایین یا کلاس سطح پایین ماژول‌هایی هستند که مستقیماً با منابعی مانند دیتابیس، شبکه، فایل‌ها یا وب‌سرویس‌ها (API) ارتباط دارند. این‌ها اغلب شامل کلاس‌هایی هستند که وظیفه انجام عملیات خاصی مانند ارسال ایمیل، ذخیره اطلاعات در پایگاه داده و تعامل با APIهای خارج از سیستم را بر عهده دارند.

در دنیای واقعی، فرض کنید که شما مدیر یک رستوران هستند. این رستوران خدمت ارائه غذا (ماژول سطح بالا) را ارائه می‌دهد. ارائه این خدمات منوط به پخت غذاست که به آشپز (ماژول سطح پایین) که با مواد اولیه و منابع اصلی رستوران سروکار دارد وابسته است.

اگر شما به یک آشپز خاص وابسته باشید، وقتی او در دسترس نباشد یا از رستوران شما برود، دیگر نمی‌توانید غذاهایی که تا به‌حال ارائه می‌دادید را تهیه کنید. این یک مشکل اساسی برای شما ایجاد می‌کند.

حال اگر به‌جای وابستگی به یک آشپز خاص، به یک دستور پخت خاص وابسته باشید، می‌توانید از هر آشپز دیگری بخواهید که بر اساس دستورالعمل، غذا را آماده کند. یعنی بدون اینکه به فرد خاصی وابسته باشید، رستوران شما همیشه غذاهای مخصوص خودش را ارائه می‌کند.

در مثال رستوران، موجودیت رستوران و ارائه غذاهایش را می‌توان یک ماژول سطح بالا و آشپز خاصی که به او وابسته‌ایم را ماژول سطح پایین در نظر گرفت. اگر به‌جای وابستگی به این فردِ خاص، به دستورالعمل غذا (که مشابه یک انتزاع یا abstraction از غذای نهایی است) وابسته باشیم، می‌توانیم آشپز و حتی فروشندگان منابع (موارد غذایی) را تغییر دهیم.

اصل Liskov Substitution در برنامه‌نویسی به زبان ساده

اصل Liskov Substitution در برنامه‌نویسی به زبان ساده

مثال اصل Dependency Inversion

حالا نوبت آن است که دست به کد شویم و ببینیم چگونه می‌توانیم از اصل وارونگی وابستگی در کدهایمان کمک بگیریم.

فرض کنید که در یک سیستم سفارش آنلاین، کلاسی به نام DBConnection داریم که وظیفه اتصال به دیتابیس (مثلاً MySQL) و اجرای کوئری روی آن را بر عهده دارد.

<?php
 class DBConnection {
     public function connect(){
         // کدهای اتصال به دیتابیس
     }

     public function update($id, $columns, $values){
         // کدهای اجرای کوئری آپدیت مقادیر
     }
 }

همچنین کلاس OrderProcessor را داریم که وظایف مربوط به پردازش سفارش را در آن انجام می‌دهیم.

در یک نگاه کلی (در حالی که اصل Dependenvy Inversio را نقض می‌کنیم) این کلاس به‌طور مستقیم به کلاس DBConnection وابسته است؛ چون به‌طور مستقیم از آن درون خودش استفاده می‌کند.

<?php
 class OrderProcessor {
     private $db_connection;

     public function __construct(){
         $this->db_connection = new DBConnection();
     }

     public function process($order){
         // کدهای پردازش سفارش

         $this->db_connection->update($order->id, ['details'], [$order->details]);
     }
 }

چنین ساختاری سه چالش دارد:

  • کلاس OrderProcessor مستقیماً به DBConnection وابسته است. یعنی اگر بخواهیم روش اتصال به دیتابیس را تغییر دهیم (مثلاً از MySQL به MongoDB) باید این کلاس را تغییر دهیم.
  • تست‌پذیری پایین است؛ زیرا نمی‌توانیم به راحتی DBConnection را در تست‌های شبیه‌سازی (یا اصطلاحاً Mock) کنیم.
  • توسعه‌پذیری کدها کم است؛ چون تغییر یک بخش از برنامه (مثلاً کلاس DBConnection) باعث تغییر در کلاس‌های دیگر می‌شود.

کدهایی که در این آموزش می‌نویسم به زبان برنامه نویسی PHP است. اما مفهوم اصل وارونگی وابستگی و نکاتی که می‌گویم برای تمام زبان‌های برنامه نویسی شیءگرا قابل پیاده‌سازی و اجراست.

پیاده‌سازی اصل وارونگی وابستگی

برای حذف وابستگی بین کلاس‌های OrderProcessor و DBConnection ابتدا باید یک اینترفیس (interface) برای کلاس‌های مرتبط با دیتابیس تعریف کنیم:

<?php
 interface DBCOnnectionInterface {
     public function connect();
     public function update($id, $columns, $values);
 }

اینترفیس DBConnectionInterface مشخص می‌کند که هر سرویس که به دیتابیس متصل می‌شود، باید متدهای مدنظر (در اینجا connect() و update()) را پیاده‌سازی کند.

حالا کلاس DBConnection را به‌گونه‌ای تغییر می‌دهیم که این اینترفیس را پیاده‌سازی کند:

<?php
 class DBCOnnection implements DBConnectionInterface {
     public function connect(){
         // کدهای اتصال به دیتابیس
     }

     public function update($id, $columns, $values){
         // کدهای اجرای کوئری آپدیت مقادیر
     }
 }

چرا چنین کاری می‌کنیم؟ این‌طوری اگر بخواهیم در آینده روش اتصال را تغییر دهیم (مثلاً از MySQL به MongoDB مهاجرت کنیم)، کافی است یک کلاس جدید بسازیم که این اینترفیس را پیاده‌سازی می‌کند. سپس مطمئنیم که تمام کلاس‌هایی که برای کار با دیتابیس از این اینترفیس استفاده می‌کنند، می‌توانند با دیتابیس جدید نیز کار کنند.

اکنون OrderProcessor را تغییر می‌دهیم تا به‌جای وابستگی مستقیم به DBConnection، به DBConnectionInterface وابسته باشد:

<?php
 class OrderProcessor {
     private $db_connection;

     public function __construct(DBCOnnectionInterface $db_connection){
         $this->db_connection = $db_connection;
     }

     public function process($order){
         // کدهای پردازش سفارش

         $this->db_connection->update($order->id, ['details'], [$order->details]);
     }
 }

این‌گونه OrderProcessor فقط به یک اینترفیس وابسته است و هیچ وابستگی‌ای به نوع پیاده‌سازی روش اتصال به دیتابیس ندارد.

در اینجا از متد سازنده PHP برای تعریف شیء کار با دیتابیس در شیء پردازش سفارش استفاده کرده‌ام. دقت کنید که نوع پارامتر ورودی این شیء در متد سازنده، طبق اینترفیس تعریف شده است. یعنی هر کلاس دیگری (غیر از DBConnection که اینترفیس را پیاده‌سازی کرده باشد) را می‌توان برایش تعریف کرد.

اصل Open Closed در برنامه‌نویسی به زبان ساده

اصل Open Closed در برنامه‌نویسی به زبان ساده

استفاده از اصل Dependency Inversion

حالا که همه چیز را جدا کردیم، برای استفاده از این کلاس‌ها به‌صورت زیر عمل می‌کنیم:

<?php
 // سایر کدهای برنامه
 
 $db_connection = new DBConnection();
 $order_processor = new OrderProcessor($db_connection);
 $order_processor->process($order);

اکنون اگر بخواهیم یک سرویس جدید برای اتصال به دیتابیس (غیر از MySQL) به سیستم اضافه کرده و از آن استفاده کنیم، کافی است یک کلاس جدید بسازیم:

<?php
 class DBMongoCOnnection implements DBConnectionInterface {
     public function connect(){
         // کدهای اتصال به دیتابیس
     }

     public function update($id, $columns, $values){
         // کدهای اجرای کوئری آپدیت مقادیر
     }
 }

سپس بدون اینکه در کدهای OrderProcessor تغییری ایجاد کنیم، شبیه به قطعه کد زیر، می‌توانیم از روش جدید استفاده کنیم:

<?php
 // سایر کدهای برنامه
 
 $db_connection = new DBMongoConnection();
 $order_processor = new OrderProcessor($db_connection);
 $order_processor->process($order);
مثال انتزاعی و تصویری از اصل DIP یا وارونگی وابستگی در برنامه‌نویسی
مثال انتزاعی و تصویری از اصل DIP یا وارونگی وابستگی در برنامه‌نویسی

مثال بیشتر: ارسال پیامک

همان‌طور که در بخش اول گفتم، اصل Dependency Inversion را می‌توانیم بین تمام کلاس‌های سطح بالا (منطق و پردازش) و پایین (کار با منابع) استفاده کنیم.

در مثالی که برای آموزش استفاده شد، شیوه کار با دیتابیس به‌عنوان ماژول سطح پایین فرض شد. اما ما می‌توانیم هر نوع منبع دیگری را نیز با همین روند و ساختار در کدهای خود استفاده کنیم.

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

در نگاه اول ممکن است به ذهنمان برسد که یک کلاس برای ارتباط با API سرویس‌دهنده پیامک (مثلاً شرکت x) بنویسیم. سپس از این کلاس در جایی که می‌خواهیم استفاده کنیم.

اما با دیدگاهی که از اصل وارونگی وابستگی گرفتیم، بهتر است یک interface برای سرویس‌های ارسال پیامک ایجاد کنیم. سپس کلاسی برای پیاده‌سازی APIهای شرکت x بنویسیم که از این اینترفیس پیروی می‌کند.

حالا از شیءهایی که از اینترفیس ارسال پیامک پیروی می‌کنند در کلاس OrderProcessor به‌جهت ارسال پیامک استفاده می‌کنیم.

این‌گونه اگر بخواهیم از سرویس‌دهنده پیامک خود را از x به شرکت y تغییر دهیم، فقط با تعریف یک کلاس جدید و استفاده از آن و بدون تغییر کدهای OrderProcessor این کار به راحتی قابل انجام است.

جمع‌بندی اصل وارونگی وابستگی

اصل وارونگی وابستگی که ممکن است به اختصار DIP (مخفف Dependency Inversion Principle) نیز گفته شود، یکی از اصول SOLID است که باعث کاهش وابستگی مستقیم بین کلاس‌ها و افزایش انعطاف‌پذیری کد می‌شود. این اصل می‌گوید که ماژول‌های سطح بالا نباید به ماژول‌های سطح پایین وابسته باشند؛ بلکه هر دو باید به یک انتزاع (abstraction) وابسته باشند.

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

سپس شیء مرتبط را از طریق سازنده (constructor) به کلاس مورد نظر بدهید. به این کار اصطلاحاً مدیریت وابستگی یا تزریق وابستگی در برنامه نویسی می‌گویند. البته روش‌های دیگری نیز غیر از استفاده از سازنده وجود دارد.

در مجموع، همیشه سعی کنید کدهای خود را طوری طراحی کنید که تغییر در جزئیات، تأثیری روی ساختار کلی سیستم نگذارد. با رعایت این نکات کدهای شما انعطاف‌پذیرتر، تست‌پذیرتر و مقیاس‌پذیرتر خواهد شد. همچنین تغییرات احتمالی را در آینده با دردسرهای کمتری مدیریت خواهید کرد.

این مفهوم توسط رابرت مارتین (Robert C. Marting معروف به عمو باب) در کتاب Agile Software Development: Principles, Patterns, and Practice (+) مطرح شده است.

امیدوارم از این آموزش نهایت استفاده را برده باشید. اگر سؤال یا چالشی درباره اصل Dependency Inversion دارید، بخش دیدگاه‌ها برای شماست! 🙂