1. 程式人生 > >Java依賴注入(DI)例項詳解

Java依賴注入(DI)例項詳解

Java依賴注入模式允許我們擺脫硬編碼,使我們的應用更加鬆耦合、增強擴充套件性以及可維護性。通過依賴注入我們可以降低從編譯到執行時的依賴性。

Java依賴注入

Java的依賴注入僅僅通過理論是很難解明白的,所以我們通過幾個簡單的示例來描述它,怎樣利用依賴注入模式降低我們應用之間的耦合性和增強可擴充套件性。

假設我們的應用需要通過 EmailService 去傳送email,通常情況下,我們是這樣實現的:

package com.byron4j.hightLevel.java8.pattern.di;


/**
 * 
 * <pre>
 *      通常用於傳送emai的服務
 * </pre>
 * @author
Byron.Y.Y */
public class EmailService { /* * 傳送email的方法 */ public void sendEmail(String message, String receiver){ //傳送email的業務邏輯 System.out.println("傳送訊息:" + message + "給接收者:" + receiver); } }

EmailService 類是提供傳送email的服務類,在我們的應用中,可能會這樣使用傳送email的服務:

package
com.byron4j.hightLevel.java8.pattern.di; /** * * <pre> * 我們的應用,利用EmailService來發送email * </pre> * @author Byron.Y.Y */ public class MyApplication { private EmailService emailService = new EmailService(); public void processMessages(String msg, String rec){ //做一些資訊驗證、操作邏輯等等
this.emailService.sendEmail(msg, rec); } }

在我們的客戶端,則可能使用我們的應用MyApplication 來處理email:

package com.byron4j.hightLevel.java8.pattern.di;


/**
 * 
 * <pre>
 *      呼叫應用提供的處理eamil的服務
 * </pre>
 * @author Byron.Y.Y
 */
public class MyLegacyTest {

    public static void main(String[] args) {
        MyApplication myApplication = new MyApplication();

        //純屬虛擬,勿投訴
        myApplication.processMessages("馬雲,你好!", "[email protected]");
    }
}

初窺以上程式碼,貌似沒什麼缺陷,但是業務邏輯上有幾個限制。

  • MyApplication 類需要負責初始化emailService並且使用它。這樣就導致了硬編碼依賴。如果以後我們想使用其他更好的email服務進行傳送email,我們不得不去修改MyApplication 的程式碼。這使得我們的應用難以擴充套件,如果emailService需要在更多的類中使用,可維護性則更差了。

  • 如果我們需要擴展出其他的傳送訊息的方式如SMS、Facebook message等,迫使我們需要寫一個其他的application,這需要服務端以及客戶端都需要修改相關程式碼。

  • 測試application將會變得很麻煩,因為我們的應用是直接建立emailService例項的。 我們根本無法在測試用例中MOCK出這個emailService物件。

一個較好的方案,我們可以不在MyApplication 中直接建立emailService例項,而是讓那些需要使用該傳送eamil服務的應用通過構造器的引數去設定emailService

package com.byron4j.hightLevel.java8.pattern.di;


/**
 * 
 * <pre>
 *      為那些需要使用email服務的應用提供專有的構造器
 * </pre>
 * @author Byron.Y.Y
 */
public class MyApplication4Constructor {

    private EmailService email = null;

    public MyApplication4Constructor(EmailService svc){
        this.email=svc;
    }

    public void processMessages(String msg, String rec){
        //做一些資訊驗證、操作邏輯等等
        this.email.sendEmail(msg, rec);
    }
}

儘管如此,我們還是得需要在客戶端或者測試用例中去初始化emailService例項,顯然這並不是我們所理想的。

現在,我們想想怎麼利用Java DI依賴注入模式前面的問題……

  • 1 服務元件需要設計成基類 or 介面( 實際中我們更多的是使用抽象類或者介面來規約服務規範 )

  • 2 服務實現需要實現服務元件約定的服務規範

  • 3 注入類Injector Class負責初始化服務以及服務實現

Java依賴注入—-Service元件

在這個設計中,我們使用 MessageService 來指定服務規範。

package com.byron4j.hightLevel.java8.pattern.di.pattern;

/**
 * 
 * <pre>
 *      該介面用於制定服務規範
 * </pre>
 * @author Byron.Y.Y
 */
public interface MessageService {

    /**傳送訊息的服務*/
    void sendMessage(String msg, String rec);

}

現在我們可以有Email和SMS 的兩種傳送訊息的服務實現。

package com.byron4j.hightLevel.java8.pattern.di.pattern.impl;

import com.byron4j.hightLevel.java8.pattern.di.pattern.MessageService;

/**
 * 
 * <pre>
 *  傳送email的服務實現,實現了訊息服務的規範
 * </pre>
 * @author Byron.Y.Y
 */
public class EmailServiceImpl implements MessageService {

    @Override
    public void sendMessage(String msg, String rec) {
        System.out.println("傳送郵件給 "+rec+ " ,內容為:"+msg);
    }

}
package com.byron4j.hightLevel.java8.pattern.di.pattern.impl;

import com.byron4j.hightLevel.java8.pattern.di.pattern.MessageService;


/**
 * 
 * <pre>
 *      傳送簡訊的服務實現
 * </pre>
 * @author Byron.Y.Y
 */
public class SMSServiceImpl implements MessageService {

    @Override
    public void sendMessage(String msg, String rec) {
        System.out.println("傳送簡訊給 "+rec+ " ,內容為:"+msg);
    }

}

我們的可用於依賴注入的服務實現已經開發完畢,接下來我們需要編寫消費服務的類。

Java依賴注入—-服務呼叫者(消費者)

我們需要制定服務消費的規範Consumer 。

package com.byron4j.hightLevel.java8.pattern.di.pattern;

/**
 * <pre>
 *      制定消費服務的規範
 * </pre>
 * @author Byron.Y.Y
 */
public interface Consumer {
    void processMessages(String msg, String rec);
}

以及消費的具體實現:

package com.byron4j.hightLevel.java8.pattern.di.pattern.impl;

import com.byron4j.hightLevel.java8.pattern.di.pattern.Consumer;
import com.byron4j.hightLevel.java8.pattern.di.pattern.MessageService;


/**
 * 
 * <pre>
 *      服務消費的具體實現----使用構造器注入的方式
 * </pre>
 * @author Byron.Y.Y
 */
public class MyDIApplication implements Consumer {

    /**介面形式--多型--服務屬性*/
    MessageService messageService;


    /**構造器注入服務屬性*/
    public MyDIApplication(MessageService messageService) {
        super();
        this.messageService = messageService;
    }



    @Override
    public void processMessages(String msg, String rec) {
        //傳送訊息,取決於具體的服務實現的行為
        this.messageService.sendMessage(msg, rec);
    }

}

請注意,上面我們僅僅是使用到了service,並沒有初始化它,儘量達到“關注點分離”—– 對於我來說我僅僅是使用它這就是我能做且只能做的分內事,那麼我不應該去生成它那不是我的職責範圍另外,使用介面服務的形式,我們可以更好的測試應用,MOCK MessageService 並在執行時繫結service而不是在編譯期。



現在,我們可以編寫Java依賴注入類了——–用來初始化service、consumer

Java依賴注入—-注入類

我們編寫一個MessageServiceInjector 介面,宣告一個獲得Consumer 的方法。

package com.byron4j.hightLevel.java8.pattern.di.pattern;


/**
 * 
 * <pre>
 *      依賴注入服務
 * </pre>
 * @author Byron.Y.Y
 */
public interface MessageServiceInjector {

    /**獲得消費類*/
    public Consumer getConsumer();

}

現在為每一個服務,我們都可以建立其依賴注入類了:

package com.byron4j.hightLevel.java8.pattern.di.pattern.impl;

import com.byron4j.hightLevel.java8.pattern.di.pattern.Consumer;
import com.byron4j.hightLevel.java8.pattern.di.pattern.MessageServiceInjector;


/**
 * 
 * <pre>
 *      email服務的依賴注入類
 * </pre>
 * @author Byron.Y.Y
 */
public class EmailServiceInjector implements MessageServiceInjector {

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

}
package com.byron4j.hightLevel.java8.pattern.di.pattern.impl;

import com.byron4j.hightLevel.java8.pattern.di.pattern.Consumer;
import com.byron4j.hightLevel.java8.pattern.di.pattern.MessageServiceInjector;


/**
 * 
 * <pre>
 *      SMS服務的依賴注入類
 * </pre>
 * @author Byron.Y.Y
 */
public class SMSServiceInjector implements MessageServiceInjector {

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

}

現在,在客戶端的簡單實用例項如下:

package com.byron4j.hightLevel.java8.pattern.di.pattern.impl;

import com.byron4j.hightLevel.java8.pattern.di.pattern.Consumer;
import com.byron4j.hightLevel.java8.pattern.di.pattern.MessageServiceInjector;

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;

        //Send email
        injector = new EmailServiceInjector();
        app = injector.getConsumer();
        app.processMessages(msg, email);

        //Send SMS
        injector = new SMSServiceInjector();
        app = injector.getConsumer();
        app.processMessages(msg, phone);
    }

}

執行結果如下:

傳送郵件給 [email protected] ,內容為:Hi Pankaj
傳送簡訊給 4088888888 ,內容為:Hi Pankaj

至此,你會發現我們的application僅僅負責使用service。

So,依賴注入解決硬編碼問題,使我們的應用變得更加靈活易擴充套件了。

再來看看我們的測試如何更加容易MOCK了吧。

Java依賴注入—-單元測試MOCK注入服務

package com.byron4j.hightLevel.java8.pattern.di.pattern.impl;

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

import com.byron4j.hightLevel.java8.pattern.di.pattern.Consumer;
import com.byron4j.hightLevel.java8.pattern.di.pattern.MessageService;
import com.byron4j.hightLevel.java8.pattern.di.pattern.MessageServiceInjector;

/**
 * 
 * <pre>
 *      單元測試類,MOCK服務,負責注入
 * </pre>
 * @author Byron.Y.Y
 */
public class MyDIApplicationJUnitTest {

    /**注入類*/
    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 testInjector(){

        Consumer  consumer  = injector.getConsumer();
        consumer.processMessages("Hi Pankaj", "[email protected]");

    }

    @After
    public void clean(){
        //回收
        injector = null;
    }

}

在上述測試類中,我們使用了匿名內部類來mock 注入器和服務,使得測試介面服務變得容易些。

我們是使用構造器來注入服務的、另外一種方式是在application類中使用setter方法來注入服務

package com.byron4j.hightLevel.java8.pattern.di.pattern.impl;

import com.byron4j.hightLevel.java8.pattern.di.pattern.Consumer;
import com.byron4j.hightLevel.java8.pattern.di.pattern.MessageService;

/**
 * 
 * <pre>
 *      使用setter注入
 * </pre>
 * @author Byron.Y.Y
 */
public class MyDIApplication4Setter implements Consumer{

    private MessageService service;

    public MyDIApplication4Setter(){}

    //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.byron4j.hightLevel.java8.pattern.di.pattern.impl;

import com.byron4j.hightLevel.java8.pattern.di.pattern.Consumer;
import com.byron4j.hightLevel.java8.pattern.di.pattern.MessageServiceInjector;

/**
 * 
 * <pre>
 *      屬性注入的注入器
 * </pre>
 * @author Byron.Y.Y
 */
public class EmailServiceInjector4Setter implements MessageServiceInjector{

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



}

具體採用構造器注入還是setter注入方式,取決於你的需求。假如我的應用不能離開服務類而運作那麼會採用構造器注入,否則採用setter注入方式。

依賴注入總結

依賴注入( DI )的方式可以達到控制反轉( IOC )的目的,將物件從繫結從編譯器轉移到執行時。我們也可以通過工廠模式、模板模式或者策略模式等方式達到控制反轉 ( IOC )。

Spring依賴注入、Google Guice和Java EE CDI框架通過反射、註解技術使得依賴注入變得更簡單。我們要做的僅僅是在屬性、構造器或setter中新增某些註解。

Java依賴注入的好處

  • 關注點分離

  • 減少樣板程式碼,因為所有服務的初始化都是通過我們的注入器元件來完成的

  • 可配置化的元件使得應用更易於擴充套件

  • 單元測試更易於MOCK物件

Java依賴注入的缺陷

  • 濫用有可能難以維護,因為很多錯誤都從編譯器轉移到了執行時

  • 依賴注入隱藏了服務類的依賴,可能導致執行時錯誤,而這之前是可能在編譯器就能發現的