Reputation: 3
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
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