跳到主要內容

Java Dependency Injection

首先

這篇主要是翻譯 這篇 的教學, 再用自己的方式解釋。

Email Service

文章開頭以 Email Service 來解釋, UML 應該是長這樣, Application 強依賴 EmailService, Application 在一開始就初始化了 EmailService, 而 Main 也強依賴 Application, 這樣寫並沒有太大的問題, 但不容易對 Application class 做測試, 因為不容易對 EmailService 做 mock.



修正 Application 方便測試

為了讓 Application 能夠被測試, 所以改成 inject EmailService 的建構方式




public class Application {

    private EmailService emailService;

    public Application(EmailService emailService) {
        this.emailService = emailService;
    }

    public void notification(String message, String receiver) {
        this.emailService.sendEmail(message, receiver);
    }
}

簡單的使用 Mockito 來 mock EmailService 測試

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>2.12.0</version>
</dependency>


package dependency.injection;

import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

/**
 * Created by jerry on 2017/11/21.
 */
public class ApplicationTest {

    @Mock
    private EmailService mockEmailService;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void assertApplication() {
        Application app = new Application(mockEmailService);

        // trigger
        app.notification("test-message", "someone");

        // mock
        doNothing()
                .when(mockEmailService)
                .sendEmail(anyString(), anyString());

        // verify emailService will be call once
        verify(mockEmailService, times(1));
    }

}

MessageService Interface

Interface 的好處, 是讓我們方便活用物件導向的多型(Polymorphism)概念, 透過 MessageService Interface, 讓 EmailServiceSmsService 擁有一致性的操作 void sendMessage(), 好處是 Application 不再依賴一個明確的 class instance, 而是依賴一個抽象的 interface, 對 Application 來說, 只要能做 sendMessage 的物件都可以接受, Application 不再 “強烈” 依賴 EmailService 或是 SmsService.

日後擴增 FacebookMessage 只要 implement MessageService, 也一樣可以被 Application 使用.




public interface MessageService {

    void sendMessage(String message, String receiver);
}


public class EmailServiceImpl implements MessageService {

    @Override
    public void sendMessage(String message, String receiver) {
        System.out.println("Email sent to " + receiver + " with Message=" + message);
    }
}


public class SmsServiceImpl implements MessageService {

    @Override
    public void sendMessage(String message, String receiver) {
        System.out.println("SMS sent to " + receiver + " with Message=" + message);
    }
}


public class Application {

    private MessageService messageService;

    public Application(MessageService messageService) {
        this.messageService = messageService;
    }

    public void notification(String message, String receiver) {
        this.messageService.sendMessage(message, receiver);

    }
}


public class Main {

    public static void main(String[] args) {
        Application app = new Application(new EmailServiceImpl());
        app.notification("nice to meet you", "someone");
    }
}

Consumer

文章末端, 用了 Consumer interface 抽象了原本的 Application, 而 consumer 做的也只是 sendMessage 這個動作, 多墊一個 interface 好處並不多, 反而增加多餘的 code 造成維護上的不便.


優點

  1. 關注點分離(Separation of Concerns), 對 Application class 來說只需要關注 sendMessage 的動作.
  2. 方便測試, 對 Application 來說, 只要知道 sendMessage 有被執行, 執行的內容不是重點.
  3. 解耦合, Application 不強烈依賴任何 instance.
  4. 操作反轉(IoC), Application 不在需要在操作的時候決定要用 EmailService 還是 SmsService, 而交由外部 class (Main)決定.

缺點

  1. 維護跟擴增並不一定容易, 假設 FacebookMessage 的 sendMessage 是需要互動的情境, 有回傳值 String sendMessage(), 那應該是寫 2 個 interface ? 還是在相同的 interface 寫 2 個 methods ?
  2. 單純的 Code Review 的階段, 無法了解是被 injection 是哪個 instance ? EmailServiceImpl or SmsServiceImpl ?


留言

這個網誌中的熱門文章

Parse URI query string to Key Value

Parse URI query String to Map 做 urlDecode 處理 沒有任何 query String 回傳 Empty Map 確保只處理 key-value 結構的 query String package com.example.util; import lombok.extern.slf4j.Slf4j; import org.apache.http.client.utils.URIBuilder; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.net.URLDecoder; import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; /** * Created by jerry on 2017/12/28. * * @author jerry */ @Slf4j public class UriUtil { private UriUtil() { } public static Map splitQuery(final String uri) { Map queryPairs = new LinkedHashMap (); try { final URI uri = new URIBuilder(uri).build(); final String rawQuery = uri.getRawQuery(); log.info("CurrentUrl Query: {}", rawQuery); // 過濾沒有 query string // 還有過濾無法成對 keyValue 的 query, e.g. http://host/path?123 if (Objects.isNull(rawQuery) |...

Google Compute Engine‎ - AccessDeniedExceptions 403

原因 打算從 instance 打包 logs 到 google cloud storage 發生了 AccessDeniedException: 403 Insufficient OAuth2 scope to perform this operation. , 看起來是 instance 沒有 storage 權限 解決 Reference: https://cloud.google.com/compute/docs/access/create-enable-service-accounts-for-instances#changeserviceaccountandscopes 重新設定 service account 權限 instance 上內建有 gcloud , 就直接用現有的工具查詢一下 instance 的 account. $ gsutil info 或者在本機直接 gcloud compute instances describe INSTANCE_NAMES Account: [alpha-number-compute@developer.gserviceaccount.com] Project: [our-project-name] 會看到 instance 的一些狀態, 接下來就簡單多了, 按照下列的說明, 要先 stop instance, 更改 storage scope 再重新 start 。 To change an instance's service account and access scopes, the instance must be temporarily stopped. To stop your instance, read the documentation for Stopping an instance. After changing the service account or access scopes, remember to restart the instance. # Stop Instance gcloud compute instances stop INSTANCE_NAMES # 設定 storage scope 為 full (Read, Write) gcloud co...

Spring-boot Thymeleaf Html5 SAXParseException 解析錯誤

thymeleaf 解析 html5 出錯 <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="description" content=""> <meta name="author" content=""> <title>SB Admin - Start Bootstrap Template</title> <!-- Bootstrap core CSS--> <link href="../static/vendor/bootstrap/css/bootstrap.min.css" rel="stylesheet"> <!-- Custom fonts for this template--> <link href="../static/vendor/font-awesome/css/font-awesome.min.css" rel="stylesheet" type="text/css"> <!-- Page level plugin CSS--> <link href="../static/vendor/datatables/dataTables.bootstrap4.css" rel="stylesheet"> <!-- Custom styles for this template--> <li...