# Pattern Matching vs. Polymorphism

Comparing two architectural approaches and when to favor one over the other.

## TL;DR

Subtype polymorphism is great for adding more entity types. Pattern matching is a better choice for adding more functionality. In many use cases, it’s likelier that more functionality is requested than that more entity types are requested.

In this article, we implement a program based on a set of simple requirements. First, we implement it using subtype polymorphism, then using pattern matching. Given these implementations, we add two more requirements and compare the necessary changes to implement the new requirements.

### Disclaimer

When we use the term `pattern matching`

, I’m referring to a limited subset. You can find a link to a more detailed explanation of pattern matching in the `Resources`

section.

## Initial Requirements

Let’s consider the example of two-dimensional shapes. We want to allow two different types: rectangles and circles. All shapes have a perimeter and an area. A rectangle is defined by two sides with individual lengths. Its perimeter is `(x + y) * 2`

and its area `x * y`

. A circle is defined by a radius. Its perimeter is `2 * PI * r`

and its area `PI * r * r`

.

## Using Subtype Polymorphism

Let’s model this example with subtype polymorphism in Java.

We model a shape as an interface. It has a method for retrieving the perimeter and one for retrieving the area.

`interface Shape {`

double perimeter();

double area();

}

A rectangle is an implementation of the `Shape`

interface.

`class Rectangle implements Shape {`

double x, y;

public double perimeter() {

return (x + y) * 2;

}

public double area() {

return x * y;

}

}

A circle is a second implementation of the `Shape`

interface.

`class Circle implements Shape {`

double radius;

public double perimeter() {

return 2 * PI * radius;

}

public double area() {

return PI * radius * radius;

}

}

This class hierarchy successfully implements the requirements.

Our stakeholders can now compute the perimeter and area of rectangles and circles. Great.

### Additional Requirements

Now our stakeholders come up with two additional requirements.

- They want to compute these properties for parallelograms too.
- They want to compute the diameter of a shape. That is the longest distance within a shape.

### Adding a New Class Into a Subtype Architecture

With our given subtype architecture, satisfying the first requirement is easy.

We start with creating a class `Parallelogram`

and add an `implements`

clause to the class signature.

`class Parallelogram implements Shape { }`

At this point, the Java compiler already notifies us that the class is missing implementations of the methods `perimeter`

and `area`

. The IDE gives us the option to generate method stubs.

`class Parallelogram implements Shape {`

public double perimeter() {

return 0.;

}

public double area() {

return 0.;

}

}

Now we can add attributes to the class and implement both methods.

`class Parallelogram implements Shape {`

double base, height, side;

public double perimeter() {

return 2 * (base + side);

}

public double area() {

return base * height;

}

}

Subtype polymorphism allowed us to fulfill this requirement with a single new file. We did not need to touch the existing code base.

Subtype polymorphism simplifies adding new entity types.

### Adding a New Method Into a Subtype Architecture

Let’s look at the second requirement.

The stakeholders want to compute the diameter of a shape. That is the longest distance within a shape.

We add a new method to the interface `Shape`

.

`interface Shape {`

double perimeter();

double area();

double diameter();

}

At this point, the Java compiler notifies us in the classes `Rectangle`

, `Shape`

, and `Parallelogram`

. Each class is missing an implementation of the new method `diameter`

.

We add an implementation of the method `diameter`

to the class `Rectangle`

.

`class Rectangle implements Shape {`

double x, y;

/* ... */

public double diameter() {

return Math.sqrt(x * x + y * y);

}

}

And to the class `Circle`

.

`class Circle implements Shape {`

double radius;

/* ... */

public double diameter() {

return 2 * radius;

}

}

Aaaand to the class `Parallelogram`

.

`class Parallelogram implements Shape {`

double base, height, side;

/* ... */

public double diameter() {

return /* Math */;

}

}

To add the diameter, we needed to touch every class in the hierarchy. The changes are split all over the application.

Subtype polymorphism complicates adding new methods.

## Using Pattern Matching

Let’s look at a different approach: pattern matching. Pattern matching is currently not sufficiently supported in Java. (This is likely to change in upcoming versions: JEP 305, JEP 354.) For the time being, we’ll use Kotlin for the pattern matching approach. Kotlin provides sufficient support for pattern matching to showcase this example.

A shape is modeled as a `sealed`

class without any properties. The `sealed`

keyword enforces that all subclasses of `Shape`

are declared within the same file. We define `Rectangle`

and `Circle`

as the only subclasses of the class `Shape`

.

`sealed class Shape`

class Rectangle(val x: Double, val y: Double) : Shape()

class Circle(val radius: Double) : Shape()

The new hierarchy looks similar to the hierarchy using subtype polymorphism. The key difference is that we do not specify the methods in the class hierarchy. Instead, we define the methods as functions outside of the hierarchy.

Let’s implement the function `area`

.

`fun area(shape: Shape): Double = when (shape) {`

is Rectangle -> shape.x * shape.y

is Circle -> PI * shape.radius * shape.radius

}

Since the function is defined outside of the hierarchy, we do not know if the passed shape is a rectangle or a circle. We use pattern matching to differentiate the two cases. If the shape is a subtype of `Rectangle`

, the compiler casts the argument shape to a rectangle and evaluates `shape.x * shape.y`

. If the shape is a subtype of `Circle`

, the compiler casts the argument shape to a circle and evaluates `PI * shape.radius * shape.radius`

.

Note that it is not necessary to define a default clause since the class `Shape`

is sealed. If one class would not be covered, the compiler would notify us.

We can similarly implement the function perimeter.

`fun perimeter(shape: Shape): Double = when (shape) {`

is Rectangle -> (shape.x + shape.y) * 2

is Circle -> 2 * PI * shape.radius

}

We add two clauses for the two implementations: rectangles and circles.

### Adding a New Method into a Pattern Matching Architecture

We successfully fulfilled the initial requirements. Now, let’s consider one of the additional requirements.

- The stakeholders want to compute the diameter of a shape. That is the longest distance within a shape.

With the new architecture, fulfilling this requirement is as easy as adding a new function.

`fun diameter(shape: Shape): Double = when (shape) {`

is Rectangle -> Math.sqrt(shape.x * shape.x + shape.y * shape.y)

is Circle -> 2 * shape.radius

}

No existing file needs to be touched. Also, as mentioned before, the compiler would automatically notify us if we missed one of the subclasses of `Shape`

.

Pattern matching simplifies adding new functions.

### Adding a New Class Into a Pattern Matching Architecture

Let’s consider the other requirement.

- The stakeholders want to compute these properties for parallelograms too.

Fulfilling this requirement was easy with the subtype architecture. It’s harder with pattern matching.

First, we add a new subclass of `Shape`

.

`sealed class Shape`

class Rectangle(val x: Double, val y: Double) : Shape()

class Circle(val radius: Double) : Shape()

class Parallelogram(val base: Double, val height: Double): Shape()

At this point, the compiler notifies us that the when expressions in the functions `perimeter`

, `area`

, and `diameter`

are missing a clause for `Parallelogram`

.

We add the missing case to the function `perimeter`

.

`fun perimeter(shape: Shape): Double = when (shape) {`

is Rectangle -> (shape.x + shape.y) * 2

is Circle -> 2 * PI * shape.radius

is Parallelogram -> (shape.base + shape.side) * 2

}

And to the function `area`

.

`fun area(shape: Shape): Double = when (shape) {`

is Rectangle -> shape.x * shape.y

is Circle -> PI * shape.radius * shape.radius

is Parallelogram -> shape.side * shape.height

}

Aaaand to the function `diameter`

.

`fun diameter(shape: Shape): Double = when (shape) {`

is Rectangle -> Math.sqrt(shape.x * shape.x + shape.y * shape.y)

is Circle -> 2 * shape.radius

is Parallelogram -> /* Math */

}

To add the new class, `Parallelogram`

, we needed to touch every existing function. The changes are split all over the application.

Pattern matching complicates adding new entity types.

## Summary

Pattern matching and subtype polymorphism are two different tools to design an architecture. While the former is great for adding new functionality, the latter is great for adding new entity types. Before deciding on an architecture, ask yourself if you’re likelier to need more functionality or more entity types. In my experience, the majority of use cases disproportionately require more functionality rather than more entity types over time.

## Resources

- Example with Pattern Matching in Java
- Example with Subtype Polymorphism in Kotlin
- Explanation of Pattern Matching
- JEP 305
- JEP 325

Published by Fabian Böller

Visit author page