Summary of Common Design Patterns and Their Practice - TypeScript Implementation
If there are any shortcomings or errors, please feel free to provide guidance.
Preface:
Thanks to TypeScript’s powerful and flexible OOP syntax, using classic and reliable OOP design patterns in JavaScript has become a reality. Although some companies have already been using TS for large-scale project development, very few projects truly leverage the advantages of TS’s robust object-oriented syntax. Java-like OOP syntax makes it almost identical to Java design patterns in terms of concepts. This article selects some common and important design patterns for refinement and summary, avoiding discussions about which is better between OOP and functional programming. All specific implementations are written in TypeScript, and there’s an easter egg at the end of the article.
GitHub repository address for all practical code examples in this article, welcome to Star
Significance of using design patterns: Libraries and frameworks cannot help us organize applications into architectures that are easy to understand, maintain, and resilient, so we need design patterns.
Basic Design Principles:
- Encapsulate what varies
- Favor composition over inheritance
- Consider potential changes the system may need in the future and principles to handle changes
- Program to interfaces, not implementations
- Strive for loose coupling in design between interacting objects
- The purpose of loose coupling is to minimize object dependencies and enhance system flexibility
- Classes should be open for extension but closed for modification (Open-Closed Principle)
- Depend on abstractions, not concrete classes
- Dependency Inversion Principle: Variables should not hold references to concrete classes; don’t let classes derive from concrete classes; don’t override implemented methods in base classes (methods shared by subclasses); objects that rarely change can violate this, changing objects can use factories to encapsulate changes
- Least Knowledge Principle: Don’t couple too many classes together during design, making as few classes as possible interact with each other
- Methods within objects should only call methods of the object itself, methods of objects passed as parameters, methods of any objects created by this method, or methods of components within the object (if calling other methods returns an object, don’t call its methods)
- Consider all factors comprehensively before adopting principles
- High-level components can decide when low-level components participate, but low-level components cannot directly call high-level components (in some cases they can, but never form circular dependencies)
- Assign one responsibility to one class; when designed to support a set of related functions, it achieves high cohesion
- All the above principles are not golden rules in engineering; actual situations require continuous balancing and trade-offs
Strategy Pattern
The core step is to delegate different variable behaviors in Class 1 into individual behavior classes. Behavior classes 1, 2, 3… will implement different variants of the same interface and exist as properties in various similar classes of Class 1, making algorithm variations independent of customers using the algorithm.
Advantages:
- Can always expand new behaviors
- The same class can dynamically change its behavior through setter methods
e.g: Strategy
Observer Pattern
Used to better describe one-to-many relationships between objects. When multiple objects (observers) depend on state changes of one object (subject), the Observer pattern helps us distribute state and send updates to various observers, making them loosely coupled.
Core Steps:
Subject class implements Observable interface, implementing management of observers and judgment of whether to push data in the class. Observer class implements Observer interface, implementing update method for subject class to call. When registering new observers, subject objects store observer objects in observer array, and when pushing data, iterate through array calling update method to pass data into their observer objects.
e.g: Observer
Decorator Pattern
Dynamically assign new responsibilities to existing objects, more flexible than inheritance. It uses inheritance, but only to achieve type matching with superclass without obtaining behavior. Behavior comes from decorators and basic components or combinations with other decorators.
As the domino effect in the design pattern world, each new decorated object not only contains the previous decorator’s state but can also extend its own attributes. Through a common superclass, state commonality is preserved and upward chain calls can be made, similar to subclass recursive calls to parent class. This pattern can well replace inheritance.
Core Steps:
Decorated classes uniformly derive from a super abstract class A to obtain their type. Decorator abstract class B inherits from A and saves all states of A-type objects in constructor. Each new decorator wraps the previous decorated object. After decoration, decorated classes can still be continuously extended.
Disadvantages:
Customer code cannot rely on special types, many small objects appear, excessive use leads to program complexity
e.g: Decorator
Factory Pattern
A good way to create objects and solve coupling. Strictly speaking, this is not a pattern but a coding habit. Encapsulate derived class instantiation in static methods of factory class. Abstract factory methods allow subclasses to decide manufacturing methods themselves. Externally, we call abstract methods with great flexibility, implementation doesn’t need to bind specific classes, which brings another advantage: decoupling superclass and subclass objects’ code. And instantiation can be deferred to subclasses.
Core Points:
Use abstract factory methods to replace methods requiring flexibility. To make products customizable, define product classes as abstract classes providing some default attributes and methods or abstract methods. Derived classes can override or implement them themselves. Different factory classes can hide different product manufacturing details while maintaining the same production process.
e.g: Simple Factory
Abstract Factory
Different factory classes implement the same abstract factory interface to manufacture similar products. Similar products then depend on common product interfaces, allowing customers to use them without caring about specific actual products.
The difference between the two is that factory method delegates object instantiation to subclasses, while abstract factory establishes interface contracts between products and customers. Both can decouple customers from specific products.
e.g: Abstract Factory
Singleton Pattern
Instantiate a unique object, provide a global access point. The simplest among all design patterns but very practical and widely used.
Core Points:
Establish a static private variable as the unique instance. External access to this unique instance through static methods, and set constructor to private.
e.g: Singleton
Command Pattern
Encapsulate requests by introducing intermediate proxy objects, decoupling requesters from executors. Often used in work queues or operation logging of large data structures and transaction processing.
Core Steps:
Specific task execution details encapsulated in execute method implementation of Command interface, then pass instances of classes implementing the interface to clients, which execute by calling their execute method.
e.g: Command
Adapter Pattern
Wrap certain interfaces to achieve different purposes, convert interfaces to make them usable by other objects, change interfaces to meet customer expectations.
Core Points:
Object adapter class implements target interface and calls corresponding methods of adapted objects within it, enabling customers to call different interface objects under unchanged customer code.
e.g: Adapter
Facade Pattern
Simplify interfaces using facade class, encapsulate complex subsystems, expose fewer simpler interfaces, create higher-level functionality. Programming to facade can decouple customers from subsystem components.
Core Points:
When constructing Facade class, aggregate various component objects of subsystem, exposed interfaces provide more direct higher-level operations, conforming to least knowledge principle.
e.g: Facade
Template Method Pattern
This encapsulation method allows subclasses to hook themselves into operations at any time. Template method defines algorithm steps and allows subclasses to provide their own implementation for partial steps.
Core Points:
Extract identical steps and implement them in abstract class, different steps implemented separately in subclasses (existing as abstract methods in parent class). Subclasses achieve respective functions by calling the same template method (functions differ partially but cannot be known from template method alone due to unified abstraction). Hook methods can be added to template methods, providing default implementation or left empty for subclasses to override. Through conditional flow control, we can greatly increase algorithm flexibility. Hook methods are usually optional parts of algorithms.
Common Application Scenarios:
When sorting objects, objects need to implement a comparison method first.
Summary (Easter Egg)
If you still find these numerous patterns dizzying and hard to apply at this point, please take these solid practical tips:
- Any problem in computer science field can be solved by adding an intermediate layer. Almost all design patterns follow this principle to handle changes, increase flexibility, or perform extensions.
- Abstraction and reuse are core requirements. everyone works tirelessly to satisfy these two points.
- No perfect pattern can perfectly handle all changes.
Finally, I hope you can apply patterns more to business or framework writing, striving to write clean, extensible, and maintainable architectures.
Reference Materials: Head First Design Patterns Chinese Edition, First Edition 2007
Views