Java design pattern (1) simple factory pattern is not simple
Simple factory Use Cases
There is an abstract product-Car, and there are a variety of specific sub-products, such as BenzCar, BMWCar, and LandRoverCar. The class diagram is as follows:
As a driver, if you want to drive one of them, such as BenzCar, the most direct way is to directly create the BenzCar instance and execute its drive method, as follows:
package com.jasongj.client;import com.jasongj.product.BenzCar;public class Driver1 { public static void main(String[] args) { BenzCar car = new BenzCar(); car.drive(); }}
If you want to change to Land Rover, You need to modify the code, create a Land Rover instance, and execute its drive method. This means that the client code must be modified whenever a vehicle needs to be switched.
A slightly better way is to read the configuration file, get the desired Car, create an instance, and point to it by referencing the parent Car, use polymorphism to execute drive methods for different vehicles. As follows:
package com.jasongj.client;import org.apache.commons.configuration.ConfigurationException;import org.apache.commons.configuration.XMLConfiguration;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import com.jasongj.product.BMWCar;import com.jasongj.product.BenzCar;import com.jasongj.product.Car;import com.jasongj.product.LandRoverCar;public class Driver2 { private static final Logger LOG = LoggerFactory.getLogger(Driver2.class); public static void main(String[] args) throws ConfigurationException { XMLConfiguration config = new XMLConfiguration("car.xml"); String name = config.getString("driver2.name"); Car car; switch (name) { case "Land Rover": car = new LandRoverCar(); break; case "BMW": car = new BMWCar(); break; case "Benz": car = new BenzCar(); break; default: car = null; break; } LOG.info("Created car name is {}", name); car.drive(); }}
For Car users, you only need to use parameters to specify the desired Car types and obtain its instances. No matter which Car is used, you do not need to modify subsequent Car operations. So far, the prototype of the simple factory model has been formed. If the above logic judgment is encapsulated into a static method of a special class, a simple factory mode is implemented. The Factory Code is as follows:
package com.jasongj.factory;import org.apache.commons.configuration.ConfigurationException;import org.apache.commons.configuration.XMLConfiguration;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import com.jasongj.product.BMWCar;import com.jasongj.product.BenzCar;import com.jasongj.product.Car;import com.jasongj.product.LandRoverCar;public class CarFactory1 { private static final Logger LOG = LoggerFactory.getLogger(CarFactory1.class); public static Car newCar() { Car car = null; String name = null; try { XMLConfiguration config = new XMLConfiguration("car.xml"); name = config.getString("factory1.name"); } catch (ConfigurationException ex) { LOG.error("parse xml configuration file failed", ex); } switch (name) { case "Land Rover": car = new LandRoverCar(); break; case "BMW": car = new BMWCar(); break; case "Benz": car = new BenzCar(); break; default: car = null; break; } LOG.info("Created car name is {}", name); return car; }}
The caller code is as follows:
package com.jasongj.client;import com.jasongj.factory.CarFactory1;import com.jasongj.product.Car;public class Driver3 { public static void main(String[] args) { Car car = CarFactory1.newCar(); car.drive(); }}
Compared with Driver2, all the judgment logic is encapsulated in the CarFactory1. Driver3 no longer needs to care about Car instantiation and achieves object creation and use isolation.
Of course, the simple factory mode does not require you to read the configuration file to determine which class to instantiate. You can pass parameters as parameters of the factory static method.
Simple factory mode advanced use of reflection for scalability
We can see from the implementation of Driver2 and CarFactory1 that when a new car is added, the code of Driver2 and CarFactory1 needs to be updated to support the new car. This violatesPrinciple of opening/closing
(Open-Close Principle ). Reflection can be used to solve this problem.
package com.jasongj.factory;import org.apache.commons.configuration.ConfigurationException;import org.apache.commons.configuration.XMLConfiguration;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import com.jasongj.product.Car;public class CarFactory2 { private static final Logger LOG = LoggerFactory.getLogger(CarFactory2.class); public static Car newCar() { Car car = null; String name = null; try { XMLConfiguration config = new XMLConfiguration("car.xml"); name = config.getString("factory2.class"); } catch (ConfigurationException ex) { LOG.error("Parsing xml configuration file failed", ex); } try { car = (Car)Class.forName(name).newInstance(); LOG.info("Created car class name is {}", name); } catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) { LOG.error("Instantiate car {} failed", name); } return car; }}
The code above shows that if you need to introduce a new Car, you only need to specify the complete class name (including package name) of the Car in the configuration file ), carFactory2 can be instantiated through reflection. Enable the extension and disable the modification. Readers familiar with Spring should think of the implementation of Spring IoC.
Annotations make the simple factory model not simple
In the preceding example, reflection is used to enable extension and disable modification. However, in some cases, it is not convenient to use the full name of a class, so it is more appropriate to use aliases. For example, each Bean in Spring has an ID, which is also referenced by the ID when the Bean is referenced. A Data Stream tool such as Apache Nifi uses the responsibility chain mode in the process, while a factory is used for the creation of a single Processor, the user-defined Processor does not need to be registered through the code, but uses annotations (for a more convenient understanding of the following code, please read the author's article "Java series (1) annotation (Annotation).
Next we will continue to use annotations to upgrade the simple factory model based on the above cases.
package com.jasongj.factory;import java.util.Collections;import java.util.Map;import java.util.Set;import java.util.concurrent.ConcurrentHashMap;import org.apache.commons.configuration.ConfigurationException;import org.apache.commons.configuration.XMLConfiguration;import org.reflections.Reflections;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import com.jasongj.annotation.Vehicle;import com.jasongj.product.Car;public class CarFactory3 { private static final Logger LOG = LoggerFactory.getLogger(CarFactory3.class); private static Map
allCars; static { Reflections reflections = new Reflections("com.jasongj.product"); Set
> annotatedClasses = reflections.getTypesAnnotatedWith(Vehicle.class); allCars = new ConcurrentHashMap
(); for (Class
classObject : annotatedClasses) { Vehicle vehicle = (Vehicle) classObject.getAnnotation(Vehicle.class); allCars.put(vehicle.type(), classObject); } allCars = Collections.unmodifiableMap(allCars); } public static Car newCar() { Car car = null; String type = null; try { XMLConfiguration config = new XMLConfiguration("car.xml"); type = config.getString("factory3.type"); LOG.info("car type is {}", type); } catch (ConfigurationException ex) { LOG.error("Parsing xml configuration file failed", ex); } if (allCars.containsKey(type)) { LOG.info("created car type is {}", type); try { car = (Car) allCars.get(type).newInstance(); } catch (InstantiationException | IllegalAccessException ex) { LOG.error("Instantiate car failed", ex); } } else { LOG.error("specified car type {} does not exist", type); } return car; }}
From the code above, we can see that the factory will scan all cars annotated by Vehicle (each Car declares its type in the annotation and can be used as the alias of this Car) then, the original Class ing between the Car alias and the Class of the specific Car is established. In this case, the static method of the factory can instantiate the corresponding Car according to the target alias.
All the code in this article can be downloaded from the author GitHub.
Simple factory mode description simple factory mode definition
Simple Factory Pattern is also called Static FactoryMethod Pattern ). Define a class (for example, CarFactory1, CarFactory2, and CarFactory3 in the preceding Section) to create instances of other classes and decide which specific class to instantiate, this avoids explicit designation in the client code and achieves decoupling. This class can create different subclass objects under the same abstract class (or interface), just like a factory, so it is called a factory class.
Simple factory pattern class diagram
The simple factory pattern class diagram is as follows:
Simple factory model role division factory role (for example, CarFactory1/2/3 in the preceding Section): This is the core of the simple factory model and is responsible for creating the internal logic of all classes. Of course, the factory class must be called by the outside world to create the required product objects. Generally, the factory class provides a static method by which external programs create the required objects. Abstract Product role (for example, Car): parent class of all objects created in simple factory mode. Note that the parent class can be an interface or an abstract class, which describes the common public interfaces of the created instance. Specific product roles (such as BMWCar, BenzCar, and LandRoverCar): specific instance objects created in a simple factory. These specific products often share a common parent class. Advantages of the simple factory model the factory class is the key to the whole simple factory model. It contains the necessary judgment logic and determines the object of a specific class based on the information (configuration, or parameter) given by the outside world. You can directly create required instances based on the factory class during use without understanding how these objects are created and organized. It is conducive to the optimization of the entire software architecture. By introducing configuration files and reflection, you can replace and add new product classes without modifying any client code, which improves system flexibility (such as CarFactory2) to a certain extent ). The client does not need to know the class name of the created product class, but only needs to know the parameter corresponding to the specific product class. For some complex class names, the simple factory mode can reduce users' memory volumes (such as CarFactory3 ). The simple factory model and the OOP principle have followed the principle of relying on the inverted principle of the dummit law the Lee's replacement principle the interface isolation principle does not follow the principle of the open and closed principle (as described above, this can be avoided by using configuration files + reflection or annotations.) single responsibility principle (factory class: Responsible for logical judgment and instance creation) the disadvantage of the simple factory mode is that the factory class integrates the creation logic of all instances, which directly causes all clients to be implicated once a problem occurs in the factory. Because the product in the simple factory mode is based on a common abstract class or interface, when the product type increases, there are different product interfaces or abstract classes, the factory class needs to determine when to create the interface product, which is confused with the product of the type created, violating the single responsibility principle, leading to the loss of flexibility and maintainability of the system. As mentioned above, in general cases (such as CarFactory1), the simple factory mode violates the "open-close principle", because when we add a new product, we must modify the factory class, the corresponding factory classes need to be re-compiled. But this can be solved to some extent by Using Reflection (CarFactory3 also uses reflection in essence) (such as CarFactory2 ). The simple factory mode uses the static factory method, which makes the factory role unable to form a hierarchy based on inheritance. The author holds a reserved attitude, because inheritance is not an aim. Without such a requirement, this is not a disadvantage, such as JDBC DriverManager. Typical application of simple factory mode in JDK
JDBC is the most typical application of the simple factory mode in JDK. Relational databases can be considered as abstract products. Specific relational databases (MySQL, PostgreSQL, and Oracle) provided by various vendors are specific products. DriverManager is a factory class. When an application uses a relational database through the JDBC interface, it does not need to care about which database is used, but directly uses the DriverManager static method to obtain the Connection of the database.
package com.jasongj.client;import java.sql.Connection;import java.sql.DriverManager;import java.sql.PreparedStatement;import java.sql.SQLException;import org.slf4j.Logger;import org.slf4j.LoggerFactory;public class JDBC { private static final Logger LOG = LoggerFactory.getLogger(JDBC.class); public static void main(String[] args) { Connection conn = null; try { Class.forName("org.apache.hive.jdbc.HiveDriver"); conn = DriverManager.getConnection("jdbc:hive2://127.0.0.1:10000/default"); PreparedStatement ps = conn.prepareStatement("select count(*) from test.test"); ps.execute(); } catch (SQLException ex) { LOG.warn("Execute query failed", ex); } catch(ClassNotFoundException e) { LOG.warn("Load Hive driver failed", e); } finally { if(conn != null ){ try { conn.close(); } catch (SQLException e) { // NO-OPT } } } }}