How to use an interface

Introduction

This article may seem too basic, but I decided to write it after seeing that many programmers do not know how to use interfaces in Java. This topic is related to design patterns in object-oriented programming (OOP).

An apparently simple class

We all have seen code similar to this one:

enum EnergyPlan {
    PlanA, PlanB, PlanC
}
class EnergyCalculator {

    BigDecimal calculateEnergy(EnergyPlan plan) {
        if (plan == EnergyPlan.PlanA) {
            System.out.println("Calculating energy for Plan A");
            return new BigDecimal(100);
        } else if (plan == EnergyPlan.PlanB) {
            System.out.println("Calculating energy for Plan B");
            return new BigDecimal(300);
        } else if (plan == EnergyPlan.PlanC) {
            System.out.println("Calculating energy for Plan C");
            return new BigDecimal(400);
        }
        else {
            throw new IllegalArgumentException("Unknown plan");
        }
    }
}

An enumeration is used and the EnergyCalculator class has only one method, which is not hard to follow.

Although it's probably easier to read if a switch statement is used:

class EnergyCalculator {

    BigDecimal calculateEnergy(EnergyPlan plan) {
        switch (plan) {
            case PlanA:
                System.out.println("Calculating energy for Plan A");
                return new BigDecimal(100);
            case PlanB:
                System.out.println("Calculating energy for Plan B");
                return new BigDecimal(300);
            case PlanC:
                System.out.println("Calculating energy for Plan C");
                return new BigDecimal(400);
            default:
                throw new IllegalArgumentException("Unknown plan");
        }
    }
}

Starting from Java 14 a switch expression can be used:

class EnergyCalculator {

    BigDecimal calculateEnergy(EnergyPlan plan) {
        BigDecimal result = switch (plan) {
            case PlanA -> {
                System.out.println("Calculating energy for Plan A");
                yield new BigDecimal(100);
            }
            case PlanB -> {
                System.out.println("Calculating energy for Plan B");
                yield new BigDecimal(300);
            }
            case PlanC -> {
                System.out.println("Calculating energy for Plan C");
                yield new BigDecimal(400);
            }
        };
        return result;
    }
}

The problem

You might be thinking: So what? I don't see any issue. And you're right...... until the logic for the different plans start to grow:

class EnergyCalculator {

    BigDecimal calculateEnergy(EnergyPlan plan) {
        BigDecimal result = switch (plan) {
            case PlanA -> {
                // Step 1
                // Step 2
                // Step 3
                yield new BigDecimal(100);
            }
            case PlanB -> {
                // Step 1
                // Step 2
                // Step 3
                yield new BigDecimal(300);
            }
            case PlanC -> {
                // Step 1
                // Step 2
                // Step 3
                yield new BigDecimal(400);
            }
        };
        return result;
    }
}

Imagine that each step is different for each plan and you easily end up having a one-hundred line class. One week later, you have to add a new plan and in one month you have up to six different plans.

The number of lines is now three hundred and you have to maintain it. It's very hard to read and to modify. It's not an exaggeration, this has happened to me.

Precisely this is what we are referring to when we say that the traditional if/else or switch block is not scalable.

Some of you may be thinking: But it's not too bad, since the logic is contained in methods from other classes. Even in that case, a long method is hard to maintain. Not to mention how you are going to test it. You have to mock all the classes that are involved in the logic. It's a mess.

The solution

Instead of having a very big block for each case, wouldn't it be better to create a class per plan and execute the same method:

class EnergyCalculator {
    public BigDecimal calculateEnergy(EnergyPlan plan) {
        EnergyPlanStrategy strategy;
        switch (plan) {
            case PlanA -> strategy = new PlanAStrategy();
            case PlanB -> strategy = new PlanBStrategy();
            case PlanC -> strategy = new PlanCStrategy();
            default -> throw new IllegalArgumentException("Unknown plan: " + plan);
        }
        return strategy.calculateEnergy();
    }
}

A number of classes with the same calculateEnergy() method. Does this sound familiar? It's like ArrayList and LinkedList implementing the same get() method (see my previous article).

Exactly, they are different implementations of the same interface:

interface EnergyPlanStrategy {
    BigDecimal calculateEnergy();
}

// Implementations for each plan
class PlanAStrategy implements EnergyPlanStrategy {
    @Override
    public BigDecimal calculateEnergy() {
        // Steps 1, 2, 3
    }
}

class PlanBStrategy implements EnergyPlanStrategy {
    @Override
    public BigDecimal calculateEnergy() {
        // Steps 1, 2, 3
    }
}

class PlanCStrategy implements EnergyPlanStrategy {
    @Override
    public BigDecimal calculateEnergy() {
        // Steps 1, 2, 3
    }
}

This is the diagram for the Energy Plan Strategy Pattern:

%% Title: Energy Plan Strategy Pattern
classDiagram
    title Energy Plan Strategy Pattern

    class EnergyPlanStrategy {
        <<interface>>
        +calculateEnergy(): BigDecimal
    }

    class PlanAStrategy {
        +calculateEnergy(): BigDecimal
    }
    class PlanBStrategy {
        +calculateEnergy(): BigDecimal
    }
    class PlanCStrategy {
        +calculateEnergy(): BigDecimal
    }

    EnergyPlanStrategy <|.. PlanAStrategy
    EnergyPlanStrategy <|.. PlanBStrategy
    EnergyPlanStrategy <|.. PlanCStrategy

The logic is contained in the different implementations of the EnergyPlanStrategy interface. They are different classes, as a result the code is easier to read and easier to test.

If a plan must be added, only one class needs to be added and a single line inside the switch/case block. We say this technique is scalable.

Notice we are not even using lambda expressions (see my article here) or any other feature introduced in Java 8. Except the new syntax for the switch/case block, this is all about using interfaces in a clever way.

Formal Definition

This technique is used so often that is a design pattern called Strategy Pattern. The main idea is to use interfaces to define a family of algorithms, encapsulate each one of them in a different class, and make them interchangeable.

Disadvantages

For complex logic, this pattern results in a more testable and maintainable code. This is a concise way of saying that the code is easier to read and easier to test.

The main disadvantage is that the code is more verbose. Every time a plan (algorithm) is added, a new class implementing the interface must be added, in addition to the line inside the switch/case block.

Summary

The interface pattern defines behaviors through interfaces and provides concrete implementations via classes. Interfaces act as contracts that specify method/s to be implemented, enabling abstraction and decoupling between the definition and the implementation of behaviors.

This pattern enhances flexibility, scalability, and the ability to extend implementations without affecting client code (the open to extension, closed to modification principle).

It relies solely on interfaces and classes, promoting the principle of program to an interface, not an implementation. This allows multiple classes to implement the same interface, encouraging code reuse and maintainability.

As a secondary goal, the evolution of the switch/case block was shown.

0
Subscribe to my newsletter

Read articles from José Ramón (JR) directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

José Ramón (JR)
José Ramón (JR)

Software Engineer for quite a few years. From C programmer to Java web programmer. Very interested in automated testing and functional programming.