A Simple Take on Design Patterns in Programming
Design patterns are mainly aimed at object-oriented thinking — abstracting all things into classes for description, then generating concrete objects from those classes.
Such a class is in fact a template, so the most basic design pattern is the Template pattern. The Template pattern is very simple: define a base class, provide some virtual or abstract methods, and complete the concrete implementation in subclasses. This is the most fundamental and simplest use in object-oriented programming.
An article, for instance, has a title and content, so it can become a template — title as one method, content as another; with different strings returned in the subclass, it becomes different types of articles. In the title-string description, for example, there may be variants with images and without images.
The Template Method emphasizes having a unified interface for calls on the base class, so as to achieve sufficient extensibility. What it stresses is the template style.
Going further, if the interface methods are very few, it can be regarded as the Strategy pattern. For example, define a method name taking two integer parameters; inside the method you can implement addition, subtraction, multiplication, and various other operations. From the outside, choosing different derived classes yields different functions — so this becomes the Strategy pattern.
The biggest difference between the Strategy and Template patterns is that Strategy focuses on implementation choice. In the Strategy pattern one typically introduces a Context class; the strategy's own base class is the strategy template, and which strategy to choose is up to the Context to invoke.
Why have a Context? When multiple strategies must be judged in sequence, after Context calls one strategy's method it can record that strategy's result, then carry the previous result into the next strategy method for further computation.
In other words, the purpose of objects created by introducing a Context class is to record and pass on the results of strategies, so it can be context-dependent.
A common scenario is using Context to record the current computation state or step. When processing an image, for instance, one first applies grayscale treatment and then extracts contours; with a context object, the system always knows the current status — highly beneficial for both asynchronous processing and multithreaded programming.
Objects that can simply be created new can just be created directly. But things rarely go entirely as one wishes: sometimes the objects produced need unified management, especially complex ones, while there is no need for such close context-relatedness among them.
Producing a sedan and then producing a truck, for example, is a choice based on different needs; yet there is no need for context linkage between building a sedan and building a truck — they can even proceed in parallel.
In such cases, one can undoubtedly consider the Factory pattern. Objects that leave the same factory can all be marked accordingly, and afterwards whether they need unified destruction or other handling becomes very convenient.
The Factory pattern only cares about producing objects; it does not care about the order of production or any combination requirements among them.
A simple factory is very useful in simple scenarios. A logging record, for instance — whether it goes to disk, memory, or elsewhere — can use a factory to create the recording object and decide later.
But opening up the factory method, when the internal process of building a car involves concrete assembly, different methods are needed for construction.
Most such construction flows are reusable: putting wheels on a closed sedan and putting wheels on a convertible differ in no way.
But putting a roof on a car and putting wheels on it are clearly two different jobs; the latter cannot be completed directly through the Strategy pattern, because the strategy model can care about which wheels to install and the steps for installing them, yet conceptually cannot assemble the car.
Hence here arises the Builder pattern. In the Builder pattern, complex construction is separated from its representation. It resembles the Strategy pattern, but the construction of the source does not rest on the same strategy class.
A series of strategies can form a class of parts; different parts can form different components; and different components can build a finished product.
This is their difference. The Builder pattern contains both parts and components: a car, say, can have a round dome installed or a square roof installed — so a Round-Dome Builder or Square-Roof Builder can be realized with the Builder pattern.
The Builder pattern's greatest feature is that it can compose different components into a new object on the basis of an existing one.
A hamburger may be wrapped in paper or in a box, and the hamburger contained may be made of Orleans chicken leg or fried chicken leg. Whether paper or box is used, and which chicken leg — these have no relation to one another, so this suits the Builder pattern well.
Relations among objects are not always perfect and simple. Sometimes mutual dependencies between objects appear, which is very troublesome. The most effective method, besides unified scheduling, is to anticipate contingencies so temporary scheduling can be done.
The various trains on a high-speed rail line, for instance, have a critical order and must proceed in parallel without chaos. Relying on inter-train communication would inevitably cause disorder, because message propagation is very inefficient.
So a broadcast mechanism is needed: a mediating object interacts with the other objects. It is like a data center, while the other objects are merely terminals; all terminals communicate only with the data center.
The center established in this situation is called the Mediator. Broadcasting data broadly so all other corresponding objects can see it is like a channel, and all object information then interacts on it.
This star-shaped structure is also the most common structure in modern networks; for now, it is a relatively good solution.
When terminals connect, their status sometimes needs checking — whether already connected or already disconnected — to determine whether data can be sent. This situation is called an event.
It is not confined to this scenario, but taking it as an example: this event-based model has already been implemented very well in some languages — the event model, achieving status notification by invoking a certain event.
Because events are discrete, they must be handled with discrete methods — this can be done by polling or method invocation. Subscribing to and unsubscribing from events is the Publisher-Subscriber pattern, which can likewise be called the Visitor pattern.
The advantage of this pattern is that connected terminals can be granted permissions revealing some details inside objects. A member variable in a class may be non-public, but the data center can tell the connected party this value via an event method — on one hand keeping the outer interface unified, and on the other disclosing some private values to externally connected objects for special needs.
A button on a window, for instance, when its click event fires may pass out some internal value; this is allowed. One might even allow passing out a core object for the outside to change — which clearly violates the principle of encapsulation — but which things may be passed out is ultimately still controlled by the class's design.
If event A triggers event B, and event B under some condition triggers event C, this can be called the Chain of Responsibility pattern. A typical scenario is selecting province, city, district, and street: it filters layer by layer, in fact forming an event chain.
The Chain of Responsibility pattern's clear virtue is that a change in some object's state is based on a notification of change in another object, which brings many benefits.
When a province is selected, for instance, once the province selection is fixed, one must screen which lists to display in B. But if A is a class and B is a class, and A directly controls B's class members, that becomes very troublesome, and design considerations multiply — how many of B's members A should be able to control directly to meet design needs is hard to say in complex scenarios.
So it is better to have A merely notify B that a certain value is selected here, hand it over to B, and let B decide for itself what data to present based on this value. Flexibility and extensibility are thereby greatly enhanced, while the duties and roles of B's internal members are also kept within B.
In multi-person collaborative programming projects, the author of class A only needs to write class A, and class B only needs to take care of its own B; responsibilities become very clear and management becomes very convenient.
When objects are passed along, the original object is sometimes not rich enough and needs some information attached. In some dynamic languages this is simple — just extend the properties. But this can sometimes cause disputes, because the original object has been modified, and such modification is usually irreversible: the newly added dynamic properties cannot be deleted.
So it may be better to simply make a new class to wrap it, with the original object becoming one member of the new class — this constitutes the Decorator pattern.
The Decorator pattern's greatest advantage is that the newly created object can be discarded at any time while still using the original object, so it is very flexible.