Explore the JavaScript implementations of some classic and modern design patterns.
There is no best, only the most suitable.
There are a total of 23 design patterns divided into 3 categories.
Pattern Type | Design Patterns |
---|---|
Creational | Singleton, Factory, Builder |
Structural | Adapter, Decorator, Proxy |
Behavioral | Strategy, Observer, Publish-Subscribe, Chain of Responsibility, Mediator |
Singleton Pattern#
A class that only has one instance and provides a global access point to it.
-
Singleton
: Specific class that we need to access, and visitors need to obtain its instance. -
instance
: Singleton, an instance of the specific class. The specific class usually provides thegetInstance
method to obtain the singleton. -
getInstance
: Method to obtain the singleton.
class Singleton {
let _instance = null;
static getInstance() {
if (!Singleton._instance) {
Singleton.instance = new Singleton()
}
// If the unique instance already exists, return it directly
return Singleton._instance
}
}
const s1 = Singleton.getInstance()
const s2 = Singleton.getInstance()
console.log(s1 === s2) // true
Example#
Vuex implements a global store to store all application states. The implementation of this store is a typical application of the singleton pattern.
Use Cases#
-
If instantiating a class consumes a lot of resources, a singleton can be used to avoid performance waste.
-
If there is a need for shared state, a singleton can be used to ensure consistency of access.
Factory Pattern#
Factory pattern: Returns instances of different classes based on different parameters. Separates the creation of objects from their implementation. Implementation is complex, but usage is simple. Simply use the methods provided by the factory.
Advantages:
-
Good encapsulation, visitors do not need to know the creation process, and the code structure is clear.
-
Good extensibility, the factory method isolates the user and the creation process, conforming to the open-closed principle.
Disadvantages:
It adds abstraction to the system, resulting in additional system complexity. It should not be overused.
Example#
document.createElement
creates a DOM element. This method uses the factory pattern, which is complex internally but simple to use externally.
Use Cases#
-
Object creation is complex, and visitors do not need to know the creation process.
-
Need to handle a large number of small objects with similar properties.
Adapter Pattern#
Used to solve compatibility issues when interfaces/methods/data are incompatible. Converts them into the format expected by the visitor.
Characteristics:
- Integrating third-party SDKs.
- Encapsulating old interfaces.
Decorator Pattern#
- Dynamically adds additional responsibilities to an object, serving as an alternative to inheritance.
- Extends the original object by wrapping it, allowing the original object to meet more complex requirements without affecting other objects derived from the same class.
Has a taste of the prototype chain.
Proxy Pattern#
Provides a substitute or placeholder for an object to control access to it.
Use Cases#
- ES6 Proxy
- jQuery.proxy() method
Difference Between Decorator and Proxy Patterns#
- Decorator Pattern: Extends functionality, keeps the original functionality unchanged and can be used directly.
- Proxy Pattern: Displays the original functionality, but with restrictions.
Strategy Pattern#
Defines a series of algorithms and selects the appropriate one based on the input parameters.
Example#
Scenario: Discounts for Singles' Day. Discounts include $20 off for orders over $200, $50 off for orders over $300, and $100 off for orders over $500. How would you implement this?
// if-else: Bulky and difficult to modify
function priceCalculate(discountType, price) {
if (discountType === 'discount200-20') {
return price - Math.floor(price / 200) * 20;
} else if (discountType === 'discount300-50') {
return price - Math.floor(price / 300) * 50;
} else if (userType === 'discount500-100') {
return price - Math.floor(price / 500) * 100;
}
}
// Rewrite with the strategy pattern, hiding the algorithm, and reserving an entry point for adding strategies for easy expansion
const priceCalculate = (function() {
const discountMap = {
'discount200-20': function(price) {
return price - Math.floor(price / 200) * 20;
},
'discount300-50': function(price) {
return price - Math.floor(price / 300) * 50;
},
'discount500-100': function(price) {
return price - Math.floor(price / 500) * 100;
},
};
return {
addStrategy(strategyName, fn) {
if (discountMap[strategyName]) return;
discountMap[strategyName] = fn;
},
priceCal(discountType, price) {
return discountMap[discountType] && discountMap[discountType](price);
}
}
})()
Advantages:
- Strategies are independent of each other and can be switched. This improves flexibility and reusability.
- No need to use
if-else
for strategy selection, which improves maintainability. - Good extensibility, conforms to the open-closed principle.
Disadvantages:
- Strategies are independent of each other, so some complex algorithm logic cannot be shared, resulting in resource waste.
- When using strategies, users need to understand the specific strategy implementation, which does not meet the principle of least knowledge and increases usage costs.
Use Cases#
- Scenarios where algorithms need to be freely switched.
- Multiple algorithms have slight differences in behavior, and the strategy pattern can be considered to dynamically select algorithms.
- Need to avoid multiple conditional judgments, and the strategy pattern can be considered to avoid them.
Observer Pattern#
An object (subject) maintains a list of objects (observers) that depend on it and automatically notifies them of any changes in state.
Pros and Cons#
Advantages: The observer pattern's biggest advantage is that observers are notified when the target changes.
Disadvantages: The target and observers are coupled together. To implement the observer pattern, both the observer and the subject must be introduced to achieve a responsive effect.
Use Cases#
Suppose Bilibili users are observers and Bilibili content creators are subjects. Many Bilibili users follow a specific content creator, and when the content creator updates a video, these followers will be notified.
Publish-Subscribe Pattern#
Based on a topic, objects that want to receive notifications (subscribers) subscribe to the topic through custom events. Objects that trigger events (publishers) notify subscribers by publishing topic events.
Use Cases#
WeChat users subscribe to many official accounts. When an official account publishes a new article, users are notified in a timely manner.
In this case, the official account is the publisher, and the users are subscribers. Users register their subscription to the official account's events with the event dispatcher. When the publisher publishes a new article, it publishes the event to the event dispatcher, which then sends a message to the subscribers.
Publish-Subscribe Pattern in Vue Two-Way Binding#
Vue's two-way binding is implemented through data interception and the publish-subscribe pattern.
-
By intercepting the
setter
andgetter
of each data usingDefineProperty
, each data is added with a list of subscribers, which records all components that depend on this data.Reactive data is the publisher.
-
Each component corresponds to a
Watcher
subscriber. When the component's render function is executed, the component'sWatcher
is added to the list of subscribers of the reactive data it depends on.This process is called "dependency collection".
-
When reactive data changes, the
setter
is triggered. Thesetter
is responsible for notifying theWatcher
in the subscriber list of the data, and theWatcher
triggers the component to re-render to update the view.The view layer is the subscriber.
Difference Between Observer and Publish-Subscribe Patterns#
The observer pattern is one of the classic software design patterns, while publish-subscribe is just a message paradigm in software architecture.
Observer Pattern | Publish-Subscribe Pattern |
---|---|
2 roles | 3 roles |
Focus on subject | Focus on event dispatcher |
The relationship between observer and subject is established by the subject actively, and the subject needs at least three methods: add observer, remove observer, and notify observer.
Publish-subscribe is built on a central event dispatcher. Publishers and subscribers do not communicate directly, but the publisher hands over the message to the dispatcher, and subscribers subscribe to the messages in the dispatcher according to their own needs.
The implementation of publish-subscribe internally utilizes the observer pattern, but the presence of the publish-subscribe center makes communication management between producers and consumers more manageable and extensible.