【デザインパターン】新人が教える「Strategyパターン」

デザインパターン

こんにちは。新人プログラマーの岩本です。

今回はデザインパターンの一つ「Strategyパターン」について、自分なりに調べたことをまとめます。

デザインパターンというのは、先人たちが過去に学んだ知識や教訓を使いやすい形でまとめた物です。これを学ぶことで一気に思考の幅が広がると思います。

ぜひ最後までご覧ください。

この記事は新卒エンジニアが執筆しています。
そのため内容に間違いや不備がある場合があります。
もし間違いを発見しましたら、どんどん指摘していただけると幸いです。

実装環境

  • 言語…Swift 5.8.1
  • OS… macOS Ventura 13.1
  • アプリ… Xcode 14.3.1

Strategyパターンとは

Strategyパターンは簡単にいうと以下のような書き方です。

外部から処理(アルゴリズム)を指定する

言葉だけ見てもわからないと思うので、実際のコードを見ながら解説してきます。

Strategyパターンを使わなかった場合

サンプルコードでは、生徒をある数値を基準として並び替え出力する、というものです。

まずはStrategyパターンを使わずに実装していきます。

struct Student {
    let name: String // 名前
    let height: Int  // 身長
    let score: Int   // 成績
}
struct StudentPrinter { // 生徒をソートして出力する構造体
    enum SortType { // どの値を基準にソートするかを指定する列挙型
        case name
        case height
        case score
    }
    
    let students: [Student]
    let sortType: SortType
    
    func sortPrint() {
        var sortStudents: [Student] = []
        
	// 基準値が増えるとどんどん複雑になる恐れがある
        switch sortType {
        case .name:
            sortStudents = students.sorted(by: { $0.name < $1.name })
        case .height:
            sortStudents = students.sorted(by: { $0.height < $1.height })
        case .score:
            sortStudents = students.sorted(by: { $0.score < $1.score })
        }
        
        sortStudents.forEach { student in
            print("----------------------")
            print("名前: \(student.name)")
            print("身長: \(student.height)")
            print("成績: \(student.score)")
            print("----------------------\n")
        }
    }
}

let studentPrinter = StudentPrinter(
    students: [
        Student(name: "Tanaka", height: 170, score: 70),
        Student(name: "Suzuki", height: 175, score: 65),
        Student(name: "Sato", height: 160, score: 80),
        Student(name: "Yamada", height: 165, score: 85),
        Student(name: "Ito", height: 180, score: 60)
    ],
    sortType: .name
)

studentPrinter.sortPrint()

上記のコードは一見問題がないように見えます。

しかし、以下のような仕様変更があった場合はどうでしょうか?

仕様変更
  • 年齢順でソートしたい
  • 各教科の成績ごとにソートしたい
  • もっと複雑な並び替えを行いたい
// 基準値が増えるとどんどん複雑になる恐れがある
switch sortType {
   case .name:
       sortStudents = students.sorted(by: { $0.name < $1.name })
    case .height:
        sortStudents = students.sorted(by: { $0.height < $1.height })
    case .score:
        sortStudents = students.sorted(by: { $0.score < $1.score })
}

コードの上記の部分が仕様変更により、どんどん複雑になってしまいます。

可読性もどんどん悪くなり、メンテナンスが困難になることも考えられます。

では次に、このコードにStrategyパターンを適応するとどうなるのか見ていきましょう。

Strategyパターンを適応した場合

Strategyパターンを適応するにあたって、3つの登場人物を紹介します。

ファイル役割
Strategy使い分けたいアルゴリズムが実装する共通のインターフェース
この例の場合、sortメソッドを持つインターフェースのことです
Strategyを適応したクラス共通のインターフェースの具体的な処理を実装したクラス
この例の場合、実際のソート処理を行うクラスのことです
Context呼び出し元がどのアルゴリズムを使用するかを決定し、呼び出すクラス
この例の場合、 StudentPrinterがこれにあたります

ではStrategyパターンを使って、上記のコードを書き直していきます。

protocol SortStrategy {
    func sort(_ students: [Student]) -> [Student]
}

まず、切り替えたいアルゴリズムが持つ共通のインターフェースを定義します。

今回の目的は生徒の並び替え、なのでsortを共通のメソッドとします。

class NameSort: SortStrategy { // 名前順
    func sort(_ students: [Student]) -> [Student] {
        return students.sorted(by: { $0.name < $1.name })
    }
}

class HeightSort: SortStrategy { // 身長順
    func sort(_ students: [Student]) -> [Student] {
        return students.sorted(by: { $0.height < $1.height })
    }
}

class ScoreSort: SortStrategy { // 成績順
    func sort(_ students: [Student]) -> [Student] {
        return students.sorted(by: { $0.score < $1.score })
    }
}

次に、 SortStrategy を適応したクラスを定義します。

ここで具体的な処理を記述します。

struct StudentPrinter { // 生徒をソートして出力する構造体
    let students: [Student]
    let sortType: SortStrategy

    func sortPrint() {
        sortType
            .sort(students) // 具体的な並び替えの処理は移譲する
            .forEach { student in
                print("----------------------")
                print("名前: \(student.name)")
                print("身長: \(student.height)")
                print("成績: \(student.score)")
                print("----------------------\n")
            }
    }
}

let studentPrinter = StudentPrinter(
    students: [
        Student(name: "Tanaka", height: 170, score: 70),
        Student(name: "Suzuki", height: 175, score: 65),
        Student(name: "Sato", height: 160, score: 80),
        Student(name: "Yamada", height: 165, score: 85),
        Student(name: "Ito", height: 180, score: 60)
    ],
    sortType: ScoreSort()
)

studentPrinter.sortPrint()

SortStrategyをプロパティとして持つことで、 sortPrintの処理がかなり見やすくなりました。

これで仕様変更が起こっても、SortStrategyを適応したクラスを用意するだけでよくなりました。
既存のコードを変更する必要はありません。

これが外部から処理(アルゴリズム)を指定するということです。

クラス図も載せておきます。

StrategyパターンのQ&A

メリットは何?

Strategyパターンを使うメリットは大きく2つあります。

  • 余計な分岐処理を減らせる
  • 新機能の追加が容易になる

コード例で見たようにStrategyパターンを適応すると、ロジック内の余計な if/else文switch文 を減らすことができます。
これにより可読性が上がり、メンテナンスが容易になります。

また既存のコードを変更することなく、新機能を追加することができます。
これを「オープン・クローズドの原則」といいます。前に解説記事を出したので、ぜひご覧ください。

どんなときに使うの?

Strategyパターンを適応するタイミングは大きく以下の2つです。

  • 「目的」は一緒だけど「手段」が違う場合
    • 目的: データを出力したい
    • 手段: CSV, JSON, etc…
  • メソッド内で条件分岐によって処理を分けている場合

これらを意識することでStrategyパターンを使うか否かを判断できるはずです。

まとめ

今回はデザインパターンの1つ「Strategyパターン」を解説しました。

これを意識することで、コードの可読性、柔軟性が向上します。
メソッド内で条件分岐によって処理を分けがちな人は、ぜひ試してみてください。

ここまでのご閲覧ありがとうございました!

参考資料

コメント

タイトルとURLをコピーしました