見出し画像

3分間プログラミング講座 第13回: 抽象化・具体化・DI ~Spring Frameworkにおける設計の柔軟性~



登場人物:

  • チャット先生(通称:先生): オブジェクト指向マスター。普段は優しく、たまにユーモアも交えながら解説。

  • ボット助手(通称:ボット君): プログラミングを始めたばかりの若手。素朴な疑問を投げかけるのが得意。


ボット君: 先生!前回はインターフェース抽象クラスでプログラムの「設計図」がぐっと整理されることがわかりました!なんだか複雑なシステムも設計できる気がしてきました!

先生: その調子だ、ボット君!インターフェースと抽象クラスは、まさに複雑なシステムを柔軟に設計するための強力なツールだ。今日は、それらの概念がSpring Frameworkという現代のJavaアプリケーション開発でいかに活用され、システムをより賢く、そして変更に強くしていくかを見ていこう。キーワードは「抽象化」「具体化」、そして「DI (Dependency Injection) / 依存性の注入」だ!

ボット君: 難しい言葉が並びましたね…!でも、先生と一緒なら乗り越えられる気がします!

先生: もちろんさ!一つずつ丁寧に見ていこう。特にDIはSpring Frameworkの心臓部とも言える機能だから、しっかり理解しておくと、君のプログラムがまるで魔法にかかったように動き出すぞ!



1. 抽象化と具体化の再確認



先生: まずは、インターフェースと抽象クラスで学んだ「抽象化」と、それを具体的なクラスで実現する「具体化」の概念を再確認しよう。

ボット君: 抽象化は「何をするか」だけ決めることで、具体化は「どうするか」を実際に書くこと、でしたよね?

先生: 大正解だ!例えば、「通知を送る」という機能を考えてみよう。

  • 抽象化: 「通知を送る」という**振る舞い(契約)**だけを、NotificationSenderというインターフェースで定義する。

Java

// 抽象化されたインターフェース
public interface NotificationSender {
    void send(String message);
}
  • 具体化: NotificationSenderインターフェースを実装して、実際にメールで送ったり、Slackで送ったりする具体的なクラスを作る。

Java

// 具体化された実装1
public class EmailNotificationSender implements NotificationSender {
    @Override
    public void send(String message) {
        System.out.println("Email送信: " + message);
    }
}

// 具体化された実装2
public class SlackNotificationSender implements NotificationSender {
    @Override
    public void send(String message) {
        System.out.println("Slack送信: " + message);
    }
}

先生: ここまでは前回の復習だ。では、これらの「通知を送る機能」を、別のクラス(例えば、注文処理を行うOrderService)で使いたい場合、どうするだろう?

ボット君: ええと、OrderServiceの中でnew EmailNotificationSender()とかnew SlackNotificationSender()ってすればいいんじゃないですか?

先生: その通り!それは一つの方法だ。しかし、もし「Slackでの通知をやめて、別のチャットツールにしたい」とか、「メールとSlackの両方で送りたい」となったらどうなるかな?

ボット君: OrderServiceのコードを書き換えないといけなくなりますね…。新しい通知方法が増えるたびに、OrderServiceを修正するのは大変そうです。



2. DI (Dependency Injection) / 依存性の注入とは?~Springの魔法~



先生: そこで登場するのが、「DI (Dependency Injection) / 依存性の注入」だ!これがSpring Frameworkの最も強力な機能の一つと言ってもいい。

ボット君: 依存性の注入…?

先生: OrderServiceがEmailNotificationSenderやSlackNotificationSenderに「依存している」状態を考えてみよう。つまり、OrderServiceが特定の具体的なNotificationSenderを自分で作り出して使っている、という状態だ。

DIは、「依存しているオブジェクトを、そのオブジェクト自身が作らず、外部から与えてもらう(注入してもらう)」という考え方だ。

ボット君: 外部から与えてもらう…?どういうことですか?

先生: こういうことだ!

Java

// Spring FrameworkにおけるDIの例
import org.springframework.stereotype.Service;
import org.springframework.stereotype.Component;
import java.util.List;

// 抽象化されたインターフェース(再掲)
// public interface NotificationSender { ... }

// 具体化された実装1(再掲) - Springがコンポーネントとして認識できるようにアノテーションを付ける
@Component // Springに「これは汎用的なコンポーネントだよ」と教えるアノテーション
public class EmailNotificationSender implements NotificationSender {
    @Override
    public void send(String message) {
        System.out.println("Email送信: " + message);
    }
}

// 具体化された実装2(再掲) - Springがコンポーネントとして認識できるようにアノテーションを付ける
@Component // Springに「これも汎用的なコンポーネントだよ」と教えるアノテーション
public class SlackNotificationSender implements NotificationSender {
    @Override
    public void send(String message) {
        System.out.println("Slack送信: " + message);
    }
}

// 通知サービス - Springが管理するサービスとして定義
@Service // Springに「これはビジネスロジックを扱うサービスだよ」と教えるアノテーション
public class NotificationService {
    // なお、@Serviceはビジネスロジック、@Repositoryはデータアクセス層、@Controllerは制御層を表す
    // これらは全て@Componentの特殊化されたバージョンで、役割を明確にする意味合いがある
    private final List<NotificationSender> senders; // NotificationSenderインターフェースに依存

    // コンストラクタインジェクション:Springが自動的に全ての実装を注入してくれる
    // Spring 4.3以降は、コンストラクタが一つだけなら@Autowiredアノテーションは不要
    public NotificationService(List<NotificationSender> senders) {
        this.senders = senders; // Springが、見つけた全てのNotificationSender実装をリストとして渡してくれる
        System.out.println("NotificationServiceが初期化されました。利用可能な通知サービス: " + senders.size() + "種類");
    }

    public void sendNotification(String message) {
        System.out.println("\n--- 通知処理開始 ---");
        // リスト内の全てのNotificationSenderを使って通知を送る
        senders.forEach(sender -> sender.send(message));
        System.out.println("--- 通知処理完了 ---"); // ←ここを修正
    }
}

先生: ここで注目してほしいのは、NotificationServiceのコンストラクタだ。

  1. NotificationServiceは、NotificationSenderというインターフェースに依存している。具体的なEmailNotificationSenderやSlackNotificationSenderには依存していない。これが**「依存性逆転の原則」**とも呼ばれる考え方だ。

  2. そして、NotificationServiceのインスタンスを作る際に、List<NotificationSender> sendersとして受け取っているだろう?

  3. Spring Frameworkが、@Componentアノテーションを付けたNotificationSenderインターフェースの全ての実装クラス(EmailNotificationSenderとSlackNotificationSender)を自動で見つけ出し、そのインスタンスをリストとして作成し、NotificationServiceのコンストラクタに「注入」してくれるんだ!

ボット君: すごい!NotificationServiceは自分でnewしなくてもいいし、どのNotificationSenderを使うかを知らなくてもいいってことですか?魔法みたいですね!

先生: その通り!これがDIの強力なポイントだ。

  • 結合度の低下: NotificationServiceは、具体的な実装クラス(EmailNotificationSenderなど)ではなく、抽象(インターフェース)に依存する。これにより、各クラスの結びつきが弱くなり、変更に強くなる。

  • テストのしやすさ: 特定の実装クラスに依存しないため、テスト時にダミーのNotificationSender(モックオブジェクト)を簡単に差し込むことができる。

  • 再利用性の向上: NotificationServiceはあらゆるNotificationSenderで機能するため、汎用性が高い。

  • 設定の容易さ: どの実装を使うかは、コードの内部でなく、Springの設定(@Componentなど)で決めることができる。

先生: Spring Frameworkでは、このDIの仕組みを「IoCコンテナ (Inversion of Control Container) / 制御の反転コンテナ」が実現している。オブジェクトの生成や依存関係の解決を、プログラマが直接コードで書く代わりに、Springが自動で行ってくれるんだ。これにより、君はビジネスロジックの記述に集中できるようになる。 また、Beanのスコープ(例:@Scope("singleton")、@Scope("prototype")など)やセキュリティ関連(@PreAuthorize、@Securedなどによる認可・認証など)もIoCコンテナが管理できるが、これらはさらに高度な話題なので、今回は触れないでおこう。



3. Spring BootでのDI実践例



先生: より実践的な例として、Webアプリケーションの注文処理サービスを見てみよう。ここでは、注文処理サービスが支払い、在庫、そして通知サービスに依存している形だ。

Java

// Spring BootでのDI実践例
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; // トランザクション管理用

// (補足) PaymentServiceとInventoryServiceは、それぞれ@Serviceアノテーションを持つ別のクラスとする
interface PaymentService {
    Payment processPayment(double amount); // Paymentオブジェクトを返すとする
}

interface InventoryService {
    void checkAvailability(String productId);
}

// 注文関連のクラス(簡易的な定義)
class OrderRequest {
    String productId;
    double paymentInfo;

    public OrderRequest(String productId, double paymentInfo) {
        this.productId = productId;
        this.paymentInfo = paymentInfo;
    }

    public String getProductId() { return productId; }
    public double getPaymentInfo() { return paymentInfo; }
}

class Order {
    Long id; // 仮の注文ID
    String productId;

    public Order(OrderRequest request) {
        this.id = System.currentTimeMillis(); // 仮のID生成
        this.productId = request.getProductId();
    }

    public Long getId() { return id; }
}

class Payment {
    // 支払いに関する情報(今回はシンプルに)
}


// 実際の実装クラス(省略)
@Service
class PaymentServiceImpl implements PaymentService {
    @Override
    public Payment processPayment(double amount) {
        System.out.println("支払いサービス: " + amount + "円の支払い処理を実行。");
        return new Payment();
    }
}

@Service
class InventoryServiceImpl implements InventoryService {
    @Override
    public void checkAvailability(String productId) {
        System.out.println("在庫サービス: " + productId + "の在庫確認を実行。");
    }
}


@Service // このクラスがSpringによって管理されるサービスであることを示す
public class OrderService {
    private final PaymentService paymentService;     // 支払いサービスへの依存
    private final InventoryService inventoryService; // 在庫サービスへの依存
    private final NotificationService notificationService; // 通知サービスへの依存

    // コンストラクタインジェクション:SpringがPaymentServiceとInventoryService、NotificationServiceのインスタンスを注入する
    // @Autowiredアノテーションは、コンストラクタが一つだけの場合、Spring 4.3以降は省略可能
    public OrderService(PaymentService paymentService,
                        InventoryService inventoryService,
                        NotificationService notificationService) {
        this.paymentService = paymentService;
        this.inventoryService = inventoryService;
        this.notificationService = notificationService;
        System.out.println("OrderServiceが初期化され、必要なサービスが注入されました。");
    }

    @Transactional // 複数のデータベース操作やサービス連携を一つのまとまりとして扱う
    public Order processOrder(OrderRequest request) {
        System.out.println("\n--- 注文処理開始 ---");

        // 在庫チェック
        inventoryService.checkAvailability(request.getProductId());
        System.out.println(request.getProductId() + "の在庫確認完了。");

        // 支払い処理
        Payment payment = paymentService.processPayment(request.getPaymentInfo());
        System.out.println(request.getPaymentInfo() + "円の支払い処理完了。");

        // 注文作成 (ここでは簡易的にオブジェクトを生成)
        Order order = new Order(request);
        System.out.println("注文が作成されました。注文番号: " + order.getId());

        // 通知送信(複数の通知方法で自動送信)
        notificationService.sendNotification(
            "注文が完了しました。注文番号: " + order.getId() + " - 製品: " + order.getProductId()
        );

        System.out.println("注文が正常に処理されました。");
        System.out.println("--- 注文処理完了 ---");
        return order;
    }
}

// (Spring Bootアプリケーションのメインクラスからの呼び出しイメージ)
/*
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.beans.factory.annotation.Autowired;

@SpringBootApplication
public class MyApplication implements CommandLineRunner {

    @Autowired
    private OrderService orderService; // SpringがOrderServiceのインスタンスを注入

    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }

    @Override
    public void run(String... args) throws Exception {
        System.out.println("アプリケーション起動完了。テスト注文を処理します。");
        orderService.processOrder(new OrderRequest("LaptopX", 1200.00));
        orderService.processOrder(new OrderRequest("SmartphoneY", 800.00));
    }
}
*/

先生: このOrderServiceは、具体的なPaymentServiceImplやInventoryServiceImpl、NotificationServiceを知る必要がない。ただPaymentServiceとInventoryService、NotificationServiceという「役割」を持ったオブジェクトが必要なだけだ。これらのオブジェクトはSpringが自動的に見つけて、OrderServiceを生成する時に渡してくれるんだ。 そして、@Transactionalアノテーションにも注目してほしい。これは、このメソッド内の複数の操作(在庫チェック、支払い処理、注文作成、通知送信)が、すべて成功するか、すべて失敗するかを保証してくれるものだ。例えば、支払い処理が失敗したら、注文も通知も行われず、まるで何も起こらなかったかのように元に戻る。これもSpringが提供する強力な機能の一つなんだ。

ボット君: わー!本当に魔法みたい!これなら、支払い方法が変わっても、OrderServiceのコードは全然変えなくていいんですね!新しい支払いサービスを作って、@Serviceって付ければ、Springが勝手に切り替えてくれるってことですか?しかも、処理の途中でエラーが起きても安心なんですね!

先生: その通り!それがDIの最大のメリットの一つであり、@Transactionalが加わることで、さらに信頼性の高いビジネスロジックが構築できるようになる。まさに柔軟性と拡張性、そして堅牢性の実現だね。



今日のまとめ!



  • 抽象化: 「何をすべきか」を定義し、具体的な実装から独立させること。インターフェースや抽象クラスを使う。

  • 具体化: 抽象化された定義に基づき、「どのようにすべきか」を実際にコードで実装すること。

  • DI (Dependency Injection) / 依存性の注入:

    • オブジェクトが必要とする依存(他のオブジェクト)を、オブジェクト自身が作らず、外部(SpringのIoCコンテナ)から与えてもらう仕組み。

    • これにより、クラス間の結合度が下がり、システムが変更に強く、テストしやすくなる。

    • Spring Frameworkでは@Service、@ComponentなどのアノテーションとコンストラクタインジェクションなどでDIが実現される。

  • @Transactional: メソッド内のデータベース操作などをトランザクションとして管理し、処理の原子性(成功か失敗か)を保証する。


ボット君: 先生、今日はDIと@Transactionalがよく分かりました!プログラムがまるで生き物みたいに賢く連携し合う仕組みで、しかも堅牢になるんですね!Spring Frameworkがなぜ便利なのか、さらに深く分かった気がします!

先生: よくぞ気づいた、ボット君!DIとトランザクション管理はSpring Frameworkの強力な武器であり、現代のエンタープライズアプリケーション開発には欠かせない考え方だ。この概念をしっかり掴んでいれば、君のプログラムはさらに洗練されたものになるはずだ。

先生: 次回は、アプリケーションがデータを保存・管理するための非常に重要な技術、「O/Rマッピング」について学んでいこう。そして、それがSpring Frameworkでどのように簡潔に実現されているか、実際のデータベース(PostgreSQL)を使った例を交えて解説するぞ!

ボット君: データベース!ついにデータが保存できるようになるんですね!楽しみです!ありがとうございました!


次回:第14回: O/RマッピングとDI ~Spring Data JPAでデータベース連携~

#オブジェクト指向 #プログラミング初心者 #Java #クラスとメソッド #プログラミング学習 #入門


いいなと思ったら応援しよう!

ナオユキ|物語と知識のコンシェルジュ もしこの記事が少しでも役に立ったり、楽しんでいただけたりしたら、チップをいただけるととても嬉しいです! いただいたチップは、今後のより良いコンテンツ制作のための書籍代📚とnote購入🗒️、他の方へのチップ代💰に使わせていただきます。 いつも応援ありがとうございます!