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.
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()
Related
I've seen a tutorial where a possible function is passed to the parent like below:
class Car {
constructor(door, engine, color){
this.door = door;
this.engine = engine;
this.color = color;
}
carStats(){
console.log('in car stats...');
}
}
class SUV extends Car {
constructor(door, engine, color, brand, carStats){
super(door, engine, color, carStats){
this.brand = brand;
this.wheels = 5;
this.cheap = true;
}
}
Is there any possibility of overwriting the parent method if you pass another function to it?
Thanks
When calling super, you actually call the constructor for Car, but the Car constructor only expects 3 arguments, not 5.
You should be able to override the carStats method by redefining it, but I don't think this is considered to be best practice.
class SUV extends Car {
constructor(door, engine, color, brand, carStats){
super(door, engine, color);
this.carStats = carStats;
this.brand = brand;
}
}
I was dabbling with singletons and extending classes and came up with this 'working' code.
let instance = null;
class Motorcycle {
constructor(engine, color) {
if(!instance) {
this.engine = engine;
this.color = color;
instance = this;
} else {
return instance;
}
}
}
class Car extends Motorcycle {
constructor(engine, color) {
if(!instance) {
super(engine, color);
this.doors = 4;
} else {
return instance;
}
}
}
class SUV extends Car {
constructor(engine, color, doors) {
if(!instance) {
super(engine,color);
instance.doors = doors == undefined ? 5 : doors;
} else {
return instance;
}
}
}
I realized that in the constructor for sub-classes I can set doors with instance.doors and this.doors.
So I have two questions:
Does it matter enough to have a best practice?
What is that best practice?
Does it matter enough to have a best practice?
Not in your case probably, but there is a difference between them.
What is that best practice?
Do not use singletons. Especially not when subclassing. Your code is unable to create an instance of a separate subclass, is unable to create multiple cars, is unable to create a new SUV after creating a new Car (and also a Car is not a MotorCycle).
Your singleton objects should exist outside the constructor; preferably inside a map and accessed by their class name. Constructors should really only call their super constructor or set instance variables.
You will also need a way to dynamically bind constructor arguments, this is taken care of in the ctorBind function below.
You could also call getSingleton(SUV) a second time without args. This example just shows that the original SUV was not modified upon returning.
// REFLECTION UTILS
const getClassName = (obj) => obj.toString().match(/ (\w+)/)[1]
const ctorBind = (ctor, args) => new (ctor.bind.apply(ctor, [null].concat(args)))()
// MAIN FUNCTION
const main = () => {
console.log(getSingleton(SUV, 'V6', 'Red', 5))
console.log(getSingleton(SUV, 'V8', 'Blue', 6)) // Not updated...
}
// BEGIN SINGLETON MAP AND LOOKUP
const vehicleSingletons = {};
function getSingleton(vehicleClass, ...args) {
const className = getClassName(vehicleClass)
if (vehicleSingletons[className] == null) {
vehicleSingletons[className] = ctorBind(vehicleClass, args);
}
return vehicleSingletons[className];
}
// BEGIN CLASS DEFINITIONS
class Vehicle {
constructor(engine, color) {
this.engine = engine;
this.color = color;
}
}
class Motorcycle extends Vehicle {
constructor(engine, color) {
super(engine, color);
}
}
class Car extends Vehicle {
constructor(engine, color, doors) {
super(engine, color);
this.doors = doors;
}
}
class SUV extends Car {
constructor(engine, color, doors) {
super(engine, color, doors);
}
}
main()
.as-console-wrapper { top: 0; max-height: 100% !important; }
This is a short piece of code from mazeContainer.js with only necessary part-
import Cell from "./cell.js";
import Player from "./player.js";
export default class Maze {
....
setup(){
for (let rowNum = 0; rowNum < this.rows; rowNum++) {
let row = [];
for (let colNum = 0; colNum < this.columns; colNum++) {
let cell = new Cell(this.ctx, rowNum, colNum, this.cellWidth, this.cellHeight);
row.push(cell);
}
this.grid.push(row);
}
drawMap(){
....
let player = new Player(this.goal, this.lastRow, this.lastColumn);
....
}
}
And player.js-
import Cell from "./cell.js";
export default
class Player extends Cell {
constructor(goal, lastRow, lastColumn) {
super(); // need to manage this statement
this.goal = goal;
this.lastRow = lastRow;
this.lastColumn = lastColumn;
}
....
}
Now here's what I'm having trouble with.
I've just encountered the super keyword and what I've got to know so far is that I need to call super method before using this. That's not an issue. But here I also need to provide all parameters for Cell's constructor.
As you can see, the Cell class has got many parameters in its constructor, so how do I hand them over to new Player(....)?
Is there a better way to achieve this?
• The extends keyword makes the methods of the animal class available inside the cat class.
• The constructor, called when you create a new Cat object, accepts two arguments, name and usesLitter.
• The super keyword calls the constructor of the parent class. In this case, super(name) passes the name argument of the Cat class to the constructor of the Animal class. When the Animal constructor runs, it sets this._name = name; for new Cat instances.
• _usesLitter is a new property that is unique to the Cat class, so we set it in the Cat constructor.
class Animal {
constructor(name) {
this._name = name;
}
get name() {
return this._name;
}
}
class Cat extends Animal {
constructor(name, usesLitter) {
super(name); // The super keyword calls the constructor of the parent class.
this._usesLitter = usesLitter;
}
get usesLitter() {
return this._usesLitter;
}
}
const bryceCat = new Cat('Bryce', true);
console.log(bryceCat.name); //Output: Bryce
console.log(bryceCat.usesLitter); //Output: true
I have a question about super() in javascript.
Should i call the parent class method 'stringy()' with super() in the child class or i can use it like this:
function solve() {
class Melon {
constructor(weight, melonSort) {
if (new.target === Melon) {
throw new Error('Abstract class cannot be instantiated directly.')
}
this.weight = weight;
this.melonSort = melonSort;
this._elementIndex = weight * Number(melonSort.length);
}
stringy() {
return `Element: ${this.element}\nSort: ${this.melonSort}\nElement Index: ${this._elementIndex}`
}
}
class Watermelon extends Melon {
constructor(weight, melonSort) {
super(weight, melonSort);
this.element = 'Water';
}
}
or
function solve() {
class Melon {
constructor(weight, melonSort) {
if (new.target === Melon) {
throw new Error('Abstract class cannot be instantiated directly.')
}
this.weight = weight;
this.melonSort = melonSort;
this._elementIndex = weight * Number(melonSort.length);
}
stringy() {
return `Element: ${this.element}\nSort: ${this.melonSort}\nElement Index: ${this._elementIndex}`
}
}
class Watermelon extends Melon {
constructor(weight, melonSort) {
super(weight, melonSort);
this.element = 'Water';
}
stringy(){
return super.stringy();
}
}
which one is correct and what is the difference.
There's no need to include stringy in Watermelon unless you want to change the behavior. Watermelon instances will inherit the Melon version if you don't. Your two versions of Watermelon are very nearly identical, and calls to stringy on instances of either version will create and return the same string. If stringy used arguments, your overwritten one would need to be sure to pass all arguments it received along, but stringy doesn't use any, so...
(Just for completeness: The only slight difference is that in your second version of Watermelon, there is a Watermelon.prototype.stringy function, whereas in your first version there isn't. It's fine that there isn't, because Watermelon.prototype inherits from Melon.prototype, which has stringy.)
Assume that class Cheddar inherits from a class which expects an object as a parameter; essentially composition:
class Brand {
constructor(b) {
this.brand = b;
}
getBrand() {
return this.brand;
}
}
class Cheese {
constructor(brand_obj) {
// do stuff...
}
}
1
class Cheddar extends Cheese {
constructor(b) {
super(new Brand(b)); // <-- HERE, I'm a "one-liner" kinda guy :D
}
}
Now when I instantiate:
let snack = new Cheddar("Cabot Clothbound");
I can't access the Brand object, because it was created as an argument.
So, I tried to create the Brand and put it on the object before calling super, like this:
2
class Cheddar extends Cheese {
constructor(b) {
this.brand = new Brand(b);
super(this.brand);
}
}
...which results in the following error:
'this' is not allowed before super()
Grr.. so, I could do it this way:
3
class Cheddar extends Cheese {
constructor(b) {
let myBrand = new Brand(b);
super(myBrand);
this.brand = myBrand;
}
getBrand() {
return this.brand.getBrand();
}
}
And I can now happily access the methods on the cheese object like so:
let snack = new Cheese("Cabot Clothbound");
console.log(snack.getBrand()); //-> Cabot Clothbound
...but, it's getting messy. I want to be a "one-liner guy."
Anyway to access the object created as an argument to this constructor, or can I possibly structure things a bit differently to make this easier? I feel like I'm working too hard here. Thx, Keith :^D
You are not finishing the construction of Cheese. Your // do stuff should include saving the brand_obj if you want to access it from subclasses.
When you call super(new Brand(b)) the brand object will end up in the super class's constructor. But you aren't doing anything with it there. If save the brand as a property on the super class it will be available to the subclass:
class Brand {
constructor(b) {
this.brand = b;
}
getBrand() {
return this.brand;
}
}
class Cheese {
constructor(brand_obj) {
this.brand = brand_obj // <= save this!!
}
}
class Cheddar extends Cheese {
constructor(b) {
super(new Brand(b));
console.log("brand in cheddar constructor", this.brand) //<== now you can use it
}
}
let snack = new Cheddar("Cabot Clothbound");
console.log("Brand on instance: ", snack.brand)