Problem overview
A drawing app keeps rectangles, squares and triangles in one array. The task is to write the shape classes so that code holding that array can ask any shape for its name and its perimeter without knowing which kind it is. One call, shape.perimeter(), runs a different calculation for each kind. This is called polymorphism.
The classes all extend a common class, Shape, whose defaults are the name "Shape" and perimeter 0. Each subclass overrides both methods:
Rectangle(width, height): name"Rectangle", perimeter2 * (width + height).Square(side): name"Square", perimeter4 * side.Triangle(a, b, c): name"Triangle", perimetera + b + c.
Two plain functions then work on an array of any shapes. totalPerimeter(shapes) returns the sum of the perimeters. largestShape(shapes) returns the name of the shape with the largest perimeter, and on a tie the shape that comes first wins.
For a 3 by 4 rectangle, a square of side 5 and a 3-4-5 triangle, the perimeters are 14, 20 and 12. The total is 46 and the largest is the square.
Approach
The functions must not check what kind each shape is. They only call perimeter() and name(), the two methods every shape has. Then a new kind of shape, written later, works with both functions unchanged, as long as it extends Shape and overrides the two methods.
Each function is one pass over the array. totalPerimeter keeps a running sum. largestShape keeps the best shape so far, starting with the first one, and replaces it only when it meets a strictly larger perimeter. That strict comparison is what makes the earlier shape win a tie.
Storing the sizes on this is what lets perimeter() work later: the constructor receives the numbers once, and every method call afterwards reads them from the object. Each class keeps only the sizes its own formula needs.
Step by step
- Give
Shapeits defaults.namereturns"Shape"andperimeterreturns0. The subclasses override both, so these versions only give the common type its two methods. - Write
Rectangle's constructor. It first callssuper(), then storeswidthandheightonthis, becauseperimeterneeds them later. - Override
nameandperimeterinRectangle. The perimeter is twice the sum of the two sizes. - Do the same for
Square. One size is enough, since the four sides are equal. - Do the same for
Triangle. It stores all three sides, because they can differ. - Write
totalPerimeter. Start a total at 0, loop over the shapes withfor...of, add eachshape.perimeter(), and return the total. - Write
largestShape. Start withshapes[0], loop over every shape, replace the best shape whenshape.perimeter() > largest.perimeter(), and returnlargest.name().
The solution in JavaScript
class Shape {
name() {
return "Shape";
}
perimeter() {
return 0;
}
}
class Rectangle extends Shape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}
name() {
return "Rectangle";
}
perimeter() {
return 2 * (this.width + this.height);
}
}
class Square extends Shape {
constructor(side) {
super();
this.side = side;
}
name() {
return "Square";
}
perimeter() {
return 4 * this.side;
}
}
class Triangle extends Shape {
constructor(a, b, c) {
super();
this.a = a;
this.b = b;
this.c = c;
}
name() {
return "Triangle";
}
perimeter() {
return this.a + this.b + this.c;
}
}
function totalPerimeter(shapes) {
let total = 0;
for (let shape of shapes) {
total += shape.perimeter();
}
return total;
}
function largestShape(shapes) {
let largest = shapes[0];
for (let shape of shapes) {
if (shape.perimeter() > largest.perimeter()) {
largest = shape;
}
}
return largest.name();
}
super() comes first
In a subclass constructor, this does not exist until super() has run. Using this first is an error. If Square's constructor were written without the super() call, new Square(2) would fail with ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor. Shape has no constructor of its own, so super() takes no arguments.
The list can mix classes
Both functions call perimeter() on whatever is in the array. The loop in totalPerimeter does not know whether shape is a rectangle or a triangle, and it does not need to.
Complexity
Each function visits every shape once and calls one or two constant-time methods per shape, so both run in O(n) time for n shapes. They use O(1) extra space: a number, or a reference to one shape.
Trying it
Create the shapes from the first example and call both functions:
const shapes = [new Rectangle(3, 4), new Square(5), new Triangle(3, 4, 5)];
console.log(totalPerimeter(shapes), largestShape(shapes));
46 Square
With a rectangle 2 by 4, a square of side 3 and a triangle 4, 4, 4, all three perimeters are 12. The total is 36, and the tie goes to the first shape in the list:
36 Rectangle
A 1 by 1 rectangle has the same sides as a square, but it is still a Rectangle. For [new Square(1), new Rectangle(1, 1), new Triangle(2, 2, 3)] the perimeters are 4, 4 and 7, and the result is:
15 Triangle
A shape added later needs no change in the functions. Given this class, largestShape([new Square(1), new Circle(5)]) returns Circle, and totalPerimeter of the same array returns 34:
class Circle extends Shape {
constructor(r) {
super();
this.r = r;
}
name() { return "Circle"; }
perimeter() { return 6 * this.r; }
}
Edge cases and common mistakes
- Using
>=inlargestShape. It would let a later shape replace an equal earlier one, so the tie rule would break. - Checking the class. Code such as
if (shape instanceof Square)works for today's shapes but must be edited for every new kind. The lesson asks for the methods only. - Returning the shape instead of its name.
largestShapereturnslargest.name(), the text. - Forgetting to store a size. A constructor that skips
this.height = heightmakesperimeter()returnNaN, sinceundefinedin arithmetic givesNaN.
Key takeaways
- Subclasses override the common class's methods, and each object runs its own version.
- Call
super()before usingthisin a subclass constructor. - Write functions against the common type's methods, never against the concrete kinds.
