如果某个子类不能使用某些策略,如何使用策略设计模式?

How to use strategy design pattern if a certain subclass cannot use some of the strategies?

我的任务是实现一个游戏。我的部分任务是实现怪物的行为。任务中说怪物有不同的攻击方式,每次攻击都有一定的伤害率。我还必须为每个怪物随机生成一次攻击。

A 可以击中(伤害:5)和喷火(伤害:20)。
蜘蛛 可以击中(伤害:5)和撕咬(伤害:8)。

我的想法是创建一个抽象的 Monster class 并让 class Spider and Dragon 扩展这个 class.
然后我会创建一个名为 Attack 的接口,方法是 attack 并创建 subclasses Hit, FireBite 将此接口实现为策略。我还创建了两种生成器方法,一种用于 Dragon,一种用于 Spider,但我认为这不太好,也许有人知道更好的方法。

abstract class Monster {
  private health = 200;
  attack: Attack;

  constructor(attack: Attack) {
    this.attack = attack;
  }

  getAttackDamage() {
    return this.attack.attack();
  }

  getHealth() {
    return this.health;
  }

  damage(damage: number) {
    let isDefeated = false;
    this.health -= damage;

    if (this.health <= 0) {
      isDefeated = true;
    }

    return isDefeated;
  }
}

class Dragon extends Monster {
  constructor() {
    super(attackDragonGenerator());
  }

  setAttack() {
    this.attack = attackDragonGenerator();
  }
}

class Spider extends Monster {
  constructor() {
    super(attackSpiderGenerator());
  }

  setAttack() {
    this.attack = attackSpiderGenerator();
  }
}

interface Attack {
  attack: () => number;
  damage: number;
}

class Hit implements Attack {
  damage;

  constructor(damage: number) {
    this.damage = damage;
  }

  attack() {
    return this.damage;
  }
}

class Bite implements Attack {
  damage;

  constructor(damage: number) {
    this.damage = damage;
  }

  attack() {
    return this.damage;
  }
}

class Fire implements Attack {
  damage;

  constructor(damage: number) {
    this.damage = damage;
  }

  attack() {
    return this.damage;
  }
}

const attacksSpider: Attack[] = [new Hit(5), new Bite(8)];
const attacksDragon: Attack[] = [new Hit(5), new Fire(20)];

const attackSpiderGenerator = () => {
  const index = randomIntFromInterval(0, 1);
  return attacksSpider[index];
};

const attackDragonGenerator = () => {
  const index = randomIntFromInterval(0, 1);
  return attacksDragon[index];
};

怪物之间的互动

显然您的 有效 ,但还有改进的余地。您可以通过多种方式将各个部分组合在一起。现在你的 Monster 可以攻击 return 一个 number 并且可以接收 damage 需要一个 number。我建议这些方法之一应该与其他对象而不是数字进行交互。

现在我要定义一个 AttackStrategy 作为方法 attack() 其中 return 是一个对象 damage number 作为 属性 而不仅仅是 returning number。我将在“组合攻击”部分解释推理。

interface AttackData {
    damage: number;
    name: string;
}

interface AttackStrategy {
    attack(): AttackData;
}

在此版本中,一个 Monster 调用另一个 MonstertakeDamage() 方法,并使用其 doAttack() 方法中的 number

interface CanAttack {
    attack(target: CanTakeDamage): void;
}

interface CanTakeDamage {
    takeDamage(damage: number): void;
}

class Monster implements CanTakeDamage, CanAttack {
    constructor(
        public readonly name: string,
        private strategy: AttackStrategy,
        private _health = 200
    ) { }

    attack(target: CanTakeDamage): void {
        const attack = this.strategy.attack();
        target.takeDamage(attack.damage);
        console.log( `${this.name} used ${attack.name}` );
    }

    takeDamage(damage: number): void {
        // don't allow negative health
        this._health = Math.max(this._health - damage, 0);
    }

    get health(): number {
        return this._health;
    }

    get isDefeated(): boolean {
        return this._health === 0;
    }
}

反向方法是 takeDamage() 方法接收攻击 Monster 作为参数,doAttack() 到 return 伤害 number .

我认为在这种情况下这没有多大意义。但希望它能说明一点。不一定有“错误”的拼凑方式,但有一些方式感觉更合乎逻辑和自然。所以和那些一起去吧!我不喜欢这个,因为目标负责调用攻击者的 doAttack() 方法,感觉倒退了。

class Monster implements CanTakeDamage, CanAttack {
    constructor(
        public readonly name: string,
        private strategy: AttackStrategy,
        private _health = 200
    ) { }

    attack(): AttackData {
        const attack = this.strategy.attack();
        console.log(`${this.name} used ${attack.name}`);
        return attack;
    }

    takeDamage(attacker: CanAttack): void {
        const { damage } = attacker.attack();
        // don't allow negative health
        this._health = Math.max(this._health - damage, 0);
    }

    get health(): number {
        return this._health;
    }

    get isDefeated(): boolean {
        return this._health === 0;
    }
}

创建攻击

我会从 interface Attack 中删除 属性 damage 并将其视为实现细节。如果我们想要 Monster 具有多种具有不同伤害值的攻击类型,这将很重要。现在 Attack 只有一个方法 attack() 可以执行攻击和 return 伤害量。

HitBiteFire 现在完全相同。他们要么需要更多的差异化,要么需要成为一般 Attack class 的实例。我们仍然可以通过将不同的参数传递给构造函数(如 message: stringname: string

来支持对一般 Attack 实例的一定程度的区分

分开Classes

interface Attack {
    attack(): number;
}

class BaseAttack implements Attack {
    constructor(public readonly damage: number) { }

    protected message(): string {
        return "You've been attacked!";
    }

    attack(): number {
        console.log(this.message());
        return this.damage;
    }
}

class Hit extends BaseAttack {
    protected message(): string {
        return `POW! Strength ${this.damage} punch incoming!`;
    }
}

class Bite extends BaseAttack {
    protected message(): string {
        return "Chomp!";
    }
}

class Fire extends BaseAttack {
    protected message(): string {
        return "Burn baby, burn!";
    }
}

单身Class

class Attack implements AttackStrategy {
    constructor(private damage: number, private name: string) { }

    attack(): AttackData {
        return {
            name: this.name,
            damage: this.damage
        }
    }
}

const attack1 = new Attack(10, "Chomp!");
const attack2 = new Attack(5, "Slap");

单个class更灵活,因为我们可以创造无限的攻击。


组合攻击

你的 attackSpiderGeneratorattackDragonGenerator 不允许单个怪物实例在攻击之间切换。构造函数随机选择一个,就是这个实例的攻击。

我们想要创建一个组合攻击的助手,同时仍然符合与单个攻击相同的接口。

如果我们想知道调用的攻击的 name,那么我们的方法 attack(): number 是不够的,因为组合攻击的名称不同。所以让我们稍微改变一下接口。我们定义了一个包含 namedamage 属性的 AttackDataAttackStrategy 有一个函数 attack(),return 是一个 AttackData

interface AttackData {
    damage: number;
    name: string;
}

interface AttackStrategy {
    attack(): AttackData;
}

我让 AttackSwitcher constructor 接受可变数量的参数,其中每个参数要么是 AttackStrategy 要么是 AttackStrategyweight为频率。每次攻击的默认权重为 1。我们将对这些值进行归一化,以便以正确的概率选择随机攻击。

type AttackArg = AttackStrategy | [AttackStrategy, number];

class AttackSwitcher implements AttackStrategy {
    private attacks: [AttackStrategy, number][];

    // must have at least one arg
    constructor(...args: [AttackArg, ...AttackArg[]]) {
        // default weight is 1 per attack if not assigned
        const tuples = args.map<[AttackStrategy, number]>((arg) =>
            Array.isArray(arg) ? arg : [arg, 1]
        );
        // normalize so that the sum of all weights is 1
        const sum = tuples.reduce((total, [_, weight]) => total + weight, 0);
        this.attacks = tuples.map(([attack, weight]) => [attack, weight / sum]);
    }

    private getRandomAttack(): AttackStrategy {
        // compare a random number to the rolling sum of weights
        const num = Math.random();
        let sum = 0;
        for (let i = 0; i < this.attacks.length; i++) {
            const [attack, weight] = this.attacks[i];
            sum += weight;
            if (sum >= num) {
                return attack;
            }
        }
        // should not be here except due to rounding errors
        console.warn("check your math");
        return this.attacks[0][0];
    }

    attack(): AttackData {
        return this.getRandomAttack().attack();
    }
}

创造怪物

对于具有相同伤害值的相同攻击,是否所有蜘蛛都具有相同的权重?这些选择取决于您。

这个 SpiderDragon 根本 不需要 成为 class 因为我们真正做的只是构建具有特定参数的 Monster 的特定实例。

class Spider extends Monster {
    constructor(name: string = "Spider") {
        super(
            name,
            new AttackSwitcher(
                // 5:1 ratio of Bite to Hit
                new Attack(5, "8-Legged Slap"),
                [new Attack(8, "Spider Bite"), 5]
            ),
            // 100 base health
            100
        );
    }
}

class Dragon extends Monster {
    constructor(name: string = "Dragon") {
        super(
            name,
            new AttackSwitcher(
                // equal incidence of both attacks
                new Attack(5, "Tail Whip"),
                new Attack(20, "Fire  Breath")
            )
        );
    }
}

怪物队

我们的怪物不是势均力敌,所以我不得不给予 Spider 更多的攻击机会来获得战斗结果的任何差异。我们调用 spider.attack(dragon)dragon.attack(spider).

function testAttacks() {
    const spider = new Spider();
    const dragon = new Dragon();

    let i = 0;
    while (! spider.isDefeated && ! dragon.isDefeated ) {
        if ( i % 5 ) {
            spider.attack(dragon);
            console.log(`dragon health: ${dragon.health}`);
        } else {
            dragon.attack(spider);
            console.log(`spider health: ${spider.health}`);
        }
        i++;
    }
    console.log( spider.isDefeated ? "DRAGON WINS!" : "SPIDER WINS!" );
}

让一队蜘蛛对抗一条龙怎么样?使用与组合攻击相同的方法,我们定义了一个接口 CanBattle 由单个 MonsterBattleTeam 怪物共享。

interface CanBattle extends CanAttack, CanTakeDamage {
    health: number;
    isDefeated: boolean;
    name: string;
}
class BattleTeam implements CanBattle {
    private monsters: CanBattle[];
    private currentIndex: number;

    // must have at least one monster
    constructor(
        public readonly name: string,
        ...monsters: [CanBattle, ...CanBattle[]]
    ) {
        this.monsters = monsters;
        this.currentIndex = 0;
    }

    // total health for all monsters
    get health(): number {
        return this.monsters.reduce(
            (total, monster) => total + monster.health
            , 0);
    }

    // true if all monsters are defeated
    get isDefeated(): boolean {
        return this.health === 0;
    }

    // the current attacker/defender
    get current(): CanBattle {
        return this.monsters[this.currentIndex];
    }

    // damage applies to the current monster only
    takeDamage(damage: number): void {
        this.current.takeDamage(damage);
        // maybe move on to the next monster
        if (this.current.isDefeated) {
            console.log(`${this.current.name} knocked out`);
            if (this.currentIndex + 1 < this.monsters.length) {
                this.currentIndex++;
                console.log(`${this.current.name} up next`);
            }
        }
    }

    // current monster does the attack
    attack(target: CanTakeDamage): void {
        this.current.attack(target);
    }
}

战斗

一场战斗需要两个 CanBattle 物体轮流攻击对方,直到一个被打败。这一系列的攻击是一个迭代,所以我们可以用iterator protocol.

一场战斗可以看作是一个迭代器,每次迭代都会从交替的双方发生攻击

class Battle implements Iterator<CanBattle, CanBattle>, Iterable<CanBattle> {

    private leftIsAttacker: boolean = true;
    private _winner: CanBattle | undefined;

    constructor(public readonly left: CanBattle, public readonly right: CanBattle) { }

    // returns the target of the current attack as `value`
    public next(): IteratorResult<CanBattle, CanBattle> {
        const attacker = this.leftIsAttacker ? this.left : this.right;
        const target = this.leftIsAttacker ? this.right : this.left;
        if (!this.isCompleted) {
            attacker.attack(target);
            this.leftIsAttacker = !this.leftIsAttacker;
        }
        if (target.isDefeated) {
            this._winner = attacker;
        }
        return {
            done: this.isCompleted,
            value: target,
        }
    }

    [Symbol.iterator]() {
        return this;
    }

    get winner(): CanBattle | undefined {
        return this._winner;
    }

    get isCompleted(): boolean {
        return this._winner !== undefined;
    }
}
function testBattle() {
    const dragon = new Dragon("Dragon");
    const spiderTeam = new BattleTeam(
        "Spider Team",
        // @ts-ignore warning about needed at least 1
        ...[1, 2, 3, 4].map(n => new Spider(`Spider ${n}`))
    )

    const battle = new Battle(dragon, spiderTeam);
    for (let target of battle) {
        console.log(`${target.name} health: ${target.health}`);
    }
    console.log(spiderTeam.isDefeated ? "DRAGON WINS!" : "SPIDER WINS!");
}

完整代码

Typescript Playground - click "Run" to see who wins!