Featured image of post 第10回-これがStrategyパターンだ! - Mooを使ってデータエクスポーターを作ってみよう

第10回-これがStrategyパターンだ! - Mooを使ってデータエクスポーターを作ってみよう

実はこれがStrategyパターンだった!作ってきた設計がGoFデザインパターンの一つであることを明かし、SOLID原則との関係も解説します。

@nqounetです。

前回で、データエクスポーターが完成しました。CSV、JSON、YAML形式に対応し、新しい形式の追加も簡単になりましたね。

最終回の今回は、これまで作ってきた設計に名前があることを明かします。

実は、これがStrategyパターンです!

このシリーズで作ってきた設計は、Strategyパターン(ストラテジーパターン)というデザインパターンです。

デザインパターンとは、ソフトウェア設計でよく使われる問題解決のパターン(定石)のことです。1994年に発表された「GoF本」で23個のパターンが紹介され、Strategyパターンはその1つです。

GoFとは「Gang of Four(4人組)」の略で、この本の4人の著者を指します。

	classDiagram
    class Context {
        -strategy: Strategy
        +setStrategy(Strategy)
        +executeStrategy()
    }
    class Strategy {
        <<interface>>
        +execute()*
    }
    class ConcreteStrategyA {
        +execute()
    }
    class ConcreteStrategyB {
        +execute()
    }
    class ConcreteStrategyC {
        +execute()
    }
    
    Context o-- Strategy : has
    Strategy <|.. ConcreteStrategyA : implements
    Strategy <|.. ConcreteStrategyB : implements
    Strategy <|.. ConcreteStrategyC : implements
    
    note for Context "私たちの実装ではDataExporter"
    note for Strategy "私たちの実装ではExporterRole"

Strategyパターンの構造

作ってきたものを振り返りながら、Strategyパターンの構造を確認しましょう。

	classDiagram
    class DataExporter {
        -exporter: ExporterRole
        +exporter_for(format)$ ExporterRole
        +export_data(data) string
    }
    class ExporterRole {
        <<Role>>
        +export(data)* string
    }
    class CsvExporter {
        +export(data) string
    }
    class JsonExporter {
        +export(data) string
    }
    class YamlExporter {
        +export(data) string
    }
    
    DataExporter o-- ExporterRole : has exporter
    ExporterRole <|.. CsvExporter : with
    ExporterRole <|.. JsonExporter : with
    ExporterRole <|.. YamlExporter : with
用語私たちが作ったもの役割
StrategyExporterRole共通のインターフェース(約束)
ConcreteStrategyCsvExporter, JsonExporter, YamlExporter具体的な処理の実装
ContextDataExporterStrategyを保持し、処理を委譲

なぜこの設計が良いのか?

Strategyパターンを使うと、SOLID原則という設計原則に自然と従えます。

開放/閉鎖原則(OCP: Open/Closed Principle)

「拡張に開かれ、修正に閉じている」という原則です。

新しい形式(例:XML形式)を追加したい場合:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# 新しいエクスポーターを追加
package XmlExporter {
    use Moo;
    use v5.36;
    with 'ExporterRole';
    sub export ($self, $data) { ... }
}

# マッピングに登録
my %exporter_map = (
    csv  => 'CsvExporter',
    json => 'JsonExporter',
    yaml => 'YamlExporter',
    xml  => 'XmlExporter',  # 追加
);

既存のCsvExporter、JsonExporter、YamlExporterのコードは一切変更していません!

これが「拡張に開かれ、修正に閉じている」ということです。

単一責任の原則(SRP: Single Responsibility Principle)

「クラスは1つの責任だけを持つべき」という原則です。

  • CsvExporter → CSV形式での出力だけを担当
  • JsonExporter → JSON形式での出力だけを担当
  • YamlExporter → YAML形式での出力だけを担当
  • DataExporter → エクスポーターの管理だけを担当

各クラスが1つのことだけを担当しています。

依存性逆転の原則(DIP: Dependency Inversion Principle)

「具体ではなく抽象に依存すべき」という原則です。

DataExporterは具体的なエクスポーター(CsvExporterなど)に直接依存せず、抽象(ExporterRole)に依存しています。

1
2
3
4
has exporter => (
    is   => 'rw',
    does => 'ExporterRole',  # 抽象に依存
);

if/elseとの比較

最初のif/elseによる実装と比較してみましょう。

観点if/else版Strategyパターン版
新形式の追加if/elseに条件追加新クラス作成のみ
既存コードへの影響影響あり影響なし
テスト全体をテスト個別にテスト可能
コードの分離1ファイルに集中形式ごとに分離

他のパターンへの発展

Strategyパターンを理解したら、次は関連するパターンも学んでみてください。

  • Stateパターン - オブジェクトの状態に応じて振る舞いを変える
  • Template Methodパターン - アルゴリズムの骨格を定義し、一部をサブクラスで変える
  • Commandパターン - 操作自体をオブジェクトとしてカプセル化する

これらは「Mooを使ってディスパッチャーを作ってみよう」シリーズでも触れています。

シリーズのまとめ

全10回のシリーズを振り返りましょう。

タイトル学んだこと
第1回CSVとJSONでデータを保存しようif/elseでの形式切り替え
第2回新しい形式を追加すると大変!if/elseの肥大化問題
第3回出力処理を専用クラスに分けよう責務分離
第4回Moo::Roleで共通の約束を決めようMoo::Roleとrequires
第5回エクスポーターを管理するクラスを作ろうContextクラスと委譲
第6回実行時に出力形式を切り替えよう動的な切り替え
第7回does制約でバグを防ごう型チェック
第8回形式名から自動でエクスポーターを選ぼうFactoryパターン的アプローチ
第9回完成!データエクスポーター機能統合
第10回これがStrategyパターンだ!デザインパターンの理解

おわりに

このシリーズでは、「アドレス帳データを複数の形式で出力したい」という身近な課題から始めて、if/elseの問題を体験し、それを解決するためにコードを改善してきました。

そして最後に、作ってきた設計が「Strategyパターン」という名前を持つことを学びました。

デザインパターンは「パターン名を覚えて使う」のではなく、「問題を解決しようとしたら自然とパターンになっていた」という経験が大切です。

このシリーズがその体験の助けになれば幸いです。

お疲れ様でした!

comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。