Featured image of post 形式ごとのクラスを差し替えて使う設計には、Strategyという名前がある。作ったクラスを3つの役割に当てはめる【Strategyパターン】

形式ごとのクラスを差し替えて使う設計には、Strategyという名前がある。作ったクラスを3つの役割に当てはめる【Strategyパターン】

9回かけて作ったデータエクスポーターは、GoF の Strategy パターンの実装だった。ExporterRole、CsvExporter などのエクスポーター、DataExporter を Strategy・ConcreteStrategy・Context に当てはめ、XML を足すときに既存のエクスポーターへ手を入れずに済むことを SOLID 原則とつなげて、最初の if/else 版と比べます。

@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のコードは変更していません。変えたのは、DataExporterが持つ%exporter_mapに1行足したことだけです。

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

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

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

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

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

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

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

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

1
2
3
4
5
6
7
has exporter => (
    is   => 'rw',
    isa => sub {
        my $value = shift;
        die "exporter must does ExporterRole" unless $value->does('ExporterRole') # 抽象に依存
    },
);

if/elseとの比較

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

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

他のパターンへの発展

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

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

Commandパターンは「Mooを使ってTodo CLIを作ってみよう」シリーズで詳しく扱っています。

シリーズのまとめ

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

回タイトル学んだこと
第1回アドレス帳をCSVとJSONで書き出すif/elseでの形式切り替え
第2回形式を足すたびにelsifが伸びるif/elseの肥大化問題
第3回形式ごとのクラスに分ける責務分離
第4回exportを持つことを約束にするMoo::Roleとrequires
第5回DataExporterに処理を任せるContextクラスと委譲
第6回実行中にエクスポーターを切り替える動的な切り替え
第7回設定した時点で型を確かめる型チェック
第8回形式名からエクスポーターを選ぶFactoryパターン的アプローチ
第9回YamlExporterを足して1本にまとめる機能統合
第10回設計にStrategyという名前が付くデザインパターンの理解

おわりに

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

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

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

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

お疲れ様でした!

comments powered by Disqus
システム開発・AIワークフローのご相談は Meetsource へ
Hugo で構築されています。
テーマ Stack は Jimmy によって設計されています。