When making a sandwich, bread is only our first and most basic ingredient; we obviously need some kind of filling. In programming terms, this could mean simply building another interface like Bread
but calling it Filling
and providing it with its own associated factory. Equally, we could create a global interface called Ingredient
and have both Bread
and Filling
as examples of this. Either way, we would have to do a fair bit of re-coding elsewhere.
Working with more than one factory
The similarities between this next project and the last are striking, as they should be; one of the best things about patterns is that we can reuse structures. You can either edit the previous example or start one from scratch. Here, we will be starting a new project; hopefully that will help make the pattern itself clearer.
The abstract factory works in a slightly different way to our previous example. Here, our activity makes uses of a factory generator, which in turn makes use of an abstract factory class that handles the task of deciding which actual factory to call, and hence which concrete class to create.
As before we will not concern ourselves with the actual mechanics of input and output, but rather concentrate on the pattern's structure. Before continuing, start a new Android Studio project. Call it whatever you choose, set the minimum API level as low as you like, and use the Blank Activity template:
- We begin, just as we did before, by creating the interface; only this time, we will need two of them: one for the bread and one for the filling. They should look like this:
public interface Bread {
String name();
String calories();
}
public interface Filling {
String name();
String calories();
}
- As before, create concrete examples of these interfaces. Here, to save space, we will just create two of each. They are all almost identical, so here is just one:
public class Baguette implements Bread {
@Override
public String name() {
return "Baguette";
}
@Override
public String calories() {
return " : 65 kcal";
}
}
- Create another
Bread
called Brioche
and two fillings called Cheese
and Tomato
. - Next, create a class that can call on each type of factory:
public abstract class AbstractFactory {
abstract Bread getBread(String bread);
abstract Filling getFilling(String filling);
}
- Now create the factories themselves. First,
BreadFactory
:public class BreadFactory extends AbstractFactory {
@Override
Bread getBread(String bread) {
if (bread == null) {
return null;
}
if (bread == "BAG") {
return new Baguette();
} else if (bread == "BRI") {
return new Brioche();
}
return null;
}
@Override
Filling getFilling(String filling) {
return null;
}
}
- And then,
FillingFactory
:public class FillingFactory extends AbstractFactory {
@Override
Filling getFilling(String filling) {
if (filling == null) {
return null;
}
if (filling == "CHE") {
return new Cheese();
} else if (filling == "TOM") {
return new Tomato();
}
return null;
}
@Override
Bread getBread(String bread) {
return null;
}
}
- Finally, add the factory generator class itself:
public class FactoryGenerator {
public static AbstractFactory getFactory(String factory) {
if (factory == null) {
return null;
}
if (factory == "BRE") {
return new BreadFactory();
} else if (factory == "FIL") {
return new FillingFactory();
}
return null;
}
}
- We can test our code just as before, with a debug tag, like so:
AbstractFactory fillingFactory = FactoryGenerator.getFactory("FIL");
Filling filling = fillingFactory.getFilling("CHE");
Log.d(DEBUG_TAG, filling.name()+" : "+filling.calories());
AbstractFactory breadFactory = FactoryGenerator.getFactory("BRE");
Bread bread = breadFactory.getBread("BRI");
Log.d(DEBUG_TAG, bread.name()+" : "+bread.calories());
When tested, this should produce the following output in the Android monitor:
com.example.kyle.abstractfactory D/tag: Cheese : : 155 kcal
com.example.kyle.abstractfactory D/tag: Brioche : : 85 kcal
By the time we reach the end of the book, each ingredient will be a complex object in its own right, with associated imagery and descriptive text, price, calorific value, and more. This is when adhering to patterns will really pay off, but a very simple example like the one here is a great way to demonstrate how creational patterns such as the abstract factory allow us to make changes to our products without affecting client code or deployment.
As before, our understanding of the pattern can be enhanced with a visual representation:
Imagine we wanted to include soft drinks in our menu. These are neither bread nor filling, and we would need to introduce a whole new type of object. The pattern of how to add this is already laid out. We will need a new interface that would be identical to the others, only called Drink
; it would utilize the same name() and calories()
methods, and concrete classes such as IcedTea
could be implemented along exactly the same lines as above, for example:
public class IcedTeaimplements Drink {
@Override
public String name() {
return "Iced tea";
}
@Override
public String calories() {
return " : 110 kcal";
}
}
We would need to extend our abstract factory with something like this:
abstract Drink getDrink(String drinkType);
We also, of course, need to implement a DrinkFactory
class, but this too would have the same structure as the other factories.
In other words, we can add, delete, change, and generally muck around with the nuts and bolts of a project, without ever really having to bother with how these changes are perceived by the higher-level logic of our software.
The factory pattern is one of the most frequently used of all patterns. It can and should be used in many situations. However, like all patterns, it can be overused or underused, if not thought about carefully. When considering the overall architecture of a project, there are, as we shall see, many other patterns at our disposal.