Lester
Lester

Reputation: 3

How to create a class which extends a not predetermined other class

I'm trying to create a class which should be able the extend several base classes. In the following example I would like to use either stone or wood as base classes for class house. For that I tried creating a material class which would select the proper base class. But I don't get it to work.

const EventEmitter = require('events').EventEmitter; class Stone extends EventEmitter{ constructor(weight, color){ super(); this.color = color; this.weight = weight; this.hard = true this.start(); } testEmitterFunction(){ this.emit('test'); } start(){ setInterval(this.testFunc.bind(this), 500); } } class Wood{ constructor(weight, color){ this.color = color; this.weight = weight; this.hard = false; } } class Material{ constructor(mat, weight, color){ switch(mat){ case 'wood': return new Wood(weight, color); case 'stone': return new Stone(weight, color); } } } class House extends Material{ constructor(mat, weight, color, name){ super(mat, weight, color) this.name = name; this.on('test', (arg) => { console.log('1') }); this.test(); } test(){ console.log('test house function'); } } class House2 extends Stone{ constructor(weight, color, name){ super(weight, color) this.name = name; this.on('test', (arg) => { console.log('2') }); this.test(); } test(){ console.log('test house2 function'); } } const home = new House('stone', 8, 'green', 'homesweethome'); const home2 = new House2(8, 'green', 'homesweethome');

I would like that instance home would have the same behaviour as instance home2. But in this case the test function which does the console.log('test house function') does not work. I tried other solutions, but then the EventEmitter would not work or the stone properties would not be available.

Upvotes: 0

Views: 46

Answers (1)

Khauri
Khauri

Reputation: 3863

As I mentioned in my comment, what you're trying to do might be better done by using composition over inheritance.

As a general simplification if you can say "My X is a type of Y" or "My X is a Y" and it makes sense, that's inheritance, but if you say "My X is made of Y" or "My X contains Y" then you should use composition. Applied to your case, Stone and Wood are both a type of Material. Is a House a type of Material? I wouldn't say so, but a House is made of Stone or Wood, or rather, a House is made of Material, which means we should use composition for that.

If you want to keep the ability to pass a string to the House constructor that sets the material, then you can still do that. See House#setMaterial in the code sample at the bottom, though a factory pattern might work better for you in the future.

Another problem with your structure is that it kills polymorphism. If you wanted methods that did the same thing in both Stone and Wood, say "breaking", then you'd have to copy-paste the same code over, but if they both inherited from a generic Material type, then you only need to create the method once in the base class.

I want to be able to use the EventEmitter from stone for my house as well. i.e. house.on(...) instead of house.stone.on(...)

When it comes to using an eventemitter, I'd recommend you create one at the highest level possible and then pass it to the components that need it. In this case, House can pass an event emitter to the material or any other components (such as a room). Due to the craziness of Javascript, House can be the eventemitter and pass itself to the material. See the House#setEmitter function in the House class below. The observe how it's used in the polymorphic function Material#Break.

/** Unimportant */
class EventEmitter {
  constructor(){ this.map = {} }
  on(e, cb){
    if(!this.map[e]) this.map[e] = []
    this.map[e].push(cb)
  }
  emit(event,...data){
    if(!this.map[event]) return
    this.map[event].forEach(cb=>cb(...data))
  }
}
/**/

class Material {
  constructor(name = 'Something', weight = 5, color = 'black', hard = true){
    this.weight = weight
    this.color = color
    this.hard = hard
    this.name = name
  }
  setEmitter(emitter){
    this.emitter = emitter
  }
  
  break(){
    if(this.emitter){
      this.emitter.emit(`break`, `The ${this.name} has broken` )
    }
  }
  
  describe(){
    return `${this.weight}lb ${this.hard?'hard':'soft'} ${this.color} ${this.name}`
  }
}

class Stone extends Material {
  constructor(weight = 8, color = 'gray'){
    super("Stone", weight, color, true)
  }
}

class Wood extends Material {
  constructor(weight=4, color="brown"){
    super("Wood", weight, color, false)
  }
}

class House extends EventEmitter {
  constructor(material, name){
    super()
    this.material = this.setMaterial(material)
    this.name = name
    this.on('break', (what)=>{
      console.log(`${this.name} Event: `+what)
    })
  }
  
  setMaterial(mat){
    const matMap = {
      stone : Stone,
      wood : Wood
    }
    // Just gets a default material
    if(typeof mat == 'string'){
      mat = new matMap[mat]()
    }
    mat.setEmitter(this)
    return mat
  }
  // Logs information about the material
  describe(){
    console.log(`A house named ${this.name} made out of ${this.material.describe()}`)
  }
}


// Examples

// Method 1: Create a basic stone house and set material color later
const stoneHouse = new House("stone", "MyHouse")
stoneHouse.describe()
stoneHouse.material.color = "blue"
stoneHouse.describe()
stoneHouse.material.break()

// Method 2: Create a material and pass it to the house
const myMaterial = new Wood(6, "green")
const woodHouse = new House(myMaterial, "WoodHouse")
woodHouse.describe()
// Call a function that emits an event to the house
myMaterial.break()

Upvotes: 2

Related Questions