Javaを鍛える|設計編 #1

DIとAOPの基礎、Springでの実践

  • Java
  • DI
  • AOP
  • Spring
  • Javaを鍛える

📋 この記事の目次
  1. DIは「部品を自分で仕入れず、外部から届けてもらう」ことだと考える
  2. 実践:TicketNotifierを疎結合にする
  3. AOPは「共通の警備ゲートを1箇所に置く」ことだと考える
  4. 関連する設計の考え方(次のステップとして知っておきたいキーワード)
  5. 挫折ポイントTOP3と対処法
  6. この先、何を学べばいいか
  7. まとめ
  8. 🎓 理解度テスト

OOP基礎編で、TicketCraftの通知処理はNotificationChannelインターフェースで抽象化されました。ところが、実際の呼び出し側のコードはこうなっています。

public class TicketNotifier {
    private final NotificationChannel channel = new EmailNotificationChannel();

    public void notify(Ticket ticket) {
        channel.notify(ticket);
    }
}

先日、TicketNotifierのテストコードを書いたところ、テスト実行のたびに本物のメールが送信されてしまう事故がありました。原因は単純です。TicketNotifierEmailNotificationChannelを自分でnewしているため、テスト用の偽物(モック)に差し替えられないのです。今回は DI(Dependency Injection、依存性注入) でこれを解決し、さらに AOP(Aspect Oriented Programming、アスペクト指向プログラミング) にも触れます。


DIは「部品を自分で仕入れず、外部から届けてもらう」ことだと考える

家具を組み立てる職人が、材料の木材まで自分の山から伐採してくるとしたら非効率です。材料は外部から届けてもらい、職人は組み立てに専念する方が合理的です。

DIとは、オブジェクトが自分の依存先(今回で言えばNotificationChannel)を自分で生成せず、外部から渡してもらうという考え方です。


実践:TicketNotifierを疎結合にする

Step 1: コンストラクタインジェクション

public class TicketNotifier {
    private final NotificationChannel channel;

    public TicketNotifier(NotificationChannel channel) {
        this.channel = channel;
    }

    public void notify(Ticket ticket) {
        channel.notify(ticket);
    }
}
// 本番用
TicketNotifier notifier = new TicketNotifier(new EmailNotificationChannel());

// テスト用(本物のメールを送らない偽物を注入できる)
TicketNotifier testNotifier = new TicketNotifier(new FakeNotificationChannel());

TicketNotifierNotificationChannelインターフェースにしか依存しなくなり、実際の実装がメールなのかSlackなのかテスト用の偽物なのかを知りません。コンストラクタで受け取るこの方法をコンストラクタインジェクションと呼び、必須の依存関係を注入する方法として最も推奨されます。

Step 2: セッターインジェクション・フィールドインジェクション

// セッターインジェクション:後から差し替え可能にしたい場合に使う
public void setChannel(NotificationChannel channel) {
    this.channel = channel;
}
// フィールドインジェクション:DIフレームワーク利用時に簡潔に書けるが、
// フィールドがfinalにできない、テストで直接差し替えにくいという弱点がある
@Autowired
private NotificationChannel channel;

任意の依存関係や、DIフレームワークでの簡潔さを優先する場合に使われますが、基本はコンストラクタインジェクションを優先するのが安全です。

Step 3: Springフレームワークで自動化する

ここまでは手動でのDIでした。Springのようなフレームワークを使うと、依存関係の生成・注入を自動化できます。

@Component
public class EmailNotificationChannel implements NotificationChannel {
    @Override
    public void notify(Ticket ticket) {
        // メール送信処理
    }
}

@Service
public class TicketNotifier {
    private final NotificationChannel channel;

    @Autowired // コンストラクタが1つだけなら省略可能
    public TicketNotifier(NotificationChannel channel) {
        this.channel = channel;
    }

    public void notify(Ticket ticket) {
        channel.notify(ticket);
    }
}

@Component@Serviceを付けたクラスは、SpringのDIコンテナ(ApplicationContext)に「Bean」として登録され、TicketNotifierのコンストラクタが必要とするNotificationChannelを、Springが自動的に見つけて注入してくれます。開発者は「誰が・いつ生成するか」を意識せず、注入済みのオブジェクトをそのまま使えます。

🧓 ベテランの現場コラム DIを知らずに書かれたコードでテストがうまく書けないという相談は、現場でも非常によくあります。「テストが書きにくいコードだ」と感じたら、まず疑うべきは 「このクラスは自分の依存先を自分でnewしていないか」 という点です。TicketNotifierのようにコンストラクタで受け取る形にするだけで、テストの書きやすさが劇的に変わります。


AOPは「共通の警備ゲートを1箇所に置く」ことだと考える

TicketCraftが成長するにつれ、「チケットのステータス変更をすべてログに残したい」という要望が来ました。素朴に実装すると、ステータスを変更するメソッドすべてにログ出力コードを埋め込むことになります。

public void setStatus(String newStatus) {
    System.out.println("[LOG] ステータス変更を開始"); // あちこちのメソッドに同じ埋め込み
    // 本来の処理
    System.out.println("[LOG] ステータス変更を終了");
}

各部署(各クラス)の入り口に、いちいち同じ警備員を個別に配置しているようなものです。AOPは、こうした 「本来のビジネスロジックとは別の、あちこちに共通して顔を出す処理(横断的関心事)」 を1箇所にまとめ、必要な場所に後から差し込む仕組みです。

@Aspect
@Component
public class TicketLoggingAspect {

    @Around("execution(* com.ticketcraft.Ticket.setStatus(..))")
    public Object logStatusChange(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("[LOG] ステータス変更を開始: " + joinPoint.getSignature());
        Object result = joinPoint.proceed(); // 本来の処理を実行
        System.out.println("[LOG] ステータス変更を終了");
        return result;
    }
}

@Aroundに書いた条件(ここではTicketクラスのsetStatusメソッド)に合致する呼び出しが発生するたびに、Springが自動的にこのログ処理を差し込んでくれます。Ticketクラス自身のコードは、ログ出力の存在すら知る必要がありません。

※ここではコードを簡潔にするためSystem.out.printlnで表現していますが、実務ではSLF4JLogbackのような専用のロギングライブラリを使い、ログレベル(INFO/WARN/ERROR等)を制御しながらファイルや外部サービスへ出力するのが一般的です。

🧓 ベテランの現場コラム AOPは非常に強力ですが、乱用すると「このメソッドを呼んだときに、実際には裏で何が実行されているのか」が、コードを読むだけでは分からなくなるという副作用があります。ログ出力・認可チェック・トランザクション管理のような、本当に横断的で、かつ業務ロジックの本質ではない処理に絞って使うのが安全です。


関連する設計の考え方(次のステップとして知っておきたいキーワード)

DIとAOPは、それ単体ではなく、より大きな設計思想の一部です。この連載では深追いしませんが、今後さらに設計力を伸ばしたい場合に押さえておくとよいキーワードを紹介します。

キーワード 一言で言うと
SOLID原則 単一責任・開放閉鎖・リスコフの置換・インターフェース分離・依存性逆転という5つの設計原則
関心の分離 システムを役割ごとに分割し、独立して機能させる考え方
レイヤードアーキテクチャ プレゼンテーション層・ビジネス層・データアクセス層のように責務ごとに層を分ける設計
デメテルの法則 オブジェクトは自身の直接の協力者とだけやり取りすべきという指針

これらはOOP基礎編の4原則ともつながっており、TicketCraftを長期的に育てていく上での次の一歩になります。


挫折ポイントTOP3と対処法

1. コンストラクタ・セッター・フィールドインジェクションの使い分けに迷う

迷ったら「必須の依存関係はコンストラクタインジェクション」を基本線にしましょう。

2. Springの自動注入が「魔法」に見えて不安になる

@Component@Serviceが付いたクラスがBeanとしてDIコンテナに登録され、コンストラクタの引数の型を見てSpringが該当するBeanを注入している、という仕組みだけ押さえれば十分です。乱暴に言えば、DIコンテナ(ApplicationContext)は「クラス名(型)をキーにしたただの巨大なMap(辞書)」で、必要な部品をそこから取り出して渡してくれているだけです。魔法ではありません。

3. AOPを何にでも使いたくなる

ログ出力・認可・トランザクション管理のような、本当に横断的な処理にだけ使いましょう。ビジネスロジックの本質的な分岐にAOPを使うと、コードの見通しがかえって悪くなります。


この先、何を学べばいいか

OOP基礎編・Java8モダン文法編・設計編を通して、TicketCraftはずいぶん整理されたコードベースになりました。次回からはGoFデザインパターン全23回の連載を開始し、TicketCraftの機能追加・保守の過程で出てくる典型的な課題を、パターンで解決していきます。


まとめ

  • DIは「依存先を自分で仕入れず、外部から届けてもらう」考え方。コンストラクタインジェクションが基本
  • Springは@Component@Service@Autowiredで、依存関係の生成・注入を自動化してくれる
  • AOPは「横断的関心事を1箇所にまとめ、後から差し込む」仕組み。ログ出力等に向くが乱用は禁物
  • DI・AOPはSOLID原則等、より大きな設計思想の一部として今後さらに学べる

🎓 理解度テスト

学んだ内容を確認しましょう。全5問。