指向性メモ::2005-02-24

ページ情報
制作日
2005-02-24T02:07:08+09:00
最終更新日
2005-02-26T16:15:45+09:00
ページ内目次

JavaScriptでデザインパターンその2

Created:
2005-02-24T02:07:08+09:00

Singleton。インスタンスの同一性を保証したい場合。「人口」プロパティを持つEarthクラスのインスタンスを例に書いてみる。

function Earth() {
    this.population = 2;
}

// このクラス(静的/static)変数にインスタンスが保存される。
Earth.instance = null;

// このクラス(静的/static)メソッドで上のインスタンスを受け取る。
function Earth.getInstance() {
    if (Earth.instance == null) {
        Earth.instance = new Earth;
    }
    return this.instance;
}

function Earth.prototype.increasePopulation() {
    this.population = this.population * 2; // 人口が2倍に増える
}

function Earth.prototype.getPopulation() {
    return this.population;
}


// 実際に使用する場面1
var e1 = Earth.getInstance();
print("1st test: " + e1.getPopulation() + "(e1)"); // print は適当に定義するべし


// 実際に使用する場面2 (1とは違う場面)
var e2 = Earth.getInstance(); // 参照渡しなので結局 e2 = e1
e2.increasePopulation();
print("2nd tset: " + e2.getPopulation() + "(e2)");


// 再び場面1に戻る
print("3rd test: " + e1.getPopulation() + "(e1)");


// Result:
// 1st tset: 2(e1)
// 2nd test: 4(e2)
// 3rd tset: 4(e1)  <- e1 の population も増えている!

たとえば、データベースの操作用クラスのインスタンスが複数出来てしまうと、トランザクションの途中経過がそれぞれ別のインスタンスに渡されてしまい、結果的に不整合が生じることがある。次の例で、db1とdb2がそれぞれ別にnewされたATMのデータベース操作用クラスのインスタンスだとすると、

このような事になってしまう。もちろん、コーディング中にみんなでdb1を使うことを徹底すれば問題ないが、大規模なプロジェクトだとどこかでミスが起こるかも知れない。そこで、インスタンスが1つであることを保証するSingletonパターンの出番になる。上のサンプルコードを見れば分かるが、Earth.instanceに実際に必要なインスタンスをキャッシュし、Earthクラスが必要になった場合は必ずEarth.getInstanceでそのインスタンスを得るようにしている。これにより、db1、db2がそれぞれnewされた場合と異なり、使用されるインスタンスは常に1つになる。

と、言いたいところだが、JavaScriptにはアクセス制限の概念がないのでJavaのようにはうまく行かない。e3 = new Earth()と書いても通ってしまうのだ。よって、言語レベルでの同一性保証は得られない。まあ、スーパーグローバルな変数を持たなくて済むと言う利点もあるので……。

関係ないけど、メソッドの定義はClass.prototype.method = function() {...}の方がスマートかなぁ。

次はStrategyでも書くかな。OOPの利点がハッキリ実感できるデザインパターンだと思う。

Comments
2
Trackbacks
0
PermaLink
http://yudai.arielworks.com/memo/2005/02/24/020708

JavaScriptでデザインパターンその3

Created:
2005-02-24T09:05:33+09:00

Strategy。アルゴリズムの切り替え。とかいうと、難しく感じるけど、やってることはOOPの基本。

RPGの戦闘シーンで説明してみよう(ジャンケンだとパクリとか言われそうなので)。簡単のため、味方は主人公だけで。

function Battle(monsterList) {
    this.turnNumber = 1;
    this.hpOfHero = 500;
    this.offenseOfHero = 12;
    this.monsters = monsterList; // モンスターのリストを配列で保存
}

function Battle.prototype.start() {
    // モンスターがいなくなるか、主人公のHPが無くなるまでループ
    while(true) {
        if (this.monsters.length == 0) {
            print("勇者は勝利した!"); // print は適当に定義すべし
            break;
        } else if (this.hpOfHero > 0) {
            this.nextTurn();
        } else {
            print("勇者は死んでしまった");
            break;
        }
    }
}

function Battle.prototype.nextTurn() {
    print(this.turnNumber + "ターン: ");

    // まずは勇者の攻撃(本題とはあんまり関係ない)
    var targetNum = Math.floor(Math.random() * this.monsters.length) // 勇者攻撃対象をランダムに設定
    var target = this.monsters[targetNum];
    target.hp -= (this.offenseOfHero - target.defence); // (勇者の攻撃力 - 敵の防御力)をHPから引く
    print("勇者の攻撃: " + target.getName() + "に" + (this.offenseOfHero - target.defence) + "のダメージを与えた!");
    if (target.hp <= 0) {
        print(target.getName() + "を倒した!");
        var newList = new Array();
        for (var i = 0; i < this.monsters.length; i++) {
            if (i != targetNum) {
                newList.push(this.monsters[i]);
            }
        }
        this.monsters = newList;
    }
    

    // 本題のモンスターの行動決定
    for (var i = 0; i < this.monsters.length; i++) {
        var damage = this.monsters[i].act() * 3; // ここがStrategy。 攻撃力 * 3 のダメージを受ける
        print(this.monsters[i].getName() + "の攻撃: " + damage + "のダメージを受けた!")
        this.hpOfHero -= damage;
    }

    this.turnNumber++;
}

function Monster() {
    this.name; // abstract
    this.hp; // abstract
    this.defence; // abstract

    function Monster.prototype.act() {
        // abstract
        // 攻撃力の値を return するメソッド
    }

    function Monster.prototype.getName() {
        return this.name;
    }
}


function Slime() {
    Slime.prototype.name = "スライム";
    Slime.prototype.hp = 20;
    Slime.prototype.defence = 3;

    function Slime.prototype.act() {
        return 3; // 攻撃力3
    }
}
Slime.prototype = new Monster(); // Monster クラスを継承


function HoimiSlime() {
    HoimiSlime.prototype.name = "ホイミスライム";
    HoimiSlime.prototype.hp = 25;
    HoimiSlime.prototype.defence = 2;

    function HoimiSlime.prototype.act() {
        var rand = Math.floor(Math.random() * 3);
        if (this.hp < 10 && rand == 1) {
            this.hp += 30; // HP が少なかったら1/3の確率でHPを回復する
            return 0; // 魔法を使ったので攻撃しない
        } else {
            return 2; // 攻撃力2
        }
    }
}
HoimiSlime.prototype = new Monster();


function KingSlime() {
    KingSlime.prototype.name = "キングスライム";
    KingSlime.prototype.hp = 50;
    KingSlime.prototype.defence = 5;

    function KingSlime.prototype.act() {
        var rand = Math.floor(Math.random() * 2); // 0 or 1
        if (rand == 1) {
            return 8 // 会心の一撃!
        } else {
            return 4 // 通常攻撃
        }
    }
}
KingSlime.prototype = new Monster();


// デモコード
var monsters = new Array(new Slime, new Slime, new HoimiSlime, new KingSlime); // スライム * 2、ホイミスライム、キングスライムの組み合わせ
var btl = new Battle(monsters);
btl.start();

実行すると次のような感じになる。

1ターン:
    勇者の攻撃: スライムに9のダメージを与えた!
    スライムの攻撃: 9のダメージを受けた!
    スライムの攻撃: 9のダメージを受けた!
    ホイミスライムの攻撃: 6のダメージを受けた!
    キングスライムの攻撃: 12のダメージを受けた!
2ターン:
    勇者の攻撃: キングスライムに7のダメージを与えた!
    スライムの攻撃: 9のダメージを受けた!
    スライムの攻撃: 9のダメージを受けた!
    ホイミスライムの攻撃: 6のダメージを受けた!
    キングスライムの攻撃: 24のダメージを受けた!
3ターン:
    勇者の攻撃: スライムに9のダメージを与えた!
    スライムの攻撃: 9のダメージを受けた!
    スライムの攻撃: 9のダメージを受けた!
    ホイミスライムの攻撃: 6のダメージを受けた!
    キングスライムの攻撃: 12のダメージを受けた!
4ターン:
    勇者の攻撃: スライムに9のダメージを与えた!
    スライムを倒した!
    スライムの攻撃: 9のダメージを受けた!
    ホイミスライムの攻撃: 6のダメージを受けた!
    キングスライムの攻撃: 24のダメージを受けた!
5ターン:
    勇者の攻撃: ホイミスライムに10のダメージを与えた!
    スライムの攻撃: 9のダメージを受けた!
    ホイミスライムの攻撃: 6のダメージを受けた!
    キングスライムの攻撃: 12のダメージを受けた!
6ターン:
    勇者の攻撃: ホイミスライムに10のダメージを与えた!
    スライムの攻撃: 9のダメージを受けた!
    ホイミスライムの攻撃: 6のダメージを受けた!
    キングスライムの攻撃: 12のダメージを受けた!
7ターン:
    勇者の攻撃: スライムに9のダメージを与えた!
    スライムの攻撃: 9のダメージを受けた!
    ホイミスライムの攻撃: 6のダメージを受けた!
    キングスライムの攻撃: 24のダメージを受けた!
8ターン:
    勇者の攻撃: ホイミスライムに10のダメージを与えた!
    ホイミスライムを倒した!
    スライムの攻撃: 9のダメージを受けた!
    キングスライムの攻撃: 12のダメージを受けた!
9ターン:
    勇者の攻撃: スライムに9のダメージを与えた!
    スライムの攻撃: 9のダメージを受けた!
    キングスライムの攻撃: 24のダメージを受けた!
10ターン:
    勇者の攻撃: スライムに9のダメージを与えた!
    スライムを倒した!
    キングスライムの攻撃: 24のダメージを受けた!
11ターン:
    勇者の攻撃: キングスライムに7のダメージを与えた!
    キングスライムの攻撃: 24のダメージを受けた!
12ターン:
    勇者の攻撃: キングスライムに7のダメージを与えた!
    キングスライムの攻撃: 24のダメージを受けた!
...
17ターン:
    勇者の攻撃: キングスライムに7のダメージを与えた!
    キングスライムを倒した!
    勇者は勝利した!

モンスターは「スライム」「ホイミスライム」「キングスライム」の3種類を用意した。そのターンに何をするかはモンスターごとに戦略が違う。スライムはただただ攻撃するだけだし、ホイミスライムはHPが少ないと一定の確率でホイミを唱える。また、キングスライムは1/2の確率で会心の一撃を放つようにしてみた。

それぞれのモンスターはMonsterクラスを継承しており、必ず(といってもAbstractが無いのでJavaと違って言語レベルでの強制はできていないのだが)actメソッドを持っている。モンスターを扱うBattleクラスはこのactメソッドを使うことのみを知っていれば良く、モンスターの種類やそのモンスターが何を考えているのかについては無関心で構わない。

このactメソッドがOOPの利点として非常に重要で、戦闘ごとに違う種類のモンスターを組み合わせたり、色々な種類のモンスターを作っても、Monsterクラスを継承したモンスターを使う限りは、actメソッドを媒体としてBattleクラスは全く変更無く使用することが出来る。

モンスターの種類による戦略(AI)の切り替えをifによる条件分岐で行うとしたらどうなるだろうか? Battle.prototype.nextTurnにはモンスターの種類が増えるゴトににどんどんifが増えていき、メンテナンス性が最悪になるだろう。

function Battle.prototype.nextTurn() {
    ...
    for (var i = 0; i < this.monsters.length; i++) {
        var monsterName = this.monsters[i];
        if (monsterName == "スライム") {
            ...
        } else if (monsterName == "ホイミスライム") {
            ...
        } else if (...) {
            ...
        }
    }
}

Strategyパターンでは「モンスター」という「単位」でオブジェクトを分割することにより処理を分散し、メンテナンス性を高めていると言える。

Comments
2
Trackbacks
0
PermaLink
http://yudai.arielworks.com/memo/2005/02/24/090533

JavaScriptでOOP?

Created:
2005-02-24T22:54:15+09:00

JavaScriptでデザインパターンのサンプルコードに対してfunction Class.prototype.method() {...}という書き方はJScriptの方言というコメントを頂いた。そもそも石川はJavaScriptに余り興味がないため、ECMAScript方面の詳細な情報を知らなかったりする。正直なところ、ブラウザごとの互換性を考えるのが面倒なのでJavaScriptはあんまり使ったことがない。ECMASciptの使い道としてはむしろWSHがメインなので、JScriptとして動けばそれでよいのだ。なお、ECMAScriptとJavaScript、JScriptの違いについては各自検索されたし。

とはいえ、標準化された構文についても一応は知っておいた方が良いので、軽く調べてみた。

ふーむ。なるほど。コンストラクタ内でプロパティに関数オブジェクトを代入するのが正解かな。匿名関数を使ってClass.prototype.method = function() {}と書くのがスマートっぽい予感。あれ、でもECMAだとnew Function()じゃないとダメなのかな? ところで、メソッドをコンストラクタの外で定義した場合、何か差異があるのだろうか。スコープとメモリの使い方に違いが出るような出ないような。

ていうか、this.propClass.prototype.propvar propの使い分けとか、真面目に考えると混乱しそうな雰囲気。一度ちゃんと言語仕様読まないとダメかな。

Comments
0
Trackbacks
0
PermaLink
http://yudai.arielworks.com/memo/2005/02/24/225415
連絡先、リンク、転載や複製などについては『サイト案内』をご覧ください。Powered by HIMMEL

I ♥ Validator