This is the sixth day of my participation in the First Challenge 2022. For details: First Challenge 2022.
In project development, we often encounter scenarios where we need to create complex objects, such as a constructor for a class with many arguments. If we didn’t use any design patterns, the project would become so complex that we would have to change parameters one by one and make changes wherever the class was referenced, resulting in an unmaintainable mess. So how to solve this problem?
The answer is to use the factory model. When we need to create complex objects and use them frequently, we can choose the factory pattern.
Three implementations of the factory pattern
There are three implementations of the factory pattern
- Simple factory (static factory)
- Factory Method (core of this section)
- The abstract factory
Note: Simple factories are not included in the GoF’s 23 design patterns
In this section, we will take an in-depth look at the factory method pattern and its application.
Simple factory (static factory)
Before I introduce the factory method pattern, I’ll introduce the simple factory. In the simple factory pattern, the method for creating instances is usually static, so the simple factory pattern is also known as static factory method. A simple factory has only one specific factory class, which is used to produce specific products. When multiple products need to be produced, multiple products need to be defined in the same factory for production. The Simple factory lives up to its name and is very simple. The advantage of this is that the product is easy to create, customers directly raise the demand, the factory directly realize, do not need to go through any product line. The problem is that simple factories can become “overwhelmed” when they need to create a lot of different products. Going back to software, simple factories add complexity to systems, and in the case of multiple products, a single class is too responsible and the software becomes difficult to maintain. Adding a factory class for every product violates the “open closed principle.”
Example of a simple factory
We have apple and pear trees and a factory that takes all the fruit off the fruit trees.
package com.yeliheng.factory.simplefactory;
public class Main {
public static void main(String[] args) {
Apple apple = (Apple) SimpleFactory.make(0);
Banana banana = (Banana) SimpleFactory.make(1);
apple.show();
banana.show();
}
// Fruit products
public interface Fruit {
void show(a);
}
// Specific products
static class Apple implements Fruit {
public void show(a) {
System.out.println("Apples grow on apple trees..."); }}// Specific products
static class Banana implements Fruit {
public void show(a) {
System.out.println("Bananas grow on banana trees..."); }}// Factory production
static class SimpleFactory {
public static Fruit make(int kind) {
switch (kind) {
case 0:
return new Apple();
case 1:
return new Banana();
}
return null; }}}Copy the code
We can see from the code example that for each specific fruit produced, a new static product class is defined so that the fruit can be produced by the factory. The code for the factory class will become more complex as the product grows.
To solve this problem, the Factory Method pattern emerged.
The factory method
The factory method pattern can be further abstracted on the basis of the simple factory pattern, which can make the system expand the new concrete class without modifying the original factory code and meet the open and close principle. To expand new demands and create new products, we only need to add a factory class on the original basis, so that we can have multiple factories to serve us. And the upper-layer application only needs to understand the abstract product class, and does not need to extend the other classes. It also satisfies Demeter’s law, dependence inversion principle and Richter’s substitution principle. But the factory approach pattern also has drawbacks. It will increase the number of factory classes and reduce the readability of system code. And in abstract products, only one concrete product can be produced. (This problem can be solved using abstract factories, described in the next section).
The factory method pattern consists of the following structure:
- AbstractFactory: Provides an abstract class or interface for a product.
- ConcreteFactory: A concrete method for creating classes that implement concrete products.
- Abstract Product: An abstract creation class for a Product.
- Concrete Products: Implement abstract products.
Actual Application Scenarios
- Log factory
- File storage
.
The sample
Let’s implement a log factory with the following three functions:
- Prints logs to the console
- Prints logs to files
We implement the factory method one by one according to the structure described above:
- Abstract product
Logger.java
package com.yeliheng.factory.factorymethod;
/** * Log product abstraction */
public interface Logger {
// Log level
void debug(a);
void info(a);
void warning(a);
void error(a);
}
Copy the code
We defined four product interfaces in the abstract product that correspond to four different levels of logging.
- The abstract factory
LoggerFactory.java
package com.yeliheng.factory.factorymethod;
public interface LoggerFactory {
Logger getLogger(a);
}
Copy the code
In the abstract factory, let’s use Logger and define a getLogger() method to make it easier for the specific factory to get the Logger instance later.
- Specific products
We implement two concrete product classes, one for output to console and one for output to files. Here for the sake of code brevity, I will not write out the logic of the output to the file in detail, just use the output for a simple example.
ConsoleLogger.java
package com.yeliheng.factory.factorymethod;
public class ConsoleLogger implements Logger{
@Override
public void debug(a) {}@Override
public void info(a) {
System.out.println("[INFO] This log is output to [console]");
}
@Override
public void warning(a) {}@Override
public void error(a) {}}Copy the code
FileLogger.java
package com.yeliheng.factory.factorymethod;
public class FileLogger implements Logger{
@Override
public void debug(a) {}@Override
public void info(a) {
System.out.println("[INFO] This log is output to [file]");
}
@Override
public void warning(a) {}@Override
public void error(a) {}}Copy the code
- Implement specific factories
ConsoleLoggerFactory.java
package com.yeliheng.factory.factorymethod;
/** * Specific factory */
public class ConsoleLoggerFactory implements LoggerFactory{
@Override
public Logger getLogger(a) {
return newConsoleLogger(); }}Copy the code
We rewrite the implementation of the abstract Factory: LoggerFactory and rewrite the getLogger method to return an instance of ConsoleLogger.
FileLoggerFactory.java
package com.yeliheng.factory.factorymethod;
public class FileLoggerFactory implements LoggerFactory{
@Override
public Logger getLogger(a) {
return newFileLogger(); }}Copy the code
Same principle.
- Finally, we can start using the log factory we created with the factory method pattern.
Main.java
package com.yeliheng.factory.factorymethod;
public class Main {
public static void main(String[] args) {
// Output to the console
LoggerFactory consoleLoggerFactory = new ConsoleLoggerFactory();
Logger logger1 = consoleLoggerFactory.getLogger();
logger1.info();
// Output to a file
LoggerFactory fileLoggerFactory = newFileLoggerFactory(); Logger logger2 = fileLoggerFactory.getLogger(); logger2.info(); }}Copy the code
You can see the final output as follows:
If you are careful, you may notice that this is very similar to the slF4J logging library. Yes, a review of the SLF4J source code shows that it uses a number of factory patterns to implement a powerful logging framework.
summary
At this point, we have completed a simple log factory using the factory method pattern, and in the next section we will continue to refine the examples in this section using the abstract method pattern.
Source code reference: Github