One of my favourite design pattern is the Factory Method. The design pattern encapsulates object creation, i.e. the factory method decides which implementation you'll get. I often use a slightly different version of the pattern, which returns a specific implementation of an interface based on a name parameter. The supported names are defined as static constants in the factory class. This way, I decouple the actual implementation from its user. The only link to the actual implementation is the name constant.
The implementation I use has several problems. Everytime I add a new manufacturable class, I have to change the factory class by adding a new name constant to identify the new manufacturable. And I have to add nearly the same code to actually instantiate and return the new type by the factory, which violates the DRY-principle. This also makes the factory tightly coupled to the new type, which I want to avoid if possible.
Another problem I had in the past was that I use to same code in every Singleton class (Factory Methods are often Singletons). I solved this by using a Dependency Injection Framework like Google Guice or Spring. It's possible to mark singleton classes using these frameworks by annotations or configuration. The frameworks will then make sure that the marked classes are singletons during execution.
So, I felt a need to improve my implementation of the factory methods. I wanted the following properties for my factory methods:
- It should be easy to create new factories, without much (duplicate) code.
- It should be easy to add new manufacturables, without changing code in the factory class.
The result is the implementation I'll describe next. Before we go to the actual implementation, let's create an interface for the manufacturable classes, and two example implementation of the manufacturable classes.
The manufacturable interface:
package com.javaeenotes; public interface MyManufacturable { public String getName(); }
Two implementations of the manufacturable interface:
package com.javaeenotes; public class MyManufacturableAImpl implements MyManufacturable { @Override public String getName() { return getClass().getSimpleName(); } }
package com.javaeenotes; public class MyManufacturableBImpl implements MyManufacturable { @Override public String getName() { return getClass().getSimpleName(); } }
The abstract factory method class:
package com.javaeenotes; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.util.HashMap; import java.util.Map; import java.util.Set; public abstract class AbstractFactory<T> { private static final String CONFIG_FILE_EXTENSION = ".conf"; private final String factoryName; private final Map<String, Class<? extends T>> manufacturableClasses; public AbstractFactory(String name) { this.factoryName = name; this.manufacturableClasses = new HashMap<String, Class<? extends T>>(); try { loadClasses(); } catch (IOException ioException) { throw new IllegalArgumentException( "Error reading factory configuration file.", ioException); } catch (ClassNotFoundException classNotFoundException) { throw new IllegalStateException( "Could not find class for name in factory configuration file.", classNotFoundException); } } @SuppressWarnings("unchecked") private void loadClasses() throws IOException, ClassNotFoundException { BufferedReader configReader = null; try { configReader = new BufferedReader(new InputStreamReader(getClass() .getResourceAsStream(factoryName + CONFIG_FILE_EXTENSION))); String className; while ((className = configReader.readLine()) != null) { manufacturableClasses.put( className, (Class<? extends T>) Class.forName(className)); } } finally { if (configReader != null) { configReader.close(); } } } public Set<String> getNamesManufacturables() { return manufacturableClasses.keySet(); } public T getInstance(String nameManufacturable) throws InstantiationException, IllegalAccessException { return manufacturableClasses.get(nameManufacturable).newInstance(); } }
The factory method class contains no hard references to our manufacturable implementation classes. Hey, there's even no reference to our manufacturable interface, because we use generics!
The factory is designed as a generic abstract class, which should be extended by every factory method class in the application. The manufacturables are defined in a configuration file in the class path, which has the same name as the factory. The configuration file has a .conf file extension, and should contain the fully qualified names of the manufacturable classes. Upon factory instantiation, the configuration file is read, and all supporting manufacturable classes are loaded.
Here is the example factory:
package com.javaeenotes; public class MyFactory extends AbstractFactory<MyManufacturable> { private static final String name = "MyFactory"; public MyFactory() { super(name); } }
The factory configuration file we use to support our manufacturable classes (MyFactory.conf):
com.javaeenotes.MyManufacturableAImpl com.javaeenotes.MyManufacturableBImplWe can use the following main class, which prints the names of the manufacturables, as demonstration:
package com.javaeenotes; public class Main { public void run() { MyFactory factory = new MyFactory(); for (String name : factory.getNamesManufacturables()) { try { MyManufacturable myManufacturable = factory.getInstance(name); System.out.println(myManufacturable.getName()); } catch (Exception e) { e.printStackTrace(); } } } public static void main(String[] args) { new Main().run(); } }
Conclusion
The factory method class implementation makes adding new factories very simple. Every factory class just has to extend the abstract factory class, and call its constructor with the name of the factory. The whole factory class consists of just several lines!
Registering new manufacturable classes to the factory is also much easier. Just add the fully qualified name of the new class to the factory configuration file, and we're done!
This concludes this post. If you have any improvements, please don't hesitate to share them in the comments.