如果某个子类不能使用某些策略,如何使用策略设计模式?
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, Fire 和 Bite 将此接口实现为策略。我还创建了两种生成器方法,一种用于 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
调用另一个 Monster
的 takeDamage()
方法,并使用其 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 伤害量。
Hit
、Bite
和 Fire
现在完全相同。他们要么需要更多的差异化,要么需要成为一般 Attack
class 的实例。我们仍然可以通过将不同的参数传递给构造函数(如 message: string
、name: 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更灵活,因为我们可以创造无限的攻击。
组合攻击
你的 attackSpiderGenerator
和 attackDragonGenerator
不允许单个怪物实例在攻击之间切换。构造函数随机选择一个,就是这个实例的攻击。
我们想要创建一个组合攻击的助手,同时仍然符合与单个攻击相同的接口。
如果我们想知道调用的攻击的 name
,那么我们的方法 attack(): number
是不够的,因为组合攻击的名称不同。所以让我们稍微改变一下接口。我们定义了一个包含 name
和 damage
属性的 AttackData
。 AttackStrategy
有一个函数 attack()
,return 是一个 AttackData
。
interface AttackData {
damage: number;
name: string;
}
interface AttackStrategy {
attack(): AttackData;
}
我让 AttackSwitcher
constructor
接受可变数量的参数,其中每个参数要么是 AttackStrategy
要么是 AttackStrategy
和 weight
为频率。每次攻击的默认权重为 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();
}
}
创造怪物
对于具有相同伤害值的相同攻击,是否所有蜘蛛都具有相同的权重?这些选择取决于您。
这个 Spider
和 Dragon
根本 不需要 成为 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
由单个 Monster
和 BattleTeam
怪物共享。
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!");
}
完整代码
我的任务是实现一个游戏。我的部分任务是实现怪物的行为。任务中说怪物有不同的攻击方式,每次攻击都有一定的伤害率。我还必须为每个怪物随机生成一次攻击。
A 龙 可以击中(伤害:5)和喷火(伤害:20)。
蜘蛛 可以击中(伤害:5)和撕咬(伤害:8)。
我的想法是创建一个抽象的 Monster class 并让 class Spider and Dragon 扩展这个 class.
然后我会创建一个名为 Attack 的接口,方法是 attack 并创建 subclasses Hit, Fire 和 Bite 将此接口实现为策略。我还创建了两种生成器方法,一种用于 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
调用另一个 Monster
的 takeDamage()
方法,并使用其 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 伤害量。
Hit
、Bite
和 Fire
现在完全相同。他们要么需要更多的差异化,要么需要成为一般 Attack
class 的实例。我们仍然可以通过将不同的参数传递给构造函数(如 message: string
、name: 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更灵活,因为我们可以创造无限的攻击。
组合攻击
你的 attackSpiderGenerator
和 attackDragonGenerator
不允许单个怪物实例在攻击之间切换。构造函数随机选择一个,就是这个实例的攻击。
我们想要创建一个组合攻击的助手,同时仍然符合与单个攻击相同的接口。
如果我们想知道调用的攻击的 name
,那么我们的方法 attack(): number
是不够的,因为组合攻击的名称不同。所以让我们稍微改变一下接口。我们定义了一个包含 name
和 damage
属性的 AttackData
。 AttackStrategy
有一个函数 attack()
,return 是一个 AttackData
。
interface AttackData {
damage: number;
name: string;
}
interface AttackStrategy {
attack(): AttackData;
}
我让 AttackSwitcher
constructor
接受可变数量的参数,其中每个参数要么是 AttackStrategy
要么是 AttackStrategy
和 weight
为频率。每次攻击的默认权重为 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();
}
}
创造怪物
对于具有相同伤害值的相同攻击,是否所有蜘蛛都具有相同的权重?这些选择取决于您。
这个 Spider
和 Dragon
根本 不需要 成为 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
由单个 Monster
和 BattleTeam
怪物共享。
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!");
}