بسیاری از توسعهدهندگان نرمافزار با شنیدن واژه تزریق وابستگی، عقب نشینی میکنند. این الگو به خاطر پیچیدگیهایی که دارد برای تازهکارها مناسب نیست. این چیزی است که خیلی از شماها باور دارید. اما حقیقت این است که تزریق وابستگی یک الگوی بنیادیست و به راحتی قابل استفاده است.
نقل قول مورد علاقه من در رابطه با تزریق وابستگی جملهای است از جیمز شار. در این جمله او به صورت خلاصه مفهوم تزریق وابستگی را شرح داده است.
تزریق وابستگی یک واژه ۲۵ دلاریست برای یک مفهوم ۵ سنتی. جیمز شار
زمانی که من نیز برای اولین بار واژه تزریق وابستگی را شنیدم، با خود فکر کردم که حتما روشی بسیار پیشرفته برای نیازهای حال حاضر من است. من امکان اینکه بدون تزریق وابستگی کارهایم را پیش ببرم داشتم، هرکاری که بود.
بعدها دریافتم، اگر تنها به اصول بنیادی تزریق وابستگی نگاه کنیم، با یک مفهوم ساده روبرو هستیم. جیمز شار یک تعریف سر راست و خلاصه در رابطه با تزریق وابستگیها ارائه دادهاست.
تزریق وابستگی یعنی به یک شئ متغيرهای نمونهاش (Instance Variable) را بدهیم. واقعا به همین سادگی. جیمز شار
برای برنامهنویسانی که با این مفهوم آشنا نیستند، یادگیری مفاهیم اولیه قبل از این که به سراغ framework یا کتابخانه برویم از اهمیت زیادی برخوردار است. احتمال این وجود دارد که شما در حال حاضر از مفهوم تزریق وابستگیها استفاده میکنید، بدون آن که از این موضوع آگاه باشید.
تزریق وابستگی چیزی بیشتر از تزریق وابستگی به یک شئ به جای آن که خود شئ را موظف به ساخت وابستگیهایش کنید، نیست. یا همانطور که جیمز شار گفته، شما به جای ساخت متغیرهای نمونهی یک شئ در خودش، آنها را به خودش پاس میدهید. بگذارید با یک مثال به شما نشان بدهم که منظور از این جمله چیست.
در این مثال ما یک کلاس UIViewController را تعریف میکنیم که یک property با نام requestManager به صورت زیر دارد:
import UIKit class ViewController: UIViewController { var requestManager: RequestManager? }
ما برای مقدار دهی به requestManager با یکی از دو روش زیر میتوانیم اقدام کنیم:
۱- بدون تزریق وابستگی
اولین گزینه این است که در همان جا که متغیر را تعریف کردهایم آن را مقداردهی نیز کنیم. ما میتوانیم متغیر را از نوع lazy تعریف کنیم یا اینکه در initializer خود ViewController آن را مقدار دهی نماییم. نکته مهمی اینجا نیست، نکته اصلی این است که ViewController وظیفه مقدار دهی به requestManager را به عهده دارد.
import UIKit class ViewController: UIViewController { lazy var requestManager: RequestManager? = RequestManager() }
این بدان معناست که کلاس ViewController نه تنها در رابطه با رفتار RequestManager آگاه است بلکه نمونه سازی (instantiation) آن را نیز میداند. نکته ظریفی است اما جزئیات مهمی را بیان میکند.
۲- به وسیله تزریق وابستگی
اما روش دیگری نیز وجود دارد. ما میتوانیم با مقدار اولیه requestManager را به ViewController تزریق کنیم. البته چنین به نظر خواهد رسید که نتیجه نهایی یکسان است، اما در حقیقت اینگونه نیست. با تزریق requestManager کلاس ViewController از نمونه سازی آن آگاه نیست.
// Initialize View Controller let viewController = ViewController() // Configure View Controller viewController.requestManager = RequestManager()
بسیاری از توسعهدهندگان به سرعت این گزینه را به دليل پیچیدگی غیرضروری و زحمت اضافی، رد میکنند. اما اگر مزایای این روش را در نظر داشته باشد، تزریق وابستگی جذابتر خواهد شد.
به منظور تاکید بیشتر روی هدفی پیشتر بیان شد، مثال دیگری خواهیم زد. به کد زیر نگاهی بیاندازید:
protocol Serializer { func serialize(data: AnyObject) -> NSData? } class RequestSerializer: Serializer { func serialize(data: AnyObject) -> NSData? { ... } } class DataManager { var serializer: Serializer? = RequestSerializer() }
کلاس DataManager یک property به نام serializer از نوع Serializer? دارد. در این مثال Serializer یک پروتکل است. کلاس DataManager مسئول مقداردهی به یک نمونه از نوع پروتکل Serializer است که در این مثال ًRequestSerializer نام دارد.
آیا کلاس DataManager باید بداند که شئای از نوع Serializer? چگونه مقداردهی میشود؟ نگاهی به مثال زیر بیاندازید. این کد به شما قدرت پروتکلها و تزریق وابستگی را نشان خواهد داد.
// Initialize Data Manager let dataManager = DataManager() // Configure Data Manager dataManager.serializer = RequestSerializer()
کلاس DataManager دیگر مسئول مقداردهی به کلاس RequestSerializer نیست. دیگر به property از نوع serializer خورد مقداری را اطلاق نمیکند. در حقیقت ما میتوانیم هر type دیگری را با RequestSerializer جابجا کنیم البته تا زمانی که از پروتکل Serializer پیروی کند. کلاس DataManager دیگر نه از این جزئیات خبر دارد نه این قضیه برایش اهمیتی دارد.
امیدوارم که با این مثالها نظر شما را جلب کرده باشم. بیایید لیستی از مزایای تزریق وابستگیها را با هم نگاه کنیم.
با تزریق وابستگیها به یک شئ، نیازمندیها و مسئولیتهای یک کلاس یا ساختار تمیزتر و شفافتر میشود. با تزریق requestManager به ViewController ما فهمیدیم که به آن نیاز دارد و ما میتوانیم فرض کنیم که ViewController مسئول مدیریت و بررسی requestها است.
تست unit به وسیله تزریق وابستگی بسیار سادهتر خواهد شد، چرا که به راحتی میتوان وابستگیهای یک شئ را با مقادير ساختگی جابجا کرد. همچنین پیادهسازی تست unit را به وسیله ایزوله کردن رفتار آسانتر خواهد نمود.
در این مثال، ما یک کلاس به نام MockSerializer تعریف کردهایم. از آنجا که این کلاس با پروتکل Serializer مطابقت دارد، ما میتوانیم dataManager را به آن اطلاق کنیم.
class MockSerializer: Serializer { func serialize(data: AnyObject) -> NSData? { ... } }
// Initialize Data Manager let dataManager = DataManager() // Configure Data Manager dataManager.serializer = MockSerializer()
همانطور که پیشتر بیان شد، یکی دیگر از مزایای ظریف تزریق وابستگیها جدایی نگرانیهای دقیقتری است. کلاس DataManager در مثال قبلی دیگر مسئولیتی در قبال RequestSerializer ندارد.
البته که کلاس DataManager باید نگران رفتار serializer خود باشد، اما نگرانی در باره مقداردهی آن ندارد. چه اتفاقی رخ میداد اگر ReqeustManager در مثال اول تعدادی وابستگی نیز داشت؟ آیا نیاز بود که ViewController از این وابستگیها آگاه باشد؟ این مساله به سرعت میتواند خیلی آشفته شود.
مثالی که در آن کلاس DataManager وجود داشت نشان داد که استفاده از پروتکلها و تزریق وابستگی میتواند باعث کاهش جفتشدگی در یک پروژه شود. پروتکلها در Swift به طور اعجابانگیری مفید و همهکاره هستند و این مثال یک سناریو بود که در آن پروتکلها واقعا درخشیدند.
بیشتر توسعهدهندگان سه نوع از تزریق وابستگی را در نظر میگیرند:
به هیچ عنوان این موارد با هم یکسان نیستند. بیاید لیستی از مزايا و معایب هریک را بررسی کنیم.
من به شخصه ترجیح میدهدم که وابستگیهارا در فاز مقداردهی یک شئ پاس بدهم چرا که این امر مزایای کلیدی متعددی دارد. مهمترین مزیت این است که وابستگی پاس داده شده هنگام مقداردهی میتواند، تغییرناپذیر (immutable) شود. این در Swift با contant تعریف کردن دارائیها برای وابستگیها به سادگی امکان پذیر است. نگاهی به مثال زیر بیاندازید:
class DataManager { private let serializer: Serializer init(serializer: Serializer) { self.serializer = serializer } } // Initialize Request Serializer let serializer = RequestSerializer() // Initialize Data Manager let dataManager = DataManager(serializer: serializer)
تنها روش راهاندازی serializer پاس دادن متغیر هنگام مقداردهی آن است. متد init برای مقدار دهی طراحی شده و تضمین میکند که DataManager به درستی تنظیم شده است. مزیت دیگر این است که serializer نمیتواند تغییر کند.
چون که ما نیاز داریم serializer را به عنوان ورودی هنگام مقداردهی پاس بدهیم، مقداردهندهی طراحی شده به وضوح نشان میدهد که وابستگیهای کلاس DataManager چیست.
وابستگیها همچنین میتوانند هنگام تعریف یک دارائی داخلی یا عمومی به کلاس یا ساختاری که به آن نیاز دارد، تزریق شوند. این مساله شاید راحت به نظر برسد، اما این کار باعث بوجود آمدن این روزنه خواهد شد که وابستگیها میتوانند جایگزین یا ویرایش شوند. به عبارت دیگر، وابستگیها تغییر ناپذیر نیستند.
import UIKit class ViewController: UIViewController { var requestManager: RequestManager? } // Initialize View Controller let viewController = ViewController() // Configure View Controller viewController.requestManager = RequestManager()
تزریق دارائی گاهی اوقات تنها گزینه پیش روی شماست. اگر شما از storyboard استفاده کنید برای مثال شما نمیتوانید یک initializer دلخواه بسازید و باید از تزریق آغازگر استفاده کنید. تزریق دارائی گزینه دوم شما خواهد بود.
وابستگیها همچنین میتوانند هرزمان که به آنها نیاز است، تزریق شوند. راه سادهایست که هنگام تعریف یک متد وابستگیهارا به عنوان پارامترهای ورودی قبول کنید. در این مثال serializer دارائی DataManager نیست و به جای آن serializer به عنوان یک متغیر به serializerRequest تزریق شده است.
class DataManager { func serializeRequest(request: Request, with serializer: Serializer) -> NSData? { ... } }
اگرچه کلاس DataManager مقداری از کنترل خود را روی وابستگی و serializer است دست میدهد، اما این نوع از تزریق وابستگی، انعطاف پذیری را به همراه دارد. بسته به مورد استفاده، شما میتوانید تصمیم بگیرید که چه نوع serializerای را به serializeRequest پاس بدهید.
از اهمیت زیادی برخوردار است که تاکید کنم، هر نوع از تزریق وابستگی مورد استفاده به خصوص خود را دارد. از آن جا که تزریق آغازگر در بیشتر سناریوها گزینه خوبی است، آن را به بهترین یا ارجحترین نوع تبدیل نمیکند. مورد استفاده را در نظر داشته باشید، آنگاه انتخاب کنید که کدام نوع تزریق وابستگی، مناسب کار شماست.
تزریق وابستگیها الگوییست که میتواند به منظور حذف singeltonها از یک پروژه استفاده گردد. من طرفدار الگوی singelton نیستم و تا جایی که ممکن باشد از آن دوری میکنم. البته من الگوی singelton را یک ضد الگو نمیدانم، من بر این باورم که باید از آن به ندرت استفاده کرد. الگوی singelton باعث بوجود آمدن جفت شدگی میشود و تزریق وابستگی از این اتفاق جلوگیری میکند.
در خیلی از مواقع، توسعهدهندگان به دلیل اینکه singleton راه حل سادهای برای مشکلات جزئی است از آن استفاده میکنند. اما تزریق وابستگی شفافیت را به پروژه اضافه میکند. با تزریق وابستگیها هنگام مقدار دهی یک شئ، مشخص میشود که یک کلاس یا ساختار چه وابستگیهایی دارد و همچنین برخی مسئولیتهای آن مشخص میشود.
تزریق وابستگیهای یکی از الگویهای مورد علاقه من است چرا که به من کمک میکند که از پس پروژههای پیچیده بر بیایم. این الگوی مزایای زیادی دارد و تنها عیبی که به ذهن من میرسد این است که باید چند خط بیشتر کد بنویسم.
منبع: https://cocoacasts.com/dependency-injection-in-swift