Programming

Modern Strategy Pattern in Functional Java

Spread the love


There’s a way to think about design patterns that stuck with me. Like Venkat said at the beginning of his 2019 Devoxx talk, they are a lot like grandma’s recipes. We all love when our grandmas cook for us. But try to ask for the recipes —the amount of flour or sugar to use is never precise. And when you prepare the food yourself, it comes out completely different.

In our biggest Java project at Evojam right now, we have tweaked the recipe for strategy pattern. We added our personal touch with a pinch of enums and functional syntactic sugar. As always, the easiest way to explain it is with an example. Let’s jump right into it.

Setting the Scene

When you take a picture, exposure depends on three values. If you pick them at random, chances are the result will come out more bright or blurry than you expected. Luckily, even vintage analogue cameras let you use modes other than full manual. The following class represents all controls you need before taking a picture:

@lombok.Value
class CameraControls {

    Mode mode;

    FilmSpeed filmSpeed;
    Aperture aperture;
    Shutter shutter;
}

In analogue photography, the speed (also called ISO) depends on the film roll that you use. Your camera then either reads it or needs you to set it. You do this once after inserting a new roll.

This leaves you with aperture and shutter. We go about them differently in each mode:

enum Mode {
    MANUAL, APERTURE_PRIORITY
}

When in manual mode, both an aperture and shutter speed are what the user requested. If the picture comes out too dark or too bright, it is not the camera’s fault. Aperture priority is different. As the name suggests, it uses the aperture set by the user to pick the right shutter speed considering a specific film speed. It is a balancing act, best illustrated by what is often called the ‘exposure triangle.’

exposure triangle

Cameras use light meters to pick the right shutter speed in aperture priority mode. Desirable areas of the triangle on a sunny day outside will be different than in your living room. This is why you need to adjust controls for the right exposure for each new scene.

All right, if you don’t know about photography, it can all sound a bit overwhelming. It is not that complicated, though. Each of the controls has a finite set of values:

enum FilmSpeed {
    _100, _200, _400, _800
}

enum Aperture {
    F2, F4, F8, F16
}

enum Shutter {
    _60, _250, _500, _1000
}

By the time you press the shutter release, film speed is already fixed for the roll. This leaves us with just two values to pass to a shoot() method:

void shoot(Aperture aperture, Shutter shutter) {...}

Keeping It Simple

As I said before, modes influence the handling of aperture and shutter values. In the manual, the camera takes values from user controls for granted. This is exactly what happens inside the first if the block below.

Aperture priority means you want to use the aperture value passed in controls. For shutter speed, you refer to the light meter. You provide it with film speed and desired aperture. In return, you get a shutter speed recommendation based on light measurement. You can follow this logic in the second if block.

void shutterRelease(CameraControls controls, LightMeter meter) {

    if (controls.getMode() == Mode.MANUAL) {

        shoot(controls.getAperture(), controls.getShutter());

    } else if (controls.getMode() == Mode.APERTURE_PRIORITY) {

        Shutter shutter = meter.pickShutter(
                controls.getFilmSpeed(),
                controls.getAperture()
        );

        shoot(controls.getAperture(), shutter);
    }
}

One Step Too Far

As the saying goes, the only thing that’s constant in software is change. No, if statement inside a Java method is likely to remain untouched for long. Let alone two if statements inside one method.

In our case, one possible change request could be to handle a new mode. Shutter priority is the reverse of aperture priority. Let’s add it to the original enum:

enum Mode {
    MANUAL, APERTURE_PRIORITY, SHUTTER_PRIORITY
}

Only adding the third value is very naive, but the code compiles. In our original design, no new modes were coming. When the photographer goes for shutter priority and presses shutter release, nothing happens. To handle the new mode, you need to add another if block. 

There must be a better way to handle this, though. And there is — it goes by the name of the strategy pattern.

The first thing to do is define the interface for picking shutter and aperture values. You can use a generic type T to represent either Shutter or Aperture:

interface Picker<T> {
    T pick(CameraControls settings, LightMeter meter);
}

Modes differ in the way an aperture and shutter get picked. It seems natural to parameterize modes with Picker strategies. Introducing private final fields to the enum makes them obligatory and immutable. That’s exactly what you need for each existing as well as any new mode in the future:

@lombok.Getter
@lombok.RequiredArgsConstructor
enum Mode {

    MANUAL(),
    APERTURE_PRIORITY(),
    SHUTTER_PRIORITY();

    private final Picker<Aperture> aperturePicker;
    private final Picker<Shutter> shutterPicker;
}

For the code above to compile, you also need to pass two arguments to each constructor. Picker is a functional interface as it only has one method. You can implement it with simple Java lambdas:

@lombok.Getter
@lombok.RequiredArgsConstructor
enum Mode {

    MANUAL(
            (controls, meter) -> controls.getAperture(),
            (controls, meter) -> controls.getShutter()
    ), 

    ...
}

Final Touches

On second thought, we could end up repeating ourselves. Both manual and aperture priority take aperture from user controls. Both manual and shutter priority take shutter speed from user controls. Keeping the DRY principle in mind, let’s extract these lambdas to static methods.

interface Picker<T> {

    T pick(CameraControls settings, LightMeter meter);

    static Aperture apertureFixed(CameraControls controls, 
                                  LightMeter meter) {
        return controls.getAperture();
    }

    static Shutter pickShutter(CameraControls controls, 
                                    LightMeter meter) {
        return meter.pickShutter(
                controls.getFilmSpeed(), 
                controls.getAperture()
        );
    }

    ...
}

You have to admit, the end result is a beautiful piece of clean code:

@lombok.Getter
@lombok.RequiredArgsConstructor
enum Mode {

    MANUAL(Picker::apertureFixed, Picker::shutterFixed),
    APERTURE_PRIORITY(Picker::apertureFixed, Picker::pickShutter),
    SHUTTER_PRIORITY(Picker::pickAperture, Picker::shutterFixed);

    private final Picker<Aperture> aperturePicker;
    private final Picker<Shutter> shutterPicker;
}

First, it is self-explanatory, thanks to the careful choice of method and field names. Second, it won’t let your future self introduce a new mode without handling both pickers. Finally, it lends itself to elegant use in other parts of the code with no if statements at all:

@lombok.Value
class CameraControls {

    Mode mode;

    Speed speed;
    Aperture aperture;
    Shutter shutter;

    Aperture pickAperture(LightMeter meter) {
        return mode.getAperturePicker().pick(this, meter);
    }

    Shutter pickShutter(LightMeter meter) {
        return mode.getShutterPicker().pick(this, meter);
    }
}

void shutterRelease(CameraControls controls, LightMeter meter) {

    Aperture aperture = controls.pickAperture(meter);
    Shutter shutter = controls.pickShutter(meter);

    shoot(aperture, shutter);
}

Make sure you compare this code to the earlier shutterRelease() with two if statements. Which one is easier to read? Remember, at that point, we were not even handling the third mode. For another mode, we would have needed yet another if block.

Keeping it SOLID

Strategy pattern makes our implementation SOLID in more than one way. The only reason to change the Mode enum is to handle a new choice. It is easy to extend the enum with new modes without modifying shutterRelease(). 

Go ahead and find proof of all five SOLID principles yourself. Please put them in the comments section below. Once you do that, you should start noticing perfect use cases in your current project.



Source link

Related Articles

Leave a Reply

Your email address will not be published.

Back to top button