۳۰ روز با TDD: روز ششم - تزریق وابستگی (Dependency Injection) چیست؟

نوشته به زبان انگلیسی روز ششم را در این آدرس می‌توانید مشاهده کنید. در روز پنجم درباره اصل Dependency Inversion گفتم و اینکه با Dependency Injection یا تزریق وابستگی متفاوت است. امروز می‌خواهیم بیشتر درباره تزریق وابستگی صحبت کنیم.

Tinker Toy و Lego

همان‌طور که قبلاً هم در این سری نوشته‌ها گفتم، اتصال (Coupling) و انقیاد (binding) در نرم‌افزار یک حقیقت است. به عنوان برنامه‌نویس سعی می‌کنیم که اتصالات برنامه‌مان تا حد ممکن ضعیف باشد (به اصطلاح loosely coupled باشد) اما در نهایت برای اینکه اجزای مختلف برنامه‌ای که می‌سازیم قابل استفاده باشد باید آن‌ها را bind کنیم. مثال مشابه پیوندهای ضعیف نرم‌افزار، که در زندگی روزمره از آن استفاده کرده‌ایم بازی‌های لگو و تینکر توی (یک بازی مخصوص بچه‌ها که مشابه لگو با ترکیب اجزای مختلف می‌توان وسایل جدید ساخت) هستند.
ایده مشابه در نرم‌افزار هم مشابه لگو این است که اگر برنامه‌تان را از اجزا (component) مختلف که بر اساس یک اینترفیس استاندارد ساخته شده‌اند بسازید، باید بتوانید نرم‌افزارهای مختلف را با استفاده از ترکیب‌های مختلف این اجزا ایجاد کنید.

به نظر ایده جالبی است، اما چطور این کار را با استفاده از کد نرم‌افزاری انجام دهیم؟ برای پاسخ به این سوال به اصول SOLID‌ که در نوشته قبلی به آن‌ها اشاره کردیم نیاز داریم. گرچه هر ۵ اصل به تولید نرم‌افزاری که loosely coupled باشد کمک می‌کنند اما دو تا از آن‌ها برای به صورت خاص برای این منظور لازم هستند.

اصل Liskov Substitution یا LSP‌ می‌گفت که ما می‌توانیم یک کلاس مشتق شده را جایگزین کلاس پایه (base) کنیم بدون اینکه برنامه دچار مشکل شود. ما می‌توانیم این اصل را به Interface ها به این صورت تعمیم دهیم که کلاس‌هایی که یک اینترفیس مشترک را پیاده‌سازی می‌کنند را می‌توان جایگزین هم کرد. بر این اساس، پرنده و هواپیما و سوپرمن گرچه خیلی با هم متفاوتند اما هر سه می‌توانند پرواز کنند. بنابراین تا زمانی که آن‌ها اینترفیس IFly را پیاده‌سازی کنند و برای من فقط پرواز مهم باشد، می‌توانم از هر کدام از آن کلاس‌ها (پرنده و هواپیما و سوپرمن) استفاده کنم.

اصل Dependency Inversion یا DIP به ما می‌گوید که کد ما باید به abstraction ها وابسته باشد نه یک پیاده‌سازی واقعی. برنامه من ممکن است به یک انبار داده جهت ذخیره‌سازی اطلاعات نیاز داشته باشد، اگر من خودم را به یک دیتابیس رابطه‌ای مثل SQL Server محدود کنم،‌ راه خودم بر دیگر پیاده‌سازی‌ها برای انبار داده مثل file system و web service و object database یا هر چیز دیگری که برای ذخیره‌سازی اطلاعات می‌توان از آن استفاده کرد بسته‌ام. این روش اشتباه طراحی باعث ایجاد محدودیت در نرم‌افزار من از طریق اتکا به یک نوع خاص از انبار داده به جای استفاده از ایده «چیزی که داده ذخیره می‌کند» می‌شود.

decouple اجزا (components) از طریق تزریق وابستگی

با استفاده از LSP و DIP ما می‌توانیم به یک روش برای تولید نرم‌افزار با وابستگی‌ها ضعیف (loosely coupled dependencies) برسیم. این روش Dependency Injection یا تزریق وابستگی نام دارد و بر خلاف نامی که دارد بسیار ساده است. کد نمونه زیر را در نظر بگیرید

تعریف SqlDataStoreProvider و DbLoggingProvider و ProdWebServiceProvider خالی است ولی اگر بخواهید می‌توانید کد کامل را در این gist‌ ببینید.

کد بالا احتمالاً آشنا به نظر می رسد. بدون شک هر برنامه تجاری در NET. نیازمند انبار داده جهت ذخیره‌سازی اطلاعات است. خیلی از شرکت‌ها می‌خواهند که اتفاقاتی که در برنامه می‌افتد را ثبت کنند و این یعنی نیاز به کدی برای log کردن. با متداول شدن استفاده از سیستم‌های توزیع شده استفاده از وب سرویس هم به یک نیاز طبیعی تبدیل شده است.

برنامه‌های زیادی در NET. به خصوص قدیمی‌ترها از کد مشابه بالا برای ایجاد instance از کلاس‌هایی که به آن‌ها وابسته هستند استفاده می‌کنند. مشکل این روش این است که کلاس BusinessService  به شکل محکم (tight) یا استاتیک به پیاده‌سازی‌های خاصی bind شده‌ است. این کار، نرم‌افزار را شکننده می‌کند و تغییرات به ظاهر ساده در کلاس‌ها در آینده ممکن است عواقب غیرقابل پیش‌بینی داشته باشد. این همچنین امکان تصمیم‌گیری درباره object ای که می‌خواهیم به آن bind کنیم را از بین می‌برد. این یعنی من محدود به هر کدی که در زمان کامپایل استفاده شده است هستم.

کاری که تزریق وابستگی یا DI انجام می‌دهد این است که static binding به وابستگی‌ها را حذف می‌کند. برای این کار کلاس مصرف‌کننده (consumer) پیاده‌سازی‌های مختلفی که object می‌خواهد از آن‌ها استفاده کند را ارائه می‌کند. به کد زیر نگاه کنید:

با تزریق وابستگی‌های کلاس از طریق سازنده (constructor) می‌توانم پیاده‌سازی اصلی که واقعاً می‌خواهم در زمان اجرا (runtime) از آن استفاده کنم را کنترل کنم. پیاده‌سازی اصلی فقط باید بر اساس abstraction ای که کلاس مصرف کننده (کلاس BusinessService) نیاز دارد باشد.
به عنوان مثال من می‌توانم هر کلاسی که از DataStoreProvider مشتق شده باشد (مثلاً SqlDataStoreProvider) را به کلاس BusinessService  از طریق سازنده (constructor) پاس بدهم و کلاس BusinessService می‌تواند از آن استفاده کند. این روش همچنین کد را انعطاف‌پذیرتر در برابر تغییرات می‌کند. از آنجایی که کلاس فقط به یک abstraction از dependency وابسته است من می‌توانم در کلاس مشتق شده (که به کلاس BusinessService پاس داده می‌شود) تغییر ایجاد کنم و کلاس مصرف کننده یعنی BusinessService همچنان کار کند.

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

اگر کد مربوط به کلاس BusinessService را برای اینکه به Interface متکی باشد refactor کنم، انعطاف‌پذیری بیشتر در binding بدست می‌آورم و خروجی شبیه کد زیر خواهد بود

همان‌طور که می‌بینید تزریق وابستگی یک تکنیک ساده اما قدرتمند است. این تکنیک به شما این امکان را می‌دهد که با ایجاد تنوع در binding وابستگی‌ها در زمان اجرا، برنامه‌های loosely couple ایجاد کنید. در نوشته بعدی درباره software factories و فریمورک‌های DI صحبت خواهیم کرد.

ادامه دارد…