The basic concept
Wikipedia’s definition of a design pattern:
In software engineering, design pattern is a proposed solution to various problems that are ubiquitous (recurring) in software design. Design patterns are not used to write code directly, but rather to describe a solution to a problem in a variety of different situations. Object-oriented design patterns typically describe relationships and interactions in terms of categories or objects, but do not refer to the specific categories or objects used to complete the application. Design patterns can make the unstable depend on the relative stability and the concrete depend on the relative abstraction, avoid the tight coupling that can cause trouble, and enhance the ability of software design to face and adapt to changes.
In common sense, a design pattern is a set of repeatedly used, cataloged, code design experience summary, it is a series of ideas to solve a specific problem, with universality and reuse. Using design patterns can improve code readability, reusability, and reliability.
The essence of design patterns is object-oriented design principles, which are good solutions to problems in practice, so that developers can apply them to real projects. At present, there are as many as 23 design patterns was collected in 1995 published “design patterns: elements of reusable object-oriented software, such as: simple factory, abstract factory, the proxy pattern and adapter pattern, the flyweight pattern, strategy pattern, observer pattern, the iterator pattern, etc., they tend to meet different needs. The seven principles of design, such as the Open closed principle, Richter substitution, etc., will not be introduced here. The original intention of this article is to let everyone learn and apply it. Do not apply concepts and principles mechanically. Design patterns are based on business problem solving, and do not use patterns for patterns’ sake.
As front-end engineers, of course, we are most concerned about which of these dozens or twenty design patterns can be used in production to solve practical problems. Following the general classification of design patterns, front-end design patterns are also divided into the following categories:
- Creation pattern: An object instantiation pattern used to decouple the object instantiation process. Its focus on class instantiation makes it easier to create entities. Focus on: singleton pattern, factory pattern, abstract factory pattern
- Structural patterns: They deal with building relationships between different components (or classes) and forming new structures to provide new functionality. Focus on: proxy pattern, decorator pattern
- Behavior patterns: how classes and objects interact and divide responsibilities and algorithms. Focus: Command mode, Chain of Responsibility mode, observer mode
The singleton pattern
Singletons are probably one of the most well-known design patterns, so let’s start with singletons. Singletons ensure that no matter how many times a class is instantiated, there will always be only one instance available, and they provide an access point to the whole world. The singleton pattern is the one I’m sure you’re most familiar with, so let’s take a look at some of the scenarios where the singleton pattern can be used in real code development.
- Login popup: We need to have a globally unique popup that ensures multiple clicks are an instance.
- Database connections: We want to handle only one connection per user request, so we don’t want to keep setting up new connections for multiple user requests.
- Data cache: Data cache processing is necessary in the project. We need a global cache center to collect and process cache, and centrally manage the addition, deletion, check and change of cache data.
- Vue.use(plugins): To ensure that plug-ins are not registered repeatedly, singletons must be unique within each plug-in.
If we wanted to implement a singleton that would return the same instance every time we called it, we could do it this way. CreateSingleton is a self-executing function that ultimately provides a point of global access to getInstance. Every time we call getInstance, if the instance exists, The instantiation process is ignored, the instance instance is returned, and the real constructor is instantiated if it does not exist. The end result is that after creating the instance twice, instance1 and Instance2 point to the same instance.
const createSingleton = (function() {
let instance
let Singleton = function() {
console.log('constructor')
return this
}
return {
getInstance: () = > {
if(! instance) { instance =new Singleton()
}
return instance
}
}
})()
let instance1 = createSingleton.getInstance()
let instance2 = createSingleton.getInstance()
console.log(instance1 === instance2)
// constructor
// true
Copy the code
Of course, with ES6 and typescript, implementing singletons is much simpler and more intuitive.
class Singleton {
// Static properties of the Singleton
private static instance: Singleton
private constructor() {
console.log('constructor called')}// Static methods for Singleton
public static getInstance(): Singleton {
if(!this.instance) {
this.instance = new Singleton()
}
return this.instance
}
}
let instance3: Singleton = Singleton.getInstance()
let instance4: Singleton = Singleton.getInstance()
console.log(instance3 === instance4)
// constructor
// true
Copy the code
If you try to instantiate this singleton, the following error is reported
The practical application
The above is an implementation of the pseudocode for the singleton pattern, and if the idea is not quite clear, let’s move on to a practical application scenario. We will, in turn, implement a cache processor and examine Vuex’s application of the singleton pattern.
Cache processor
When writing programs, we sometimes need some calculated data, or keep some repeatable data to improve performance. This is where a good cache processor becomes very important. A cache processor needs to store, fetch, delete, and empty. After instantiating Cache, we can call get,set,del,clear and other methods to Cache operations.
class Cache {
protected cacheMap: Map<string.any>
constructor() {
this.cacheMap = new Map()
}
get(key: string) :any {
return this.cacheMap.get(key)
}
set(key: string.value: any) :void {
this.cacheMap.set(key, value)
}
has(key: string) :boolean {
return this.cacheMap.has(key)
}
del(key: string) :void {
this.cacheMap.delete(key)
}
clear(): void {
this.cacheMap.clear()
}
}
export default Cache
Copy the code
We implement a Cache class with a constructor that is de-instantiated by new. So what’s the problem with this approach?
import Cache from './cache'
let c1 = new Cache()
let c2 = new Cache()
c1.set('test'.1)
c2.set('test2'.2)
console.log(c2.get('test'))
// undefined
Copy the code
Obviously, every time new Cache() is executed, a new object is created, and we expect that no matter how many Cache handlers are instantiated, the Cache operation will be globally unique. We expect that the test stored in C1 will also get this Cache record in C2. Otherwise the cache processor will be meaningless. This is where the singleton pattern comes in.
There are many ways to solve this problem, but let’s look at the first one. If the Cache constructor is mature and does not want to do internal logical refactoring, we can hide the Cache class and create a new SingletonCache class that is responsible for maintaining unique instances.
class Cache {
protected cacheMap: Map<string.any>
constructor() {
this.cacheMap = new Map()
}
get(key: string) :any {
return this.cacheMap.get(key)
}
set(key: string.value: any) :void {
this.cacheMap.set(key, value)
}
has(key: string) :boolean {
return this.cacheMap.has(key)
}
del(key: string) :void {
this.cacheMap.delete(key)
}
clear(): void {
this.cacheMap.clear()
}
}
// Add a singletonCache class that maintains unique instances
class SingletonCache {
private static instance: Cache
constructor() {}
public static getInstance(): Cache {
if(! SingletonCache.instance) { SingletonCache.instance =new Cache()
}
return SingletonCache.instance
}
}
export default SingletonCache
Copy the code
No matter in any place in the program at this time to instantiate operation, SinlgetonCache. GetInstance () ensures that every time is the same instance.
import Cache from './cache'
let c1 = Cache.getInstance()
let c2 = Cache.getInstance()
c1.set('test'.1)
c2.set('test2'.2)
console.log(c2.get('test'))
/ / 1
Copy the code
In this way, if we knew from the beginning that the Cache class should be designed as a singleton, then we could move the maintenance of the singleton to the Cache class
class Cache {
protected cacheMap: Map<string.any>
private static instance: Cache
private constructor() {
this.cacheMap = new Map()
}
get(key: string) :any {
return this.cacheMap.get(key)
}
set(key: string.value: any) :void {
this.cacheMap.set(key, value)
}
has(key: string) :boolean {
return this.cacheMap.has(key)
}
del(key: string) :void {
this.cacheMap.delete(key)
}
clear(): void {
this.cacheMap.clear()
}
public static getInstance(): Cache {
if(! Cache.instance) { Cache.instance =new Cache()
}
return Cache.instance
}
}
export default Cache
Copy the code
Again, the call and the result are the same as in the first method. Finally, let’s consider the question, if not using the singleton pattern to implement this series of logic, is there any other way to ensure the singleton pattern?
The obvious answer is yes. The simplest way to guarantee singletons is to take advantage of Node modularity’s own caching mechanism.
class Cache {
protected cacheMap: Map<string.any>
constructor() {
this.cacheMap = new Map()
}
get(key: string) :any {
return this.cacheMap.get(key)
}
...
}
export const cacheContainer = new Cache()
Copy the code
We introduced the instantiated cacheContainer in the main and Main1 modules, respectively, so that we could use a globally unique instance.
// main.ts
import { cacheContainer } from './cache'
cacheContainer.set('test1'.1)
console.log(cacheContainer.get('test'))
// main1.ts
import { cacheContainer } from './cache'
cacheContainer.set('test2'.2)
console.log(cacheContainer.get('test1'))
/ / 1
Copy the code
On the server, import is eventually compiled as require, and V8 caches the require object when we reference a module using require. If you require again, This object will be taken from require.cache, where cacheContainer is the same object instance. So we can also take advantage of the modularity feature to complete singletons.
The compiled result is:
A singleton for a plug-in
The final application practice, let’s look at a practical library Vuex, which is used more often. We know that Vue provides a simple and convenient plug-in method to add global functions, such as global methods, global resources, global blending and so on. And this is all based on vue.use (). Take Vuex as an example. How do I ensure that after the Vuex plug-in is installed several times, there is only one global Store for storing all the state of the application?
If we installed Vuex multiple times, why wouldn’t there be multiple stores?
Vue.use(Vuex)
Vue.use(Vuex)
...
Copy the code
The next step is to look at the implementation of the plug-in from the source code. Let’s first look at how to introduce Vuex into the project:
const Vuex = require('vuex')
Vue.use(Vuex)
new Vue({
el: '#app',})Copy the code
Use is implemented by calling the install method of the plug-in itself, which does not maintain the singleton of the plug-in itself.
Vue.use = function (plugin: Function | Object) {...// If the plug-in has the install method, execute install to install the plug-in
if (typeof plugin.install === 'function') {
plugin.install.apply(plugin, args)
} else if (typeof plugin === 'function') {
// If the plugins themselves are functions, the functions are executed to install the plugins
plugin.apply(null, args)
}
installedPlugins.push(plugin)
return this
}
Copy the code
The real implementation of singletons is in the Vuex plug-in. Let Vue will hold a globally unique value if it has been assigned. If the plug-in is successfully installed, repeated installation will be skipped.
// Global flags
let Vue
export function install (_Vue) {
// If the value has been assigned, no further mixing of applyMixins will be performed.
if (Vue && _Vue === Vue) {
if (__DEV__) {
console.error(
'[vuex] already installed. Vue.use(Vuex) should be called only once.')}return
}
// If it is already installed, Vue is assigned
Vue = _Vue
// Inject the core logic
applyMixin(Vue)
}
Copy the code
What problem does the singleton pattern solve?
After looking at these two examples in action, let’s go back and think about what the singleton pattern solves.
- Ensure that there is only one instance of a class. It is well known that ordinary constructors do not implement this behavior; constructors are designed to return a new object every time they are instantiated. The singleton pattern lets you get the created object each time you create a new one, instead of a new one.
- Provide a global access point for the instance. The singleton pattern allows code to access a particular object anywhere, but is concerned only with the particular access point, regardless of its internal implementation logic.
The implementation of the singleton pattern
- The default constructor is no longer public, but is wrapped as private, preventing external instantiation via direct new.
- Create a static method that is the only externally accessible interface. The method calls the private constructor to instantiate the object and store the instance object, or if it has been saved, return the cached instance object.
The factory pattern
Let’s look at another creative design pattern, the factory pattern. The factory pattern needs to define a factory interface for creating product objects. The user is presented with a unified factory interface, and the actual creation work is placed in each specific factory class.
Production application
To better understand this passage, let’s start with a practical example.
Let’s say your company is a vehicle manufacturer and your boss gives you a requirement. The requirement is to design a BMW car and put it into production. The first thing that comes to mind is the constructor. Design a BmwCar class that produces a car every time it is instantiated. We simply describe the car with a desc method.
class BmwCar {
constructor() {}
desc(): void {
console.log('BmwCar')}}let bmw = new BmwCar()
bmw.desc() // BmwCar
Copy the code
Very good! You instantiate as many BMWS as you need. At this time, I received a new task to design another Benz brand car. Maybe the first reaction was very simple. I could also complete the task by building a new class of BenzCar.
class BenzCar {
constructor() {}
desc(): void {
console.log('BenzCar')}}let benz = new BenzCar()
benz.desc() // BenzCar
Copy the code
Everything seemed fine until it became clear that there were other models to be built, and that each model was a separate constructor. The problem at this time is that ** users need to determine which constructor the model belongs to before choosing the corresponding constructor for instantiation when they instantiate a car. ** Is there a better way for users not to have to care about the creation process of each vehicle? At this point we can think of the factory model.
Firstly, the commonness of each model is extracted and processed into the interface, and then the constructor of each model is the implementation of this common interface.
// Common interface of Car
interface Car {
desc(): void;
}
/ / the mercedes-benz brand
class BenzCar implements Car {
constructor() {}
public desc(): void {
console.log("BenzCar"); }}/ / the BMW brand
class BmwCar implements Car {
constructor() {}
public desc(): void {
console.log("BmwCar"); }}// Byd
class BydCar implements Car {
constructor() {}
public desc(): void {
console.log("BydCar"); }}Copy the code
We then create a vehicle factory function that internally determines which vehicle to instantiate. When the user builds the car, he just needs to instantiate the factory to produce the different cars as needed.
// Factory function for vehicle production
class CarFactory {
public static TYPE_BENZ: string = "benz";
public static TYPE_BMW: string = "bmw";
public static TYPE_BYD: string = "byd";
constructor() {}
public static createCar(type: string): Car {
switch (type) {
case CarFactory.TYPE_BENZ:
return new BenzCar();
case CarFactory.TYPE_BMW:
return new BmwCar();
case CarFactory.TYPE_BYD:
return new BydCar();
default:
throw new Error('Illegal parameter'); }}}// Use the factory method to create different types of cars
let ben = CarFactory.createCar("benz");
let bmw = CarFactory.createCar("bmw");
ben.desc(); // benz
bmw.desc(); // bmw
Copy the code
It looks much better now that you don’t have to worry about which constructor to call when creating a vehicle, and the factory functions take care of it all behind the scenes. A unified factory approach avoids coupling between the developer and the specific product, and the code is easier to maintain.
Applicable scenario
Summarize the application scenarios of the factory pattern
- You can use the factory pattern when you don’t know the categories and dependencies
- If you are developing a library, using the factory pattern provides a way for developers to extend internal components without having to access other sources.
Abstract Factory pattern
Is a simple factory function going to solve all the production problems? Obviously not. In actual production and development, the complexity of ** business is often beyond the reach of a single factory. ** We need a larger abstraction from the simple factory pattern to deal with the relationships between related factories. This is the abstract factory pattern to be introduced next.
It’s a little hard to understand conceptually, so let’s go ahead and introduce this concept from a practical example. And then the last demand for cars. Now, to make A car, you have to consider the combination of different manufacturers, no matter what kind of model, you have to have different colors, like red, black, white, and each car can have different engines, so let’s say there are two engines, one is A engine, and the other is A transmitter B engine. Every car needs to be made with the color of the manufacturer, the engine of the manufacturer, and the combination of the two is the complete car. For example, we need to produce A red BMW with an A engine.
Production application
If we follow the previous idea, we might need to modify the constructors for each car as many combinations as possible. And we also need to modify our factory function CarFactory. Let it go swtich Case various models. Obviously, this is not a good idea. In addition to the convenience of modifying the CarFactory every time you add a model, the only factory function gets bigger and bigger.
The abstract factory pattern can well solve the problem, we will launch a more abstract, to the factory and to decouple the lower factory, an idea before this, javascript as a weakly typed language, it is difficult to decouple when creating objects, after all, it has natural polymorphism, do not need to consider the type of coupling problem, But with typescript, we can restore abstract classes very well.
Starting with each of the underlying factory classes, we need to define abstract classes that perform different functions. IColor is an abstract class for configuring the color of the car, and IEngine is an abstract class for configuring the engine.
// Configure color - abstract product
abstract class IColor {
abstract setColor(): string
}
// Configure the engine
abstract class IEngine {
abstract setEngine(): string
}
Copy the code
** Abstract classes do not implement functions. concrete implementations are placed on concrete classes that inherit from abstract classes. ** The next step is to implement each concrete class according to these abstract classes. RedColor is the concrete class that finally gives red to the body
// Specific products
// RedColor is an inheritance of IColor
class RedColor extends IColor {
public setColor(): string {
return 'red color'}}class BlackColor extends IColor {
public setColor(): string {
return 'black color'}}Copy the code
A similar AEngine is a concrete class that configures AEngine for cars
// AEngine inherits from IEngine
class AEngine extends IEngine {
public setEngine(): string {
return 'AEngine'}}class BEngine extends IEngine {
public setEngine(): string {
return 'BEngine'}}Copy the code
Once we have defined the classes associated with the eventual creation of the car, we create a structure, AbstractCarFactory, which is responsible for defining and assembling the various underlying factory classes above. We use it to agree on what common features the car has. CreateColor means that the car needs to select a body color. CreateEngine indicates that the car needs an engine.
// Abstract factory
abstract class AbstractCarFactory {
abstract createColor(): IColor
abstract createEngine(): IEngine
}
Copy the code
Finally, you need to define each concrete factory where the concrete classes are instantiated. BmwCarFactory is responsible for creating a red body, AEngine cars, BenzCarFactory is responsible for creating a black body, BEngine cars.
// The specific factory
class BmwCarFactory extends AbstractCarFactory {
public createColor() {
return new RedColor()
}
public createEngine() {
return new AEngine()
}
}
class BenzCarFactory extends AbstractCarFactory {
public createColor() {
return new BlackColor()
}
public createEngine() {
return new BEngine()
}
}
Copy the code
Finally, different models are instantiated
let benz = new BenzCarFactory()
let bmw = new BmwCarFactory()
console.log(benz.createColor().setColor())
console.log(bmw.createEngine().setEngine())
Copy the code
This is the implementation of the underlying abstract factory pattern, which delegates the responsibility for creating objects to specific classes (concrete factories) that use polymorphism, making the code more extensible. If there is a new requirement and another model needs to be added, you can simply create a new factory class that inherits AbstractCarFactory.
class BydCarFactory extends AbstractCarFactory {... }Copy the code
Implement abstract factory classes
The use of an abstract factory differs from that of a normal factory in the complexity of the scenario. An abstract factory is a higher level abstraction of a normal factory. How to implement abstract factory class, need to clear four categories
- Abstract class: Each small granular set of features, which is abstract class, is responsible for description, not implementation, in order to remove product commonalities from the example
IColor, IEngine
- Concrete class: The implementation of an abstract class, that is, the implementation of each of the minimum granularity functions, which is subject to the abstract class constraint, as in the example
RedColor,BlackColor
- Abstract factory: the commonality of the final product, a set of descriptions of the functional characteristics of each small particle, in the example
AbstractCarFactory
- Concrete factory: For each concrete product, you need to inherit the method that implements the abstract factory definition, which is used to create the concrete product, as shown in the example
BmwCarFactory
To summarize its usage scenarios, you can use abstract factories if your code needs to interact with multiple different sets of related products, but you don’t want to build your code based on specific classes of the product because you can’t get the information ahead of time, or because of future extensibility.