Start learning free

LESSON 560 OF 569 · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

Shapes Perimeter in JavaScript: Polymorphism with Classes

Practice O(n) Read the full explanation

A drawing app keeps rectangles, squares and triangles in one list. Write the shape types so that code holding that list can ask any shape for its name and its perimeter (the length all the way around it) without knowing which kind of shape it is. That is polymorphism: one call, shape.perimeter(), and each kind of shape runs its own version.

Every shape class inherits from the class Shape, the common type. Shape has two methods:

  • name() returns the kind of shape as text: "Shape" for a plain Shape.
  • perimeter() returns the length around the shape: 0 for a plain Shape.

Each shape class overrides both:

  • Rectangle(width, height): its name is "Rectangle", its perimeter 2 × (width + height).
  • Square(side): its name is "Square", its perimeter 4 × side.
  • Triangle(a, b, c): its name is "Triangle", its perimeter a + b + c.

In JavaScript a class inherits with class Rectangle extends Shape, and a method with the same name as the parent's replaces it for that class. A subclass's constructor calls super() before it stores anything in this.

Then write two plain functions that get an array of any shapes:

  • totalPerimeter(shapes) returns the sum of the perimeters of all the shapes.
  • largestShape(shapes) returns the name of the shape with the largest perimeter. If several shapes share the largest perimeter, the one that comes first wins.

Both functions must work through the common type's methods only: never check which kind a shape is. Then a new kind of shape, added later, works with them unchanged.

The starter code already has every class, constructor and method header: write their bodies. For each test, the harness reads the shapes, builds them in the order given, calls totalPerimeter and largestShape, and prints the two results on one line, separated by a space.

 

Example 1:

Input:
    new Rectangle(3, 4)
    new Square(5)
    new Triangle(3, 4, 5)
Output: 46 Square
Explanation: The perimeters are 2 × (3 + 4) = 14, 4 × 5 = 20 and 3 + 4 + 5 = 12. Their total is 46, and the square's 20 is the largest.

Example 2:

Input:
    new Rectangle(2, 4)
    new Square(3)
    new Triangle(4, 4, 4)
Output: 36 Rectangle
Explanation: All three perimeters are 12: 2 × (2 + 4), 4 × 3 and 4 + 4 + 4. On a tie the shape that comes first in the list wins, so the answer is the rectangle.

Example 3:

Input:
    new Square(1)
    new Rectangle(1, 1)
    new Triangle(2, 2, 3)
Output: 15 Triangle
Explanation: The perimeters are 4, 4 and 7, so the last shape, the triangle, is the largest. A 1 by 1 rectangle has the same sides as a square, but it is still a Rectangle: each object answers name() for its own class.

 

Constraints:

  • Every test has between 1 and 100 shapes: the list is never empty.
  • Every size (width, height, side, and each side of a triangle) is an integer from 1 to 104.
  • A triangle's three sides always make a real triangle: each side is shorter than the other two together.

Note:

Each function looks at every shape once: O(n) time for n shapes, and no extra space.

STEP 1 OF 21
    Ctrl + Enter to check

    JAVASCRIPT · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

    Shapes Perimeter in JavaScript, explained

    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", perimeter 2 * (width + height).
    • Square(side): name "Square", perimeter 4 * side.
    • Triangle(a, b, c): name "Triangle", perimeter a + 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

    1. Give Shape its defaults. name returns "Shape" and perimeter returns 0. The subclasses override both, so these versions only give the common type its two methods.
    2. Write Rectangle's constructor. It first calls super(), then stores width and height on this, because perimeter needs them later.
    3. Override name and perimeter in Rectangle. The perimeter is twice the sum of the two sizes.
    4. Do the same for Square. One size is enough, since the four sides are equal.
    5. Do the same for Triangle. It stores all three sides, because they can differ.
    6. Write totalPerimeter. Start a total at 0, loop over the shapes with for...of, add each shape.perimeter(), and return the total.
    7. Write largestShape. Start with shapes[0], loop over every shape, replace the best shape when shape.perimeter() > largest.perimeter(), and return largest.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 >= in largestShape. 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. largestShape returns largest.name(), the text.
    • Forgetting to store a size. A constructor that skips this.height = height makes perimeter() return NaN, since undefined in arithmetic gives NaN.

    Key takeaways

    • Subclasses override the common class's methods, and each object runs its own version.
    • Call super() before using this in a subclass constructor.
    • Write functions against the common type's methods, never against the concrete kinds.
    Keep going with JavaScript500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free