Design Patterns is a summary of experience in software engineering, which provides solutions to common software design problems. Although design patterns themselves are not specific codes, they are some reusable design ideas and principles that can help developers write more flexible, maintainable and scalable codes.
This blog will introduce the definition, meaning, classification and common examples of each type of design pattern in detail. By understanding these contents in depth, readers can have a clearer understanding of how to apply design patterns in projects and quickly find appropriate solutions when encountering practical problems.
1. What is a design pattern?
Design patterns are frequently used design structures and best practices in object-oriented programming. They are reusable solutions to specific problems that occur repeatedly in software design. In simple terms, design patterns are not specific codes, but a way of thinking that guides developers on how to organize code so that the code is more scalable, reusable, and maintainable.
Design patterns originated in the field of architecture and were first proposed by architect Christopher Alexander, who defined design patterns as "descriptions of recurring problems and their solutions in a certain context". In software engineering, design patterns have been accumulated through years of development experience to help programmers avoid common mistakes and improve development efficiency.
2. Why use design patterns?
In software development, when faced with complex requirements, we often need to create a large number of interrelated classes and objects. If the relationships between these classes are written too tightly coupled, modifying one class may affect the operation of the entire system. In addition, the scalability and maintainability of the system will also be greatly limited. Design patterns help developers solve these problems by providing standardized design methods.
The advantages of design patterns include:
- Improve code reusability: Patterns provide some common solutions that can be reused in multiple projects.
- Improve code flexibility and scalability: Through decoupling and abstraction, design patterns make code easier to extend and maintain.
- Facilitate team communication: Design patterns provide a common language that allows developers to clearly express their design intentions through the names of design patterns.
- Simplify complex problem solving:Design patterns provide concise and clear solutions, avoiding code confusion caused by complex designs.
3. Classification of design patterns
Design patterns are generally divided into three categories:Creational Patterns,Structural PatternsandBehavioral ModelThere are several design patterns under each category, targeting different design requirements.
3.1 Creational Patterns
Creational patterns deal with the process of creating objects. They provide a way to create objects that makes the system more flexible and extensible. These patterns can decouple the process of object creation from the rest of the system, thus avoiding dependency issues caused by instantiating objects directly.
Common creational patterns include:
- Singleton: Ensures that a class has only one instance and provides a global access point.
- Factory Method: Defines an interface for creating objects, but defers the actual creation of the object to subclasses.
- Abstract Factory: Provides an interface for creating families of related or dependent objects without specifying concrete classes.
- Builder: Separate the construction process of a complex object from its representation so that the same construction process can create different representations.
- Prototype: Creates a new object by copying an existing object, rather than by instantiating a new class.
Singleton
The goal of the singleton pattern is to ensure that a class has only one instance and provide a global access point. This pattern is suitable for scenarios where only one instance is needed in the system, such as loggers, configuration managers, or thread pools.
package main import ( "fmt" "sync" ) // Singleton is an implementation of the singleton pattern type Singleton struct{} var instance *Singleton var once sync.Once // GetInstance ensures that only one instance is created func GetInstance() *Singleton { once.Do(func() { instance = &Singleton{} }) return instance } func main() { s1 := GetInstance() s2 := GetInstance() if s1 == s2 { fmt.Println("s1 and s2 are the same instance") } }
Factory Method
The factory method pattern allows subclasses to decide which class to instantiate and achieves flexibility in object creation by placing object creation logic in subclasses. The factory method pattern is particularly suitable when the object creation process is complex or changes frequently.
package main import "fmt" // Product defines the product interface type Product interface { Use() } // ConcreteProduct is the concrete implementation of the Product interface type ConcreteProduct struct{} func (p *ConcreteProduct) Use() { fmt.Println("Use specific products") } // Factory defines the factory interface type Factory interface { CreateProduct() Product } // ConcreteFactory is a specific factory implementation type ConcreteFactory struct{} func (f *ConcreteFactory) CreateProduct() Product { return &ConcreteProduct{} } func main() { factory := &ConcreteFactory{} product := factory.CreateProduct() product.Use() }
3.2 Structural Patterns
Structural patterns focus on the composition of objects and classes. These patterns simplify the interaction of objects by making the interfaces between different classes more consistent. Structural patterns usually combine classes or objects together to form larger structures by using inheritance or interfaces.
Common structural patterns include:
- Adapter: Convert the interface of a class into another interface expected by the client, so that classes with incompatible interfaces can work together.
- Bridge Mode: Separate the abstract part from its implementation so that they can vary independently.
- Composite: Group objects into a tree structure to represent a part-whole hierarchy.
- Decorator pattern: Dynamically add new functionality to an object without changing its structure.
- Facade: Provides a unified interface for a set of interfaces in a subsystem, thereby simplifying the use of the subsystem.
- Flyweight: Supports efficient use of a large number of objects by sharing fine-grained objects.
Adapter
The adapter pattern enables otherwise incompatible classes to work together by converting the interface of one class into another interface that the client expects. For example, suppose you have a class OldSystem
, which has an interface DoOldWork()
, and now need to pass NewSystem
Interface DoNewWork()
to use it.
package main import "fmt" // OldSystem old system interface type OldSystem struct{} func (o *OldSystem) DoOldWork() { fmt.Println("Do the old system's work") } // NewSystem new system interface type NewSystem interface { DoNewWork() } // Adapter adapter type Adapter struct { oldSystem *OldSystem } func (a *Adapter) DoNewWork() { a.oldSystem.DoOldWork() } func main() { oldSys := &OldSystem{} adapter := &Adapter{oldSystem: oldSys} adapter.DoNewWork() }
3.3 Behavioral Patterns
Behavioral patterns focus on the distribution of responsibilities and communication methods between objects. This type of pattern not only focuses on the structure of classes and objects, but also on the interaction between them in order to complete tasks more flexibly and efficiently.
Common behavioral patterns include:
- Observer: Defines a one-to-many dependency relationship between objects. When the state of an object changes, all objects that depend on it will be automatically notified.
- Strategy: Define a series of algorithms, encapsulate each algorithm, and make them interchangeable.
- Command: Encapsulate requests into objects so that other objects can be parameterized using different requests, queues, or logs.
- State: Allows an object to change behavior based on its internal state.
- Chain of Responsibility: Decouple the sender and receiver of a request by passing the request through a series of processing objects until one object handles the request.
Observer
The observer pattern is used to define dependencies between objects. It allows dependent objects to automatically receive update notifications when the state of an object changes. This pattern is often used in event handling systems, such as GUI frameworks, message broadcasting systems, etc.
package main import "fmt" // Observer is the observer interface type Observer interface { Update(string) } // ConcreteObserver is the specific observer implementation type ConcreteObserver struct { name string } func (o *ConcreteObserver) Update(message string) { fmt.Printf("%s Received message: %s\n", o.name, message) } // Subject is the observed object type Subject struct { observers []Observer } func (s *Subject) Attach(observer Observer) { s.observers = append(s.observers, observer) } func (s *Subject) Detach(observer Observer) { for i, obs := range s.observers { if obs == observer { s.observers = append(s.observers[:i], s.observers[i+1:]...) break } } } func (s *Subject) NotifyObservers(message string) { for _, observer := range s.observers { observer.Update(message) } } func main() { subject := &Subject{} observer1 := &ConcreteObserver{name: "Observer 1"} observer2 := &ConcreteObserver{name: "Observer 2"} subject.Attach(observer1) subject.Attach(observer2) subject.NotifyObservers("Design pattern learning") subject.Detach(observer1) subject.NotifyObservers("Progress update: completed") }
4. Application examples of design patterns
Application of singleton mode in database connection management: The singleton mode is often used to manage database connection pools, because the system only needs one global connection pool instance to handle database connection and release.
Application of Observer Mode in Event Monitoring: In GUI applications, button click events are usually handled through the observer pattern. The button maintains a list of observers, and when the user clicks the button, it notifies all registered observers to perform corresponding actions.
5. Conclusion
Design patterns are an important tool in software development. They provide standardized solutions to common design problems and help developers write more readable, scalable, and maintainable code. By understanding and applying different types of design patterns, developers can more easily solve complex software design problems and improve development efficiency.
Design patterns are not immutable rules, but a flexible design method. In actual projects, developers can choose appropriate design patterns according to their needs, or make appropriate adjustments according to actual conditions. After mastering design patterns, you will find that many problems in software development can be easily solved, and the code will become more concise and clear.