The Factory Pattern is one of the popular creational design patterns in Java. It provides a single access point to obtain the suitable implementation of a service that is exposed as an abstract class or interface by using a service identifier that’s often the shorthand name of the implementation type and represented by a String or Enum. This pattern is used to create objects without specifying the exact class of object that will be created in the runtime and many of the frameworks and Java APIs developers come across every day use this pattern.
This article aims to augment the design pattern to provide better readability and maintainability. It illustrates how the service discovery is automated by the proposed approach without requiring the factory method to manually accommodate code for the creation of a new service implementation every time a new service implementation is created for a different business need.
Audiences of this article are assumed to have prior exposure to the Java programming language along with basic concepts of design patterns. However, we touch upon the basics of the Factory Design Pattern in the following section so we can elaborate on and show how the augmentation we proposed works on the example we will have covered there.
1. Factory Design Pattern: A Quick Revision
The Factory Design Pattern is a creational pattern that provides an interface for creating objects in a superclass but allows subclasses to alter the type of objects that will be created. This pattern is particularly useful when the exact type of object to be created is determined at runtime.
Key Components
- Service: An abstract business functionality exposed as an interface
- Service Implementation: These are implementing classes of the above interface. They, of course, implement the different abstract methods defined for different but similar business functions.
- Factory Method: This is normally the creator, often simplified as a static method in a class, known as the
factory
class, which returns an object of the proper service implementation class based on the argument(s) passed to it via the consumer code.
Example Scenario
A warehouse packs an item based on the material it is made of. They take utmost care to prevent in-transit damage to the items being dispatched. Packing items that are fragile must be different from packing electronic goods that need to be protected from extreme temperature and shock, for example. They have dedicated packers for each category of items. So, we may assume that if Packer
is an abstract type, getting the right packer is one who needs knowledge of the type of the item to be packed. The code snippets quickly describe the scenario:
Example With Code Snippet
- We have a POJO representing the items in the warehouse:
- The service
Packer
is exposed as an interface: - We have these coupled implementations for the interface:
- Finally, the class
PackerFactory
has a method to return an object of the suitable implementation based on the argument passed to it.
A simple consumer code looks like this:
2. The Problem Statement and the Motivation Behind It
Let us assume that the warehouse was to accommodate a few more categories of items like medicine, inflammable, non-inflammable liquid, etc. The changes to the codebase include not only the respective implementations of Packer
, but the change needed in the factory method getSuitablePacker(String)
to accommodate those implementations, too.
The implementations may be independent of each other, requiring no or little more than just the service interface to implement. It is imperative that the factory method has knowledge of each of the service implementations, and the business condition to return the appropriate one based on the method argument, that is itemCategory
in the above example. New implementations may not be rolled out at one shot, but usually one or a few at a single release cycle.
We aim to relieve developers from writing and maintain the factory method. It’s often that different small teams work on different implementations of the service and maintain the factory method. There’s always a chance — however little — that teams might override each other while trying to accommodate their respective service implementations (or versions of service implementation: often a couple or more versions of the same service implementations coexist; until the older ones are made to smoothly retire, but this is beyond the scope of our discussion), despite the version control system, DevOps etc. in a large project.
There is yet another use case for this: often service implementations may be released as individual artifacts. The factory method itself might be in one, and why should we still consider it open for development just for adding some boilerplate codes to accommodate newer services? We strongly felt that we should not write any boilerplate code in the factory method itself. In fact, it’d be even better if we never had to write and maintain one, at least for a simple use-case!
3. Proposed Approach
Given a defined service abstraction (interface), we aim to address the following:
- Tagging the service implementations with respective look-up identifiers (like
itemCategory
in the above example) - Offloading development of factory methods and subsequent boilerplate maintenance
3.1 Process Flow
This process flow illustrates how to achieve it:
We shall discuss every step in detail followed by supporting code snippets. It is worth noting that the same flow can be achieved in various ways, depending on the frameworks (e.g., Spring) and dependency injection containers being used. We, therefore, strongly recommend that you achieve things the way that best suits your project landscape, as we are going to keep things agnostic of any framework just adhering to Vanilla Java 8.
First, we must introduce the following terminologies that shall be referred to frequently in this section and beyond.
Service Registry Map
This is a map that keeps track of the implementations of service
interfaces. We represent this data structure with a nested java.util.HashMap
implementation.
private static Map<Class<?>, Map<String, Object>> serviceRegistryMap = new HashMap<>();
The key to the outer Map
is java.lang.Class
of a service
interface and the value is itself a Map
whose keys are binding identifiers (discussed in the next section — it’s okay to skip this terminology for now) and whose values are service implementation objects. A sample service registry map for the Packer
interface we have been discussing since Section 1 looks like this:
The key
is Class<Packer Interface>
and the value
is a map that contains the objects of GlassPacker
and ElectronicsPacker
implementations against the keys “glass” and “electronics”, respectively.
Service Binding Identifier
Though this appeared something fascinating in the beginning to many of you, possibly you have already realized by now that it is the same as itemCategory
in the Packer
example. This is truly what helps the factory method pick up the right implementation of a service interface. It is usually a human-readable identifier such as String or Enum.
It’s a good idea to use Enum instead of String, though we used String in the example for simplicity.
Populating Service Implementation Registry
This is a process that dynamically loads the elements in the Service Registry Map behind the scenes. We shall discuss this in detail in Section 3.2.
Service Meta Information
Service Meta information tells us about the service interface and the respective bindings. It is worth mentioning: notice the plural, bindings. We had come across scenarios where a single service implementation held good for more than one scenario. For example, a GlassPacker
could cater exactly the same way as something like BrittleGlassUtensilsPacker
would. The former could, therefore, be used with itemCategory =” brittleGlassUtensils”
, too; and GlassPacker
works for both “glass”
and “brittleUtensils”
.
In short, we provisioned 1-to-many cardinality.
Provided Factory Method
This is a pre-written piece of code that picks up the correct implementation from the Service Registry Map, using bindings. The code snippet below shows its implementation. However, we shall come back to this later, too.
Please note that the above method is simply written to illustrate the article with brevity. It has ample scope for refinement.
3.2 Populating Service Implementation Registry: A Simple Mechanism
We have seen thus far that the service registry is populated with service meta information and a string identifier is used to fetch suitable service implementation. This can be designed in several ways depending on the framework, especially the one for dependency injection you are using. But again, we adhere to Vanilla Java only.
Adaption of this approach is even easier when you’re using frameworks like Spring, Micronaut, Dagger, Guice etc. The approaches of some of these are coarsely aligned to how we achieve a minimally working prototype using Java.
We define service metainformation by annotation. We annotate the service implementation classes accordingly.
We have two attributes: one is the java.lang.Class
of the target service interface and another is a String
array, containing bindings that define a service implementation.
The GlassPicker
service implementation class looks like this: it’s apt that bindings
hold the look-up keys and the factory method would use “glass”
to find a suitable Packer
implementation, if available. It is worth mentioning here that the attribute targetService
appears to be redundant. Why must a developer ever care about re-declaring what the class implements? We felt the same at the beginning of the work and went ahead without it. However, we accommodated this after we had come across some hurdles with complex legacy-type hierarchy in the real project for which we had envisaged this.
You must feel motivated to get rid of this attribute unless yours is a project that involves complex, multilevel hierarchies!
We realized, as mentioned at the beginning of this article, that service implementations may be rolled out in phases in different artifacts. We leveraged Java SPI (Service Provider Interface) to find service implantation. This helped reduce application startup time. Recursive checks for implementation classes would have been too expensive!
The logic is simple for average use cases:
- Gather service implementation classes using Java Service Provider Interface (you might like this brief brush-up).
- Parse the annotation on the implementation classes to fetch
targetService
andbindings
.
Populate the service registry map keeping Class<Service Interface>
as key
of the outer map and value
of a binding as the key to the nested map. The object of the service implementation class was the value of the inner map.
This small piece of validation is performed to account for any human error while annotating the service.
Implementation classes:
3.3 Accessing the Service Registry in a Simple Java Application
Though an application is never as simple as a "Hello, World!"
Java class with a main
method, they exist everywhere to facilitate learning anything, during knowledge transfer, during self-assessment on a technology or framework. We, therefore, present this part as simplest we can. The audience must use it as per their need.
Please note that we have considered only one service interface for simplicity. The method lookUpService(Class<T> cls)
maybe called on any number of classes from an overloaded version of the same method.
3.4 Important Notes on the Hiccups
The most inevitable question that we faced while deploying this mechanism into the project landscape was the usability of Java SPI. We had over 100 implementation classes of a few services that had existed for years. Fortunately, they were either owned by us or not closed for maintenance, and we could go ahead making relevant changes such as annotating the classes and declaring the service implementation under the folder src/main/resources/META-INF/services (this is where we declare services for a Maven project). It would have been a very tedious, frustrating, and error-prone refactoring had we not resorted to a Script written specifically to do this refactoring — but that’s an altogether different story falling beyond our current scope. However, if service interfaces in your project landscape are closed for development or you cannot legally deploy the changes you make due to license issues or compliance restrictions, neither Java SPI nor annotations approach works for you, at least for existing services. However, newer services may still benefit from this or similar approaches.
- Until Java SE 1.8, we expose a service by declaring its fully qualified implementation class name in
META-INF/services/ fullyQualifiedServiceInterface
. A servicecom.somecompany.fooService
that has been implemented incom.anothercompany.pkg1.SomeFooServiceImpl
needs to be declared in the fileMETA-INF/services/com.somecompany.fooService
and its content will becom.anothercompany.pkg1.SomeFooServiceImpl
. However, starting with Java 9, modules have been introduced. Though the majority of the legacy projects that are upgraded to higher and LTS versions of Java, seldom embrace modularity, you need to expose the service accordingly (in our honest opinion, that’s easier, though!) if you must use modularity. - It is worth mentioning that this approach does not necessarily encourage you to load all the services in the service registry at once. But loading them when they’re needed is provisioned here. This does not affect the existing service registry.
- This approach is suitable and can be extended fruitfully to accomplish the Abstract Factory Pattern, too.
Loading service via SPI suffers from the usual shortcomings of ServiceLoader
. Unless we’re doing something to customize class loaders and listeners for dynamicity, this should not bother you.
3.5 What Is Yet Untold
We did not tell you that our project landscape included the Spring framework and Spring Boot. We used the concept of annotating service implementations, but the service registry map was easily achieved with the Spring container. In Spring Boot, you can get implementations of an interface in the following way:
- The service implementation classes are to be marked
@Service
or@Component
as applicable. - Inside the class
FactoryService
, which itself is a@Service
, it can be injected with a field of typeList<ServiceInterface>
. Just be careful that at least one implementation exists to prevent the application from failing to start up due to unsatisfied dependency. You could otherwise mark the dependency as not mandatory or usejava.util.Optional
of the injected field. You can choose whichever suits you best regarding constructor vs. field injection because it is very opinion-based. But we prefer constructor injection. Also, certain service classes might be heavy, and you could decide if you want them to be loaded lazily; in this case, due care needs to be taken to ensure that we populate the service map only when all the beans have been loaded. The remaining stuff, like parsing the annotation, etc. was the same as the method we illustrated in section 3.2.
Final Thought
Finally, we conclude by saying that compile-time dependency injection is way faster and we’re aiming to write an article dedicated solely for the purpose.