Skip to content
wannacode

Abstract classes without abstract

Problem

Zero square meters

You are writing a program that orders tiles for a terrace. The terrace has three parts: a 4 × 3 m rectangle, a round part with a radius of 1 m, and a triangle with a base of 2 m and a height of 3 m. There is a parent Shape with area(), which returns 0, and each shape overrides it.

The colleague who wrote Triangle forgot:

class Triangle extends Shape {
#base;
#height;
constructor(base, height) {
super();
this.#base = base;
this.#height = height;
}
}
const terrace = [new Rectangle(4, 3), new Circle(1), new Triangle(2, 3)];
let total = 0;
for (const s of terrace) {
total += s.area();
}
console.log(`Tiles: ${total.toFixed(2)} m²`);
Output
Tiles: 15.14 m²

You need 18.14 m². The program gives no error, and the order is 3 m² short. The parent's zero has hidden the missing method.

And what is new Shape()? A shape with no shape. An object like that has no reason to exist.

The idea

Abstract class: a promise the child classes keep

Shape is not a real shape. All it says is "every shape has an area". So make it abstract:

class Shape {
constructor() {
if (new.target === Shape) {
throw new Error("Shape is abstract: create a concrete shape");
}
}
area() {
throw new Error("area() must be written in the child class");
}
}

JavaScript has no abstract. The parent enforces the rule itself, with two checks:

  • The "abstract" method is a method of the parent that returns nothing sensible and instead throws an error: throw new Error(...) stops the program and says why. You will learn more about throw in the "Errors and exceptions" module.
  • The "abstract" class checks new.target in its constructor: the class whose object is being created. With new Shape() it is Shape, and the constructor throws an error. With new Circle(1) it is Circle, and the check lets it through.
  • That is why the child classes call super() in their constructor, even when they have nothing to pass.

None of this is checked ahead of time. The forgotten Triangle gets created without complaint. But when the loop reaches its area(), the program stops with "Error: area() must be written in the child class", instead of quietly coming up 3 m² short.

A program that stops with a clear error is better than a silent error in the order.
More precisely, if you are curious
  • new.target works only in a constructor. Outside one it is undefined.
  • TypeScript, which is JavaScript with types, has a real abstract and checks it before the program starts. Plain JavaScript has nothing like it.

See it

The triangle keeps the promise

Add the method to Triangle:

class Triangle extends Shape {
// ... #base, #height and the constructor
area() {
return this.#base * this.#height / 2;
}
}
const terrace = [new Rectangle(4, 3), new Circle(1), new Triangle(2, 3)];
let total = 0;
for (const s of terrace) {
total += s.area();
}
console.log(`Tiles: ${total.toFixed(2)} m²`);
Output
Tiles: 18.14 m²

The loop is the same. The array holds shapes of three classes, and s.area() calls the method of each one. The polymorphism from the last lesson works with an abstract parent too.

Try it

Find the bug

The program stops on one of the lines. Which one?

Pick the line with the bug.

The idea

Abstract, but not empty

An abstract class can have everything an ordinary one has: fields, a constructor, ordinary methods. Let every shape have a name and a method that describes it:

class Shape {
#name;
constructor(name) {
if (new.target === Shape) {
throw new Error("Shape is abstract: create a concrete shape");
}
this.#name = name;
}
area() {
throw new Error("area() must be written in the child class");
}
describe() {
return `${this.#name}: ${this.area().toFixed(2)} m²`;
}
}

describe() is written once, in Shape. It calls area(), which in Shape only throws an error. That is not a problem: describe() is always called on a real shape, and a real shape has an area.

You do not make objects from Shape, but its constructor is useful: the child classes call it.

class Circle extends Shape {
#radius;
constructor(radius) {
super("circle");
this.#radius = radius;
}
// ... area() as before
}
const terrace = [new Rectangle(4, 3), new Circle(1), new Triangle(2, 3)];
for (const s of terrace) {
console.log(s.describe());
}
Output
rectangle: 12.00 m²
circle: 3.14 m²
triangle: 3.00 m²

The parent writes what is shared (the name, the description), and the child classes write what is different (the area).

area() {
throw new Error("area() must be written in the child class");
}
describe() {
return `${this.#name}: ${this.area().toFixed(2)} m²`;
}
const s = new Circle(1);
console.log(s.describe());
Step by step, in words
  1. Shape "promises" area(): the body only throws an error. There can be no object that is only a Shape.
  2. The object is a circle: the Circle part has its own area(). It keeps the promise.
  3. s.describe() is written in Shape and works for every shape.
  4. It calls area(). The object is a circle, so the Circle method runs: π · 1 · 1.
  5. describe() puts the text together. What is shared lives in the parent, what differs lives in the child class.
  6. The output is the area of the circle, with two digits after the decimal point.

Try it

Whose method?

What will the program print?

// Shape and Circle are from the card above
const s = new Circle(2);
console.log(s.describe());
Answer

Try it

What is missing?

What has to go in the blank so that you cannot create an object of Shape?

class Shape {
  constructor() {
    if ( === Shape) {
      throw new Error("Shape is abstract: create a concrete shape");
    }
  }

  area() {
    throw new Error("area() must be written in the child class");
  }
}

Exercise

Shapes and animals

Solve the tasks in your editor.

1. Area and perimeter. An abstract class Shape with a name and two abstract methods: area() and perimeter(). Child classes Circle (radius) and Rectangle (width and height). Put a circle with a radius of 5 and a 4 × 2.5 rectangle in an array and print both values with two digits after the decimal point.

Output
Circle: area 78.54, perimeter 31.42
Rectangle: area 10.00, perimeter 13.00

2. Animals. An abstract class Animal with an abstract method sound() and an ordinary method breathe() that prints "The animal breathes." Child classes Cat and Dog. Call the sound of both in a loop, then breathe() on the first one.

Output
Meow, meow!
Woof, woof!
The animal breathes.

Exercise

Vehicles and performances

3. Vehicles. An abstract class Vehicle with an abstract method move(). SimpleCar and Bicycle inherit from it. Bicycle also has its own method pedal().

Output
The car sets off
The bicycle sets off
Pedaling
Try garage[1].pedal(): it works, because there is a bicycle there. And garage[0].pedal()? Why does the program stop only on that line and not earlier?

4. Performances. An abstract class Performance with a title and an abstract method start(). DanceMove and Song inherit from it. Song also has a method sing().

Output
Starting the dance moonwalk
Starting the song 'Bohemian Rhapsody'
Singing 'Bohemian Rhapsody'

More tasks

  • Triangle. Add Triangle with its three sides to task 1. The perimeter is their sum, and the area comes from Heron's formula: s = perimeter / 2, area = √(s(s − a)(s − b)(s − c)). For sides 3, 4 and 5 the area is 6.00 and the perimeter is 12.00.
  • Abstract employee. Make Employee from the last lesson abstract, with an abstract monthlyPay(). Add FullTime (the salary is paid in full) and keep Manager and Intern. What does the program gain? What does it lose?

Test

Check yourself

1. The constructor of Shape throws an error if new.target === Shape. Which one works?
2. Does JavaScript have an abstract keyword?
3. Triangle extends Shape but does not write area(). What happens?
4. Can an "abstract" class have a constructor, fields and ordinary methods?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·