הזרקת תלות ב-Java – דוגמת מדריך לתבנית עיצוב DI

תבנית העיצוב של Java Dependency Injection מאפשרת לנו להסיר את התלויות הקשות בקוד ולהפוך את היישום שלנו למקושר באופן פתוח, להרחיבו ולתחזקו. באפשרותנו ליישם הזרמת תלות ב-Java כדי להעביר את פתרון ההתלות מזמן הקומפילציה לזמן הריצה.

Java Dependency Injection

הזרמת התלות ב-Java נראית קשה להבנה דרך התיאוריה, אז אני אציג דוגמה פשוטה ולאחר מכן נראה כיצד להשתמש בתבנית הזרמת התלות כדי להשיג קישור פתוח והרחבה ביישום. נניח כי יש לנו יישום שבו אנו מצרפים EmailService כדי לשלוח דוא"לים. לרוב נממש זאת כמו בדוגמה הבאה.

package com.journaldev.java.legacy;

public class EmailService {

	public void sendEmail(String message, String receiver){
		//לוגיקה לשליחת דואר אלקטרוני
		System.out.println("Email sent to "+receiver+ " with Message="+message);
	}
}

המחלקה EmailService מכילה את הלוגיקה לשליחת הודעת דואר אלקטרוני לכתובת הדואר האלקטרוני של המקבל. קוד היישום שלנו יהיה כמו בדוגמה הבאה.

package com.journaldev.java.legacy;

public class MyApplication {

	private EmailService email = new EmailService();
	
	public void processMessages(String msg, String rec){
		//לבצע תיקוני הודעות, לוגיקת עיבוד וכו'
		this.email.sendEmail(msg, rec);
	}
}

קוד הלקוח שישתמש במחלקת MyApplication כדי לשלוח הודעות דוא"ל יהיה כמו בדוגמה הבאה.

package com.journaldev.java.legacy;

public class MyLegacyTest {

	public static void main(String[] args) {
		MyApplication app = new MyApplication();
		app.processMessages("Hi Pankaj", "[email protected]");
	}

}

ממבט ראשון, נראה שאין שום דבר שגוי עם המימוש לעיל. אך לקוד לעיל יש הגבלות מסוימות.

  • \[
    \text{המחלקה } \texttt{MyApplication} \text{ אחראית לאתחול שירות האימייל ולשימוש בו. זה גורם לתלות בקוד מובנית. אם נרצה לעבור לשירות אימייל מתקדם אחר בעתיד, יהיה צורך בשינויי קוד במחלקת \texttt{MyApplication}. זה עושה את היישום שלנו קשה להרחבה ואם שירות האימייל משמש במחלקות מרובות, אז זה יהיה אפילו קשה יותר.}
  • \text{אם נרצה להרחיב את היישום שלנו כך שיספק תכונת הודעה נוספת, כגון SMS או הודעה בפייסבוק, אז נצטרך לכתוב יישום נוסף עבור זה. זה יכלול שינויי קוד במחלקות היישום וגם במחלקות הלקוח עצמם.}
  • \text{בדיקת היישום תהיה קשה מאוד מאחר והיישום שלנו יוצר את מופע שירות האימייל ישירות. אין לנו דרך ליצור מקורות פיקציות אלה במחלקות הבדיקה שלנו.}

\text{ניתן לטעון שנוכל להסיר את יצירת המופע של שירות האימייל מתוך מחלקת \texttt{MyApplication} על ידי הגדרת בנאי שדורש את שירות האימייל כארגומנט.}

package com.journaldev.java.legacy;

public class MyApplication {

	private EmailService email = null;
	
	public MyApplication(EmailService svc){
		this.email=svc;
	}
	
	public void processMessages(String msg, String rec){
		//\text{בצע תיקון הודעות, לוגיקת עיבוד וכו'}
		this.email.sendEmail(msg, rec);
	}
}

\text{אך במקרה כזה, אנו מבקשים מיישומי לקוח או ממחלקות הבדיקה לאתחל את שירות האימייל וזה לא החלטת עיצוב טובה. עכשיו בואו נראה איך נוכל ליישם את דפוס ההתנגדות ב- Java כדי לפתור את כל הבעיות עם המימוש הנ"ל. התנגדות התלות ב- Java דורשת לפחות את הדברים הבאים:}

  1. \text{רכיבי השירות צריכים להיות מעוצבים עם מחלקת בסיס או ממשק. נעדיף להעדיף ממשקים או מחלקות מופשטות שיגדירו חוזה עבור השירותים.}
  2. \text{מחלקות הצרכן צריכות להיות כתובות במונחים של ממשק השירות.}
    \]
  3. מחלקות האינג'קטור שיזהו את השירותים ואז מחלקות הצרכנים.

התלות בג'אווה – רכיבי שירות

במקרה שלנו, יש לנו MessageService שיצהיר על החוזה למימושי שירות.

package com.journaldev.java.dependencyinjection.service;

public interface MessageService {

	void sendMessage(String msg, String rec);
}

כעת נניח שיש לנו שירותי אימייל ו-SMS שמיישמים את הממשקים האלה למעלה.

package com.journaldev.java.dependencyinjection.service;

public class EmailServiceImpl implements MessageService {

	@Override
	public void sendMessage(String msg, String rec) {
		//לוגיקה לשליחת אימייל
		System.out.println("Email sent to "+rec+ " with Message="+msg);
	}

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

public class SMSServiceImpl implements MessageService {

	@Override
	public void sendMessage(String msg, String rec) {
		//לוגיקה לשליחת SMS
		System.out.println("SMS sent to "+rec+ " with Message="+msg);
	}

}

השירותים שלנו ב-Java Dependency Injection מוכנים ועכשיו נוכל לכתוב את מחלקת הצרכנים שלנו.

התלות בג'אווה – צרכן שירות

אין צורך להגדיר ממשקי בסיס עבור מחלקות הצרכנים, אך אני אגדיר ממשק Consumer המצהיר על חוזה עבור מחלקות הצרכנים שלי.

package com.journaldev.java.dependencyinjection.consumer;

public interface Consumer {

	void processMessages(String msg, String rec);
}

מימוש מחלקת הצרכנים שלי נראה כך.

package com.journaldev.java.dependencyinjection.consumer;

import com.journaldev.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){
		//לבצע תיקון הודעות, לוגיקת עיבוד וכו'
		this.service.sendMessage(msg, rec);
	}

}

שים לב כי כיתת היישום שלנו משתמשת רק בשירות. היא לא מאתחלת את השירות המביא ל"הפרדת עניינים" טובה יותר. גם השימוש בממשק השירות מאפשר לנו לבדוק בקלות את היישום על ידי התעתקות בשירות הודעות וקישור השירותים בזמן ריצה במקום בזמן קומפילציה. עכשיו אנו מוכנים לכתוב מחלקות אינקטור תלותיות ב-Java שיאתחלו את השירות וגם את מחלקות הצרכנים.

החיסורים של התלותיות ב-Java – מחלקות אינקטור

בואו נגדיר ממשק MessageServiceInjector עם הכרזה של שיטה המחזירה את מחלקת הConsumer.

package com.journaldev.java.dependencyinjection.injector;

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

public interface MessageServiceInjector {

	public Consumer getConsumer();
}

עכשיו עבור כל שירות, נצטרך ליצור מחלקות אינקטור כמו שמופיע למטה.

package com.journaldev.java.dependencyinjection.injector;

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

public class EmailServiceInjector implements MessageServiceInjector {

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

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

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

public class SMSServiceInjector implements MessageServiceInjector {

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

}

עכשיו בואו נראה כיצד יישומי הלקוח ישתמשו ביישום עם תוכנית פשוטה.

package com.journaldev.java.dependencyinjection.test;

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

public class MyMessageDITest {

	public static void main(String[] args) {
		String msg = "Hi Pankaj";
		String email = "[email protected]";
		String phone = "4088888888";
		MessageServiceInjector injector = null;
		Consumer app = null;
		
		//שלח אימייל
		injector = new EmailServiceInjector();
		app = injector.getConsumer();
		app.processMessages(msg, email);
		
		//שלח SMS
		injector = new SMSServiceInjector();
		app = injector.getConsumer();
		app.processMessages(msg, phone);
	}

}

כפי שניתן לראות, כיתות היישום שלנו אחראיות רק לשימוש בשירות. כיתות השירות נוצרות באינקטורים. גם אם עלינו להרחיב עוד את היישום שלנו כדי לאפשר שליחת הודעות בפייסבוק, עלינו לכתוב רק כיתות שירות ואינקטורים בלבד. לכן יישום התלותיות פתר את הבעיה עם התלות קשוחה בקוד ועזר לנו להפוך את היישום שלנו לגמיש וקל להרחבה. עכשיו בואו נראה כיצד נוכל לבדוק בקלות את כיתת היישום שלנו על ידי התעתקות האינקטור וכיתות השירות.

הזרקת תלות ב-Java – מקרה בדיקה של JUnit עם מחליק מתוזז ושירות

package com.journaldev.java.dependencyinjection.test;

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

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

public class MyDIApplicationJUnitTest {

	private MessageServiceInjector injector;
	@Before
	public void setUp(){
		//להתעלם מהמחליק באמצעות כיתה אנונימית
		injector = new MessageServiceInjector() {
			
			@Override
			public Consumer getConsumer() {
				//להתעלם משירות ההודעות
				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", "[email protected]");
	}
	
	@After
	public void tear(){
		injector = null;
	}

}

כפי שאתם יכולים לראות אני משתמש בכיתות אנונימיות כדי להתעלם מכיתות המחליק והשירות ואני יכול בקלות לבדוק את שיטות היישום שלי. אני משתמש ב-JUnit 4 עבור מחלקת הבדיקה לעיל, אז וודאו שהוא נמצא בנתיב בניית הפרויקט שלכם אם אתם מריצים מחלקת בדיקה זו מעל. אנו השתמשנו בבנאים כדי להזריק את התלותות בכיתות היישום, דרך נוספת היא להשתמש בשיטת setter כדי להזריק תלותות בכיתות יישום. עבור הזרקת תלות באמצעות שיטת setter, מחלקת היישום שלנו תיושם כמו למטה.

package com.journaldev.java.dependencyinjection.consumer;

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

public class MyDIApplication implements Consumer{

	private MessageService service;
	
	public MyDIApplication(){}

	//הזרקת תלות באמצעות setter	
	public void setService(MessageService service) {
		this.service = service;
	}

	@Override
	public void processMessages(String msg, String rec){
		//לבצע כמה אימות, מניפולציה של לוגיקת הודעה וכו'
		this.service.sendMessage(msg, rec);
	}

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

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

public class EmailServiceInjector implements MessageServiceInjector {

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

}

אחד מדוגמאות הכי טובות להטמעת התלות במגדיר הוא Struts2 Servlet API Aware interfaces. האם להשתמש בהתמצאות התלות הבנויה על בנאי או בהתמצאות התלות הבנויה על setter הוא החלטת עיצוב ותלויה בדרישות שלך. לדוגמה, אם היישום שלי לא יכול לעבוד כלל בלעדיו של מחלקת השירות, אני מעדיף התמצאות בנויה על בנאי; אחרת, אני אשתמש בהתמצאות התלות בנויה על setter כאשר זה באמת נדרש. התמצאות בתלות ב-Java היא אמצע להשיג ההפוך של שליטה (IoC) ביישום שלנו על ידי העברת קשרי אובייקטים מזמן הקומפילציה לזמן הריצה. ניתן להשיג IoC דרך תבנית הפקטוריה, תבנית תכנון שיטת התבנית, תבנית האסטרטגיה וגם דרך תבנית איתור השירות. התמצאות בתלות ב-Spring, Google Guice ו־Java EE CDI מספקים את התהליך של התמצאות בתלות דרך שימוש ב־API ההשקפה של Java וב־הערות Java. כל שאנו צריכים הוא להוסיף הערות לשדה, לבנאי או לשיטת setter ולהגדיר אותם בקבצי XML התצורה או במחלקות.

יתרונות של התלות בהזנת Java

חלק מהיתרונות של שימוש בהתלות ב-Java הם:

  • הפרדת העניינים
  • הפחתת קוד חזרה במחלקות היישום מכיוון שכל העבודה לאתחול התלות ניהולה על ידי מרכיב הכניסה
  • רכיבים הניתנים להגדרה מהפך את היישום להרחבה בקלות
  • בדיקת יחידות היא קלה עם אובייקטי מוק

חסרונות של התלות בהזנת Java

התלות בהזנת Java גם יש לה מספר חסרונות:

  • אם מופעלת באופן יתר, יכולה לגרום לבעיות תחזוקה מכיוון שהשפעת השינויים ידועה רק בזמן הריצה.
  • התלות בהזנת Java מסתירה את תלות המחלקות בשירות שיכולה לגרום לשגיאות זמן ריצה שהיו נתפסות בזמן קידוד.

הורדת פרויקט התלות בהזנה

זהו הכל בכל מה שקשור לתבנית התלות בהזנה ב-Java. חשוב לדעת ולהשתמש בה כאשר אנחנו בשליטה על השירותים.

Source:
https://www.digitalocean.com/community/tutorials/java-dependency-injection-design-pattern-example-tutorial