Java 21 Factory Pattern Revisited
The factory pattern has been a tried and tested pattern for creating objects ever since my fellow landsman Erich Gamma and his colleagues wrote the book on Design patterns. However, the way we impleent it in Java has changed quite a bit over the years. In this article, we will look at how easy it has become in Java 21.
lntroduction
The Factory Pattern is a creational design pattern used in object oriented programming. It is used when we need to create an object, but we don't know the exact class of the object we need to create. We only know the interface or abstract class of the object we need to create. This is nothing new to Java developers. With the recent versions however, it has become a lot more concise to implement this pattern. Let's have a look how it was done in the past.
The Factory Pattern
Usually we begin with an interface or abstract class that defines the methods that we need to implement. Usually I prefer to use an interface, but more on that later.
public interface ChessPiece {
void move();
}
Next we create the concrete class that implements the ChessPiece interface. If you are familiar with chess, you will know that there are 6 different types of pieces. Let's create an enum for them
enum ChessPieceType {
PAWN, KNIGHT, BISHOP, ROOK, QUEEN, KING
}
We will create a knight and a pawn for illustration purposes.
class Pawn implements ChessPiece {
@Override
public void move() {
System.out.println("Pawn moves forward 1 square");
}
}
class Knight implements ChessPiece {
@Override
public void move() {
System.out.println("Knight moves in an L shape");
}
}
Nice! So now that we implemented the interface in our concrete classes, we can create a factory class that will create the objects for us. In older java versions, there were usually to approaches to implement the factory pattern. Either using the old switch statement:
class ChessPieceFactory {
public static ChessPiece createChessPiece(ChessPieceType type) {
switch (type) {
case PAWN:
return new Pawn();
case KNIGHT:
return new Knight();
default:
throw new IllegalArgumentException("Unknown chess piece type: " + type);
}
}
}
or using the if-else statement:
public static ChessPiece createChessPiece(ChessPieceType type) {
if (Objects.requireNonNull(type) == PAWN) {
return new Pawn();
} else if (type == KNIGHT) {
return new Knight();
}
throw new IllegalArgumentException("Unknown chess piece type: " + type);
}
And that's it! Now we have a nice little factory for our chess pieces. And we can use it like this:
public static void main(String[] args) {
ChessPiece pawn = ChessPieceFactory.createChessPiece(PAWN);
pawn.move();
ChessPiece knight = ChessPieceFactory.createChessPiece(KNIGHT);
knight.move();
}
The Factory Pattern revisited
In the newer Java version, there are a couple of improvements to be made on the previous example. First of all, we can convert our concrete implementation classes to java records. This will make the objects immutable and will also make the code a lot more readable if the class has multiple fields.
record Pawn() implements ChessPiece {
@Override
public void move() {
System.out.println("Pawn moves forward 1 square");
}
}
record Knight() implements ChessPiece {
@Override
public void move() {
System.out.println("Knight moves in an L shape");
}
}
Next, we can use the new switch statement to implement the factory pattern. This will make the code a lot more concise.
public static ChessPiece createChessPiece(ChessPieceType type) {
return switch (type) {
case PAWN -> new Pawn();
case KNIGHT -> new Knight();
default -> throw new IllegalArgumentException("Unknown chess piece type: " + type);
};
}
Beautiful! Now we have created a nice little factory for our chess pieces using new Java features like records and the enhanced switch statement and we can use it in the same way as before:
public static void main(String[] args) {
ChessPiece pawn = ChessPieceFactory.createChessPiece(PAWN);
pawn.move();
ChessPiece knight = ChessPieceFactory.createChessPiece(KNIGHT);
knight.move();
}
With the enhanced Switch statement we can also easily implement conditionals using the keyword "when": (I'm aware that this is not the best example and chess promotion is more complicated than this, but it illustrates the point and stay with the theme ;) )
public static ChessPiece createChessPiece(ChessPieceType type) {
var promotion = true;
return switch (type) {
case PAWN when !promotion -> new Pawn();
case PAWN when promotion -> new Queen();
case KNIGHT -> new Knight();
default -> throw new IllegalArgumentException("Unknown chess piece type: " + type);
};
}
Interfaces vs abstract classes
In the beginning, I mentioned that abstract classes are also possible way to implement the factory pattern, and I see it quite often in different codebases to implement some kind of class hierarchy. Personally, I prefer interfaces to abstract classes, mainly because we can use java records for the concrete implementations. However, there are additional arguments to be made for both approaches. I recommend reading the chapter "Item 18 - prefer Interfaces to Abstract classes" in the great book "Effective Java" by Joshua Bloch to get a better understanding of the differences between the two. If you choose to use an abstract class, here is how it would look like:
abstract class ChessPiece {
void move() {
throw new UnsupportedOperationException("Subclass must override this method");
}
}
class Pawn extends ChessPiece {
@Override
public void move() {
System.out.println("Pawn moves forward 1 square");
}
}
class Knight extends ChessPiece {
@Override
public void move() {
System.out.println("Knight moves in an L shape");
}
}
Conclusion
In this article we learned how to implement the factory pattern in Java using the new enhanced switch statements and records. Personally, I think this is a great improvement over the old way of implementing the factory pattern and I will continue to use it in my new projects. In my opinion, it's also worth challenging colleagues that are using the factory pattern using abstract classes and ask for clarification as to why. Most of the time, an interface will do just fine and there is no need for inheritance via abstract classes.