Preface

This is a question similar to one you may see during an interview, likely for an internship or junior position, except I’ll be there to walk you through it. The goal is to get you thinking about software design at a higher level, rather than coding the first thing that comes to mind and works. The end result will be a Terminator, to be used for good or evil, and Skynet, used to control our Terminators. I highly encourage you to read Head First Design Patterns, it covers many of the popular design patterns out there including ones I will discuss here.

In the end we will have used the Factory Pattern, Command Pattern, and Observable Pattern, as well as best practices like programming to interfaces, composition over inheritance, and don’t repeat yourself.

Starting out

Alright, let’s first start out with an empty skeleton for our Terminator.

public class Terminator {

}

There are two properties we want our Terminator to have. First, our Terminator needs to have a target identifier. We can’t have our robots thinking humans are bad! They also need a weapon system. Of course, we won’t give them a weapon system, why would they ever need one? Instead, being the good engineers we are, we’ll future-proof our design and allow for one to be added later.

Shields up, weapons online

So how might we approach this problem?

The first concept I’d like to drill into your head is composition over inheritance. A naive and unsustainable approach to this problem would be making a hierarchy of subclasses to give our Terminator the properties we desire. A better way is having our Terminator be composed of properties. Try to think of the relationship between our Terminator and the properties as HAS-A and not IS-A.

public class Terminator {

    private TargetingSystem targetingSystem;
    private WeaponSystem weaponSystem;

    public Terminator(TargetingSystem targetingSystem, WeaponSystem weaponSystem) {
        this.targetingSystem = targetingSystem;
        this.weaponSystem = weaponSystem;
    }

    public TargetingSystem getTargetingSystem() {
        return targetingSystem;
    }

    public WeaponSystem getWeaponSystem() {
        return weaponSystem;
    }
}
public interface TargetingSystem {
    public boolean shouldTarget(Target target);
}
public interface WeaponSystem {
    public void shoot();
}

Our Terminator accepts both a target identifier and a weapon system. Notice how I declared each as an interface? That brings me to another point, programming to interfaces. Always think twice when you are programming something to a concrete implementation. A concrete implementation is something that actually implements an interface or abstract class. If you program to interfaces, then you can accept any class that implements that interface. For example, any class that implements TargetingSystem or WeaponSystem can be used in our Terminator.

Locked and loaded

To elaborate some more on my last point, let’s create some weapons and targeting systems.

public class HumanTargeter implements TargetingSystem {

    @Override
    public boolean shouldTarget(Target target) {
        return target == Target.HUMAN;
    }
}
public class MachineGun implements WeaponSystem {

    @Override
    public void shoot() {
        System.out.println("pew pew");
    }
}

We can easily create a Terminator with our new classes:

Terminator evilTerminator = new Terminator(new HumanTargeter(), new MachineGun());

Since we programmed to our general interfaces everything just works. Programming to interfaces is important. All too often I see people creating methods like this:

public void doSomething(ArrayList<SomeObject>) { }

instead of this:

public void doSomething(List<SomeObject>) { }

and it makes me sad inside. If you wanted to change your implementation from ArrayList to LinkedList, then you would need to refactor your method! For some additional reading, I recommend looking at the structure of the Collection API.

Mass producing our Terminator

If we want to create new Terminators throughout our codebase, it will be a pain to get them all configured the same way. Let’s create a TerminatorFactory that will build Terminators for us. As you can guess, this is called the Factory Pattern.

public class TerminatorFactory {

    public static Terminator getEvilTerminator() {
        return new Terminator(new HumanTargeter(), new MachineGun());
    }
}

The Factory Pattern is very simple. Having a central static method build objects for you ensures consistency throughout your program. It’s easy to forget a configuration when your object gets large enough, and you don’t want to repeat yourself writing code.

Sending commands to our Terminators

Our Terminators are useless if we can’t control them. First, we need to create our Command interface as part of the Command Pattern.

public interface TerminatorCommand {
    public void execute(Terminator terminator);
}

And we can create a concrete class.

public class FireWeaponCommand implements TerminatorCommand {

    @Override
    public void execute(Terminator terminator) {
        terminator.getWeaponSystem().shoot();
    }
}

Abstracting a command out to a class like this has tons of benefits. The most obvious one is you can easily re-use commands. You could also keep a stack of used commands to create an “undo” feature.

Skynet

Naturally, if we want to send commands to our Terminators then we need Skynet. To have Skynet do so we will use the Observer Pattern. Java actually has baked-in support for this.

public class Skynet extends Observable {
    public void turnEvil() {
        FireWeaponCommand command = new FireWeaponCommand();
        setChanged();
        notifyObservers(command);
    }
}

We also need to modify our Terminator class.

public class Terminator implements Observer {

    ...

    @Override
    public void update(Observable o, Object arg) {
        if (arg instanceof TerminatorCommand) {
            ((TerminatorCommand) arg).execute(this);
        }
    }
}

Unfortunately the Observer interface doesn’t use generics, so we have to cast our arg for now.

The abridged version of this pattern is you have Observers that observe an Observable. The Observable keeps a list of Observers and notifies them when something happens. In our case, Skynet is the Observable and the Terminators are the Observers because they are waiting for a command to run.

Wrapping up

To show how everything works together, here’s how you could run it.

Terminator terminator1 = TerminatorFactory.getEvilTerminator();
Terminator terminator2 = TerminatorFactory.getEvilTerminator();
Terminator terminator3 = TerminatorFactory.getEvilTerminator();

Skynet skynet = new Skynet();
skynet.addObserver(terminator1);
skynet.addObserver(terminator2);
skynet.addObserver(terminator3);
skynet.turnEvil();

This code looks short, but it’s actually very dense! These eight lines of code take advantage of all the patterns and best practices that we’ve discussed, and better yet, our code remains readable without being too abstract to understand.

You can find the source code here.