تبلیغات

الگوی طراحی تزریق وابستگی (Dependency Injection) در جاوا

الگوی طراحی تزریق وابستگی (Dependency Injection) در جاوا
Dependency Injection

همانطور که باید بدانید، Dependency Injection (تزریق وابستگی = DI)، یک الگوی طراحی (Design Pattern) معتبر، برای هر زبان برنامه‌‌نویسی می‌باشد. و همچنین مفهوم کلی پشت آن، Inversion of Control (معکوس سازی کنترل = IOC) نامیده می‌شود. و طبق این مفهوم، یک کلاس نباید وابستگی‌هایش را به صورت استاتیک پیکربندی کند (یعنی وابستگی hard-coded نداشته باشد)؛ بلکه باید آن‌ها از بیرون کلاس پیکربندی شوند.

برای مثال، اگر یک کلاس Car داشته باشیم، می‌تواند شامل ویژگی‌های Wheel, Engine و Battery باشد؛ حالا اگر برفرض داخل این کلاس، موتور آن را از کلاس PrideEngine ایجاد کرده‌ایم و در نتیجه کلاس Car ما بعد از کامپایل وابسته به موتور پراید خواهد بود! و اگر بخواهیم برفرض موقع رانتایم از PeugeotEngine استفاده کنیم، نمی‌توانیم!

بنابراین تزریق وابستگی به ما کمک می‌کند تا حد ممکن یک کلاس را مستقل طراحی کنیم؛ چرا که این موضوع، باعث می‌شود تا احتمال استفاده مجدد از آن کلاس افزایش یابد و بتوان آن را مستقل از سایر کلاس‌ها آزمایش کرد. و در واقع تزریق وابستگی، باعث می‌شود تا تجزیه و تحلیل وابستکی یک کلاس را از Complile-time به Runtime تغییر دهیم.

همچنین در این الگو، مفاهیم Loose Coupling (به حداقل رساندن وابستگی) و Tight Coupling نیز وجود دارند.

تزریق وابستگی در جاوا

از نظر تئوری، فهم مفهوم تزریق وابستگی در جاوا به نظر سخت است. بنابراین من یک مثال ساده را در نظر می‌گیرم و سپس خواهید دید که چگونه می‌توان از الگوی تزریق وابستگی به loose coupling و توسعه پذیری اپلیکیشن دست یافت.

پس اجازه دهید تا اینگونه بگویم که ما یک برنامه‌ای داریم و قرار است تا از کلاس EmailService، برای ارسال ایمیل‌ها استفاده کند. پس ما به طور معمول، آن را به صورت زیر انجام می‌دهیم:

package com.fullkade.java.legacy;

public class EmailService {

	public void sendEmail(String message, String receiver){
		//logic to send email
		System.out.println("Email sent to "+receiver+ " with Message="+message); // البته این یک مثال است که مثلا ارسال شد
	}
}

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

package com.fullkade.java.legacy;

public class MyApplication {

	private EmailService email = new EmailService();
	
	public void processMessages(String msg, String rec){
		//do some msg validation, manipulation logic etc
		this.email.sendEmail(msg, rec);
	}
}

و در نهایت داخل متد main قصد داریم تا از کلاس MyApplication خود استفاده کنیم:

package com.fullkade.java.legacy;

public class MyLegacyTest {

	public static void main(String[] args) {
		MyApplication app = new MyApplication();
		app.processMessages("Hi FuLLKade", "info@fullkade.com");
	}

}

در نگاه اول، هیچ اشتباهیی در روش پیاده‌سازی بالا به چشم نمیخورد؛ اما منطق بالا، یک محدودیت خاصی دارد!

  • کلاس MyApplication، مسئول ایجاد سرویس ایمیل و سپس استفاده از آن است؛ که به وابستگی hard-coded منجر می‌شود! و اگر ما بخواهیم در آینده به سرویس پیشرفته‌تر ایمیل سوئیچ کنیم، به تغییر کد نوشته شده در کلاس MyApplication نیاز خواهیم داشت! که باعث میشود تا برنامه‌ی ما به سختی گسترش یابد و اگر سرویس ایمیل در کلاس‌های متعددی استفاده شود، این امر سخت‌تر نیز خواهد شد.
  • اگر بخواهیم کلاس اپلیکیشن خود را جهت فراهم کردن قابلیت‌های پیام‌رسانی دیگری از جمله ارسال اس ام اس، فیسیبوک و … نیز گسترش دهیم، ما نیاز خواهیم داشت تا یک اپلیکیشن دیگری برای آن بنویسیم! که باعث تغییرات کد در کلاس‌های اپلیکیشن و … خواهد شد.
  • تست و آزمایش کلاس اپلیکیشن بسیار دشوار است؛ چرا که اپلیکیشن ما به صورت مستقیم نمونه‌ی سرویس ایمیل را ایجاد می‌کند؛ و در نتیجه هیچ راهی وجود ندارد تا یتوانیم این اشیاء را با کلاس‌های آزمایشی (test)، تقلید کنیم. (یا به عبارتی mock کنیم). (در ادامه این کار را با تزریق وابستگی انجام خواهیم داد)

بنابراین می‌توان از این موارد گفته شده، استدلال کرد که ما می‌توانیم ایجاد نمونه‌ای از کلاس EmailService که داخل کلاس اپلیکیشن وجود دارد، را با در اختیار داشتن یک متد سازنده‌ای که EmailService را به عنوان ورودی دریافت می‌کند، حذف کنیم:

package com.fullkade.java.legacy;

public class MyApplication {

	private EmailService email = null;
	
	public MyApplication(EmailService svc){
		this.email = svc;
	}
	
	public void processMessages(String msg, String rec){
		//do some msg validation, manipulation logic etc
		this.email.sendEmail(msg, rec);
	}
}

ولی در اینجا ما از بیرون می‌خواهیم تا کلاس EmailService را مقداردهی کند که متاسفانه این تصمیم درستی برای طراحی نیست!


حالا  ببینید که چگونه ما می‌توانیم الگوی تزریق وابستگی جاوا را برای حل همه‌ی مشکلات موجود در پیاده‌سازی بالا به کار ببریم؛ و همچنین تزریق وابستگی در جاوا به حداقل موارد زیر نیاز دارد:

  • کامپوننت‌های سرویس (مانند EmailService)، باید با استفاده از یک Base Class (کلاس پایه/پدر) یا اینترفیس (Interface) طراحی شوند. و بدین منظور استفاده از  اینترفیس‌ها یا کلاس‌های انتزاعی (abstract) بهتر است.
  • پکلاس‌های مصرف کننده (مثل MyApplication در بالا)، باید طبق ساختار اینترفیس یا کلاس انتزاعی نوشته شوند.
  • ساخت کلاس‌های Injector (تزریق‌کننده) که سرویس‌ها را مقداردهی کرده و سپس کلاس‌های مصرف کننده را اجرا می‌کنند.

کامپوننت‌های سرویس

برای مثالی که در نظر گرفته‌ایم، ما می‌توانیم یک اینترفیسی با نام MessageService داشته باشیم که قرار دادی را برای پیاده‌سازی سرویس تعریف و اعلام می‌کند:

package com.fullkade.java.dependencyinjection.service;

public interface MessageService {

	void sendMessage(String msg, String rec);
}

حالا اجازه دهید که بگوییم ما سرویس‌های ایمیل و اس‌ام‌اس را در اختیار داریم که واسط (اینترفیس/رابط) بالا را اجرا می‌کنند:

package com.fullkade.java.dependencyinjection.service;

public class EmailServiceImpl implements MessageService {

	@Override
	public void sendMessage(String msg, String rec) {
		//logic to send email
		System.out.println("Email sent to "+rec+ " with Message="+msg);
	}

}
package com.fullkade.java.dependencyinjection.service;

public class SMSServiceImpl implements MessageService {

	@Override
	public void sendMessage(String msg, String rec) {
		//logic to send SMS
		System.out.println("SMS sent to "+rec+ " with Message="+msg);
	}

}

سرویس‌های جاوای تزریق وابستگی ما حالا آماده هستند! و حالا باید consumer class (کلاس مصرف کننده) را بنویسیم.

کلاس مصرف کننده

ما لازم نیست که رابط‌های پایه (base interfaces) را برای کلاس مصرف کننده داشته باشیم؛ اما یک اینترفیس مصرف کننده برای کلاس‌های مصرف کننده خواهیم داشت.

package com.fullkade.java.dependencyinjection.consumer;

public interface Consumer {

	void processMessages(String msg, String rec);
}

و پیاده سازی کلاس مصرف کننده نسبت به این اینترفیس:

package com.fullkade.java.dependencyinjection.consumer;

import com.fullkade.java.dependencyinjection.service.MessageService;

public class MyDIApplication implements Consumer{

	private MessageService service;
	
	public MyDIApplication(MessageService svc){
		this.service = svc;
	}
	
	@Override
	public void processMessages(String msg, String rec){
		//do some msg validation, manipulation logic etc
		this.service.sendMessage(msg, rec);
	}

}

توجه داشته باشید که کلاس اپلیکیشن ما، فقط از سرویس (که در بالا MessageService می‌باشد) استفاده می‌کند و آن را مقداردهی اولیه (initialize) نکرده است. و همچنین استفاده از این اینترفیس سرویس (در اینجا منظور MessageService)، به ما این اجازه را می‌دهد تا به راحتی اپلیکیشن خود را با تقلید کردن MessageService، تست و آزمایش کنیم. (یعنی یک کلاس، همانند EmailServiceImpl و SMSServiceImpl بسازیم که به صورت آزمایشی نه واقعی کاری را انجام دهد و نسبت به آن اپلیکیشن خود را تست کنیم! مثلا نیازی به ارسال ایمیل واقعی نباشد و بخواهیم صرفا به صورت آزمایشی بنویسیم که ایمیل ارسال شد.)


اکنون آماده هستیم تا کلاس‌های اینجکتور (تزریق کننده) خود را بنویسیم تا سرویس‌ها را مقداردهی کرده و مصرف کننده را اجرا کنند.

کلاس‌های تزریق‌کننده (Injectors Classes)

بیایید یک رابط (اینترفیس) MessageServiceInjector بنویسیم که متدی برای تعریف کلاس مصرف کننده داشته باشد.

package com.fullkade.java.dependencyinjection.injector;

import com.fullkade.java.dependencyinjection.consumer.Consumer;

public interface MessageServiceInjector {

	public Consumer getConsumer();
}

اکنون برای هر سرویس، باید کلاس‌های تزریق‌کننده را مانند زیر ایجاد کنیم:

package com.fullkade.java.dependencyinjection.injector;

import com.fullkade.java.dependencyinjection.consumer.Consumer;
import com.fullkade.java.dependencyinjection.consumer.MyDIApplication;
import com.fullkade.java.dependencyinjection.service.EmailServiceImpl;

public class EmailServiceInjector implements MessageServiceInjector {

	@Override
	public Consumer getConsumer() {
		return new MyDIApplication(new EmailServiceImpl());
	}

}
package com.fullkade.java.dependencyinjection.injector;

import com.fullkade.java.dependencyinjection.consumer.Consumer;
import com.fullkade.java.dependencyinjection.consumer.MyDIApplication;
import com.fullkade.java.dependencyinjection.service.SMSServiceImpl;

public class SMSServiceInjector implements MessageServiceInjector {

	@Override
	public Consumer getConsumer() {
		return new MyDIApplication(new SMSServiceImpl());
	}

}

و حالا بییایید تا ببینیم چگونه از این طراحی در برنامه می‌خواهیم استفاده کنیم:

package com.fullkade.java.dependencyinjection.test;

import com.fullkade.java.dependencyinjection.consumer.Consumer;
import com.fullkade.java.dependencyinjection.injector.EmailServiceInjector;
import com.fullkade.java.dependencyinjection.injector.MessageServiceInjector;
import com.fullkade.java.dependencyinjection.injector.SMSServiceInjector;

public class MyMessageDITest {

	public static void main(String[] args) {
		String msg = "Hi FuLLKade";
		String email = "info@fullkade.com";
		String phone = "+980123456789";
		MessageServiceInjector injector = null;
		Consumer app = null;
		
		//Send email
		injector = new EmailServiceInjector();
		app = injector.getConsumer();
		app.processMessages(msg, email);
		
		//Send SMS
		injector = new SMSServiceInjector();
		app = injector.getConsumer();
		app.processMessages(msg, phone);
	}

}

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

آزمون JUnit و با Mock Injector و سرویس

package com.fullkade.java.dependencyinjection.test;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import com.fullkade.java.dependencyinjection.consumer.Consumer;
import com.fullkade.java.dependencyinjection.consumer.MyDIApplication;
import com.fullkade.java.dependencyinjection.injector.MessageServiceInjector;
import com.fullkade.java.dependencyinjection.service.MessageService;

public class MyDIApplicationJUnitTest {

	private MessageServiceInjector injector;
	@Before
	public void setUp(){
		//mock the injector with anonymous class
		injector = new MessageServiceInjector() {
			
			@Override
			public Consumer getConsumer() {
				//mock the message service
				return new MyDIApplication(new MessageService() {
					
					@Override
					public void sendMessage(String msg, String rec) {
						System.out.println("Mock Message Service implementation");
						
					}
				});
			}
		};
	}
	
	@Test
	public void test() {
		Consumer consumer = injector.getConsumer();
		consumer.processMessages("Hi Pankaj", "pankaj@abc.com");
	}
	
	@After
	public void tear(){
		injector = null;
	}

}

همانطور که می‌بینید، من از کلاس‌های ناشناس (anonymous) برای تقلید تزیرق کننده (mock کردن injector) و کلاس سرویس استفاده کرده‌ام. و من به راحتی توانستم متدهای اپلیکیشن خود را تست کنم. و همچنین در اینجا از JUnit 4 استفاده کردم و بنابراین مطمئن شوید که آن را در پروژه‌ی خود به کار برده‌اید.

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

ما در کدهای بالا از سازنده‌ها برای تزریق وابستگی‌ها در کلاس‌های اپلیکیشن استفاده کرده‌ایم؛ راه دیگر استفاده از متدهای setter است. برای مثال در اینصورت کلاس MyDIApplication  را به صورت زیر می‌نویسیم:

package com.fullkade.java.dependencyinjection.consumer;

import com.fullkade.java.dependencyinjection.service.MessageService;

public class MyDIApplication implements Consumer{

	private MessageService service;
	
	public MyDIApplication(){}

	//setter dependency injection	
	public void setService(MessageService service) {
		this.service = service;
	}

	@Override
	public void processMessages(String msg, String rec){
		//do some msg validation, manipulation logic etc
		this.service.sendMessage(msg, rec);
	}

}

و اینجکتور ایمیل:

package com.fullkade.java.dependencyinjection.injector;

import com.fullkade.java.dependencyinjection.consumer.Consumer;
import com.fullkade.java.dependencyinjection.consumer.MyDIApplication;
import com.fullkade.java.dependencyinjection.service.EmailServiceImpl;

public class EmailServiceInjector implements MessageServiceInjector {

	@Override
	public Consumer getConsumer() {
		MyDIApplication app = new MyDIApplication();
		app.setService(new EmailServiceImpl());
		return app;
	}

}
نکات تکمیلی

  • یکی از بهترین مثال‌های تزریق وابستگی با استفاده از setter ها، مثال « … – به زودی لینک خواهد شد» می‌باشد.
  • استفاده از متدسازنده یا متدهای ستر برای تنظیم وابستگی، یک تصمیم طراحی بوده و به نیازها و ملزومات شما بستگی دارد. برای مثال، اگر برنامه من نمی‌تواند بدون کلاس سرویس کار کند، بنابراین من باید متدسازنده‌ای ایجاد کنم که وابستگی را دریافت کند و در غیر این صورت، می‌توانم از setter استفاده کنم.
  • تزریق وابستگی در جاوا، یک راه برای به دست آوردن Inversion of control (IoC) یا «معکوس سازی کنترل» در اپلیکیشن‌هایمان است؛ که این کار را با انتقال Binding آبجکت‌ها از زمان کامپایل به رئال‌تایم انجام می‌دهد.
  • فریمورک‌های Spring Dependency Injection، Google Guice و Java EE CDI ، روند تزیرق وابستگی را با استفاده از رفلکشن‌ها (API Reflection Java) و annotations ها برایمان تسهیل می‌کنند.

مزایای و معایب تزریق وابستگی

برخی از مزایای انجام این کار:

  • Separation of Concerns یا تفکیک دغدغه‌ها
  • کاهش Boilerplate code (بویلرپلت)
    به دلیل اینکه تمامی کار مقداردهی وابستگی‌ها، توشظ کامپوننت تزریق‌کننده مدیریت می‌شود.
  • توسعه‌پذیر کردن کامپوننت‌ها
  • تست و آزمایش آسان

تزریق وابستگی در جاوا شامل معایبی نیز می‌باشد:

  • در صورت استفاده بیش از حد، می‌تواند منجر به مشکلات تعمیر و نگهداری کد (maintenance) شود؛ چراکه اثر تغییرات در زمان ران‌تایم شناخته می‌شوند.
  • تزریق وابستگی در جاوا، وابستگی‌های کلاس سرویس را مخفی می‌کند که می تواند خطاهای زمان اجرا را که در زمان کامپایل شدن رخ داده است، منجر شود.

پاسخ دهید

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

cp-codfk

نظرات ثبت شده بدون دیدگاه

توضیحات پیشنهادی نظرات اشتراک