Factory mode of design mode

Posted by intodesi on Thu, 17 Feb 2022 10:38:31 +0100

Factory mode

And Singleton mode Similarly, the factory pattern also belongs to a kind of creative design pattern. Singleton mode is used to ensure that there is only one instance of a class, while factory mode is used to create different objects related to types. It also has different implementation methods. It can be subdivided into simple factory, factory method and abstract factory, which are applicable to different scenarios respectively.

Simple factory

We often do some alarm related functions in our project. The alarm can be divided into SMS alarm, voice alarm, email alarm and other methods. After receiving this demand, we use the simplest way to realize it as follows.

public class Service {
    public void doSomething() {
        // Omit business code
        String type = "..";
        Alert alert = new Alert();
        if ("sms".equals(type)) {
            alert.sendSms();
        } else if ("email".equals(type)) {
            alert.sendEmail();
        } else if ("voice".equals(type)) {
            alert.sendVoice();
        }
        // Omit business code
    }
}


public class Alert {
    public void sendSms(){
    }

    public void sendEmail(){
    }

    public void sendVoice(){
    }
}

In order to ensure that there are not too many alarm codes in the business logic, we abstract the codes of different types of alarms into the methods of the alarm class. However, each time we add an alarm type, we still need to add a new method, and then modify the business class. If the alarm logic is complex, the number of codes of the alarm class will continue to increase. In order to solve the problem of complex alarm classes, We can abstract an interface for the alarm, and the changed code is as follows.

public class Service {
    public void doSomething() {
        // Omit business code
        String type = "..";
        Alert alert = null;
        if ("sms".equals(type)) {
            alert = new SmsAlert();
        } else if ("email".equals(type)) {
            alert = new EmailAlert();
        } else if ("voice".equals(type)) {
            alert = new VoiceAlert();
        }
        alert.send();
        // Omit business code
    }
}

public interface Alert {
    void send();
}

public class SmsAlert implements Alert{
    @Override
    public void send() {
    }
}
public class EmailAlert implements Alert{
    @Override
    public void send() {
    }
}
public class VoiceAlert implements Alert{
    @Override
    public void send() {
    }
}

At this time, if you add a new alarm type, add the corresponding subclass, and then modify the business code. Although we have solved the problem of complex alarm class code, the code for creating alarm instances is still coupled in the business code. If there are many types, it will affect the readability of the business code. In order to make the responsibility of the business class more single and the code clearer, we can naturally put the code for creating the notification class elsewhere, The modified code is as follows.

public class Service {
    public void doSomething() {
        // Omit business code
        String type = "..";
        Alert alert = AlertFactory.getAlert(type);
        alert.send();
        // Omit business code
    }
}

public class AlertFactory {
    public static Alert getAlert(String type){
        Alert alert = null;
        if ("sms".equals(type)) {
            alert = new SmsAlert();
        } else if ("email".equals(type)) {
            alert = new EmailAlert();
        } else if ("voice".equals(type)) {
            alert = new VoiceAlert();
        }
        return alert;
    }
}

The reconstructed AlertFactory is a factory class and #getAlert method is a factory method. This design pattern is called simple factory. Because the method of creating class instances is static, simple factory is also called static factory. It can be seen that the development of software is evolving, and the proposal of design pattern is to solve specific problems. After using the simple factory, the code that creates the instance is isolated from the business code, so that the business class conforms to the principle of single responsibility, which increases the readability and expansibility of the code.

If the resource consumption of instantiation is large and the instantiated objects can be reused, there is another way to implement a simple factory.

public class AlertFactory {

    private static Map<String, Alert> cache = new HashMap<>();

    static {
        cache.put("sms", new SmsAlert());
        cache.put("email", new EmailAlert());
        cache.put("voice", new VoiceAlert());
    }

    public static Alert getAlert(String type) {
        Alert alert = cache.get(type);
        return alert;
    }
}

Factory method

For the above simple factories, if new alarm types are added, the factory class still needs to be modified. If the changes are not frequent, it is acceptable to slightly fail to meet the opening and closing principle. For the first simple factory, if you must remove if, you can use polymorphism.

public interface AlertFactory {
    Alert getAlert(String type);
}

public class SmsAlertFactory implements AlertFactory{
    @Override
    public Alert getAlert(String type) {
        return new SmsAlert();
    }
}
public class EmailAlertFactory implements AlertFactory {
    @Override
    public Alert getAlert(String type) {
        return new EmailAlert();
    }
}
public class VoiceAlertFactory implements AlertFactory{
    @Override
    public Alert getAlert(String type) {
        return new VoiceAlert();
    }
}

We modify the factory class into an interface, and then create objects by each specific factory. In this way, if you add a new alarm type, you don't need to modify the factory class. Compared with a simple factory, it is more in line with the opening and closing principle. This factory model is called factory method.

Although adding a new alarm type factory class does not need to be modified, the user's code is indeed complex.

public class Service {
    public void doSomething() {
        // Omit business code
        String type = "..";
        Alert alert = null;
        if ("sms".equals(type)) {
            alert = new SmsAlertFactory().getAlert(type);
        } else if ("email".equals(type)) {
            alert = new EmailAlertFactory().getAlert(type);
        } else if ("voice".equals(type)) {
            alert = new VoiceAlertFactory().getAlert(type);
        }
        alert.send();
        // Omit business code
    }
}

Back to the original code, in order to solve the problem of creating factory objects, we need to create another factory for the factory.

public class Service {
    public void doSomething() {
        // Omit business code
        String type = "..";
        Alert alert = AlertFactoryMap.getAlertFactory(type).getAlert(type);
        alert.send();
        // Omit business code
    }
}

public class AlertFactoryMap {

    private static Map<String, AlertFactory> cache = new HashMap<>();

    static {
        cache.put("sms", new SmsAlertFactory());
        cache.put("email", new EmailAlertFactory());
        cache.put("voice", new VoiceAlertFactory());
    }

    public static AlertFactory getAlertFactory(String type) {
        return cache.get(type);
    }
}

After using the factory method, the classes in the project have increased a lot, so in most cases, individuals prefer to use simple factories.

Abstract factory

Abstract factories have relatively few usage scenarios compared with simple factories and factory methods. For the above alarm cases, we need to configure different alarm rules, such as the number of requests or exceptions per minute; It is also possible to place the configuration in different places, such as local files, configuration center, zookpeer, etc; It is also possible to use different formats, such as xml, json, properties, yaml, and so on. If we also need to support putting the configuration in different places, there will be different dimensions to divide the alarm. Support local file and SMS alarms, configuration center and mailbox alarms, etc. there will be more and more combination methods. If we still use simple factory or factory methods to solve our problems, we can let our factory support multiple alarm instances in multiple dimensions at the same time, as shown below.

public interface AlertFactory {

    Alert getLocalFileAlert();

    Alert getNacosAlert();
    
}

public class SmsAlertFactory implements AlertFactory{

    @Override
    public Alert getLocalFileAlert() {
        return new SmsLocalFileAlert();
    }

    @Override
    public Alert getNacosAlert() {
        return new SmsNacosAlert();
    }
}

public class EmailAlertFactory implements AlertFactory {

    @Override
    public Alert getLocalFileAlert() {
        return new EmailLocalFileAlert();
    }

    @Override
    public Alert getNacosAlert() {
        return new EmailNacosAlert();
    }
}

summary

Factory mode is used to solve the problem of object creation. When using if else to create different objects of related types, you can use factory mode for optimization; When the logic of creating objects is complex, the logic of creating objects can also be abstracted into the factory. Using the factory pattern can make the responsibility of the class clearer and encapsulate the "change" of the created object.

Topics: Java Design Pattern