Fluent design pattern factory pattern family

Posted by EGNJohn on Tue, 08 Mar 2022 08:45:56 +0100

Wen / Yang Jiakang, member of CFUG community, author of "the development journey of fluent from south to North", engineer of Xiaomi

In the topics around design patterns, the word factory appears frequently, from simple factory pattern to factory method pattern, and then to abstract factory pattern. The meaning of factory name is the industrial place where products are manufactured. When applied in object-oriented, it naturally becomes a typical creation mode.

Formally speaking, a factory can be a method / function that returns the object we want, that is, it can be used as an abstraction of the constructor.

In this article, we will use Dart to understand their respective implementations and the relationship between them.

Simple factory & factory keyword

Simple factory mode is not one of the 23 GoF design modes, but it is one of the most commonly used programming modes.
It mainly involves a special method, which is specially used to provide the instance object (object factory) we want,
We can put this method into a separate class SimpleFactory, as follows:

class SimpleFactory {

  ///Factory method
  static Product createProduct(int type) {
    if (type == 1) {
      return ConcreteProduct1();
    }
    if (type == 2) {
      return ConcreteProduct2();
    }
    return ConcreteProduct();
  }
}

We believe that the object to be created by this method belongs to the same Product class (abstract class), and specify the specific object type to be created through the parameter type.
Dart does not support the interface keyword, but we can use abstract to define the interface as an abstract class,
Then each specific type inherits and implements it:

///Abstract class
abstract class Product {
  String? name;
}

///Implementation class
class ConcreteProduct implements Product {
  @override
  String? name = 'ConcreteProduct';
}

///Implementation class 1
class ConcreteProduct1 implements Product {
  @override
  String? name = 'ConcreteProduct1';
}

///Implementation class 2
class ConcreteProduct2 implements Product {
  @override
  String? name = 'ConcreteProduct2';
}

When we want to get the corresponding type object in the code, we only need to pass in the desired type value through this method,
We don't have to care about the specific logic of how production is produced and which object is selected:

void main() {
  final Product product = SimpleFactory.createProduct(1);
  print(product.name); // ConcreteProduct1
}

This is the simple factory model.
At this point, we have to mention the unique factory keyword in Dart.

The factory keyword can be used to modify the constructor of Dart class, which means factory constructor. It can make the constructor of class naturally have the function of factory. The usage is as follows:

class Product {
  ///Factory constructor (modify create constructor)
  factory Product.createFactory(int type) {
    if (type == 1) {
      return Product.product1;
    } else if (type == 2) {
      return Product._concrete2();
    }
    return Product._concrete();
  }

  ///Named constructor
  Product._concrete() : name = 'concrete';

  ///Named constructor 1
  Product._concrete1() : name = 'concrete1';

  ///Named constructor 2
  Product._concrete2() : name = 'concrete2';

  String name;
}

The constructor decorated with factory needs to return an object instance of the current class,
We can call the corresponding constructor according to the parameters and return the corresponding object instance.

void main() {
  Product product = Product.createFactory(1);
  print(product.name); // concrete1
}

In addition, the factory constructor does not require us to generate new objects every time,
We can also pre define some objects in the class for the factory constructor,
In this way, each time an object is built with the same parameters, the same object will be returned,
stay Singleton mode We have already described in the following chapters:

class Product {
  ///Factory constructor
  factory Product.create(int type) {
    if (type == 1) {
      return product1;
    } else if (type == 2) {
      return product2();
    }
    return Product._concrete();
  }

  static final Product product1 = Product._concrete1();
  static final Product product2 = Product._concrete2();
}

In addition to modifying the named constructor, factory can also modify the default unnamed constructor,

class Product {
  factory Product(int type) {
    return Product._concrete(); 
  }

  String? name;
}

So far, a disadvantage of factory constructor has been highlighted, that is, users can not intuitively feel that they are using factory functions.
The use method of factory constructor is no different from that of ordinary constructor, but the instance produced by this constructor is equivalent to a single instance:

void main() {
  Product product = Product(1);
  print(product.name); // concrete1
}

Such usage is easy to cause trouble for users, so we should try to use specific
Name the constructor as the factory constructor (such as createFactory in the above example).

Factory method model

Factory method pattern is also the most commonly used means in our programming.

In a simple factory, its main service object is the customer, and the user of the factory method has nothing to do with the factory's own class,
The factory method pattern mainly serves its own parent class, such as ProductFactory (similar to Creator in UML):

///Abstract factory
abstract class ProductFactory {
  ///Abstract factory method
  Product factoryMethod();

  ///Business code
  void dosomthing() {
    Product product = factoryMethod();
    print(product.name);
  }
}

In the ProductFactory class, the factory method factoryMethod is an abstract method,
Each subclass must override this method and return the corresponding Product object,
When the dosomthing() method is called, you can make different responses according to the returned object.
The specific application methods are as follows:

///Specific factory
class ProductFactory1 extends ProductFactory {
  
  ///Specific factory method 1
  @override
  Product factoryMethod() {
    return ConcreteProduct1();
  }
}

class ProductFactory2 extends ProductFactory {
  ///Specific factory method 2
  @override
  Product factoryMethod() {
    return ConcreteProduct2();
  }
}

///Use
main() {
  ProductFactory product = ProductFactory1();
  product.dosomthing();    // ConcreteProduct1
}

In fluent, abstract methods have a very practical application scenario. When we use fluent to develop multi terminal applications, we usually need to consider the adaptation of multiple platforms, that is, in multiple platforms, the same operation sometimes produces different results / styles. We can put the logic generated by these different results / styles in the factory method.

As follows, we define a dialogfactory to be used as a factory for generating Dialog of different styles:

abstract class DialogFacory {
  Widget createDialog(BuildContext context);

  Future<void> show(BuildContext context) async {
    final dialog = createDialog(context);
    return showDialog<void>(
      context: context,
      builder: (_) {
        return dialog;
      },
    );
  }
}

Then, for Android and iOS platforms, you can create two different styles of Dialog:

///Android platform
class AndroidAlertDialog extends DialogFactory {

  @override
  Widget createDialog(BuildContext context) {
    return AlertDialog(
      title: Text(getTitle()),
      content: const Text('This is the material-style alert dialog!'),
      actions: <Widget>[
        TextButton(
          onPressed: () {
            Navigator.of(context).pop();
          },
          child: const Text('Close'),
        ),
      ],
    );
  }
}
///iOS platform
class IOSAlertDialog extends DialogFactory {
  
  @override
  Widget createDialog(BuildContext context) {
    return CupertinoAlertDialog(
      title: Text(getTitle()),
      content: const Text('This is the cupertino-style alert dialog!'),
      actions: <Widget>[
        CupertinoButton(
          onPressed: () {
            Navigator.of(context).pop();
          },
          child: const Text('Close'),
        ),
      ],
    );
  }
}

Now we can use the corresponding Dialog like this:

Future _showCustomDialog(BuildContext context) async {
  final dialog = AndroidAlertDialog();
  // final dialog = IOSAlertDialog();
  await selectedDialog.show(context);
}

Abstract factory

The biggest difference between abstract factory mode and simple factory method is that these two modes produce only one object, while abstract factory produces a series of objects (object family), and there must be some connection between the generated series of objects. For example, Apple will produce many products such as mobile phones and tablets, all of which belong to the brand of apple.

Such as the following abstract factory class:

abstract class ElectronicProductFactory {
  Product createComputer();
  
  Product createMobile();

  Product createPad();
  
  // ...
}

For Apple, I am the factory that produces this kind of electronic products, so I can inherit this class and implement its methods to produce various products:

class Apple extends ElectronicProductFactory {

  @override
  Product createComputer() {
    return Mac();
  }

  @override
  Product createMobile() {
    return IPhone();
  }

  @override
  Product createPad() {
    return IPad();
  }
  
  // ...
}

Similarly, Huawei, Xiaomi and other electronic product manufacturers can also use the same way to express, which is the abstract factory model.

In developing the fluent application, we can also make full use of the abstract factory mode to adapt to the application. We can define the following abstract factory to produce widget s:

abstract class IWidgetsFactory {
  
  Widget createButton(BuildContext context);
  
  Widget createDialog(BuildContext context);
  
  // ...
}

Our application usually needs to display different styles of widget s for each platform. Therefore, for each platform, we can implement the corresponding implementation factory, as follows:

///Material style component factory
class MaterialWidgetsFactory extends IWidgetsFactory {
  @override
  Widget createButton(
      BuildContext context, VoidCallback? onPressed, String text) {
    return ElevatedButton(
      child: Text(text),
      onPressed: onPressed,
    );
  }

  @override
  Widget createDialog(BuildContext context, String title, String content) {
    return AlertDialog(title: Text(title), content: Text(content));
  }
  
  /// ...
}

///Cupertino style component factory
class CupertinoWidgetsFactory extends IWidgetsFactory {
  @override
  Widget createButton(
    BuildContext context,
    VoidCallback? onPressed,
    String text,
  ) {
    return CupertinoButton(
      child: Text(text),
      onPressed: onPressed,
    );
  }

  @override
  Widget createDialog(BuildContext context, String title, String content) {
    return CupertinoAlertDialog(
      title: Text(title),
      content: Text(content),
    );
  }
  
  // ...
}

In this way, if we use MaterialWidgetsFactory on Android platform and CupertinoWidgetsFactory on iOS platform, we can use the widgets of the corresponding platform. If we want to adapt to more platforms, we only need to inherit the factory class of the corresponding platform implemented by IWidgetsFactory.

So far, we can find that as creation patterns, the main work of these three types of factory patterns is to create objects in different ways, but they have their own characteristics: simple factory patterns abstract production objects, factory method patterns abstract class methods, and factory method patterns abstract factories of production objects. Different people have different opinions on how to use them.

Expand reading

About this series

The Flutter / Dart design pattern series from south to North (hereinafter referred to as the Flutter design pattern) is expected to be released in two weeks, focusing on introducing the common design patterns and development methods in the development of Flutter applications to developers, in order to promote the popularization of Flutter / Dart language features and help developers develop high-quality and maintainable Flutter applications more efficiently.

I am happy to continue to improve the articles in this series. If you have any questions or suggestions about this article, please submit the issue to the official Github warehouse of the Chinese community or contact me directly, and I will reply in time.

Topics: Flutter