[Go implementation] practice 23 design modes of GoF: factory method mode

Previous: [Go implementation] practice 23 design modes of GoF: Builder mode Simple distributed application system (example code project): https://github.com/ruanrunxue/Practice-Design-Pattern--Go-Implementation

sketch

Similar to the builder pattern discussed in the previous article, the Factory Method Pattern encapsulates the logic of object creation and provides users with a simple and easy-to-use object creation interface. The two are slightly different in application scenarios. The builder mode is often used in scenarios where multiple parameters need to be passed for instantiation; The Factory Method Pattern is often used in scenarios where objects are created without specifying their specific types.

UML structure

code implementation

Example

stay Simple distributed application system In the example code project, we designed the Sidecar side car module. The function of Sidecar is to add additional functions to the native Socket, such as flow control, logging, etc.

The Sidecar module uses the decorator mode to decorate the Socket. So the client actually uses Sidecar as a Socket, for example:

 // demo/network/http/http_client.go
 package http
 ​
 // Create a new HTTP client with the Socket interface as the input parameter
 func NewClient(socket network.Socket, ip string) (*Client, error) {
   ... // Some initialization logic
 return client, nil
 }
 ​
 // When using the NewClient, we can pass in Sidecar to add additional flow control functions to the Http client
 client, err := http.NewClient(sidecar.NewFlowCtrlSidecar(network.DefaultSocket()), "192.168.0.1")
copy

In the service message mediation, HTTP is called every time an HTTP request from an upstream service is received Newclient to create an HTTP client and forward requests to downstream services through it:

 type ServiceMediator struct {
   ...
 server *http.Server
 }
 ​
 // Forward forward forward the request. The request URL is in the form of /{serviceType}+ServiceUri, such as /serviceA/api/v1/task
 func (s *ServiceMediator) Forward(req *http.Request) *http.Response {
     ...
     // Discover the destination IP address of downstream services
     dest, err := s.discovery(svcType)
     // Create an HTTP client and hard code sidecar NewFlowCtrlSidecar(network.DefaultSocket())
     client, err := http.NewClient(sidecar.NewFlowCtrlSidecar(network.DefaultSocket()), s.localIp)
     // Forward request through HTTP client
     resp, err := client.Send(dest, forwardReq)
     ...
 }
copy

In the above implementation, we are calling http The Sidecar Newflowctrlsidecar (network.defaultsocket()) is hard coded. If you want to extend the Sidecar in the future, you must modify the code logic, which violates the Opening and closing Principle OCP.

Experienced students may think that you can call http Newclient takes the Socket interface as the input parameter; Then, when the ServiceMediator is initialized, inject a specific type of Sidecar into the ServiceMediator:

 type ServiceMediator struct {
   ...
 server *http.Server
   // Depend on Socket abstract interface
   socket network.Socket
 }
 ​
 // Forward forward forward the request. The request URL is in the form of /{serviceType}+ServiceUri, such as /serviceA/api/v1/task
 func (s *ServiceMediator) Forward(req *http.Request) *http.Response {
     ...
     // Discover the destination IP address of downstream services
     dest, err := s.discovery(svcType)
     // Create an HTTP client and use the s.socket abstract interface as the input parameter
     client, err := http.NewClient(s.socket, s.localIp)
     // Forward request through HTTP client
     resp, err := client.Send(dest, forwardReq)
     ...
 }
 ​
 // When ServiceMediator initializes, inject Sidecar of specific type into ServiceMediator
 mediator := &ServiceMediator{
   socket: sidecar.NewFlowCtrlSidecar(network.DefaultSocket())
 }
copy

The above modification has changed from relying on concrete to relying on abstraction, which conforms to the principle of opening and closing.

However, the Forward method has a scenario of concurrent calls, so it hopes to create a new Socket/Sidecar to complete network communication every time it is called. Otherwise, it needs to be locked to ensure concurrency security. The above modifications will cause the same Socket/Sidecar to be used in the life cycle of ServiceMediator, which obviously does not meet the requirements.

Therefore, we need a method that can not only meet the opening and closing principle, but also create a new Socket/Sidecar instance each time the Forward method is called. The factory method pattern can just meet these two requirements. Let's use it to optimize the code.

realization

 // demo/sidecar/sidecar_factory.go
 ​
 // Key point 1: define a Sidecar factory abstract interface
 type Factory interface {
   // Key point 2: factory method returns Socket abstract interface
 Create() network.Socket
 }
 ​
 // Key point 3: implement specific plants as required
 ​
 ​
 // demo/sidecar/raw_socket_sidecar_factory.go
 // RawSocketFactory only has the sidecar with the function of native socket and implements the Factory interface
 type RawSocketFactory struct {
 }
 func (r RawSocketFactory) Create() network.Socket {
 return network.DefaultSocket()
 }
 ​
 // demo/sidecar/all_in_one_sidecar_factory.go
 // AllInOneFactory is a sidecar Factory with all functions and implements the Factory interface
 type AllInOneFactory struct {
 producer mq.Producible
 }
 func (a AllInOneFactory) Create() network.Socket {
 return NewAccessLogSidecar(NewFlowCtrlSidecar(network.DefaultSocket()), a.producer)
 }
copy

In the above code, we define a Factory abstract interface Factory, and have two concrete implementations, RawSocketFactory and allionefactory. Finally, ServiceMediator relies on Factory and creates a new Socket/Sidecar through Factory in the Forward method:

 // demo/service/mediator/service_mediator.go
 ​
 type ServiceMediator struct {
   ...
 server *http.Server
   // Key point 4: the client depends on the Factory abstract interface
   sidecarFactory sidecar.Factory
 }
 ​
 // Forward forward forward the request. The request URL is in the form of /{serviceType}+ServiceUri, such as /serviceA/api/v1/task
 func (s *ServiceMediator) Forward(req *http.Request) *http.Response {
     ...
     // Discover the destination IP address of downstream services
     dest, err := s.discovery(svcType)
     // Create an HTTP client and call sidecarfactory Create() generates a Socket as an input parameter
     client, err := http.NewClient(s.sidecarFactory.Create(), s.localIp)
     // Forward request through HTTP client
     resp, err := client.Send(dest, forwardReq)
     ...
 }
 ​
 // Key point 5: when ServiceMediator is initialized, the sidecar Factory injection into ServiceMediator
 mediator := &ServiceMediator{
   sidecarFactory: &AllInOneFactory{}
   // sidecarFactory: &RawSocketFactory{}
 }
copy

The following is a summary of the key points to realize the factory method mode:

  1. Define a factory method abstract interface, such as sidecar Factory.
  2. In the factory method, return the object / interface to be created, such as network Socket. The factory method is usually named Create.
  3. Define the concrete implementation objects of the factory method abstract interface according to specific needs, such as RawSocketFactory and allionefactory.
  4. When used by the client, it depends on the factory method abstract interface.
  5. In the client initialization phase, complete the dependency injection of specific factory objects.

extend

Go style implementation

The factory method pattern implementation mentioned above is a very typical object-oriented style. Next, we give a more Go style implementation.

 // demo/sidecar/sidecar_factory_func.go
 ​
 // Key point 1: define Sidecar factory method type
 type FactoryFunc func() network.Socket
 ​
 // Key point 2: define the specific factory method implementation as required. Note that the factory method defined here is the factory method of the factory method, and the factory method type returned is the FactoryFunc factory method type
 func RawSocketFactoryFunc() FactoryFunc {
 return func() network.Socket {
 return network.DefaultSocket()
 }
 }
 ​
 func AllInOneFactoryFunc(producer mq.Producible) FactoryFunc {
 return func() network.Socket {
 return NewAccessLogSidecar(NewFlowCtrlSidecar(network.DefaultSocket()), producer)
 }
 }
 ​
 type ServiceMediator struct {
   ...
 server *http.Server
   // Key point 3: client depends on FactoryFunc factory method type
   sidecarFactoryFunc FactoryFunc
 }
 ​
 func (s *ServiceMediator) Forward(req *http.Request) *http.Response {
     ...
     dest, err := s.discovery(svcType)
     // Key point 4: create an HTTP client and call sidecarFactoryFunc() to generate a Socket as an input parameter
     client, err := http.NewClient(s.sidecarFactoryFunc(), s.localIp)
     resp, err := client.Send(dest, forwardReq)
     ...
 }
 ​
 // Key 5: when ServiceMediator initializes, inject FactoryFunc of specific type into ServiceMediator
 mediator := &ServiceMediator{
   sidecarFactoryFunc: RawSocketFactoryFunc()
   // sidecarFactory: AllInOneFactoryFunc(producer)
 }
copy

The above implementation makes use of the characteristics of the function as a first-class citizen in the Go language, with fewer interface s and struct s defined, and the code is more concise.

Several key implementation points are similar to the implementation of object-oriented style. It is worth noting that key point 2 is that we have defined a factory method of a factory method. This is to use the characteristics of function closures to pass parameters. If factory methods are defined directly, AllInOneFactoryFunc is implemented as follows, and polymorphism cannot be implemented:

 // Not FactoryFunc type, cannot implement polymorphism
 func AllInOneFactoryFunc(producer mq.Producible) network.Socket {
     return NewAccessLogSidecar(NewFlowCtrlSidecar(network.DefaultSocket()), producer)
 }
copy

Simple factory

Another variant of the factory method pattern is the simple factory. Instead of polymorphism, it uses simple switch case / if else criteria to determine which product to create:

 // demo/sidecar/sidecar_simple_factory.go
 ​
 // Key 1: define sidecar type
 type Type uint8
 ​
 // Key point 2: define sidecar specific types as required
 const (
 Raw Type = iota
 AllInOne
 )
 ​
 // Key 3: define simple factory objects
 type SimpleFactory struct {
 producer mq.Producible
 }
 ​
 // Key point 4: define factory methods, input parameters as sidecar types, and create products according to switch case or if else
 func (s SimpleFactory) Create(sidecarType Type) network.Socket {
 switch sidecarType {
 case Raw:
 return network.DefaultSocket()
 case AllInOne:
 return NewAccessLogSidecar(NewFlowCtrlSidecar(network.DefaultSocket()), s.producer)
 default:
 return nil
 }
 }
 ​
 // Key point 5: when creating a product, a specific sidecar type is passed in, such as sidecar AllInOne
 simpleFactory := &sidecar.SimpleFactory{producer: producer}
 sidecar := simpleFactory.Create(sidecar.AllInOne)
copy

Static factory method

The static factory method is a Java/C++ expression. It is mainly used to replace the constructor to complete the instantiation of objects. It can make the code more readable and decouple from the client. For example, the static factory method of Java is implemented as follows:

public class Packet {
    private final Endpoint src;
    private final Endpoint dest;
    private final Object payload;

    private Packet(Endpoint src, Endpoint dest, Object payload) {
        this.src = src;
        this.dest = dest;
        this.payload = payload;
    }

    // Static factory method
    public static Packet of(Endpoint src, Endpoint dest, Object payload) {
        return new Packet(src, dest, payload);
    }
		...
}

// usage
packet = Packet.of(src, dest, payload)
copy

There is no static statement in Go. You can directly complete the construction of objects through ordinary functions, such as:

// demo/network/packet.go
type Packet struct {
	src     Endpoint
	dest    Endpoint
	payload interface{}
}

// Factory method
func NewPacket(src, dest Endpoint, payload interface{}) *Packet {
	return &Packet{
		src:     src,
		dest:    dest,
		payload: payload,
	}
}

// usage
packet := NewPacket(src, dest, payload)
copy

Typical application scenarios

  1. When the object instantiation logic is complex, you can choose to use the factory method pattern / simple factory / static factory method for encapsulation, providing an easy-to-use interface for the client.
  2. If the instantiated object / interface involves multiple implementations, you can use the factory method pattern to implement polymorphism.
  3. For the creation of common objects, it is recommended to use the static factory method, which has better readability and low coupling than direct instantiation (such as &packet{src: SRC, dest: DeST, payload: payload}).

Advantages and disadvantages

advantage

  1. The code is more readable.
  2. It is decoupled from the client program. When the instantiation logic changes, it only needs to change the factory method, avoiding the shotgun modification.

shortcoming

  1. The introduction of factory method pattern will add some object / interface definitions, and misuse will lead to more complex code.

Association with other modes

Many students tend to confuse the factory method pattern with the abstract factory pattern. The abstract factory pattern is mainly used in the scenario of instantiating "product family", which can be regarded as an evolution of the factory method pattern.

Tags: Design Pattern

Posted by FURQAN on Thu, 02 Jun 2022 01:29:12 +0530