A little about generative design patterns

The theme of design patterns is quite popular. A lot of videos were shot on it and articles were written. Combines all of these materials with an “anti-pattern." Accidental complexity. As a result, the examples are abstruse, the description is confusing, how to apply is not clear. And the main task of design patterns - simplification (of code, and work in general) is not achieved. After all, the use of the template requires additional effort. About the same as Unit testing.







I’ll try to explain the design patterns in terms of how to apply them, where and why.







Six generators can be attributed to:









All other patterns that relate to generators are a special case of application and there is no point in dwelling on them.







Generating patterns can be divided into three groups, on the question to which they answer. So, three questions:









Where?



Three patterns answer this question: Prototype, Abstract Factory, and Factory Method.







A bit about terms

Within the framework of the concept of OOP, there are only three places where theoretically it is possible to generate a new instance (instance):







  • Product is the class that is instantiated.
  • Client is the class that will use the instantiated instance.
  • Partner - any third class in the visibility of the Client.




Actually, these patterns determine the place of generation. In addition, they are connected hierarchically and have a different scope. The connection is shown in the figure, the arrows determine the direction of calls.







The hierarchy of generative patterns

In its implementation, the “Factory Method” can delegate the generation of an instance to either the existing “Factory” or the “Prototype”. The “prototype”, however, should not depend on anyone and do everything by itself. Now in more detail.







"Prototype"



This template corresponds to the place "Product", in fact, is the constructor of the class. Accordingly, an instance of a specific (previously known) class is always generated.

Within the framework of this template, the constructor knows only the parameters passed directly to it (the number of parameters tends to the number of class fields). Of course, there is full access to all fields and properties of the created class.







Properly implemented methods of the "Prototype" allow you to get rid of additional initialization methods in public. In turn, the external interface of the class becomes easier and less tempting to use the class for other purposes.







What this template gives us:









Of the minuses:









The most popular template. Everyone uses it, but few know what it uses. It is good until the first working model is obtained, until classes and their relations are completely defined. After that, processing and increasing abstraction are necessary.







"Abstract factory"



Some class Partner. It can be specialized, or "combine". May be static (no instance). An example of "combining" can be a configuration class. It may also be hidden behind the Facade.







"Factory" usually sees all the global settings of the application (or a separate subsystem). Immediate generation may be delegated to the Prototype. At the same time, the number of input parameters in the Factory method will be less than in a similar Prototype constructor. The factory does not decide who to create based on incoming parameters.







This template is very convenient and easy to implement, but requires preliminary design. If you create factories for everything, this will complicate the code. In fact, we get an analogue of the Prototype, but moved to a third-party class.







From the pros:









But there are also disadvantages:











Very popular. This template is used by those who attended the GoF course. As a rule, the code becomes even worse than “before applying the templates”.







It makes sense when Factories appear during the first code rework. At this stage, combinations of parameters for the created instances are already known, and it will not be difficult to write generalized Factory methods. As a result, calls in the Client will be simplified.







In some cases, it is convenient to hide Factories behind the Facade. For example, the application has a dozen of its factories, and a dozen of the libraries. For them, you can build a facade. This will allow not linking libraries to each module, and also it is easy to replace one factory with another.







Factory Method



The top of abstraction in generative patterns. Place of origin Customer. The class in which each product is placed in the Factory Method has every chance of a long life. If without fanaticism, then the alleged axis of development must necessarily be based on this template.







The factory method does not see beyond its class. The number of directly transmitted parameters should be minimal (in the limit of zero). The method itself must be built taking into account the possibility of overlapping in the descendant.







A common mistake is complicated initialization in one method. For example, when creating a complex instance (the Builder template), the creation of all parts of the future object is placed in one method. As a result, such a method is difficult to overlap in the descendant.







From the pros:









There are essentially no cons.







This template is almost never used. As a rule, it can only be seen in projects with deep preliminary elaboration. Ideal when the Factory Method delegates generation to the "Factory" or "Prototype."







Small example



We have a class for logging to a file on the hard drive. This is how generic methods within the “Where?” Patterns might look:







Prototype:



constructor Create(aFilename: string; aLogLevel: TLogLevel);
      
      





All that the designer should know is passed to him in the form of parameters.







Factory:



 function GetLogger(aLogLevel: TLogLevel): ILogger;
      
      





The factory knows which file to write to, as specified in the application settings.







Factory Method:



 function NewLogger: ILogger;
      
      





In the Client class, it is known with what detail to log.







In this design, to replace the logging class with a stub, it is enough to redefine NewLogger in the client's descendant. This is useful when conducting Unit tests.







To log to the database, it is enough to override the GetLogger method in the descendant of the Factory.








All Articles