Tagbangers Blog

Springの学習日記Part1: DIについて

この記事を書き始めようと思ったきっかけ

普段の業務でバックエンドをSpring Bootで開発しているのですが、どうも自分が書いているとSpringについて基礎を知らないことがわかり、また何がわからないのかをうまく言語化した上で相手に伝えられない事態になっていました。

また上司に言語化する練習をすることを勧められた経緯もあって、自身の過去をよくよく振り返ってみた結果、コードを書いていく中で湧いてきた疑問や、自身の考えについてよく整理できていないまま(最適なワードが見つからないまま)相手に伝えていたことがしばしばありました。

当然その状態では、自分が求めている情報について検索したり相手から情報を引っ張ってくることができないと思いました(ほんとそう)。

なので少しでも正しい理解で相手に適切な言葉や文章で伝えるための練習台として、本ブログで学習日記を書き始めようと思ったという感じです。

最初はぎこちないですが温かい目で見守ってください。

DI(Dependency Injection)とは

Baeldung(主にJavaに関連する技術的なトピックを扱う技術情報のウェブサイト)によれば、下記の通りで、

Dependency Injection is a fundamental aspect of the Spring framework, through which the Spring container “injects” objects into other objects or “dependencies”.

Simply put, this allows for loose coupling of components and moves the responsibility of managing components onto the container.

直訳すると、

依存関係の注入は Spring フレームワークの基本的な側面であり、Spring コンテナーがオブジェクトを他のオブジェクトまたは「依存関係」に「注入」します。

簡単に言えば、これによりコンポーネントの疎結合が可能になり、コンポーネントの管理責任がコンテナに移ります。

ふむふむ、Springコンテナがオブジェクトを他のオブジェクトや依存関係に注入できてコンポーネントの管理がしやすいようにしてくれるってことなのか?(分かった風)

よくわからなかったのでもうChatGPTに聞いてみた

オブジェクト間の依存関係を外部から提供するデザインパターンです。このパターンにより、オブジェクトは自身が必要とする依存オブジェクトを直接生成するのではなく、外部のメカニズム(この場合はSpringコンテナ)によって注入されるため、コードの結合度が低下し、テスト性が向上し、柔軟性と再利用性が高まります。

オブジェクト自身が必要とする依存オブジェクトを直接生成しなくてもSpringコンテナ(外部)によって注入してくれるから結合度が低下するらしい(少しわかりやすい?)。

とはいえ説明だけでは実態がなにも分からぬ、コードを見てみるまでは・・・という感じなので、コードを見てみようと思います。

因みにDIには3つのタイプがあるのですが、今回はConstructor injectionについてみていこうと思います。

  • Constructor injection
  • Setter injection
  • Interface injection

Demo1: 依存関係の注入ができていないコードを見てみる

というわけでちょっとサンプルコードを漁って確認してみました。

例えば下記のような従業員の給与に関するレポートを生成するサービスで今回説明をしてみようと思います。

コードの簡単な説明としては、下記のようにサービスクラスがあり、従業員の給与情報を収集して、それを計算し、結果をPDFレポートとして出力する一連の処理を書いています。

EmployeesSalariesReportService

class EmployeesSalariesReportService {
    void generateReport() {
        EmployeeDao employeeDao = new EmployeeDao();
        List<Employee> employees = employeeDao.findAll(); //従業員の給与情報を収集

        EmployeeSalaryCalculator employeeSalaryCalculator = new EmployeeSalaryCalculator(); 
        List<EmployeeSalary> employeeSalaries = employeeSalaryCalculator.calculateSalaries(employees); //従業員の給与情報を計算

        PdfSalaryReport pdfSalaryReport = new PdfSalaryReport();
        pdfSalaryReport.writeReport(employeeSalaries);
    }
}

EmployeeDao

@Component
public class EmployeeDao {
    public List<Employee> findAll() {
        System.out.println("Finding all employees");
        return Collections.emptyList();
    }
}

EmployeeSalaryCalculator

@Component
public class EmployeeSalaryCalculator {
    public List<EmployeeSalary> calculateSalaries(List<Employee> employees) {
        System.out.println("Calculating salaries");
        return Collections.emptyList();
    }
}

PdfSalaryReport

@Component
@Profile("pdf-reports")
public class PdfSalaryReport implements SalaryReport {
    public void writeReport(List<EmployeeSalary> employeeSalaries) {
        System.out.println("Writing Pdf Report");
    }
}

Runnerクラスでアプリケーションを実行してみます。

public class Runner {
    public static void main(String... args) {
        EmployeesSalariesReportService employeesSalariesReportService = new EmployeesSalariesReportService();

        employeesSalariesReportService.generateReport();
    }
}

すると実行結果は下記のように

Finding all employees
Calculating salaries
Writing Pdf Report

従業員の情報を全て見つけ出し、給料を計算してその結果をPDFに書き込んでいて正しく動作しました。

次に、PDFに書き込みをしていましたが例えばXls形式に書き込みたい場合はどうするか?を考えます。

下記のようにXlsSalaryReportクラスを用意し

@Component
@Profile("xls-reports")
public class XlsSalaryReport implements SalaryReport {
    public void writeReport(List<EmployeeSalary> employeeSalaries) {
        System.out.println("Writing Xls Report");
    }
}

 メソッドを呼び出すようにしてEmployeesSalariesReportService を下記のように修正しました。

※ ⌘ + Option + B: IntellJでマルチカーソル編集は Ctrl + G(割とどうでもいいが便利) 

class EmployeesSalariesReportService {
    void generateReport() {
        EmployeeDao employeeDao = new EmployeeDao();
        List<Employee> employees = employeeDao.findAll();

        EmployeeSalaryCalculator employeeSalaryCalculator = new EmployeeSalaryCalculator();
        List<EmployeeSalary> employeeSalaries = employeeSalaryCalculator.calculateSalaries(employees);

        XlsSalaryReport xlsSalaryReport = new XlsSalaryReport();
        xlsSalaryReport.writeReport(employeeSalaries);
    }
}

実行すると結果は下記のように

Finding all employees
Calculating salaries
Writing Xls Report

ちゃんとXls形式に書き込んでいるようですね。

では、今度はEmployeesSalariesReportServiceクラスの中身を変更することなくXlsに変更できるにはどうすれば良いだろうか?を考えます。

まずEmployeesSalariesReportServiceを元の状態に戻した上で、

PdfSalaryReport pdfSalaryReport = new PdfSalaryReport();

の依存関係をここで作成する代わりにこの依存関係を外部から注入することで実現できそうです。

因みにですが、このサービスクラスは下記のSalaryReportインタフェースが実装してるクラスで、writeReport メソッドを定義しています。

public interface SalaryReport {
    void writeReport(List<EmployeeSalary> employeeSalaries);
}

なので、サービスクラスでフィールドを定義して、そのフィールドから給与レポートへの参照を取得します。
そしてコンストラクタを作成して、給与レポートへの参照を提供します。
最後にPDF給与レポートを直接呼び出す代わりに正しい給与レポートを呼び出します。

class EmployeesSalariesReportService {

    private final SalaryReport salaryReport; //フィールドを定義

    public EmployeesSalariesReportService(SalaryReport salaryReport) {
        this.salaryReport = salaryReport; //コンストラクタを作成
    }
    
    void generateReport() {
        EmployeeDao employeeDao = new EmployeeDao();
        List<Employee> employees = employeeDao.findAll();

        EmployeeSalaryCalculator employeeSalaryCalculator = new EmployeeSalaryCalculator();
        List<EmployeeSalary> employeeSalaries = employeeSalaryCalculator.calculateSalaries(employees);

        salaryReport.writeReport(employeeSalaries);
    }
}

これによって、EmployeesSalariesReportService クラスのコンストラクタを通じて salaryReport オブジェクトが注入された状態になります。

試しにRunnerクラスにPDF給与レポートの依存関係を追加して実行してみると

public class Runner {
    public static void main(String... args) {
        EmployeesSalariesReportService employeesSalariesReportService = new EmployeesSalariesReportService(
                new PdfSalaryReport() //依存関係を追加
        );

        employeesSalariesReportService.generateReport();
    }
}

実行結果は

Finding all employees
Calculating salaries
Writing Pdf Report

となっていて、PDFレポートの書き込みになっていました。

つまり、Runnerクラスのnew PdfSalaryReport()で依存関係を追加するだけでサービスクラスの実装を変更する必要はなくなったというわけです。
(ここが依存性注入のメリットになるのかなるほど。。。)

では次に、このサービスクラスをどうテストするのかを考えますが、

現状下記のサービスクラスを見るに、

void generateReport() {
    EmployeeDao employeeDao = new EmployeeDao();
    List<Employee> employees = employeeDao.findAll();

    EmployeeSalaryCalculator employeeSalaryCalculator = new EmployeeSalaryCalculator();
    List<EmployeeSalary> employeeSalaries = employeeSalaryCalculator.calculateSalaries(employees);

    salaryReport.writeReport(employeeSalaries);
}

EmployeeDaoEmployeesSalaryCalculatorインスタンスが直接生成されているのでこれらの依存関係をメソッド外から書き換える(モックを注入)することができません。

そのため、SalaryReport と同様に外部から注入できるようにする必要がありそうです。

あまり実感が湧かない人のために、現実世界に置き換えてみました。

依存性の注入を理解するための現実のシチュエーションを想像してみましょう。例として、レストランでの食事を考えてみます。

現状のコードの問題点の例

あなたがレストランのシェフで、顧客に提供する各料理に必要な食材を自分で栽培し、収穫していると想像してください。この状況では、料理を作るためには必ず自分の農場から食材を用意しなければなりません。もし農場で問題が発生したら(例えば、特定の野菜が育たなかった場合)、その料理を提供できなくなってしまいます。また、他の農場や市場からより良い品質の食材を簡単に取り入れることもできません。この状況は、generateReport メソッド内で EmployeeDaoEmployeeSalaryCalculator を直接生成している状況に似ています。つまり、メソッドは固定された「食材源(依存オブジェクト)」に強く依存しており、変更や代替が困難です。

依存性の注入の例

一方で、依存性の注入を使った場合のシナリオを考えてみましょう。今度は、あなたがシェフのままで、食材は外部のサプライヤー(農場や市場)から提供されるとします。顧客から注文があった時、必要な食材をサプライヤーから選んで調達し、料理を作ります。このシステムでは、あるサプライヤーが食材を提供できなくなった場合でも、別のサプライヤーから同じ食材を簡単に入手できるため、柔軟性が高まります。さらに、特定の食材の品質が向上したり、価格が下がったりした場合には、簡単にサプライヤーを変更できます。

この例で言う「外部のサプライヤー」が、EmployeeDaoEmployeeSalaryCalculator などの依存オブジェクトに相当します。依存性の注入を使うことで、これらの依存オブジェクトを外部から「注入」できるようになり、テスト時には実際のオブジェクトの代わりにモックオブジェクトを注入することができます。これにより、テストの柔軟性が高まり、実際のデータベースアクセスや複雑な計算ロジックを使わずにメソッドをテストできるようになります。

Demo2: 手動で依存関係の注入をしたコード

では、SalaryReport と同様に外部から注入できるようにしてみます。

(因みにここでいう手動で依存関係を注入するとは、下記のように従業員給与サービスクラスにおいて全ての外部依存性をとるコンストラクタを持っている状態のことを指します。)

下記のコードでは、EmployeeDao, EmployeeSalaryCalculator, SalaryReportといったようにオブジェクトが依存関係を自分自身で作成する代わりに、外部オブジェクトによって提供されるのが期待されています。

class EmployeesSalariesReportService {
    private final EmployeeDao employeeDao;
    private final EmployeeSalaryCalculator employeeSalaryCalculator;
    private final SalaryReport salaryReport;

    EmployeesSalariesReportService(EmployeeDao employeeDao, EmployeeSalaryCalculator employeeSalaryCalculator, SalaryReport salaryReport) {
        this.employeeDao = employeeDao;
        this.employeeSalaryCalculator = employeeSalaryCalculator;
        this.salaryReport = salaryReport;
    }

    void generateReport() {
        List<Employee> employees = employeeDao.findAll();
        List<EmployeeSalary> employeeSalaries = employeeSalaryCalculator.calculateSalaries(employees);

        salaryReport.writeReport(employeeSalaries);
    }
}

つまりこれらのオブジェクトが必要とする依存関係を宣言した上で、あとは他のオブジェクトがその依存関係を提供する状態となっています。

下記のRunnerクラスでは、実際にオブジェクトを作成して、その依存性を提供しています。

public class Runner {
    public static void main(String... args) {
        EmployeesSalariesReportService employeesSalariesReportService = new EmployeesSalariesReportService(
                new EmployeeDao(),
                new EmployeeSalaryCalculator(),
                new PdfSalaryReport()
        );

        employeesSalariesReportService.generateReport();
    }
}

このようにして、EmployeesSalariesReportService クラスの設計が依存性の注入をサポートしているため、テスト時にモックを注入できるようになります。

なぜなら、従業員給与サービスクラスにはコンストラクタがあり、このコンストラクタを介して全ての依存関係を注入することができるからです。

下記のようにEmployeesSalariesReportServiceTestではMockitoでテストを書きたい場合必要なモックを全て作成した上で、検証することができます。

@RunWith(MockitoJUnitRunner.class)

public class EmployeesSalariesReportServiceTest {
    @InjectMocks //指定されたクラスのインスタンスを作成し、そのクラス内で宣言されているフィールドに対して @Mockで作成されたモックオブジェクトを自動的に注入します
    private EmployeesSalariesReportService employeesSalariesReportService;
    @Mock
    private EmployeeDao employeeDao;
    @Mock
    private EmployeeSalaryCalculator employeeSalaryCalculator;
    @Mock
    private SalaryReport salaryReport;
    @Mock
    private List<Employee> employees;
    @Mock
    private List<EmployeeSalary> employeeSalaries;

    @Test
    public void shouldGenerateSalaryReport() {
        when(employeeDao.findAll()).thenReturn(employees);
        when(employeeSalaryCalculator.calculateSalaries(employees)).thenReturn(employeeSalaries);

        employeesSalariesReportService.generateReport();

        verify(salaryReport).writeReport(employeeSalaries);
    }
}

依存性注入によって、テスト対象のクラスが直接依存するオブジェクトをテスト時にモックに置き換えることができるため、実際のデータベースアクセスや複雑なビジネスロジックを避けて、テストの焦点を特定の機能や振る舞いに絞り込むことができます。これにより、テストの実行速度が向上し、テストの信頼性が高まります。

Demo3: Springによる依存関係の注入をしたコード

では最後にSpringによる依存関係の注入を見てみます。

下記のConfigurationクラスで、@ComponentScanアノテーションによって、Springが指定されたパッケージ内をスキャンし、@Component, @Service, @Repository, @Controller などのアノテーションが付与されたクラスをBeanとして登録します。

Configuration

@ComponentScan("xxxx")
public class Configuration {
}

EmployeesSalariesReportService 

@Service
class EmployeesSalariesReportService {
    private final EmployeeDao employeeDao;
    private final EmployeeSalaryCalculator employeeSalaryCalculator;
    private final SalaryReport salaryReport;

    EmployeesSalariesReportService(EmployeeDao employeeDao, EmployeeSalaryCalculator employeeSalaryCalculator, SalaryReport salaryReport) {
        this.employeeDao = employeeDao;
        this.employeeSalaryCalculator = employeeSalaryCalculator;
        this.salaryReport = salaryReport;
    }

    void generateReport() {
        List<Employee> employees = employeeDao.findAll();
        List<EmployeeSalary> employeeSalaries = employeeSalaryCalculator.calculateSalaries(employees);

        salaryReport.writeReport(employeeSalaries);
    }
}

これによりSpringは、依存関係を作成して、従業員給与レポートサービスが下記の依存関係を全て必要としていることを認識しています。

  • private final EmployeeDao employeeDao;
  • private final EmployeeSalaryCalculator employeeSalaryCalculator;
  • private final SalaryReport salaryReport;

それぞれのレポートクラスには@Profileアノテーションが使用されているため、例えば下記のRunnerクラスでPDFプロファイルを指定して実行することで、PDFレポートサービスクラスから呼び出して利用できます。

AnnotationConfigApplicationContext context = getSpringContext("pdf-reports");では、
SpringのJavaベースの設定を使用してアプリケーションコンテキストを初期化し、特定のプロファイル(この場合は "pdf-reports")をアクティブにします。

public class Runner {
    public static void main(String... args) {
        AnnotationConfigApplicationContext context = getSpringContext("pdf-reports");

        EmployeesSalariesReportService employeesSalariesReportService = context.getBean(EmployeesSalariesReportService.class);
        employeesSalariesReportService.generateReport();

        context.close();
    }

    private static AnnotationConfigApplicationContext getSpringContext(String profile) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        context.getEnvironment().setActiveProfiles(profile);
        context.register(Configuration.class);
        context.refresh();
        return context;
    }
}

これにより、

実行時にアプリケーションの振る舞いを変更するための柔軟性を提供し、異なる条件(PDF, Xlsx)に応じて異なるBean設定(PDF形式のレポートとXLSX形式のレポートを生成するBean設定)を使用することを可能にしています。


EmployeesSalariesReportService は直接 EmployeeDao, EmployeeSalaryCalculator, および SalaryReport のインスタンスを作成しておらず、代わりに、これらのオブジェクトはコンストラクタを通じて外部から提供されています。この外部とは、SpringフレームワークのDIコンテナを指し、このコンテナが依存オブジェクトのインスタンス化、管理、および適切なコンポーネントへの注入を担います。

このように依存性を外部から注入することにより、EmployeesSalariesReportService は、使用する具体的な EmployeeDao, EmployeeSalaryCalculator, SalaryReport の実装に対して疎結合になり、拡張性やテストの容易さが向上します。たとえば、テスト時にはこれらの実際の実装の代わりにモックオブジェクトを注入して、単体テストの際の外部システムへの依存を排除することができます。

まとめ

改めてDIとは、

ソフトウェア設計のパターンの一つで、コンポーネント間の依存関係を管理する方法です。
DIを使用することで、コンポーネント(クラス、オブジェクトなど)は自身が必要とする依存オブジェクトを直接生成するのではなく、外部から提供(注入)されるようになります。

DIの特徴についても整理してみました。

  • コードが再利用しやすい
    • 結合度が低いため、コンポーネントやクラスを異なるコンテキストで再利用しやすくなります。
  • コードのテストが容易になる
    • 実際の実装の代わりにモックやスタブを注入することで、単体テストが容易になります。
  • 結合性が低くなる
    • コンポーネント間の結合度が低く、それぞれが独立しているため、変更や置換が容易になる
  • 設定とビジネスロジックが分離される
    • 依存関係の管理を設定ファイルやアノテーションに委ねることで、ビジネスロジックから設定を分離できます。


説明は以上になります。ご精読いただきありがとうございました!

次回はデザインパターンについて記載したいと思いますので、よろしくお願いします。