Problem overview
A drawing app keeps rectangles, squares and triangles in one list. The code that works with that list should be able to ask any shape for its name() and its perimeter() without knowing which kind of shape it holds. That is polymorphism: one call, and each kind of shape runs its own version.
Every shape class extends the common class Shape, which answers "Shape" and 0. Each kind 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.
Then Solution has two methods that take an Array[Shape]. totalPerimeter adds up all the perimeters, and largestShape returns the name of the shape with the largest perimeter, the first one on a tie.
For a 3 by 4 rectangle, a square of side 5 and a triangle with sides 3, 4 and 5, the perimeters are 14, 20 and 12. The total is 46 and the largest shape is the Square.
Approach
The two Solution methods work through the methods of Shape only. They never ask whether a shape is a Square. That keeps them short, and a new kind of shape, such as a circle with its own perimeter, would work with them unchanged.
- Total: start a running sum at 0 and add each shape's
perimeter(). - Largest: start with the first shape as the best so far, and replace it only when a shape has a strictly larger perimeter. The strict comparison keeps the earlier shape on a tie.
Step by step
- In
Shape, return"Shape"fromnameand0fromperimeter. These defaults give the common type its two methods. - In each shape class, override
nameandperimeter. The constructor parameters are declared withval, so the methods can readwidth,side,aand so on. - In
totalPerimeter, declaretotalwithvar, loop overshapes, addshape.perimeter()and returntotal. - In
largestShape, declarelargestasshapes(0), loop over the shapes, and setlargest = shapewhenshape.perimeter() > largest.perimeter(). - Return
largest.name().
The solution in Scala
class Shape {
def name(): String = {
return "Shape"
}
def perimeter(): Int = {
return 0
}
}
class Rectangle(val width: Int, val height: Int) extends Shape {
override def name(): String = {
return "Rectangle"
}
override def perimeter(): Int = {
return 2 * (width + height)
}
}
class Square(val side: Int) extends Shape {
override def name(): String = {
return "Square"
}
override def perimeter(): Int = {
return 4 * side
}
}
class Triangle(val a: Int, val b: Int, val c: Int) extends Shape {
override def name(): String = {
return "Triangle"
}
override def perimeter(): Int = {
return a + b + c
}
}
class Solution {
def totalPerimeter(shapes: Array[Shape]): Int = {
var total = 0
for (shape <- shapes) {
total += shape.perimeter()
}
return total
}
def largestShape(shapes: Array[Shape]): String = {
var largest = shapes(0)
for (shape <- shapes) {
if (shape.perimeter() > largest.perimeter()) {
largest = shape
}
}
return largest.name()
}
}
Details worth knowing:
- Constructor parameters. In
class Square(val side: Int) extends Shape, thevalmakessidea property that the methods can use. - Array access. Scala reads an array element with parentheses:
shapes(0), notshapes[0]. var.totalandlargestchange inside the loop, so they arevar. Scala works out their types,IntandShape, from the starting values.- Time and space. Each method visits every shape once, so the time is O(n) for n shapes, and no extra space is used.
Trying it
val solution = new Solution()
val shapes: Array[Shape] = Array(new Rectangle(3, 4), new Square(5), new Triangle(3, 4, 5))
println(solution.totalPerimeter(shapes) + " " + solution.largestShape(shapes))
This prints 46 Square. More inputs, with the results of the same two calls:
Rectangle(2, 4),Square(3),Triangle(4, 4, 4)gives36 Rectangle. All three perimeters are 12, so the first shape wins the tie.Square(1),Rectangle(1, 1),Triangle(2, 2, 3)gives15 Triangle. The perimeters are 4, 4 and 7.- A single
Square(10000)gives40000 Square.
Walking through the largest shape
Take the shapes Square(1), Rectangle(1, 1) and Triangle(2, 2, 3). largest starts as the square, with perimeter 4. The loop then visits all three shapes, including the first. Comparing the square with itself changes nothing, because 4 is not greater than 4. The rectangle also has perimeter 4, which is not strictly greater, so the square stays. The triangle has perimeter 7, which is greater, so it becomes the new largest. After the loop the method returns largest.name(), which is Triangle.
Starting the loop at the first shape is a simplification. It does one pointless comparison, but it avoids a special case for the start of the array and costs nothing noticeable.
Why the base class has defaults
Shape returns "Shape" and 0, and none of the three kinds uses those values. They exist so that the common type has both methods, which is what lets the code in Solution call them on an Array[Shape]. A plain new Shape() is a valid object that answers Shape and 0.
Edge cases
The list is never empty, which is why largestShape can safely read shapes(0). On an empty array that call would throw an exception. Sizes go up to 10000, so the total stays well inside an Int: one hundred squares of side 10000 give 4,000,000. Ties are the other case to keep in mind: the strict > keeps the shape that came first.
Adding a new kind later needs no change here. A class Circle that extends Shape and overrides the two methods works with both Solution methods as they are.
Common mistakes
- Checking the kind of shape. An
ifon the class defeats the purpose. Callperimeter()and let each object answer. - Using
>=inlargestShape. It would replace the earlier shape on a tie. With three shapes of perimeter 12 it would answerTriangleinstead ofRectangle. - Forgetting the factor 2. A rectangle has two sides of each size: 2 * (width + height).
- Assuming a 1 by 1 rectangle is a square. It has the same sides, but it is still a
Rectangle, and itsname()says so. - Forgetting
override. Scala requires it on the methods that replace the ones inShape.
