AI時代だからこそ!地力をつけろ!Strategy パターンで保守性の高いコードを書く方法
AI時代になぜ「デザインパターン」を学ぶのか?
ChatGPT や GitHub Copilot などの生成 AI の普及により、コードを書き起こすスピードは劇的に向上しました。しかし、その反面 「AI が生成したコードのレビューが辛い……」 と頭を悩ませていませんか?
AI は指示を出せば動くコードをすぐ書いてくれますが、プロンプトが曖昧だと巨大な when 文や if 文で条件分岐をベタ書きした、拡張性の低いコードを出力しがちです。その結果、意図が読み解きにくいコードの解読や修正に追われ、レビューの負担が増えてしまいます。
そこで重要になるのが、ソフトウェア設計の基盤となる 「デザインパターン」 です。
デザインパターンとは、GoF(Gang of Four)と呼ばれる4人が開発における「経験」や「内的な蓄積」を再利用可能な形にまとめたものです。もともとは人間同士のコミュニケーションを円滑にするための「共通言語」でしたが、現代においては 「人間と AI を繋ぐ指示のフォーマット」 としても強力に機能します。
AI に対して「条件分岐を綺麗にして」と頼むと揺らぎが生じますが、「Strategy パターンを使ってリファクタリングして」 と指示を出せば、意図通りに構造化されたコードが一発で生成されやすくなります。また、レビュアー側も「Strategy パターンに沿っているか」をチェックするだけで済むため、レビューの認知負荷が劇的に下がるのです。
AI が実装を代行してくれる今だからこそ、AI を適切にコントロールし、吐き出されたコードを正しく評価するための 設計の「地力」 が求められています。
今回は、GoF の23個のパターンの中から、実務で頻出し AI への指示出しにも使いやすい Strategy(ストラテジー)パターン を Kotlin のコード例とともに解説します。
ストラテジーパターンとは?
Strategy パターンは、一言で言えば 「実装したアルゴリズムをごっそりと交換できるようにするパターン」 です。
長くなりがちな if 文や when 文による条件分岐から脱却し、処理ロジックを独立したクラスにカプセル化することで、実行時にアルゴリズムを柔軟に切り替えられるようになります。
3つの登場人物
Strategy パターンは、主に以下の3つの役割(役)で構成されています。
-
Strategy(戦略)役
切り替えるアルゴリズムの共通インターフェースを定義します。 -
ConcreteStrategy(具体的な戦略)役
Strategy 役のインターフェースを実装し、個別の具体的な処理ロジックを記述します。 -
Context(文脈)役
Strategy 役の参照を保持し、実際にその処理を実行するクラスです。必要に応じて実行時に Strategy を保持・切り替えます。
Context が Strategy を持ち、呼び出し側から注入された具体クラスに応じて処理を切り替えるのがこのパターンの基本構造です。
コード例:条件分岐からのリファクタリング
ショッピングサイトの「決済処理(クレジットカード、PayPal、銀行振込)」を例に、実際のコードを見てみましょう。
【Before】when 文による実装
Strategy パターンを使わない場合、タイプに応じて when 文で処理を分岐させることになります。
enum class PaymentType {
CREDIT, PAYPAL, BANK
}
class PaymentProcessor {
fun pay(amount: Int, type: PaymentType) {
when (type) {
PaymentType.CREDIT -> println("${amount}円をクレジットカードで決済しました。")
PaymentType.PAYPAL -> println("${amount}円をPayPalで決済しました。")
PaymentType.BANK -> println("${amount}円を銀行振込で決済しました。")
}
}
}
// 利用例
fun main() {
val processor = PaymentProcessor()
processor.pay(5000, PaymentType.CREDIT)
}
このコードの問題点:
- 新しい決済手段(例: PayPay など)が増えるたびに、既存の
PaymentProcessor内のwhen文を書き換える必要があります(開閉原則の違反)。 - 決済方法ごとのロジックが1つのクラスに集中し、単体テストや保守が困難になります。
【After】Strategy パターンへリファクタリング
各決済処理を独立したクラス(ConcreteStrategy)として切り出し、Context 側で柔軟に切り替えられる構造へ変更します。
// 1. Strategy役 (戦略インターフェース)
interface PaymentStrategy {
fun pay(amount: Int)
}
// 2. ConcreteStrategy役 (具体的な戦略クラス群)
class CreditCardStrategy : PaymentStrategy {
override fun pay(amount: Int) {
println("${amount}円をクレジットカードで決済しました。")
}
}
class PayPalStrategy : PaymentStrategy {
override fun pay(amount: Int) {
println("${amount}円をPayPalで決済しました。")
}
}
class BankTransferStrategy : PaymentStrategy {
override fun pay(amount: Int) {
println("${amount}円を銀行振込で決済しました。")
}
}
// 3. Context役 (戦略を保持・実行するクラス)
class PaymentContext(private var strategy: PaymentStrategy) {
// 実行時に戦略を切り替えるメソッド
fun setStrategy(strategy: PaymentStrategy) {
this.strategy = strategy
}
fun executePayment(amount: Int) {
strategy.pay(amount)
}
}
// --- 実際の利用例 ---
fun main() {
// 最初はクレジットカード決済で処理
val paymentContext = PaymentContext(CreditCardStrategy())
paymentContext.executePayment(5000) // 5000円をクレジットカードで決済しました。
// ユーザーの選択に応じて、実行時にPayPal決済(Strategy)へ切り替える
paymentContext.setStrategy(PayPalStrategy())
paymentContext.executePayment(3000) // 3000円をPayPalで決済しました。
}
Strategy パターンのメリット
| メリット | 解説 |
|---|---|
| 開閉原則(OCP)の遵守 | 新しい決済方法を追加する際、新しい ConcreteStrategy クラスを作るだけで済みます。既存の PaymentContext などを変更する必要がありません。 |
| 単一責任の原則(SRP)の遵守 | 各決済ロジックがそれぞれのクラスに独立・完結しているため、修正の影響範囲が限定され、単体テストが容易になります。 |
| if / when 文の撤廃 | 条件分岐のネストがなくなり、コードの見通しと可読性が大幅に向上します。 |
まとめ
Strategy パターンを活用することで、アルゴリズムをカプセル化し、実行時にごっそり差し替え可能な柔軟な設計を実現できます。
AI ツールが生成するコードに振り回されるのではなく、「デザインパターン」という地力を身につけることで、以下のサイクルが回るようになります。
- プロンプトでの指定: 「Strategy パターンで実装して」と指示し、最初から綺麗なコードを AI に書かせる
- レビューの高速化: パターンに沿っているかを確認するだけで素早くクオリティ判断ができる
- 保守性の向上: 機能追加時にも既存コードを壊さずに安全に拡張できる
チーム開発でも、AI とのペアプログラミングでも、共通言語としてのデザインパターンを使いこなして保守性の高いプロダクトを作っていきましょう!