One method to achieve this is by utilizing an object-oriented approach with prototype linkage. However, for the sake of clarity, I will be sticking to class syntax.
While this approach differs significantly in technical terms, it still offers a similar interface.
To start off, we need a base object that holds essential data and defines the interface for all shapes. It's necessary that all shapes can provide their area based on their internal data:
class Shape {
/**
* @param {object} data
*/
constructor(data) {
this.data = data;
}
/**
* Computes the area of the shape. This method should be implemented by
* all extending classes.
*
* @abstract
* @return {number|NaN}
*/
calculateArea() { return NaN; }
}
We can now establish some subclasses, where each 'implements' (or technically, 'overrides') the calculateArea
method:
class Square extends Shape {
calculateArea() {
return Math.pow(this.data.a, 2);
}
}
class Rect extends Shape {
calculateArea() {
return this.data.a * this.data.b;
}
}
class Circle extends Shape {
calculateArea() {
return Math.pow(this.data.r, 2) * Math.PI;
}
}
With these subclasses in place, we can now create new Shape-extending objects like so:
const square = new Square({ a: 1 });
const rect = new Rect({ a: 2, b: 3 });
const circle = new Circle({ r: 4 });
Nevertheless, we still need to specify the type of shape we wish to create. To enable the ability to simply include an additional property type
in the given data and merge this feature with the object-oriented style, we require a builder factory. For organization purposes, let's designate that factory as a static method of Shape
:
class Shape {
/**
* Generates a new Shape extending class if a valid type is provided,
* otherwise generates and returns a new Shape base object.
*
* @param {object} data
* @param {string} data.type
* @return {Shape}
*/
static create(data) {
// It's not essential to overcomplicate this part,
// a switch-statement suffices. Optionally, you could
// move this to a separate factory function too.
switch (data.type) {
case 'square':
return new Square(data);
case 'rect':
return new Rect(data);
case 'circle':
return new Circle(data);
default:
return new this(data);
}
}
// ...
}
We now possess a consistent method to create shapes:
const square = Shape.create({ type: 'square', a: 1 });
const rect = Shape.create({ type: 'rect', a: 2, b: 3 });
const circle = Shape.create({ type: 'circle', r: 4 });
The final step is to have a straightforward means of directly calculating an area. This should not be too difficult:
// You can choose to implement this as a global function or as another static
// method of Shape. Whatever integrates better with your codebase:
function calculateArea(data) {
const shape = Shape.create(data);
return shape.calculateArea();
}
// Now you can easily calculate area:
calculateArea({ type: 'square', a: 4 }); // => 16
calculateArea({ type: 'rect', a: 2, b: 3 }); // => 6
calculateArea({ type: 'circle', r: 1 }); // 3.14159265359...