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²`);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 aboutthrowin the "Errors and exceptions" module. - The "abstract" class checks
new.targetin its constructor: the class whose object is being created. Withnew Shape()it isShape, and the constructor throws an error. Withnew Circle(1)it isCircle, 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.
More precisely, if you are curious
new.targetworks only in a constructor. Outside one it isundefined.- TypeScript, which is JavaScript with types, has a real
abstractand 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²`);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
Pick the line with the bug.
new Shape() is an attempt to create an object of an abstract class. The first line is fine: new Array(3) creates an array of three empty slots for shapes, not shapes. You can put a rectangle, a circle or a triangle in them. The constructor of Shape sees that new.target is Shape and stops the program with "Shape is abstract: create a concrete shape".
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());}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
- Shape "promises" area(): the body only throws an error. There can be no object that is only a Shape.
- The object is a circle: the Circle part has its own area(). It keeps the promise.
- s.describe() is written in Shape and works for every shape.
- It calls area(). The object is a circle, so the Circle method runs: π · 1 · 1.
- describe() puts the text together. What is shared lives in the parent, what differs lives in the child class.
- 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());The object is a circle with a radius of 2. describe() belongs to Shape, but the area() inside it is the circle's: π · 2 · 2 ≈ 12.57.
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");
}
}new.target: the class whose object is being created. Only with new Shape() is it Shape, so the child classes get through.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.
Circle: area 78.54, perimeter 31.42Rectangle: area 10.00, perimeter 13.002. 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.
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().
The car sets offThe bicycle sets offPedalinggarage[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().
Starting the dance moonwalkStarting the song 'Bohemian Rhapsody'Singing 'Bohemian Rhapsody'More tasks
- Triangle. Add
Trianglewith 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
Employeefrom the last lesson abstract, with an abstractmonthlyPay(). AddFullTime(the salary is paid in full) and keepManagerandIntern. What does the program gain? What does it lose?
Test