LoginSignup
52
44

More than 5 years have passed since last update.

デザインパターンの本質(Unity用)

Last updated at Posted at 2018-05-13

英語物語の作者Gong氏の独断と偏見に基づいたデザインパターンの考察です。
デザインパターンの原書を読まずに、webの断片的な情報から、
自分なりに整理した情報なので間違ってる可能性が大いにあります。

生成に関するパターン

【Factory Method】

使用クラスの生成をサブクラスに任せる事で、サブクラス毎に使用するクラスを切り替える事ができる。

依存性の注入をサブクラスを用いて行う。

BaseCreator.cs
public class BaseCreator{
    public void Main(){
        FactoryMethod().Hoge();
    }
    protected virtual Product FactoryMethod(){
        return new BaseProduct();
    }
}

public class SubCreator: BaseCreator{
    public override Product FactoryMethod(){
        return new SubProduct();
    }
}
public class BaseProduct{
    public virtual void Hoge(){
        //Do something
    }
}
public class SubProduct: BaseProduct{
    public override void Hoge(){
        //Do something
    }
}

☆使い所☆

サブクラス毎に特有のクラスに依存させたい場合

既存のクラスのインターフェースを変える事なく、
使用クラスを疑似オブジェクトに差し替える場合

【Abstract Factory】

使用するクラス群の生成を別の生成用クラスに任せる事で、
使用するクラス群を切り替える事ができるようになる。

例えば、
本番環境では、
Class AとBを利用するが、
テスト環境では
Class AMockとBMockを利用する場合に、

↓のようなFactoryクラスを

AbstractFactory.cs
public abstract class AbstractFactory{
    ClassA CreateA();
    ClassB CreateB();
}

public class ReleaseFactory: AbstractFactory{
    public ClassA CreateA(){
        return new ClassA();
    }

    public ClassB CreateB(){
        return new ClassA();
    }
}

public class TestFactory: AbstractFactory{
    public ClassA CreateA(){
        return new ClassAMock();
    }

    public ClassB CreateB(){
        return new ClassAMock();
    }
}

public void UseSample{

    private ClassA _classA;
    private ClassB _classB;

    public UseSample(AbstractFactory abstractFactory){
        _classA = abstractFactory.CreateA();
        _classB = abstractFactory.CreateB();
    }
}

みたいな感じにすることで、使用するクラス群を一気に切り替えられる。

☆使い所☆

状況によって、依存すべきクラス"群"が、"一気に"切り替わる時に有用になると考えられる。

独断と偏見に基づく考察

正直言って、
【Give Class】・・・使用するクラスを別クラスに任せる事で、使用クラスを切り替える用にする。

GivenClass.cs
public void GivenClass{

    private UseClass _useClass;

    public GivenClass(UseClass useClass){
        _useClass = useClass;
    }
}

【Factory Class】・・・使用するクラスを別クラスに任せる事でクラス生成の責務を切り離す。

GivenFactory.cs
public void GivenFactory{

    private UseClass _useClass;

    public GivenClass(Factory factory){
        _useClass = factory.Create();
    }
}

public void Factory{

    public UseClass Create(){
        return new UseClass();
    }
}

(僕が勝手に名付けてるので、適切な名称でない可能性があります。)

というもっと基本的なパターンを学習させる事よって、
その応用版となるこれらの【Factory Method】や【Abstract Factory】が
理解しやすくなると思うが、
基本的過ぎて、デザインパターンとしては認識されないのだろう。

初級プログラマのための、もっと基本のデザインパターンみたいなのを作りたいな。

【Builder】

使用クラスの構築を別のクラスに任せる事で、構築詳細や手順の責務を分離する。
(正直、自信がないので、参考にしないほうがいいかも。)

サンプル

吉野家の例を上げて説明してみる。
まずこんなメソッドがあったとしよう。

Yoshinoya.cs
public enum EnumVolumState{
   Nami,
   Oomori
}
public enum EnumSoupState{
   Nami,
   Tuyudaku
}

public class Yoshinoya{
    public Donburi OrderDonburi(EnumVolumState volume,EnumSoupState soup){
        Donburi donburi = new Donburi();
        if(volume == EnumVolumState.Oomori){
            donburi.AddRice(1.5f);
            donburi.AddMeet(1.5f);
        }else{
            donburi.AddRice(1f);
            donburi.AddMeet(1f);
        }
        if(soup == EnumSoupState.Tuyudaku){
            donburi.AddSoup(3f);
        }else{
            donburi.AddSoup(1f);
        }

        return donburi;
    }
}

このYoshinoyaクラスは多くの責務を持っている。
商品の生成、構築手順に関する知識、構築詳細に関する知識

商品の生成と構築詳細の責務を分離してみる。

AddBuilder.cs
public class Yoshinoya{
    public Donburi OrderDonburi(EnumVolumState volume,EnumSoupState soup){        
        DonburiBuilder builder = new DonburiBuilder();
        builder.AddRice(volume);
        builder.AddMeet(volume);
        builder.AddSoup(soup);
        return builder.Build();        
    }
}

public class DonburiBuilder
{
    private Donburi _donburi;

    public DonburiBuilder()
    {
        _donburi = new Donburi();
    }

    public void AddRice(EnumVolumState volume)
    {
        if(volume == EnumVolumState.Oomori){
            _donburi.AddRice(1.5f);
        }else{
            _donburi.AddRice(1f);
        }
    }

    public void AddMeet(EnumVolumState volume)
    {
        if(volume == EnumVolumState.Oomori){
            _donburi.AddMeet(1.5f);
        }else{
            _donburi.AddMeet(1f);
        }
    }

    public void AddSoup(EnumSoupState soup)
    {  
        if(soup == EnumSoupState.Tuyudaku){
            _donburi.AddSoup(3f);
        }else{
            _donburi.AddSoup(1f);
        }        
    }

    public Donburi Build()
    {
        return _donburi;
    }
}

こんな感じ。まだ構築手順に関する知識がYoshinoyaに残っているので、
こいつも分離してみる。

AddDirector.cs
public class Yoshinoya{
    public Donburi OrderDonburi(EnumVolumState volume,EnumSoupState soup){        
        var builder = new DonburiBuilder();
        var donburiDirector = new DonburiDirector(builder,volume,soup);
        return donburiDirector.Construct();        
    }
}

public class DonburiDirector
{
    private DonburiBuilder _donburiBuilder;
    private EnumVolumState _volume;
    private EnumSoupState _soup;

    public DonburiDirector(DonburiBuilder donburiBuilder, EnumVolumState volume, EnumSoupState soup)
    {
        _donburiBuilder = donburiBuilder;
        _volume = volume;
        _soup = soup;        

    }

    public Donburi Construct()
    {
        _donburiBuilder.AddRice(_volume);
        _donburiBuilder.AddMeet(_volume);
        _donburiBuilder.AddSoup(_soup);
        return _donburiBuilder.Build();
    }
}

まあ、正直、何でこんなことする必要あるん?って感じだとは思いますが、
それは、OrderDonburiが現状シンプルだからであって、本来はここに料金授受やお水やお箸を提供したりするので、少しでも分けれる責務があったら、分けたい。んでしょうね。
状況によりけりですが。

補足

この例は一般的なBuilderの例で使われるインターフェースを設けて、切りかえれる用にする部分を書いてません。
僕の個人的見解ですが、
デザインパターンが分かりにくいのは、
サンプルが本質的な概念以外の部分も触れているからだ。
と思っています。
このパターンの本質は構築詳細と構築手順の責務を分離で、
責務の分離さえできれば、インターフェースを使って多様化するのは、
いくらでもできるので。

また、生成の責務を分離する。のは別概念だと思っていますが、
普通に考えてbuilderにもたせてしまうので、
ここでは含めて書いてしまっています。

☆使い所☆

Builderパターンは2つに分離して考えるべきだと思っています。
それは、
Builderパターン

Directorパターン
です。

①オブジェクトの構築(各フィールドの初期化)が複雑な時。
初期化方法だけをまとめたBuilderクラスを作って責務を分離させましょう。

②オブジェクトの構築手順が複雑な時。
構築手順だけをまとめたDirectorクラスを作って責務を分離させましょう。

双方とも使ってるけど、双方とも対して複雑でない時は、
Builderクラスの中に構築手順を含めてしまっても、いいと思います。

【Prototype】

コピーして、生成・構築にかかるコストを節約する。

今までのパターンが生成に関連する責務の分離だったのに対して、
今パターンは生成・構築にかかるコストを回避する点、
をまず理解しよう。

Monster.cs
public class Monster
{
    public int hp;
    public int str;
    private int _monsterId;
    private Monster()
    {
    }        
    public Monster(int monsterId)
    {
        _monsterId = monsterId;
        //モンスターIDを元に、データベースに接続して、hpとstrを取得する。この処理は重い
    }

    public Monster Clone()
    {
        var monster = new Monster();
        monster.str = str;
        monster.hp = hp;
        return monster;
    }
}

補足

Clonableのインターフェースの実装とかがあるが、
それはあくまで、Cloneしたオブジェクトを使いまわすための便利クラスを
作るために必要なだけだと思うので、割愛してます。
また、PrototypeKeeper等のコピー元を保持して、使いやすくするクラス等を
含めている解説もありましたが、そんなのは色んな実装が考えられるので、割愛してます。

確実に1度しか、重い生成はされない用にするために、
例えばこんな実装もありじゃないかな。

Monster.cs
public class Monster
{
    public int hp;
    public int str;
    private int _monsterId;
    private static Dictionary<int,Monster> cache  = new Dictionary<int,Monster> ();

    public Monster()
    {
    }

    public static Monster Create(int monsterId)
    {
        if (cache.ContainsKey(monsterId) == false)
        {
            cache.Add(monsterId, new Monster(monsterId));
        }
        return cache[monsterId].Clone();
    }
    private Monster(int monsterId)
    {
        _monsterId = monsterId;
        //モンスターIDを元に、データベースに接続して、hpとstrを取得する。この処理は重い
    }

    public Monster Clone()
    {
        var monster = new Monster();
        monster.str = str;
        monster.hp = hp;
        return monster;
    }
}

☆使い所☆

オブジェクトの生成にコストがかかる場合や
オブジェクトの生成が複雑(ユーザの操作によって作られる)場合
に、複数のオブジェクト(または少しだけ違うオブジェクト)が必要となる時

【Singleton】

オブジェクトの個数を管理する責務を分離する。
利用者に個数の管理を意識させなくする。
主に一つだけになるようにクラス自身に管理させる事を指す。

そもそも一つだけなら、staticなクラスにstaticな変数で行えばよい。
が、状況によってはstaticクラスが適切でないことがある。

僕が思いつく状況は
①特定のクラスを継承する必要がある。(例えばUnityのMonobehavior)
②必要な時にだけメモリにロードして、必要ない時はアンロードできるようにする。
③インターフェースを使いたい。(C#だとstaticなインターフェースは作れない。)
④ポリモーフィズムを使いたい。(クラスだと動的にインスタンスを切り替えられない。)
⑤複数のインスタンスの生成を許容したい。

な感じかな。他にあったら教えてください。
特にテスト駆動開発をするに当たって、staticなクラスだと擬似オブジェクトに置き換えにくいので、僕は多くの共用クラスをシングルトンで実装してます。

実装方法も、状況に応じて色々考えられるのですが、
基本的には
①コンストラクタをprivateに
②staticなフィールドに自身のオブジェクト格納
③staticなメソッド(または公開フィールド、ゲッタ)で自身のオブジェクトを返す。

複数個にする時は変数部分を配列やListにして、取得メソッド内で個数の管理をしたり。
遅延生成しなくていいなら、フィールド内で初期化してしまってもいいと思います。

Singleton.cs
public class SingletomSample
{
    private static SingletomSample instance;

    private SingletomSample()
    {
    }

    public static SingletomSample GetInstance()
    {
        if (instance == null)
        {
            instance = new SingletomSample();
        }

        return instance;
    }

}

ちなみに、今僕がよく使ってるのは

ShareLocator.cs
public class ShareLocator : IShareManager
{
    private static IShareManager _shareManager;

    public static readonly ShareLocator Instance = new ShareLocator();

    private ShareLocator()
    {
    }

    public static void Set(IShareManager shareLocator)
    {
        _shareManager = shareLocator;
    }

    public void Share()
    {
        _shareManager.Share();
    }
}

こんな感じのクラスで、
初期化クラスで具象インスタンスの生成と代入を行います。
テスト駆動開発をするために、テスト時は、モックオブジェクトを代入して利用します。

☆使い所☆

悪名高いグローバル変数やグローバルメソッドを使いたいけど、
staticなクラスやメソッドでは都合が悪い時。

あるいは、オブジェクトの生成個数を管理したい時

です。
無闇やたらにstaticやシングルトンを使うのは、
スパゲッティーコードへの第一歩なので、
きちんとシステム・アーキテクチャを設計して、
指針に沿って適切に使うように心がけましょう。

構造に関するパターン

【Bridge】

継承ではなく委譲を使う事で、拡張性の高い構造を維持する。

というのが僕の理解です。
分かる人にはこの一言で十分だと思うんですが、
念のため解説します。継承とか委譲はgoogle様に聞いてくださいね。

では今回も事例で説明しましょう。
敵キャラでコボルトがいるとしましょう。(最近、なろう小説の影響でコボルトブームです)

コボルトには色んな種類がいます。
コボルト
コボルトファイター
コボルトメイジ
ハイコボルト
ハイコボルトファイター
ハイコボルトメイジ
の6種類がいるとしましょう。

これを継承関係で表現しようとすると、こんな感じになる。
(一つ一つのクラスを完全分離するよりはマシだけど、まあひどいよね。)

Kobold.cs
public class Kobold
{
    protected int Hp;
    protected int Mp;
    protected string SkillName;

    public Kobold()
    {
        Hp = 10 + GetAddHp();
        Mp = 1 + GetAddMp();
        SkillName = GetSkill();
    }

    protected virtual int GetAddHp()
    {
        return 0;
    }
    protected virtual int GetAddMp()
    {
        return 0;   
    }
    protected virtual string GetSkill()
    {
        return "コボルトパンチ";
    }

    public void Show()
    {
        Debug.Log(string.Format("Hp:{0} Mp:{1} Skill:{2}",Hp,Mp,SkillName));
    }
}

public class HighKobold : Kobold
{
    public HighKobold()
    {
        Hp += 10;
        Mp += 10;
    }
}

public class KoboldFighter : Kobold
{
    protected override int GetAddHp()
    {
        return 10;
    }
    protected override int GetAddMp()
    {
        return 0;   
    }
    protected override string GetSkill()
    {
        return "コボルトラッシュ";
    }    
}

public class KoboldMage : Kobold
{
    protected override int GetAddHp()
    {
        return 0;
    }
    protected override int GetAddMp()
    {
        return 10;   
    }
    protected override string GetSkill()
    {
        return "コボルトサンダー";
    }    
}

public class HighKoboldFighter : HighKobold
{
    protected override int GetAddHp()
    {
        return 10;
    }
    protected override int GetAddMp()
    {
        return 0;   
    }
    protected override string GetSkill()
    {
        return "コボルトラッシュ";
    }    
}

public class HighKoboldMage : HighKobold
{
    protected override int GetAddHp()
    {
        return 0;
    }
    protected override int GetAddMp()
    {
        return 10;   
    }
    protected override string GetSkill()
    {
        return "コボルトサンダー";
    }    
}

//利用クラス
public class TestKobold {
    public void Main() {
        new Kobold().Show();//Hp:10 Mp:1 Skill:コボルトパンチ
        new KoboldFighter().Show();//Hp:20 Mp:1 Skill:コボルトラッシュ
        new KoboldMage().Show();//Hp:10 Mp:11 Skill:コボルトサンダー
        new HighKobold().Show();//Hp:20 Mp:11 Skill:コボルトパンチ
        new HighKoboldFighter().Show();//Hp:30 Mp:11 Skill:コボルトラッシュ
        new HighKoboldMage().Show();//Hp:20 Mp:21 Skill:コボルトサンダー
    }
}

これだと、例えば、
コボルトキング等の種類を追加すると、
職種の分だけクラスを追加しなきゃいけないし、
コボルトシャーマン等の職種を追加すると、
種類の分だけクラスを追加しなきゃいけない。

これを継承ではなく委譲を使ってやると、

Kobold.cs
public class Kobold
{
    protected int Hp;
    protected int Mp;
    protected string SkillName;
    protected Job Job;

    public Kobold(Job job)
    {
        Job = job;
        Hp = 10 + Job.GetAddHp();
        Mp = 1 + Job.GetAddMp();
        SkillName = Job.GetSkill();
    }
    public void Show()
    {
        Debug.Log(string.Format("Hp:{0} Mp:{1} Skill:{2}",Hp,Mp,SkillName));
    }
}

public class HighKobold : Kobold
{
    public HighKobold(Job job) : base(job)
    {
        Hp = 20;
        Mp = 10;
    }
}

public interface Job
{
    int GetAddHp();
    int GetAddMp();
    string GetSkill();
}

public class Fighter : Job
{
    public int GetAddHp()
    {
        return 10;
    }
    public int GetAddMp()
    {
        return 0;   
    }
    public string GetSkill()
    {
        return "コボルトラッシュ";
    }    
}

public class Mage : Job
{
    public int GetAddHp()
    {
        return 0;
    }
    public int GetAddMp()
    {
        return 10;   
    }
    public string GetSkill()
    {
        return "コボルトサンダー";
    }    
}

public class Common : Job
{
    public int GetAddHp()
    {
        return 0;
    }
    public int GetAddMp()
    {
        return 0;   
    }
    public string GetSkill()
    {
        return "コボルトパンチ";
    }    
}
//利用クラス
public class TestKobold {
    [Test]
    public void Main() {
        new Kobold(new Common()).Show();//Hp:10 Mp:1 Skill:コボルトパンチ
        new Kobold(new Fighter()).Show();//Hp:20 Mp:1 Skill:コボルトラッシュ
        new Kobold(new Mage()).Show();//Hp:10 Mp:11 Skill:コボルトサンダー
        new HighKobold(new Common()).Show();//Hp:20 Mp:10 Skill:コボルトパンチ
        new HighKobold(new Fighter()).Show();//Hp:20 Mp:10 Skill:コボルトラッシュ
        new HighKobold(new Mage()).Show();//Hp:20 Mp:10 Skill:コボルトサンダー
    }
}

Jobというインターフェースを作って、
KoboltクラスにJobインターフェースを実装したFigterやMage、Commonクラスを
渡してあげる感じになります。

こうすることで、種族や職種を追加してもただ一つのクラスのみ追加すれば良くなるので、
拡張しやすい構造になるのです。

☆使い所☆

Has-a関係のある時。

あえていえば、継承がガンガン増えて行っている時がサインでしょうか。
継承すら使わず、コードがガンガン重複している場合も使い時ですが。
Is-a関係とHas-a関係をきちんと整理しながら設計すれば、
自然に使ってると思います。
(多くのデザインパターンがBridgeパターンの利用を前提としているので、一番はじめに勉強したいパターンだと思ってます。)
Let's オブジェクト指向

【Composite】

再帰的な構造を実現するためのパターン
ディレクトリとファイルの例があまりにも理解しやすいので、
わかりやすいパターンの一つだが、逆にその例があまりにも強烈すぎるので、
他の例に展開しにくいのでは、と思ってます。

ので、今回は無理やり他の例を作ってみます。
ゲームとかでありがちな生贄システムについて作ってみましょう。
あるキャラにあるキャラを生贄にすると、生贄にしたキャラの強さが加算される。
というシンプルな仕様です。

Character.cs
public class Character
{
    private List<Character> _sacrificeList = new List<Character>();

    private int _power;

    public Character(int power)
    {
        this._power = power;
    }

    public void AddSacrifice(Character sacrifice)
    {
        _sacrificeList.Add(sacrifice);
    }

    public int GetPower()
    {
        int sumPower = _power;
        foreach (var sacrifice in _sacrificeList)
        {
            sumPower += sacrifice.GetPower();
        }

        return sumPower;
    }             
}
TestCharacter.cs
public class TestCharacter
{
    [Test]
    public void Test()
    {
        var chara1 = new Character(1);
        var chara2 = new Character(2);
        var chara3 = new Character(3);

        chara1.AddSacrifice(chara2);
        chara3.AddSacrifice(chara1);
        Assert.AreEqual(6,chara3.GetPower(),"Chara3 power");
    }
}

インターフェースを使って、
生贄にはできるけど、生贄を与える事はできないキャラクター
みたいなの作れば、もっとCompositeパターンぽくなるんですが、
僕の個人的な見解としては、
「自クラスの型のフィールドをもつ事で、再帰的な構造を実現すること」
がこのパターンの肝だと思っているので、このシンプルな例に留めておきます。
自分の型の部分をインターフェースにすれば、他のクラスも含有できるようになるし、
フィールド部分をListやDictionaryにすれば、複数やより複雑な構造の含有に対応できるようになりますが、
大切なポイントは、再帰的な仕様を見つけた時に、Compositeを思い出す事だと思います。

☆使い所☆

再帰的な構造を見つけた時!

【雛形】・・・今後追加する用

Code.cs

☆使い所☆

その他

この方のブログはデザインパターンに関して有用だと思いました!
http://hamasyou.com/blog/tags/dezainpatan/

52
44
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
52
44