۳۰ روز با TDD: روز هفتم - Software Factories و DI Frameworks
قبل از هر چیز، نوشته زبان انگلیسی روز هفتم را در این آدرس میتوانید مطالعه کنید. لازم است یادآوری کنم معتقدم در کار برنامهنویسی تا حد امکان نباید عبارات تخصصی را ترجمه کرد (مثل ترجمه کردن نام داروها و ترکیبات شیمیایی آنها در پزشکی) مگر اینکه معادل مناسبی برای آنها وجود داشته باشد. در مورد نوشته امروز هم این نکته صادق است.
اگر مشکل را نبینم پس مشکلی نیست. درسته؟
تزریق وابستگی (DI) یک تکنیک برای تولید برنامههای با پیوند ضعیف به وابستگیها (loosely bound dependencies) هست. با پاس دادن کلاسهایی که برنامه به آنها وابسته است در constructor، به دو چیز دست پیدا میکنیم: اول اینکه میتوانیم کلاسهایمان را به abstraction ها وابسته کنیم نه پیادهسازی اصلی و واقعی و دوم اینکه میتوانیم در زمان اجرا تصمیم بگیریم که از کدام پیادهسازی استفاده کنیم.
این روش البته یک مشکل جدید هم برای ما ایجاد میکند. وقتی یک instance از یک کلاس ایجاد میکنیم، به دلیل اینکه در سازنده آن کلاس به کلاسهای دیگری وابستگی ایجاد کردیم، کدی که کار ساختن instance را انجام میدهد باید component هایی که instance ما به آنها وابسته است را ایجاد کند. این یعنی معرفی مجدد binding به روش tightly coupled. در واقع ما وابستگیها را یک مرحله بالاتر بردهایم و در واقع به جای حل مشکل، آن را منتقل کردیم!
به علاوه انتقال مشکل قدیم، یک مشکل جدید هم ایجاد کردهایم. ما نه تنها مسئولیت ساخت component های فرزند (child) را به کلاس parent منتقل کردیم، بلکه کلاسهای parent را در موقعیتی قرار دادیم که باید حتماً بدانند کدام پیادهسازی را برای کلاس child باید استفاده کنند. با انجام این کار یک اصل دیگر SOLID (رجوع کنید به نوشته روز پنجم) یعنی اصل Single Responsibility یا SPR را هم نقض کردهایم. کلاس parent فقط باید مسئول قابلیت اصلی خودش باشد و فقط باید زمانی تغییر کند که قابلیت اصلیاش تغییر کرده باشد، اما حالا با کاری که در تزریق وابستگی کردیم، بر اساس child ها و وابستگی آنها تغییر میکند.
Software Factories
یک راه حل برای مشکلی که گفتم، استفاده از Software Factory است. software factory یک الگو برای abstract کردن اطلاعات و کد مورد نیاز برای ساختن یک instance از کلاس است. چند مدل از الگوهای software factory وجود دارد که هر کدام برای شرایط و نیازمندیهای خاصی طراحی شدهاند. برای آشنایی با مهمترین و مشهورترین این الگوها، مطالعه کتاب Design Patterns را توصیه میکنم.
با وجود اینکه چندین الگوی مختلف software factory وجود دارد اما همه آنها یک ایده مشابه دارند: یک component داریم که همه وابستگیها و نحوه ایجادشان را یکی میکند. نمونه یک کلاس ساده factory به شکل زیر است:
الگوی software factory موثر و کاراست. همانطور که در کد بالا میتوانید ببینید من کاری کردم که تمام قابلیتهای مورد نیازم برای ساخت یک instance از کلاس BusinessService یکجا جمع شوند. این یعنی کلاسهایی که از BusinessService استفاده میکنند، میتوانند به کار خودشان مشغول بوده و نگران وابستگیهای BusinessService نباشند.
این یک مثال ساده software factory است که در آن هر interface یک رابطه یک به یک با پیادهسازی واقعی دارد. ولی الزامی به رعایت این نوع رابطه یک به یک نیست. چون software factory هم کد است، پس میتوانم هر منطقی که برای مشخص شدن پیادهسازی برگشتی لازم دارم را استفاده کنم. مثلاً ممکن است بخواهم کاربر بتواند بین MS SQL Server و Oracle انتخاب داشته باشد، میتوانم کاری کنم که factory من یک فایل تنظیمات را بخواند تا مشخص شود از کدام دیتابیس باید استفاده شود.
با وجود موثر و کارا بودن software factory، آنها لزوماً ایدهآل نیستند. حتی با اینکه الگوهای خاص factory برای شرایط خاص طراحی شدهاند، در برنامههای کوچک هم ممکن است نیازمند نگهداری و تغییرات در factory ها باشیم. خوشبختانه یک راه حتی بهتر از software factory برای مدیریت وابستگیها در نرمافزار وجود دارد.
چرا از Software Factory استفاده کنیم وقتی میشود از Framework ها استفاده کرد؟
DI Framewrok ها مشابه software factory هستند اما این مزیت را دارند که بسیاری از وظایفی که باید به صورت دستی انجام دهیم را اتوماتیک میکنند. به عنوان مثال، به جای اینکه یک متد کامل برای bind کردن یک implement به یک interface را تعریف کنم، با استفاده از فریمورکهای تزریق وابستگی، میتوانم در یک خط کد یک binding rule تعریف کنم.
فریمورکهای مختلفی برای تزریق وابستگی وجود دارد. من در این سری از نوشتهها از Ninject استفاده میکنم. در پایان این نوشته لینک چند DI Framework معروف و خوب دیگر را هم معرفی میکنم. اگر تا به حال از یک DI Framework استفاده نکردید، پیشنهاد میکنم که همه موارد معرفی شده در این نوشته یا سایر framework ها را امتحان کنید و فریمورکی که بیشتر مورد پسندتان است را انتخاب کنید.
با وجود اینکه DI Framework ها اصطلاحات مختلفی برای اجزای خودشان دارند، اما همه آنها شامل ۴ بخش مهم زیر میشوند:
- The Application: برنامه شما یا بخشی از آن که به یک instance از کلاس نیاز دارد.
- The Kernel یا the Container: اینترفیسی در برنامه شما برای DI Framework که آن را به منظور ایجاد instance ها نیاز دارید.
- The Provider: این شامل قوانین و راهنماییها برای ایجاد کلاسهای مختلف و وابستگیهای آنهاست.
- The Created Class: این چیزی است که توسط Kernel ایجاد شده و به برنامه شما برگردانده میشود. instance ای از کلاس شما با همه وابستگیهایش است.
لطفاً توجه داشته باشید که تعاریف بالا بر اساس نام اصطلاحات Ninject است و در فریمورک شما ممکن است نام متفاوتی داشته باشند.
برای بازنویسی مثال بالا با Ninject باید ابتدا reference آن را به برنامهام اضافه کنم. برای این کار میتوان dll مربوط به Ninject را از سایت خودش دانلود و به صورت دستی به برنامه اضافه کرد یا به صورت اتوماتیک و با استفاده از Nuget آن را به برنامه افزود.
در Ninject اولین کاری که باید انجام دهم تعریف یک ماژول است. جایی که binding rule های مربوط به برنامه را در آن تعریف میکنم:
برای ایجاد یک ماژول باید کلاسی که از NinjectModule ارثبری میکند ایجاد کنم. این کلاس در namespace (فضا نام) Ninject.Module قرار دارد. کلاس NinjectModule یک متد abstract به نام Load دارد که باید آن را override کرد. این جایی است که binding rule های مربوط به کلاسها را در آن تعریف میکنیم.
برای تعریف binding rule ها من از متد Bind در Ninject استفاده میکنم. در مثال بالا کلاس DataStoreProvider را به اینترفیس IDataStoreProvider در خط ۵ کد bind کردهام. این بدان معنی است که هر وقت از Ninject بخواهیم که instance از IDataStoreProvider به ما بدهد، instance ای از DataStoreProvider را ایجاد خواهد کرد.
binding rule های کد بالا سادهترین نوع binding rule هستند. Ninject قابلیت ایجاد binding های پیچیدهتر را نیز داراست. در نوشتههای بعدی چند binding پیچیده را با استفاده از Ninject توضیح خواهم داد اما تا آن موقع نگاهی به این برگه تقلب داشته باشید ;)
میخواهید یک کلک جادویی ببینید؟
binding rule رو ایجاد کردیم، حالا از Ninject میخواهیم که یک instance از کلاسمان به ما بدهد. این کار هم همانطور که در کد زیر مشخص است خیلی ساده است:
در کد بالا یک instance از کلاس StandardKernel در Ninject ایجاد میکنم. StandardKernel یک (یا حتی چند) instance از NinjectModule را به عنوان پارامتر سازنده (constructor) میگیرد. بعد از این کار شی kernel در واقع container ماست که با فراخوانی متد Get به ما instance کلاسهایی که میخواهیم را ارائه میدهد. همانطور که در کد بالا مشخص است من در خط ۹ با استفاده از فراخوانی متد Get یک instance از کلاس BusinessService را درخواست کردهام. اما اگر از کد قبلی به خاطر داشته باشید، اینجا من به صورت دقیق نگفتم که چطور کلاس BusinessService را ایجاد کن، در واقع Ninject میتواند کلاس BusinessService را آنالیز کند و خودش وابستگیهای این کلاس را از طریق سازنده (constructor) تشخیص داده و آنها را تامین کند.
اما صبر کنید! هنوز تمام نشده. اجازه بدهید یک تغییر جزئی در DataStoreProvider ایجاد کنیم:
من یک وابستگی به ILoggingProvider در کلاس DataStoreProvider ایجاد کردم. حالا کلاس BusinessService من به کلاسی وابسته است که خودش به یک interface وابستگی دارد. من یک تغییر کلی در برنامهام ایجاد کردم ولی بر خلاف software factory با Ninject نیاز به نوشتن کد بیشتر نیست. من همین حالا هم مشخص کردم که چطور LoggingProvider ایجاد شود بنابراین وقتی Ninject به کلاس DataStoreProvider میرسد خودش میداند که چطور این وابستگی به IDataStoreProvider را مدیریت کرده و instance لازم را ایجاد کند و من لازم نیست کار دیگری انجام دهم.
سایر DI Framework ها
در متن اشاره داشتم که DI Framework های مختلفی وجود دارد و در این سری نوشتهها من از Ninject استفاده میکنم، اما همانطور که گفتم بهتر است همه آنها را بررسی و فریمورکی که میپسندید را انتخاب کنید. در این مسیر نگاهی به Structure Map و Microsoft Unity هم داشته باشید.
ادامه دارد …