コンストラクタインジェクションとは
コンストラクタインジェクションは
「そのクラスが必要とするオブジェクト(依存)を、コンストラクタの引数で受け取る」
という設計のやり方です。
もっと噛み砕くと、
・自分の中で new して相棒を決めない
・「誰と組むか」は外から渡してもらう
・コンストラクタの時点で「必要な相棒は全部そろっている」状態にする
という考え方です。
これにより、クラス同士の結びつきが弱くなり、差し替えやテストが圧倒的にやりやすくなります。
まずは「よくない書き方」から見る
自分の中で new してしまう例
次のコードを見てください。
final class EmailSender {
void send(String to, String body) {
System.out.println("SEND MAIL to=" + to + " body=" + body);
}
}
final class UserService {
String register(String email) {
EmailSender sender = new EmailSender(); // ここで new
sender.send(email, "Welcome!");
return "OK";
}
}
Java一見シンプルですが、UserService が EmailSender にベッタリ依存しています。
問題点は、
「メール送信の方法を変えたい」「テストではメール送信をフェイクにしたい」
と思ったときに、UserService の中身を書き換える必要があることです。
さらにテストを書くとき、本当にメールが送られてしまうかもしれません。
「呼び出しただけ確認したい」「送信はしたくない」がやりにくい状態です。
コンストラクタインジェクションの基本形(重要)
役割をインターフェースにし、コンストラクタで受け取る
上の例を、コンストラクタインジェクションを使って書き直してみます。
まず、「メールを送る」という役割をインターフェースに切り出します。
interface Mailer {
void send(String to, String body);
}
Javaそして、UserService はこの Mailer にだけ依存します。
final class UserService {
private final Mailer mailer; // ここでは役割の型だけを持つ
UserService(Mailer mailer) { // コンストラクタで注入
this.mailer = mailer;
}
String register(String email) {
mailer.send(email, "Welcome!");
return "OK";
}
}
Javaここがコンストラクタインジェクションのポイントです。
UserService は「Mailer が必要です」と宣言するだけで、
「どの実装クラスか」は一切知りません。
誰と組むかは、インスタンスを作る側が決めます。
final class ConsoleMailer implements Mailer {
@Override
public void send(String to, String body) {
System.out.println("SEND MAIL to=" + to + " body=" + body);
}
}
Java実際に使うときは、こうなります。
Mailer mailer = new ConsoleMailer();
UserService service = new UserService(mailer); // コンストラクタで注入
service.register("test@example.com");
JavaUserService の中では、一度も new ConsoleMailer() を書いていません。
「誰と組むか」は外で決めて、コンストラクタの引数で注入しています。
なぜコンストラクタインジェクションが強いのか(深掘り)
必須の依存を「取り忘れ不可能」にできる
コンストラクタインジェクションでは、必要な相手をコンストラクタの引数にします。
final class OrderService {
private final Payment payment;
OrderService(Payment payment) { // ここで必須依存を強制
this.payment = payment;
}
}
Javaこの設計だと、Payment を渡さずに OrderService を作ることはできません。
コンパイル時点で「依存の注入忘れ」を防げます。
一方、セッターで注入するスタイルだと、
final class OrderService {
private Payment payment;
void setPayment(Payment payment) {
this.payment = payment;
}
}
Java「setPayment を呼び忘れる」というバグが起き得ます。
コンストラクタインジェクションは、「必ず必要な依存」を保証できるのが大きなメリットです。
フィールドを final にできる
コンストラクタでしか設定しないので、フィールドを final にできます。
final class OrderService {
private final Payment payment;
OrderService(Payment payment) {
this.payment = payment;
}
}
Java一度セットされたら、途中で差し替えられません。
「いつの間にか別のオブジェクトにすり替わっている」といった怖さがなくなります。
この「不変性」は、バグを減らすうえで非常に強い味方です。
「何が必要なクラスか」が一目で分かる
コンストラクタの引数を見れば、そのクラスが何に依存しているかが一目で分かります。
final class PlaceOrderUseCase {
private final OrderRepository repo;
private final Payment payment;
PlaceOrderUseCase(OrderRepository repo, Payment payment) {
this.repo = repo;
this.payment = payment;
}
}
Javaこのクラスは「注文を保存するもの」と「支払いをするもの」が必要なんだな、ということが
コンストラクタを見るだけで分かります。
これは「設計書」よりも確かな情報です。
コンストラクタは、クラスの「依存関係の顔」になってくれます。
テストとの相性:偽物を注入するだけでテストできる
本物を使いたくないときにどうするか
先ほどの UserService を思い出してください。
final class UserService {
private final Mailer mailer;
UserService(Mailer mailer) {
this.mailer = mailer;
}
String register(String email) {
mailer.send(email, "Welcome!");
return "OK";
}
}
Javaこれをテストしたいとき、実際のメール送信をしたくない場合がほとんどです。
そこで「テスト用の Mailer」を作ります。
final class FakeMailer implements Mailer {
String lastTo;
String lastBody;
@Override
public void send(String to, String body) {
this.lastTo = to;
this.lastBody = body;
}
}
Javaテストコードのイメージはこうなります。
FakeMailer fake = new FakeMailer();
UserService service = new UserService(fake); // コンストラクタで偽物注入
service.register("test@example.com");
// FakeMailer に記録された内容を検証
assert "test@example.com".equals(fake.lastTo);
assert "Welcome!".equals(fake.lastBody);
Java本物のメール送信は一切行われず、
「UserService が Mailer の send を正しい引数で呼んだか」だけを検証できます。
コンストラクタインジェクションをしていないと、この差し替えが困難です。
中で new していたら、どう頑張っても偽物にできません。
よくある「やってしまいがち」と改善例
シングルトンで直接参照してしまう
ありがちなパターンとして、次のようなコードがあります。
final class Logger {
private static final Logger INSTANCE = new Logger();
public static Logger getInstance() { return INSTANCE; }
void log(String message) {
System.out.println(message);
}
}
final class Service {
void doSomething() {
Logger.getInstance().log("start"); // どこからでも直接参照
}
}
Javaこれは依存を「隠れた形」で持ってしまっています。Service は Logger に依存しているのに、コンストラクタには何も出てきません。
改善するなら、Logger も依存として注入します。
interface Log {
void log(String message);
}
final class ConsoleLog implements Log {
@Override
public void log(String message) {
System.out.println(message);
}
}
final class Service {
private final Log log;
Service(Log log) {
this.log = log;
}
void doSomething() {
log.log("start");
}
}
Javaこうしておくと、テスト時には「記録用のフェイク Log」を渡せますし、
本番ではファイル出力ログに切り替える、といった差し替えも簡単です。
「引数が増えるのがイヤ」でコンストラクタインジェクションを避ける
「コンストラクタの引数が多くなりそうでイヤだ」という理由で、
コンストラクタインジェクションを避ける人もいます。
しかし、引数が多いのは「コンストラクタインジェクションのせい」ではなく
「そのクラスが多くの依存を持ちすぎている」サインです。
コンストラクタインジェクションを入れてみると、
「このクラス、こんなにいろいろに依存してたのか」と気付けるのがメリットでもあります。
その場合は、
責務を分けてクラスを小さくする
中間層(ファサード)を作って依存をまとめる
といったリファクタリングを検討するきっかけになります。
DI コンテナとコンストラクタインジェクション
Spring などの DI コンテナを使うとき、多くのフレームワークは
「コンストラクタインジェクション」を標準的なやり方としてサポートしています。
アノテーションで書くと、だいたい次のような形です(ざっくりイメージ)。
@Component
class UserService {
private final Mailer mailer;
@Autowired
UserService(Mailer mailer) { // フレームワークがここに注入してくれる
this.mailer = mailer;
}
}
Javaフレームワークがやっているのは、
どのクラス(Mailer の実装)を new するか決める
UserService を new するとき、そのインスタンスをコンストラクタに渡す
という、手動でやったことを自動化しているだけです。
だからこそ、先に「生の Java」でコンストラクタインジェクションの感覚を身につけると、
フレームワークの動きも自然に理解できるようになります。
まとめ:コンストラクタインジェクションを設計の標準にする
コンストラクタインジェクションは、依存を見える化し、
テストしやすく、変更にも強いクラスを作るための“標準フォーム”です。
自分のクラスで、
中で new してしまっている依存はないか
シングルトンを直接呼びに行ってないか
本当はインターフェース(役割)に依存できないか
コンストラクタで全部渡してもらう形にできないか
を一つずつ見直してみてください。
