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のコンストラクタだ。
NotificationServiceは、NotificationSenderというインターフェースに依存している。具体的なEmailNotificationSenderやSlackNotificationSenderには依存していない。これが**「依存性逆転の原則」**とも呼ばれる考え方だ。
そして、NotificationServiceのインスタンスを作る際に、List<NotificationSender> sendersとして受け取っているだろう?
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購入🗒️、他の方へのチップ代💰に使わせていただきます。
いつも応援ありがとうございます!