【デザインパターン入門】第1回:Strategy パターン – コードの柔軟性を向上させる方法

デザインパターン入門:Strategyパターン

デザインパターンは、プログラムを設計するときによく使われる解決策のパターンです。

本記事では、C# を用いて Strategy パターンの使い方や利点を解説します。

デザインパターンを学べるおすすめの本

オライリー・ジャパン出版の「Head First デザインパターン」は、デザインパターンを学びたいすべての人におすすめの一冊です。この本は、頭の中で知識が定着するように工夫されており、視覚的な説明や独自の教育手法が盛り込まれています。これにより、デザインパターンの概念を深く理解し、楽しく分かりやすい形式でデザインパターンが学べる内容となっています。

Strategy パターンとは?

Strategy(ストラテジー)パターンは、異なる振る舞いやアルゴリズムを持つオブジェクトを使い分け可能な形にするパターンです。これにより、プログラムが柔軟で拡張しやすくなります。

デザインパターン-Strategyパターン

Strategy パターンの活用方法を簡単な具体例で紹介します。

Strategy パターンの具体例

ここでは、ゲーム開発において異なる攻撃方法を持つキャラクターを実装する場合を想定します。

まず、攻撃方法を表すインターフェースIAttackBehaviorを作成します。

public interface IAttackBehavior
{
    void Attack();
}

次に、IAttackBehaviorを実装する具体的な攻撃方法クラスを作成します。

// 近接攻撃クラス
public class MeleeAttack : IAttackBehavior
{
    public void Attack()
    {
        Console.WriteLine("近接攻撃の発動!");
    }
}

// 遠隔攻撃クラス
public class RangedAttack : IAttackBehavior
{
    public void Attack()
    {
        Console.WriteLine("遠隔攻撃の発動!");
    }
}

// 魔法攻撃クラス
public class MagicAttack : IAttackBehavior
{
    public void Attack()
    {
        Console.WriteLine("魔法の呪文を唱えた!");
    }
}

その後、キャラクターを表すCharacterクラスを作成し、IAttackBehaviorインターフェースを持つようにします。

public abstract class Character
{
    protected IAttackBehavior attackBehavior;

    public void SetAttackBehavior(IAttackBehavior attackBehavior)
    {
        this.attackBehavior = attackBehavior;
    }

    public void PerformAttack()
    {
        attackBehavior.Attack();
    }
}

最後に、具体的なキャラクタークラスを作成し、Characterクラスを継承します。

public class Warrior : Character { }

public class Archer : Character { }

public class Mage : Character { }

これで Strategy パターンを実装できました。具体的なキャラクターと攻撃方法の組み合わせを簡単に変更できるようになります。

デザインパターン-Strategyパターン

実際の使用例が以下になります。

Warrior warrior = new Warrior();
warrior.SetAttackBehavior(new MeleeAttack());
warrior.PerformAttack(); // 出力結果:近接攻撃の発動!

Archer archer = new Archer();
archer.SetAttackBehavior(new RangedAttack());
archer.PerformAttack(); // 出力結果:遠隔攻撃の発動!

Mage mage = new Mage();
mage.SetAttackBehavior(new MagicAttack());
mage.PerformAttack(); // 出力結果:魔法の呪文を唱えた!

このように Strategy パターンを使用することで、異なる攻撃方法を持つキャラクターを簡単に組み合わせて使用でき、ゲームの拡張性と保守性が向上します。

Strategy パターンの利点

Strategy パータンは継承と比べて何が良いの?

継承と Strategy パターンはどちらもコードの再利用と拡張性を向上させる目的で使われますが、それぞれ異なる利点があります。

ここでは Strategy パターンの利点を継承と比較して説明します。

柔軟性

Strategy パターンでは、実行時にアルゴリズムや振る舞いを簡単に変更できます。継承では、アルゴリズムや振る舞いを変更するためには、新しいサブクラスを作成し、オブジェクトを再生成する必要があります。

コードの重複を避ける

Strategy パターンでは、振る舞いやアルゴリズムを個別のクラスに分けるため、似たようなコードが複数のクラスで重複することが少なくなります。一方、継承では、親クラスと子クラスで類似したコードが重複することがあります。

クラス階層の複雑さを抑える

Strategy パターンでは、関連する振る舞いやアルゴリズムを個別のクラスに分けることができるため、クラス階層が複雑になることが少なくなります。継承では、多くのサブクラスを持つ親クラスを作成すると、クラス階層が複雑になりがちです。

疎結合

Strategy パターンでは、コンテキストクラスとストラテジークラスが疎結合になります。これにより、一部のコードを変更するときに、他の部分に影響を与えることが少なくなります。継承では、親クラスと子クラスが密結合になるため、変更が他のクラスに影響を与えることがあります。

ただし、継承とストラテジーパターンは、それぞれ状況に応じて適切に使われるべきです。継承はクラスの共通機能をまとめる場合に適しています。一方、ストラテジーパターンは、アルゴリズムや振る舞いが異なるオブジェクトを柔軟に使い分けたい場合に適しています。

Strategy パータンが役立つ場面

Strategy パータンどんな場面で使えばいいの?

Strategy パターンは、以下のような状況で特に役立ちます。

Strategy パータンが役立つ場面

・アルゴリズムや振る舞いが複数あり、それらを簡単に切り替えたい場合

・クラスの振る舞いやアルゴリズムを、クライアント側でカスタマイズしたい場合

・クラスの機能を拡張しやすくするために、異なるアルゴリズムや振る舞いを分離・組み合わせたい場合

継承とストラテジーパターンは、それぞれ異なる目的や状況で使用されるため、適切なデザインパターンを選択することが重要です。

どちらのアプローチが適切かは問題の性質や要件によって異なります。コードの再利用性、拡張性、保守性を向上させるために、適切なデザインパターンを選択しましょう。

設計原則:実装に対してではなく、インターフェースに対してプログラミングする

Strategy パターンを学ぶ上で、「実装に対してではなく、インターフェースに対してプログラミングする」という設計原則があります。これは、コードの柔軟性と保守性を向上させるための重要な考え方です。この原則の背後にある考え方は、コンポーネント間の依存関係を最小限に抑え、変更に強い設計を目指すことです。

この原則をわかりやすく説明するために、Strategy パターンを使って具体的な例を説明します。

例えば、あなたがペットショップのアプリケーションを開発していて、動物が鳴く機能を実装したいとします。以下のようなクラスがあるとします。

public class Dog
{
    public void Bark()
    {
        Console.WriteLine("Woof!");
    }
}

public class Cat
{
    public void Meow()
    {
        Console.WriteLine("Meow!");
    }
}

このアプリケーションでは、DogクラスのBarkメソッドとCatクラスのMeowメソッドを直接呼び出すことができますが、それは「実装に対してプログラミングする」方法です。この方法では、新たな動物が追加されるたびに、アプリケーション全体のコードを修正しなければならなくなります。

そこで、「インターフェースに対してプログラミングする」方法を適用します。まず、共通のインターフェースを定義します。

public interface IAnimal
{
    void MakeSound();
}

次に、DogクラスとCatクラスがこのインターフェースを実装するように変更します。

public class Dog : IAnimal
{
    public void MakeSound()
    {
        Console.WriteLine("Woof!");
    }
}

public class Cat : IAnimal
{
    public void MakeSound()
    {
        Console.WriteLine("Meow!");
    }
}

これで、アプリケーションではIAnimalインターフェースを使用して動物が鳴く機能を呼び出すことができます。これにより、新しい動物が追加されても、アプリケーション全体のコードを修正する必要がなくなります。

また、DogクラスやCatクラスの内部実装が変更されても、それがIAnimalインターフェースを実装している限り、アプリケーションの他の部分に影響を与えません。

このように、「インターフェースに対してプログラミングする」原則を適用することで、以下のような利点が得られます。

保守性の向上

インターフェースを使うことで、個々のコンポーネントがどのように実装されているかを他のコンポーネントから隠蔽できます。これにより、一部のコードを変更するときに、他の部分に影響を与えることが少なくなります。

柔軟性の向上

インターフェースを使うことで、実装を容易に切り替えることができます。例えば、ある機能を提供するクラスが複数ある場合、インターフェースを使ってそれらのクラスを簡単に入れ替えることができます。

再利用性の向上

インターフェースを使うことで、汎用的なコードを書くことができます。これにより、特定の実装に依存しないコードを再利用しやすくなります。

テストの容易性

インターフェースを使うことで、ユニットテストや統合テストを行いやすくなります。インターフェースを使って実装を切り替えることで、テスト用のモックオブジェクトやスタブを使って、テスト対象のコードを独立してテストすることができます。

このように、「実装に対してではなく、インターフェースに対してプログラミングする」原則は、コードの品質を向上させるための重要な考え方です。この原則を適用することで、コードの保守性、柔軟性、再利用性、およびテストの容易性が向上します。

演習問題

ここでは、今まで学んできた Strategy パターンをもとに、演出問題を出題します。この問題に取り組んで、Strategy パターンの理解を深めてみてください。

問題

あなたは、様々な種類のソーシャルメディアに投稿するアプリケーションを開発しています。投稿するコンテンツは、テキスト、画像、動画です。また、投稿するソーシャルメディアプラットフォームは、Twitter、Facebook、Instagramです。アプリケーションは、ユーザーが選択したプラットフォームに応じて、適切な投稿方法を使用する必要があります。

問題 1

上記のシナリオで Strategy パターンを適用するには、どのようなインターフェースとクラスが必要ですか?

問題 2

インターフェースとクラスを設計し、Strategy パターンを実装する方法を説明してください。

問題1の解答例

必要なインターフェースPostBehaviour

必要なクラスPostTextPostImagePostMoviePlatformTwitterFacebookInstagram

PostBehaviourインターフェースは、異なる種類の投稿方法を定義するために使用されます。PostTextPostImagePostMovieクラスは、それぞれ異なる投稿方法を実装するために使用されます。

そして、Platformクラスはベースクラスとして使用され、TwitterFacebookInstagramクラスはそれぞれ異なるプラットフォームを表します。ただし、プラットフォームごとに異なる投稿方法を使うので、ストラテジーパターンを活用するために、PlatformクラスはPostBehaviourインターフェースを持つ必要があります。

問題2の解答例

以下に具体的な実装方法の解答例を説明します。

まず、投稿方法を定義するためのインターフェースIPostBehaviourを作成します。

public interface IPostBehaviour
{
    void Post(string content, string platform);
}

次に、IPostBehaviourを実装する具体的な投稿方法のクラスを作成します。

public class PostText : IPostBehaviour
{
    public void Post(string content, string platform)
    {
        Console.WriteLine($"Posting text '{content}' to {platform}.");
    }
}

public class PostImage : IPostBehaviour
{
    public void Post(string content, string platform)
    {
        Console.WriteLine($"Posting image '{content}' to {platform}.");
    }
}

public class PostMovie : IPostBehaviour
{
    public void Post(string content, string platform)
    {
        Console.WriteLine($"Posting movie '{content}' to {platform}.");
    }
}

その後、プラットフォームを表すPlatformクラスを作成し、IPostBehaviourインターフェースを持つようにします。

public abstract class Platform
{
    protected IPostBehaviour postBehaviour;

    public void SetPostBehaviour(IPostBehaviour postBehaviour)
    {
        this.postBehaviour = postBehaviour;
    }

    public void PerformPost(string content)
    {
        postBehaviour.Post(content, this.GetType().Name);
    }
}

最後に、具体的なプラットフォームクラスを作成し、Platformクラスを継承します。

public class Twitter : Platform { }

public class Facebook : Platform { }

public class Instagram : Platform { }

これでストラテジーパターンを実装できました。具体的なプラットフォームと投稿方法の組み合わせを簡単に変更できるようになります。

Platform twitter = new Twitter();
twitter.SetPostBehaviour(new PostText());
twitter.PerformPost("Hello, world!");

Platform instagram = new Instagram();
instagram.SetPostBehaviour(new PostImage());
instagram.PerformPost("my_picture.jpg");

このようにストラテジーパターンを使用することで、プラットフォームと投稿方法を簡単に組み合わせて使用でき、アプリケーションの拡張性と保守性が向上します。

おわりに

Strategy パターンは、アルゴリズムの切り替えを容易にし、コードの柔軟性を向上させるために役立ちます。また、コードの再利用性と可読性を向上させることで、効率的な開発をサポートします。

デザインパターンを学ぶことで、ソフトウェア開発のスキルを向上させることができます。この記事が Strategy パターンの理解に役立ったことを願っています。

参考図書