Pattern Matching vs. Polymorphism

Building an extensible architecture

Fabian Böller
Aug 11, 2019 · 5 min read
Photo by 贝莉儿 NG on Unsplash

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.

When 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 Resourcessection.


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.

A rectangle is an implementation of the Shape interface.

A circle is a second implementation of the Shape interface.

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.

  1. They want to compute these properties for parallelograms too.
  2. 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.

At this point, the Java compiler already notifies us that the class is missing implementations of the methods perimeterand area. The IDE gives us the option to generate method stubs.

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

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.

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 diameterto the class Rectangle.

And to the class Circle.

Aaaand to the class Parallelogram.

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 Shapeare declared within the same file. We defineRectangleandCircleas the only subclasses of the class 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.

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.

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.

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.

At this point, the compiler notifies us that the when expressions in the functions perimeter, area, and diameterare missing a clause for Parallelogram.

We add the missing case to the function perimeter.

And to the function area.

Aaaand to the function diameter.

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.

What do you think?


Better Programming

Advice for programmers.

Fabian Böller

Written by

I’m a senior software engineer working in Bonn at LeanIX. I write about software development best practices across multiple programming languages.

Better Programming

Advice for programmers.

More From Medium

More from Better Programming

More from Better Programming

More from Better Programming

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade