お茶漬けびより

"あなたに教わったことを、噛んでいるのですよ" 五等分の花嫁 7巻 「最後の試験が五月の場合」より

Factory Method を使って、スライム属を作る

Factroy Methodドラクエのスライムで表現してみる記事。

Factory Method とは

オブジェクト指向における再利用のためのデザインパターンの一つです。そのデザインパターンの中でも生成に関するパターンの一つ。

このパターンによって、オブジェクト型を指定せずにオブジェクトを生成できるようになります。
簡単に説明すると、関数内でコンストラクタを呼び、生成したオブジェクトを返すのがこのパターンの基本になります。 これに継承を組み合わせることで、動的にオブジェクトを生成することができるようになります。

で、この FactoryMethod を使って、いろんなスライムを作るというのが今回のお話です。 ちなみに、抽象基本クラスを分かっておく必要があるので、分からない方は、調べるか、以下を参考にしてみるといいでしょう。

pickles-ochazuke.hatenablog.com

全体イメージ

f:id:pickles-ochazuke:20170714010327p:plain

UML 図を初めて書いたので、間違っているところがあるかも……。

まずは大きく4つに分けて、ISlime, Slime, SheSlime, HealSlime, Status。一つずつ簡単に説明します。

Status

これは、スライムのステータスを表します。日本の(RPG)ゲームでいうステータスと英語の Status は、意味が異なるようなのだけど、 ここでは深く考えないようにします。中身は、見てみると何となく分かると思うのだけど、名前(name_)、HP(hp_)、MP(mp_)、攻撃力(attack_)、防御力(defense_)、経験値exp_)、ゴールド(gold_)が入っている。モンスター(今回は、スライム属のみ)共通のステータスとなります。

ISlime

スライム属の抽象基本クラスです。ISlimeI は、InterfaceI
スライム属に属するクラスは、これを継承します。これによってあとで作る FactoryMethod は、一つの関数で動的にオブジェクトを作れるようになります。name(), hp(), mp(), attack(), defense(), exp(), gold()は、get 関数で特に意味はなくて、status_ にアクセスする手段を用意しているだけ。あと、コンストラクタ、デストラクタは、実際にはあるのだけど、書いてないです。これは、Slime, SheSlime, HealSlime 共通。

Talk(), Skill() は、それぞれ純粋仮想関数。継承先は、この二つの処理を実装する必要があります。

ViewInformation() は、オブジェクトのステータスを出力します。

Slime

スライムを表すクラス。Talk(), Skill() をオーバライドしています(あとデストラクタも)。ここら辺は、実行したときにちゃんと Slime のクラスが作られているか分かるようにしているだけなので、実装内容は何でもいいです。

SheSlime

ベススライムを表すクラス。外国だと SheSlime と言うみたいです。内部の説明は、Slime と同じ。もちろん実装内容は、区別できるようにしてあります。

HealSlime

ホイミスライムを表すクラス。これも Slime と同じ。

以上のスライムクラスを使って、FactoryMethod を作ります。

FactoryMethod

まずは、クラス図を

f:id:pickles-ochazuke:20170714013033p:plain

FactroyMethod を実装するのは、SlimeFactory というクラス。このクラスは、今回、コンストラクタもデストラクタも必要ないので、delete しています。DoMakeSlime() という関数が FactoryMethod になります。

実際は、FactoryMethod を実装しているクラス内でオブジェクトの管理やシングルトンパターンを実装するようだけど、簡単にするためにそれらは実装していません。
というか生成と管理を一つのクラスで任せるものなのかどうか分からないです……。別々にしても良さそうだけど、どうなんだろう?

クラス図の"関連"を表す矢印の使い方が正しいのか自信はないのだけど、これらをインクルードする必要があるので、"関連"としています。
ただ、ヘッダ(SlimeFactory.h)でインクルードするのは、ISlime のみで、他はソースファイル(SlimeFactroy.cpp)でインクルードしています。

実装

実装内容は以下のようになっています。まずは、ヘッダファイル。

#pragma once
#include <string>
#include "ISlime.h"

class SlimeFactory
{
public:
    SlimeFactory() = delete; 
    virtual ~SlimeFactory() = delete;

    static ISlime* DoMakeSlime(const std::string& type);
};

スライム属のクラスの内、インクルードしているのは、 “ISlime.h” のみで、他のクラスはここで知る必要はないです。

コンストラクタとデストラクタは必要なかったので、delete しています。デストラクタを仮想にしているけど必要ないです……。

DoMakeSlime(const std::string& type) が先に書いたように FactroyMethod になります。type の値を見て、どのオブジェクトを作るかを決めています。作りたいオブジェクトが分かるなら文字列でも enum 型でもただの値でもいいです。たぶん enum 型が一番扱いやすいです。

次にソースファイル。

#include "SlimeFactory.h"
#include "Slime.h"
#include "SheSlime.h"
#include "HealSlime.h"

ISlime* SlimeFactory::DoMakeSlime(const std::string& type)
{
    if (type == "Slime") {
        return new Slime();
    }
    else if (type == "SheSlime") {
        return new SheSlime();
    }
    else if (type == "HealSlime") {
        return new HealSlime();
    }

    return nullptr;
}

こちらでは、実際にオブジェクトを作るために型を知る必要があるので、Slime.h, SheSlime.h, HealSlime.h をインクルードしています。

DoMakeSlime の処理は、とても簡単で type の中身を見て、条件式で判定して、その中のオブジェクト生成処理を実行し、そのオブジェクトを返すだけです。型の異なるオブジェクトを返せるのは、継承元が全て同じ ISlime だから。

戻り値の型を void* にすれば、継承元が異なる型を返すようにすることも出来るけど、危険だろうし、扱いづらい。それなら抽象基本クラスごとに関数を分けた方がいいと思います。

次は、実際に使うときのコードとその結果を載せます。

実行

main.cpp 内は、以下のようになっています。

#include <iostream>
#include "ISlime.h"
#include "SlimeFactory.h"

int main(void)
{
    ISlime* slime = SlimeFactory::DoMakeSlime("Slime");
    slime->Skill();
    delete slime;

    slime = SlimeFactory::DoMakeSlime("SheSlime");
    slime->Skill();
    slime->ViewInformation();
    delete slime;

    slime = SlimeFactory::DoMakeSlime("HealSlime");
    slime->Skill();
    delete slime;

    slime = SlimeFactory::DoMakeSlime("");
    if (!slime) {
        std::cout << "スライムの生成に失敗した!" << std::endl;
        std::cout << std::endl;
    }
    else {
        slime->Skill();
        delete slime;
    }

    return 0;
}

実行する側は、FactoryMethod のクラスと、抽象基本クラスだけを知るだけで使うことができます。もちろん、継承先にしかない機能を使う場合は、そのクラスのヘッダファイルをインクルードする必要があります。

実行結果は、以下のようになります。

スラりん が現れた!

スラりん のスキル発動! 逃げ出す!
しかし、逃げられなかった!

へんじがない。ただのスラりんのようだ。

スラみ のスキル発動! メラ!
しかし、MP が足りない!

class SheSlime の情報を表示します ========
スラみ
8
3
10
12
3
1
===============================

ホイミン のスキル発動! ホイミ!
ホイミン に 30 の回復!

スライムの生成に失敗した!

続行するには何かキーを押してください . . .

おわり

以上、FactoryMethod の基本を書いてみました。上手くクラスを抽象化できれば、FactoryMethod が有効活用できそうです。FactoryMethod を扱うことよりもクラスの抽象化をどうするかの方が難しいのかもしれないですね。

作成したオブジェクトを FactroyMethod の内部(今回だと SlimeFactory)で持つか、作成を依頼してきた側(main関数)に任せるかは、その時々で変わりそう。

最後に、今回のコードを以下に上げておきます。

github.com

参考にした資料は以下です。

C++のためのAPIデザイン

C++のためのAPIデザイン

オブジェクト指向における再利用のためのデザインパターン

オブジェクト指向における再利用のためのデザインパターン