Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Functional "real-time" composition

I recently came across a great article covering the benefits of object composition VS traditional inheritance.

Hopefully my question is not going to be flagged as opinionated but I'd like to know a good approach to using composition for when an object changes based on a user's game interaction.

Using the articles code as an example:

const canCast = (state) => ({
    cast: (spell) => {
        console.log(`${state.name} casts ${spell}!`);
        state.mana--;
    }
})

const canFight = (state) => ({
    fight: () => {
        console.log(`${state.name} slashes at the foe!`);
        state.stamina--;
    }
})

const fighter = (name) => {
  let state = {
    name,
    health: 100,
    stamina: 100
  }

  return Object.assign(state, canFight(state));
}

const mage = (name) => {
  let state = {
    name,
    health: 100,
    mana: 100
  }

  return Object.assign(state, canCast(state));
}

scorcher = mage('Scorcher')
scorcher.cast('fireball');    // Scorcher casts fireball!
console.log(scorcher.mana)    // 99

slasher = fighter('Slasher')
slasher.fight();              // Slasher slashes at the foe!
console.log(slasher.stamina)  // 99

How do I use composition to change the state of the Character object during run-time? Instead of the Mage object already existing I want the Character object to change based on a game event eg. Character picks up a staff and now becomes a "Mage" who can now Cast spells. First thing that comes to mind is to have a state property in Character that changes based on the interaction and the Character somehow "inherits" the ability to now Cast and gains a mana state property.

like image 297
Wancieho Avatar asked Sep 08 '26 14:09

Wancieho


1 Answers

The decorator pattern solves situations exactly like this.

class Character {
  constructor(name) {
    this.name = name;
    this.health = 100;
    this.items = [];
  }
}

const fighterDecorator = character => {
  return Object.setPrototypeOf({
    character,
    stamina: 100,
    fight() {
      console.log(`${this.name} slashes at the foe!`);
      this.stamina--;
    }
  }, character);
}

const mageDecorator = character => {
  return Object.setPrototypeOf({
    character,
    mana: 100,
    cast(spell) {
      console.log(`${this.name} casts ${spell}!`);
      this.mana--;      
    }
  }, character);
}

let character = new Character("Bob");

// Can't fight; can't cast
// character.fight(); // TypeError: not a function
// character.cast(); // TypeError: not a function

// Character becomes a fighter at runtime
// Equiping an item and decorating new behavior are separate statements
character.items.push("sword");
character = fighterDecorator(character);
character.fight();              // Bob slashes at the foe!
console.log(character.stamina)  // 99
console.log(character.items)    // ["sword"]

// Character becomes normal unit again
// Remove decoration and remove item
character = character.character;
character.items = character.items.filter(item => item !== "sword");

// Once again, can't fight, can't cast
// character.fight(); // TypeError: not a function
// character.cast(); // TypeError: not a function

// Character becomes a mage at runtime
// Equiping an item and decorating new behavior are separate statements
character.items.push("staff");
character = mageDecorator(character);
character.cast("fireball");  // Bob casts fireball!
console.log(character.mana)  // 99
console.log(character.items) // ["staff"]
like image 141
Jeff M Avatar answered Sep 10 '26 03:09

Jeff M