وارونگی کنترل و تزریق وابستگی – Spring MVC

برنامه های نرم افزاری به طور معمول به تعدادی مولفه های منطقی و یا سرویس تقسیم می شوند که با یکدیگر در تعامل هستند. در جاوا، این مولفه ها معمولا نمونه هایی از کلاس های جاوا و یا همان اشیا هستند. هر شی باید به منظور انجام کار خود، باید با اشیاء دیگر کار کرده و یا از آن ها استفاده کند. اشیائی که یک شی با آن ها در ارتباط است به وابستگی های آن شی معروف هستند. وارونگی کنترل به الگوی معماری مطلوبی اشاره دارد که در آن، یک موجودیت خارجی[۱] (در این مورد، همان ظرف) اشیا را به هم مرتبط کند، به طوری که به جای نمونه سازی مستقیم وابستگی ها توسط خود اشیا، این وابستگی ها توسط آن ظرف به اشیا داده می شود.

به مثالی در این زمینه توجه کنید.

فرض کنید سرویسی به نام سرویس اطلاعات آب و هوا وجود دارد که به صورت معمولی و غیر IoC کدنویسی شده باشد:

 

 

در ادامه کدی برای تست عملکرد این کد نوشته شده است:

 

سرویس آب و هوا، برای نگه داری اطلاعات آب و هوا، با یک DAO[2] (شی دسترسی به داده ها) در ارتباط است. این سرویس برای ارتباط با DAO از رابط WeatherDAO استفاده می کند. در این مثال، سرویس آب و هوا به طور مستقیم یک نمونه از نوع خاص و شناخته شده StaticDataWeatherDAOImpl را ایجاد می کند که این نوع خاص، رابط مورد نظر را پیاده سازی کرده است. علاوه بر این، نرم افزار تست ما، WeatherServiceTest، به طور مستقیم و بدون هیچ امکان تخصصی، از کلاس WeatherService استفاده می کند. این مثال به سبک کد غیر IoC نوشته شده است. سرویس آب و هوا از طریق یک رابط با DAO تعامل ندارد، بلکه به طور مستقیم یک نمونه از نوع خاصی از DAO را ساخته و چرخه حیات آن را کنترل می کند؛ در نتیجه این سرویس هم به رابط DAO و هم به یک پیاده سازی خاص آن وابسته است. علاوه بر این، در مثال تست، که نقش مشتری سرویس را بر عهده دارد، به جای این که از طریق یک رابط با سرویس در تعامل باشد، به طور مستقیم یک نمونه از این سرویس را ایجاد کرده است. در یک برنامه واقعی، احتمال وجود وابستگی ضمنی بیشتری وجود دارد، و با این روش این وابستگی ها دقیقا در کد فراخواننده نوشته می شوند.

حال می خواهیم در یک مثال ساده بررسی کنیم که یک ظرف اسپرینگ چگونه می تواند وارونگی کنترل را ارائه دهد. ابتدا ما سرویس آب و هوا را مجددا به یک رابط و پیاده سازی آن تقسیم می کنیم و اجازه می دهیم که یک نمونه از نوع DAO، به صورت یک ویژگی JavaBean در آن پیاده سازی قرار گیرد:

 

در این مثال برای مدیریت یک نمونه از سرویس آب و هوا از چارچوب برنامه ClasspathXmlApplicationContext اسپرینگ استفاده کرده ایم. در کد زیر، این چارچوب به این نمونه از سرویس، و یک نمونه از DAO برای دستیابی به داده ها استفاده می کند. ابتدا یک فایل پیکربندی به فرمت XML و به نام applicationContext.xml تعریف می کنیم:

 

در این قطعه کد، با استفاده از عنصر bean، یک شی به نام weatherService پیکربندی و تعیین شده است که خصیصه weatherDao آن به یک نمونه bean به نام weatherDao تنظیم شود. این bean نیز در این کد تعریف شده است. حال فقط نیاز است برنامه تست به گونه ای تغییر یابد که ظرف مورد نظر را ساخته و سرویس آب و هوا را از طریق آن به دست آورد:

 

 

در این سبک برنامه نویسی، کلاس ها به روش IoC کد شده و استقرار یافته اند. سرویس آب و هوا در مورد جزئیات پیاده سازی DAO، که به آن وابستگی دارد اطلاع نداشته و به آن توجه نمی کند. همچنین تقسیم کلاس WeatherService اصلی به یک رابط WeatherService و کلاس پیاده سازی WeatherServiceImpl باعث می شود مشتریان سرویس آب و هوا، از جزئیات پیاده سازی آن فاصله بگیرند. از جمله این جزئیات می توان به نحوه تهیه DAO برای این سرویس اشاره کرد که در این مثال به سبک JavaBeans و از طریق یک متد setter تهیه شده است. فایده دیگر این روش این است که اجازه می دهد در هر زمان که نیاز باشد، پیاده سازی واقعی سرویس آب و هوا به صورت شفاف تغییر یابد. در این روش ما می توانیم پیاده سازی واقعی WeatherDAO و یا پیاده سازی WeatherService را تنها با تغییر در فایل پیکربندی تغییر دهیم و استفاده کنندگان این مولفه ها از این تغییرات اطلاعی پیدا نکنند. این روش، یک نمونه از استفاده از رابط ها است، به گونه ای که کد یک لایه از کد لایه دیگر استفاده می کند.

اشکال مختلف تزریق وابستگی

اصطلاح تزریق وابستگی در روش IoC به روند تهیه یک مولفه به همراه کلیه وابستگی های مورد نیاز آن گفته می شود؛ به عبارت دیگر این وابستگی ها به مولفه تزریق می شوند. نمونه تزریق وابستگی که در مثال قبل آمد، به تزریق قراردهنده[۳] معروف است، زیرا از متدهای setter در JavaBean برای تامین وابستگی هایی که شی به آن ها نیاز دارد استفاده شده است.

نوع دیگر تزریق، به تزریق سازنده[۴] معروف است که در آن وابستگی های یک شی، از طریق سازنده به آن شی داده می شوند:

 

 

WeatherServiceImpl سازنده ای دارد که به جای استفاده متد setter، از طریق آن شی WeatherDAO را دریافت می کند. پیکربندی بستر برنامه نیز بر این اساس تغییر یافته است. کلاس تست از این تغییرات بی اطلاع است و نیاز به تغییر در آن وجود ندارد:

 

 

تزریق متد[۵]، روش نهایی تزریق وابستگی است که به ندرت استفاده می شود. در این روش، ظرف برنامه مسئول این است که متدها را در زمان اجرا پیاده سازی کند. به عنوان مثال، ممکن است یک شی، متد انتزاعی محافظت شده[۶]ای را تعریف کرده باشد و این ظرف است که باید آن را در زمان اجرا پیاده سازی کرده تا به عنوان نتیجه خود، شی ای که در جستجوی ظرف یافته شده را برگرداند.

یکی از بهترین زمان ها برای استفاده از تزریق متد، زمانی است که یک شی یگانه[۷] و stateless به شی ای نیاز داشته باشد که threadsafe و یگانه نبوده و stateful باشد. سرویس آب و هوا می تواند مثال مناسبی باشد، زیرا از آن جا که این سرویس stateless است تنها یک نمونه از آن مورد نیاز می باشد، و تنها نمونه آن توسط ظرف پیش فرض اسپرینگ ایجاد و سپس برای استفاده مجدد نگه داری می شود. اگر سرویس آب و هوا به StatefulWeatherDAO، که یک پیاده سازی از نوع threadsafe سرویس WeatherDAO است نیاز داشته باشد، چه اتفاقی خواهد افتاد؟ در هر بار درخواست WeatherService.getHistoricalHigh()، سرویس آب و هوا به یک نمونه جدید از DAO نیاز دارد (یا حداقل باید مطمئن شود که هیچ سرویس آب و هوا دیگری به صورت همزمان از این DAO استفاده نمی کند). همانطور که در ادامه آمده است، ما به راحتی می توانیم به ظرف بگوییم که یک شی DAO غیریگانه ایجاد کند، به طوری که برای هر درخواست، یک نمونه جدید بازگردد. اما مشکلی به وجود می آید، زیرا یک نمونه از سرویس آب و هوا تنها یک بار وابستگی های خود را تزریق می کند. یک راه حل این است که سرویس آب و هوا از حضور ظرف اسپرینگ آگاه باشد و هر زمان که به یک نمونه جدید از DAO نیاز داشته باشد، آن را از ظرف درخواست کند. با این حال، این روش، سرویس را به اسپرینگ وابسته می کند، در حالی که در واقع این سرویس نباید در مورد حضور اسپرینگ چیزی بداند. در عوض، ما می توانیم به امکان Lookup، در تزریق متد اسپرینگ اعتماد کرده و دسترسی سرویس آب و هوا به DAO را از طریق متد getWeatherDAO() از نوع JavaBean ایجاد کنیم. این متد می تواند پیاده سازی شود و یا در صورت نیاز انتزاعی باقی بماند. سپس در تعریف کارخانه bean، این متد توسط ظرف سربارگذاری و پیاده سازی مناسبی برای آن ارائه می شود، به گونه ای که یک نمونه DAO جدید را به صورت یک bean برگرداند:

 

در قطعه کد بالا به ظرف گفته شده است که شی DAO را به صورت غیریگانه ایجاد کند، و بنابراین در هر فراخوانی متد getWeatherDao() یک نمونه جدید از این کلاس بازگردانده می شود. اگر این شی به صورت یگانه تعریف می شد، در هر بار فراخوانی، همان نمونه ذخیره شده بازمی گشت. با وجود این که این رویکرد نادرست نیست، احتمالا هرگز نخواهید این کار انجام شود، زیرا فایده اصلی استفاده از متد Lookup تزریق، همانند این مورد، این است که نمونه های جدید را تزریق کنیم. در این مثال، کلاس WeatherServiceImpl و متد getWeatherDao انتزاعی هستند، اما ما می توانیم متد getter را به هر صورت که نیاز است سربارگذاری کنیم. (در واقع، متدهای کاندیدا نیاز به هیچ آرگومانی ندارند ولی با این وجود لازم نیست از قراردادهای JavaBean برای نامگذاری پیروی کرد.) نکته مهم این است که در سایر قسمت های کد، باید از متد getter و نه فیلد مربوطه، برای دسترسی به DAO استفاده کرد. این کار برای تمام فیلدهای JavaBean مفید است.

استفاده از تزریق متد این پرسش را پیش می آورد که در شرایطی مثل تست unit، چگونه می توان بدون این که ظرفی برای تزریق متد وجود داشته باشد، کد را تست کرد. ممکن است این کار به برخی از شرایط وابسته باشد، همانند این مثال که در آن کلاس پیاده سازی شده، خود انتزاعی است. یک استراتژی واقع گرایانه و نسبتا ساده این است که برای تست unit، با نوشتن زیرکلاسی برای کلاس مجرد، یک پیاده سازی خاص تست را از متد تزریق تهیه کنیم؛ مثلا:

 

 

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

تصمیم گیری بین تزریق قراردهنده و تزریق سازنده

گاهی اوقات هنگام استفاده از کلاس های موجود، امکان انتخاب بین استفاده از تزریق قراردهنده و یا تزریق سازنده وجود ندارد. اگر یک کلاس فقط یک سازنده چند آرگومانی داشته و هیچ عضو داده ای JavaBean نداشته باشد، و یا برعکس، که تنها یک سازنده ی بدون آرگومان به همراه اعضاء داده ای JavaBean داشته باشد، در عمل، انتخاب صورت گرفته است. اما ممکن است برخی از کلاس های موجود در واقع شما را مجبور به استفاده از ترکیب هر دو حالت بکنند، استفاده از یک سازنده چند آرگومانی به همراه برخی از اعضاء داده ای اختیاری JavaBean.

هنگامی که حق انتخاب بین این دو حالت وجود داشته باشد، باید به چند نکته توجه شود:

  • در مواردی که همه مقادیر مورد نیاز نیست، استفاده از اعضاء داده ای JavaBean، کنترل مقادیر پیش فرض و یا اختیاری را آسان تر می کند. در حالت استفاده از سازنده، این کار معمولا به تهیه سازنده های متعدد منجر می شود، که هر یک درون خود دیگری را فرامی خواند. انواع مختلف سازنده و یا لیست طولانی آرگومان می تواند بسیار طولانی و غیرقابل کنترل شدن کلاس گردد.
  • اعضاء داده ای JavaBean (تا زمانی که خصوصی نباشند) به طور خودکار توسط کلاس های مشتق به ارث برده می شوند، در حالی که سازنده ها هیچ گاه به ارث برده نمی شوند. در اغلب اوقات این باعث می شود در زیرکلاس ها، سازنده های تکراری ای ایجاد شود که سازنده های کلاس والد را فراخوانی می کنند. با این حال، در اکثر IDEها به کمک ابزارهایی که وجود دارد، ایجاد سازنده یا اعضاء داده ای JavaBean نسبتا آسان شده است.
  • اعضاء داده ای JavaBean در سطح کد، مسلما از نظر خود اظهاری، بهتر از آرگومان های سازنده عمل می کنند. هنگام افزودن JavaDocs، ایجاد اسناد مربوط به اعضاء داده ای به تلاش کمتری نیاز دارد، زیرا این اعضا تکرار نمی شوند.
  • از آنجایی که اعضاء داده ای JavaBean، دارای نام بوده و از طریق Java Reflection قابل مشاهده هستند، بنابراین در زمان اجرا می توانند بر اساس نام تطبیق یابند. با این حال، در یک فایل کلاس کامپایل شده نام های آرگومان های سازنده حفظ نشده و در نتیجه تطبیق خودکار آن ها بر اساس نام امکان پذیر نیست.
  • در صورت وجود متد getter برای اعضاء داده ای JavaBean اجازه دریافت مقدار فعلی (و همچنین تنظیم آن، در صورت وجود متد setter) وجود دارد. این کار برای شرایطی، مانند زمانی که نیاز است مقدار عضو در جایی دیگر ذخیره شود، مفید است.
  • در زمانی که نیاز باشد، مکانیسم PropertyEditor در JavaBean برای انجام تبدیل خودکار نوع قابل استفاده می باشد. این مکانیسم توسط اسپرینگ پشتیبانی و استفاده می شود.
  • از آنجایی که متد setter می تواند چندین بار فراخوانی شود، اعضاء داده ای JavaBean می توانند تغییرپذیر باشند. این کار اجازه تغییر مقدار یک وابستگی را می دهد. وابستگی هایی که از طریق سازنده منتقل می شوند، نمی توانند تغییرپذیر باشند، مگر اینکه این اعضا به صورت خصیصه نیز تعریف شده باشند. از سوی دیگر، اگر تغییرناپذیری کامل مورد نیاز باشد، تزریق سازنده اجازه می دهد که این اعضاء داده ای به صراحت به عنوان final اعلان شوند، در حالی که بهترین کاری که متد setter می توانست انجام دهد این بود که اگر بیش از یک بار فراخوانی شود، یک استثنا ایجاد کند. هیچ راهی وجود ندارد که بتوان فیلدی که از طریق متد setter مقدار می گیرد را به صورت final تعریف کرد، که بتوان اجازه تغییر آن را در سایر قسمت های کلاس گرفت.
  • برای ایجاد این از این که یک شیء معتبر ساخته شده است، استفاده از روش آرگومان های سازنده راه ساده تری است. چرا که تمام مقادیر مورد نیاز در سازنده اعلان شده و به این ترتیب باید این مقادیر نیز ارسال گردند. در مورد ترتیب استفاده از آن ها، کلاس آزادی عمل داشته و این ترتیب، به روند مقداردهی اولیه وابسته است. با اعضاء داده ای JavaBean این احتمال وجود دارد که قبل از استفاده از شی، برخی اعضاء داده ای تنظیم نشده باشند که در نتیجه یک حالت نامعتبر ایجاد شود. علاوه بر این، در صورت استفاده از اعضاء داده ای JavaBean، هیچ راهی وجود ندارد که فراخواننده را به رعایت ترتیب خاصی در اجرای متدهای setter مجبور کرد، و این بدین معنی است که احتمال دارد نیاز باشد مقداردهی اولیه، پس از تنظیم اعضاء داده ای و از طریق یک متد init انجام شود. متدی مثل init می تواند بررسی کند که تمام اعضاء داده ای مورد نیاز تنظیم شده باشند. اسپرینگ به طور خودکار می تواند هر متد مقداردهی اولیه ای را که اعلان شده باشد، پس از تنظیم تمام ویژگی های مورد نیاز فراخوانی کند. به جای این کار اشیاء bean می توانند رابط InitializingBean را پیاده سازی کنند. در این رابط متد afterPropertiesSet() معرفی شده است که به طور خودکار فراخوانی می شود.
  • در صورتی که تعداد سازنده ها کم باشند، استفاده از روش تزریق سازنده می تواند هزینه کد کمتری نسبت به تعداد زیاد متدهای دسترسی به اعضاء داده ای JavaBean داشته باشد. با این حال، اکثر IDEها ابزارهای متنوعی برای کامل سازی کد دارند که به ایجاد سازنده یا اعضاء داده ای JavaBean، نسبتا کمک زیادی می کند.

به طور کلی در اغلب موارد، تیم اسپرینگ استفاده از تزریق قراردهنده را به تزریق سازنده ترجیح می دهد، اگر چه این تصمیم گیری نباید سخت باشد. نکات بالا عواملی است که باید برای تصمیم گیری در شرایط خاص مورد توجه قرار گیرند. به نظر می رسد تزریق سازنده برای زمان هایی که سناریوهای مقداردهی اولیه ساده تر باشند، مناسب تر باشد. زیرا در این شرایط تعداد آرگومان های سازنده کم خواهد بود. معمولا این روش برای مطابقت انواع آرگومان های پیچیده ی غیرتکراری مناسب است. اگر پیچیدگی پیکربندی بیشتر شود، به نظر می رسد تزریق قراردهنده به کار کمتری نیاز داشته و کنترل آن راحت تر باشد. توجه داشته باشید که دیگر ظروف IoC نیز غالبا هر دو تزریق سازنده و قراردهنده را پشتیبانی می کنند.

[۱] External Entity

[۲] Data Access Object

[۳] Setter

[۴] Constructor

[۵] Method Injection

[۶] abstract protected

[۷] Singleton

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *