Geek Time-The Beauty of Design Patterns Factory Mode (Part 1): Why do I say it's okay not to use factory patterns to create objects?

In general, the factory pattern is divided into three more subdivided types: simple factory, factory method and abstract factory. However, in GoF's "Design Patterns" book, it regards the simple factory pattern as a special case of the factory method pattern, so the factory pattern is only divided into two categories: factory methods and abstract factories. In fact, the former classification method is more common, so in today's explanation, we will continue to use the first classification method.

Among these three subdivided factory models, the simple factory and factory method principles are relatively simple, and they are also commonly used in actual projects. The principle of abstract factory is slightly more complicated, and it is relatively uncommon in actual projects. Therefore, the focus of our explanation today is the first two factory models. For abstract factories, you just need to understand a little bit.

In addition, the focus of our explanation is not on the principle and implementation, because these are very simple, the point is to show you the application scenario: when should you use the factory model? Compared with creating objects directly with new, what are the advantages of using factory mode to create objects?

Simple Factory

First, let's look at what is the simple factory pattern. Let's explain through an example.

In the code below, we select different parsers (JsonRuleConfigParser, XmlRuleConfigParser...) according to the suffixes of the configuration file (json, xml, yaml, properties), and parse the configuration stored in the file into a memory object RuleConfig.


public class RuleConfigSource {
    
    
  public RuleConfig load(String ruleConfigFilePath) {
    
    
    String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);
    IRuleConfigParser parser = null;
    if ("json".equalsIgnoreCase(ruleConfigFileExtension)) {
    
    
      parser = new JsonRuleConfigParser();
    } else if ("xml".equalsIgnoreCase(ruleConfigFileExtension)) {
    
    
      parser = new XmlRuleConfigParser();
    } else if ("yaml".equalsIgnoreCase(ruleConfigFileExtension)) {
    
    
      parser = new YamlRuleConfigParser();
    } else if ("properties".equalsIgnoreCase(ruleConfigFileExtension)) {
    
    
      parser = new PropertiesRuleConfigParser();
    } else {
    
    
      throw new InvalidRuleConfigException(
             "Rule config file format is not supported: " + ruleConfigFilePath);
    }

    String configText = "";
    //从ruleConfigFilePath文件中读取配置文本到configText中
    RuleConfig ruleConfig = parser.parse(configText);
    return ruleConfig;
  }

  private String getFileExtension(String filePath) {
    
    
    //...解析文件名获取扩展名,比如rule.json,返回json
    return "json";
  }
}

In the "Specification and Refactoring" part, we mentioned that in order to make the code logic clearer and more readable, we must be good at encapsulating functionally independent code blocks into functions. According to this design idea, we can strip out part of the logic involved in parser creation in the code and abstract it into the createParser() function. The code after refactoring is as follows:


  public RuleConfig load(String ruleConfigFilePath) {
    
    
    String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);
    IRuleConfigParser parser = createParser(ruleConfigFileExtension);
    if (parser == null) {
    
    
      throw new InvalidRuleConfigException(
              "Rule config file format is not supported: " + ruleConfigFilePath);
    }

    String configText = "";
    //从ruleConfigFilePath文件中读取配置文本到configText中
    RuleConfig ruleConfig = parser.parse(configText);
    return ruleConfig;
  }

  private String getFileExtension(String filePath) {
    
    
    //...解析文件名获取扩展名,比如rule.json,返回json
    return "json";
  }

  private IRuleConfigParser createParser(String configFormat) {
    
    
    IRuleConfigParser parser = null;
    if ("json".equalsIgnoreCase(configFormat)) {
    
    
      parser = new JsonRuleConfigParser();
    } else if ("xml".equalsIgnoreCase(configFormat)) {
    
    
      parser = new XmlRuleConfigParser();
    } else if ("yaml".equalsIgnoreCase(configFormat)) {
    
    
      parser = new YamlRuleConfigParser();
    } else if ("properties".equalsIgnoreCase(configFormat)) {
    
    
      parser = new PropertiesRuleConfigParser();
    }
    return parser;
  }
}

In order to make the responsibilities of the class more single and the code clearer, we can further separate the createParser() function into a separate class so that this class is only responsible for the creation of objects. And this class is the simple factory pattern class we are going to talk about now. The specific code is as follows:


public class RuleConfigSource {
    
    
  public RuleConfig load(String ruleConfigFilePath) {
    
    
    String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);
    IRuleConfigParser parser = RuleConfigParserFactory.createParser(ruleConfigFileExtension);
    if (parser == null) {
    
    
      throw new InvalidRuleConfigException(
              "Rule config file format is not supported: " + ruleConfigFilePath);
    }

    String configText = "";
    //从ruleConfigFilePath文件中读取配置文本到configText中
    RuleConfig ruleConfig = parser.parse(configText);
    return ruleConfig;
  }

  private String getFileExtension(String filePath) {
    
    
    //...解析文件名获取扩展名,比如rule.json,返回json
    return "json";
  }
}

public class RuleConfigParserFactory {
    
    
  public static IRuleConfigParser createParser(String configFormat) {
    
    
    IRuleConfigParser parser = null;
    if ("json".equalsIgnoreCase(configFormat)) {
    
    
      parser = new JsonRuleConfigParser();
    } else if ("xml".equalsIgnoreCase(configFormat)) {
    
    
      parser = new XmlRuleConfigParser();
    } else if ("yaml".equalsIgnoreCase(configFormat)) {
    
    
      parser = new YamlRuleConfigParser();
    } else if ("properties".equalsIgnoreCase(configFormat)) {
    
    
      parser = new PropertiesRuleConfigParser();
    }
    return parser;
  }
}

Most factory classes end with the word "Factory", but it is not necessary, such as DateFormat and Calender in Java. In addition, the methods of creating objects in the factory class generally start with create, such as createParser() in the code, but some are also named getInstance(), createInstance(), newInstance(), and some are even named valueOf () (such as the valueOf() function of the Java String class) and so on. We just name it according to specific scenarios and habits.

In the above code implementation, every time we call createParser() of RuleConfigParserFactory, we must create a new parser. In fact, if the parser can be reused, in order to save memory and object creation time, we can create the parser in advance and cache it. When the createParser() function is called, we take the parser object from the cache and use it directly.

This is a bit similar to the combination of singleton pattern and simple factory pattern. The specific code implementation is shown below. In the following explanation, we call the previous implementation method the first implementation method of the simple factory pattern, and call the following implementation method the second implementation method of the simple factory pattern.


public class RuleConfigParserFactory {
    
    
  private static final Map<String, RuleConfigParser> cachedParsers = new HashMap<>();

  static {
    
    
    cachedParsers.put("json", new JsonRuleConfigParser());
    cachedParsers.put("xml", new XmlRuleConfigParser());
    cachedParsers.put("yaml", new YamlRuleConfigParser());
    cachedParsers.put("properties", new PropertiesRuleConfigParser());
  }

  public static IRuleConfigParser createParser(String configFormat) {
    
    
    if (configFormat == null || configFormat.isEmpty()) {
    
    
      return null;//返回null还是IllegalArgumentException全凭你自己说了算
    }
    IRuleConfigParser parser = cachedParsers.get(configFormat.toLowerCase());
    return parser;
  }
}

For the implementation of the above two simple factory patterns, if we want to add a new parser, it is bound to be changed to the code of RuleConfigParserFactory. Does this violate the principle of opening and closing? In fact, if you don't need to add new parser frequently, just occasionally modify the RuleConfigParserFactory code, which does not conform to the opening and closing principle, which is completely acceptable.

In addition, in the first code implementation of RuleConfigParserFactory, there is a set of if branch judgment logic. Should it be replaced by polymorphism or other design patterns? In fact, if there are not many if branches, it is perfectly acceptable to have if branches in the code. The application of polymorphism or design patterns to replace if branch judgment logic is not without any shortcomings. Although it improves the scalability of the code and is more in line with the principle of opening and closing, it also increases the number of classes and sacrifices the code's availability. Readability. On this point, we will talk about it in detail in a later chapter.

To sum up, although there are multiple if branch judgment logics in the code implementation of the simple factory pattern, which violates the principle of opening and closing, it is a trade-off between scalability and readability. Such code implementation is in most cases (for example, it does not need to be frequently Add parser, there are not too many parser) is no problem.

Factory Method

What if we have to remove the if branching logic? The more classic approach is to use polymorphism. Refactor the above code according to the idea of ​​polymorphism. The code after refactoring is as follows:


public interface IRuleConfigParserFactory {
    
    
  IRuleConfigParser createParser();
}

public class JsonRuleConfigParserFactory implements IRuleConfigParserFactory {
    
    
  @Override
  public IRuleConfigParser createParser() {
    
    
    return new JsonRuleConfigParser();
  }
}

public class XmlRuleConfigParserFactory implements IRuleConfigParserFactory {
    
    
  @Override
  public IRuleConfigParser createParser() {
    
    
    return new XmlRuleConfigParser();
  }
}

public class YamlRuleConfigParserFactory implements IRuleConfigParserFactory {
    
    
  @Override
  public IRuleConfigParser createParser() {
    
    
    return new YamlRuleConfigParser();
  }
}

public class PropertiesRuleConfigParserFactory implements IRuleConfigParserFactory {
    
    
  @Override
  public IRuleConfigParser createParser() {
    
    
    return new PropertiesRuleConfigParser();
  }
}

In fact, this is a typical code implementation of the factory method pattern. So when we add a new parser, we only need to add a Factory class that implements the IRuleConfigParserFactory interface. Therefore, the factory method model is more in line with the principle of opening and closing than the simple factory model.

From the implementation of the above factory method, everything is perfect, but there are actually quite big problems. The problem lies in the use of these factory classes. Next, let's take a look at how to use these factory classes to implement the load() function of RuleConfigSource. The specific code is as follows:


public class RuleConfigSource {
    
    
  public RuleConfig load(String ruleConfigFilePath) {
    
    
    String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);

    IRuleConfigParserFactory parserFactory = null;
    if ("json".equalsIgnoreCase(ruleConfigFileExtension)) {
    
    
      parserFactory = new JsonRuleConfigParserFactory();
    } else if ("xml".equalsIgnoreCase(ruleConfigFileExtension)) {
    
    
      parserFactory = new XmlRuleConfigParserFactory();
    } else if ("yaml".equalsIgnoreCase(ruleConfigFileExtension)) {
    
    
      parserFactory = new YamlRuleConfigParserFactory();
    } else if ("properties".equalsIgnoreCase(ruleConfigFileExtension)) {
    
    
      parserFactory = new PropertiesRuleConfigParserFactory();
    } else {
    
    
      throw new InvalidRuleConfigException("Rule config file format is not supported: " + ruleConfigFilePath);
    }
    IRuleConfigParser parser = parserFactory.createParser();

    String configText = "";
    //从ruleConfigFilePath文件中读取配置文本到configText中
    RuleConfig ruleConfig = parser.parse(configText);
    return ruleConfig;
  }

  private String getFileExtension(String filePath) {
    
    
    //...解析文件名获取扩展名,比如rule.json,返回json
    return "json";
  }
}

From the above code implementation point of view, the creation logic of the factory class object is coupled into the load() function, which is very similar to our original code version. The introduction of the factory method not only did not solve the problem, but made the design more complicated. How to solve this problem?

**We can create another simple factory for the factory class, which is the factory of the factory, to create objects of the factory class. **This paragraph sounds a bit convoluted, I implemented the code, you can understand it at a glance. Among them, the RuleConfigParserFactoryMap class is a factory class that creates a factory object, and getParserFactory() returns a cached singleton factory object.


public class RuleConfigSource {
    
    
  public RuleConfig load(String ruleConfigFilePath) {
    
    
    String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);

    IRuleConfigParserFactory parserFactory = RuleConfigParserFactoryMap.getParserFactory(ruleConfigFileExtension);
    if (parserFactory == null) {
    
    
      throw new InvalidRuleConfigException("Rule config file format is not supported: " + ruleConfigFilePath);
    }
    IRuleConfigParser parser = parserFactory.createParser();

    String configText = "";
    //从ruleConfigFilePath文件中读取配置文本到configText中
    RuleConfig ruleConfig = parser.parse(configText);
    return ruleConfig;
  }

  private String getFileExtension(String filePath) {
    
    
    //...解析文件名获取扩展名,比如rule.json,返回json
    return "json";
  }
}

//因为工厂类只包含方法,不包含成员变量,完全可以复用,
//不需要每次都创建新的工厂类对象,所以,简单工厂模式的第二种实现思路更加合适。
public class RuleConfigParserFactoryMap {
    
     //工厂的工厂
  private static final Map<String, IRuleConfigParserFactory> cachedFactories = new HashMap<>();

  static {
    
    
    cachedFactories.put("json", new JsonRuleConfigParserFactory());
    cachedFactories.put("xml", new XmlRuleConfigParserFactory());
    cachedFactories.put("yaml", new YamlRuleConfigParserFactory());
    cachedFactories.put("properties", new PropertiesRuleConfigParserFactory());
  }

  public static IRuleConfigParserFactory getParserFactory(String type) {
    
    
    if (type == null || type.isEmpty()) {
    
    
      return null;
    }
    IRuleConfigParserFactory parserFactory = cachedFactories.get(type.toLowerCase());
    return parserFactory;
  }
}

When we need to add a new rule configuration parser, we only need to create a new parser class and parser factory class, and in the RuleConfigParserFactoryMap class, add the new parser factory object to the cachedFactories. The code changes are very few, basically in line with the opening and closing principle.

In fact, for the application scenario of rule configuration file parsing, the factory mode requires the creation of many additional Factory classes, which will also increase the complexity of the code. Moreover, each Factory class is just a simple new operation with very thin functions (only one line Code), there is no need to design a separate class, so in this application scenario, the simple factory pattern is simple and easy to use, and is more suitable than the factory method pattern.

When should we use the factory method pattern instead of the simple factory pattern?

As we mentioned earlier, the reason why a certain code block is stripped out and becomes a function or class independently is that the logic of this code block is too complicated. After stripping, the code can be clearer, more readable and maintainable. However, if the code block itself is not complicated, just a few lines of code, we do not need to split it into separate functions or classes.

Based on this design idea, when the creation logic of the object is more complicated, it is not just a simple new one, but when you want to combine other types of objects and do various initialization operations, we recommend using the factory method pattern to combine the complex creation logic Split into multiple factory classes, so that each factory class is not too complicated. Using the simple factory pattern, putting all the creation logic into a factory class, will cause the factory class to become very complicated.

In addition, in some scenarios, if the object is not reusable, the factory class must return a different object each time. If we use the simple factory pattern to implement, we can only choose the first implementation that includes if branch logic. If we still want to avoid the annoying if-else branching logic, at this time, we recommend using the factory method pattern.

Abstract Factory

After talking about simple factories and factory methods, let's look at the abstract factory pattern. The application scenarios of the abstract factory model are quite special, and the first two are not commonly used, so it is not the focus of our study in this lesson, you can simply understand it. In simple factories and factory methods, there is only one way to classify classes. For example, in the rule configuration parsing example, the parser class will only be classified according to the configuration file format (Json, Xml, Yaml...). However, if the class has two classification methods, for example, we can classify according to the configuration file format, or according to the parsed object (Rule rule configuration or System system configuration), then it will correspond to the following 8 parser classes .


针对规则配置的解析器:基于接口IRuleConfigParser
JsonRuleConfigParser
XmlRuleConfigParser
YamlRuleConfigParser
PropertiesRuleConfigParser

针对系统配置的解析器:基于接口ISystemConfigParser
JsonSystemConfigParser
XmlSystemConfigParser
YamlSystemConfigParser
PropertiesSystemConfigParser

For this particular scenario, if we continue to implement the factory method, we have to write a factory class for each parser, that is, to write 8 factory classes. If we need to add a parser for business configuration in the future (such as IBizConfigParser), then we need to add 4 factory classes accordingly. And we know that too many classes will make the system difficult to maintain. How to solve this problem?

The abstract factory was born for this very special scene. We can make a factory responsible for creating multiple objects of different types (IRuleConfigParser, ISystemConfigParser, etc.) instead of creating only one type of parser object. This can effectively reduce the number of factory classes. The specific code implementation is as follows:


public interface IConfigParserFactory {
    
    
  IRuleConfigParser createRuleParser();
  ISystemConfigParser createSystemParser();
  //此处可以扩展新的parser类型,比如IBizConfigParser
}

public class JsonConfigParserFactory implements IConfigParserFactory {
    
    
  @Override
  public IRuleConfigParser createRuleParser() {
    
    
    return new JsonRuleConfigParser();
  }

  @Override
  public ISystemConfigParser createSystemParser() {
    
    
    return new JsonSystemConfigParser();
  }
}

public class XmlConfigParserFactory implements IConfigParserFactory {
    
    
  @Override
  public IRuleConfigParser createRuleParser() {
    
    
    return new XmlRuleConfigParser();
  }

  @Override
  public ISystemConfigParser createSystemParser() {
    
    
    return new XmlSystemConfigParser();
  }
}

// 省略YamlConfigParserFactory和PropertiesConfigParserFactory代码

Now, let us look at the factory model from a new level of thinking. Its functions are nothing more than the following four. This is also the most essential reference standard for judging whether to use the factory model.

● Packaging changes: The creation logic may change. After being encapsulated into a factory class, the creation logic changes are transparent to the caller.

● Code reuse: Created code can be reused after being extracted to an independent factory class. Isolate complexity: encapsulate complex creation logic, and the caller does not need to know how to create objects.

● Control complexity: separate the creation code to make the original function or class responsibilities more single and the code more concise.

Guess you like

Origin blog.csdn.net/zhujiangtaotaise/article/details/110475164