Start learning free

LESSON 565 OF 574 · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

Shapes Perimeter in Swift: Polymorphism

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 Swift each shape class inherits with : Shape and writes its versions of the methods with override func. Each class keeps its sizes in var properties that its init sets.

Then write two methods of the class Solution, as in the course's other lessons. Both get the shapes as [Shape]:

  • 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:
    Rectangle(3, 4)
    Square(5)
    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:
    Rectangle(2, 4)
    Square(3)
    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:
    Square(1)
    Rectangle(1, 1)
    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 24
    Ctrl + Enter to check

    SWIFT · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

    Shapes Perimeter in Swift, explained

    Problem overview

    A drawing app keeps rectangles, squares and triangles in one list. The code that holds the list wants to ask every shape for its name() and its perimeter() without knowing which kind it is. You write a parent class Shape, three subclasses that override both methods, and two methods of Solution: totalPerimeter(_:) and largestShape(_:).

    • Shape: name "Shape", perimeter 0.
    • Rectangle(width, height): perimeter 2 * (width + height).
    • Square(side): perimeter 4 * side.
    • Triangle(a, b, c): perimeter a + b + c.

    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 shape is the square.

    Approach

    Both Solution methods get a [Shape] and call only perimeter() and name(), the two methods the common type has. They never ask which class a shape is. Each object runs its own version, so a new kind of shape added later works with both methods unchanged.

    Each method visits every shape once, so the time is O(n) for n shapes and the extra space is O(1). largestShape keeps the best shape found so far and replaces it only when a perimeter is strictly greater, so on a tie the shape that came first stays.

    Step by step

    1. Shape's name() returns "Shape" and its perimeter() returns 0. The subclasses override both, so these are only the common defaults.
    2. Each subclass declares its sizes as var properties of type Int: width and height; side; a, b and c.
    3. Its init stores each parameter in the property of the same name, with self. in front.
    4. Its name() returns the class's name and its perimeter() returns the formula above.
    5. totalPerimeter starts total at 0, loops over the shapes and adds shape.perimeter().
    6. largestShape starts largest as shapes[0], loops over all shapes, replaces largest when shape.perimeter() > largest.perimeter(), and returns largest.name().

    The solution in Swift

    class Shape {
        func name() -> String {
            return "Shape"
        }
    
        func perimeter() -> Int {
            return 0
        }
    }
    
    class Rectangle: Shape {
        var width: Int
        var height: Int
    
        init(_ width: Int, _ height: Int) {
            self.width = width
            self.height = height
        }
    
        override func name() -> String {
            return "Rectangle"
        }
    
        override func perimeter() -> Int {
            return 2 * (width + height)
        }
    }
    
    class Square: Shape {
        var side: Int
    
        init(_ side: Int) {
            self.side = side
        }
    
        override func name() -> String {
            return "Square"
        }
    
        override func perimeter() -> Int {
            return 4 * side
        }
    }
    
    class Triangle: Shape {
        var a: Int
        var b: Int
        var c: Int
    
        init(_ a: Int, _ b: Int, _ c: Int) {
            self.a = a
            self.b = b
            self.c = c
        }
    
        override func name() -> String {
            return "Triangle"
        }
    
        override func perimeter() -> Int {
            return a + b + c
        }
    }
    
    class Solution {
        func totalPerimeter(_ shapes: [Shape]) -> Int {
            var total = 0
            for shape in shapes {
                total += shape.perimeter()
            }
            return total
        }
    
        func largestShape(_ shapes: [Shape]) -> String {
            var largest = shapes[0]
            for shape in shapes {
                if shape.perimeter() > largest.perimeter() {
                    largest = shape
                }
            }
            return largest.name()
        }
    }
    

    Here are the three examples of the lesson:

    let s = Solution()
    let a: [Shape] = [Rectangle(3,4), Square(5), Triangle(3,4,5)]
    print(s.totalPerimeter(a), s.largestShape(a))
    let b: [Shape] = [Rectangle(2,4), Square(3), Triangle(4,4,4)]
    print(s.totalPerimeter(b), s.largestShape(b))
    let c: [Shape] = [Square(1), Rectangle(1,1), Triangle(2,2,3)]
    print(s.totalPerimeter(c), s.largestShape(c))
    
    46 Square
    36 Rectangle
    15 Triangle
    

    In the second list all three perimeters are 12, and the rectangle wins because it comes first.

    Swift details worth knowing

    • The subclass init sets only its own properties. Once every property has a value, Swift runs Shape's own init by itself, so no call to super.init() is written here.
    • The type of largest is Shape. Because it starts as shapes[0], which is a Shape, it can later hold a rectangle, a square or a triangle.
    • Each object answers for its own class. A Rectangle(1, 1) has the sides of a square but its name() is still "Rectangle". In [Square(1), Rectangle(1,1)] both perimeters are 4, so largestShape gives Square.

    Edge cases and common mistakes

    • Using >= in the comparison. It would move the answer to the last shape on a tie.
    • Checking the kind of shape. An if on the class inside the loop works today and breaks the idea: every new shape would need a new branch.
    • Forgetting override. Swift refuses to compile a method that replaces the parent's without it.
    • A plain Shape. Shape().name() is "Shape" and Shape().perimeter() is 0.

    Tracing the tie example

    In the second example the list is a 2 by 4 rectangle, a square of side 3 and a triangle with sides 4, 4 and 4. Their perimeters are 2 * (2 + 4), 4 * 3 and 4 + 4 + 4: all three are 12. totalPerimeter adds them to 36. largestShape starts with the rectangle as largest. The square's 12 is not greater than 12, so it stays; the triangle's 12 is not greater either. The answer is Rectangle, and the program prints 36 Rectangle.

    If the same shapes came in another order, the winner would change with it. That is why the comparison is strict and why the first shape is the starting value rather than 0 or an empty name: with a start value of the first shape there is no special case for an empty "best so far".

    Adding a new shape

    The two Solution methods were written against Shape only, so a shape that doesn't exist yet needs no change in them. Here is a regular hexagon, defined next to the others:

    class Hexagon: Shape {{
        var side: Int
        init(_ side: Int) {{
            self.side = side
        }}
        override func name() -> String {{
            return "Hexagon"
        }}
        override func perimeter() -> Int {{
            return 6 * side
        }}
    }}
    let s = Solution()
    let shapes: [Shape] = [Square(5), Hexagon(4), Triangle(3, 4, 5)]
    print(s.totalPerimeter(shapes), s.largestShape(shapes))
    
    56 Hexagon
    

    The perimeters are 20, 24 and 12. Neither totalPerimeter nor largestShape was touched, and both handle the hexagon.

    Properties are variables

    The sizes are declared with var, so a shape can be changed after it is built. After let r = Rectangle(3, 4) and r.width = 10, the call r.perimeter() gives 28, because the method reads the current values. A class instance can be changed through a let constant, since the constant holds a reference to the object.

    Key takeaways

    • Code written against the parent type works for every subclass, including ones added later.
    • Ties are decided by comparing strictly, so the first shape stays.
    Keep going with Swift500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free