introduction
- Briefly introduce the definition and common uses of the Singleton pattern.
- Propose the problems existing in the singleton model in actual development, especially the complexity in multi-threaded environments.
- Description This article will explore the dilemma of the singleton pattern and provide several alternatives.
1. Dilemma of single piece model
1.1 Complexity in multi-threaded scenarios
-
question:
- In a multi-threaded environment, the implementation of singleton mode needs to consider thread safety issues.
- The complexity of Double-Checked Locking.
-
Example:
Singleton* Singleton::getInstance() { if (instance == nullptr) { // first check std::lock_guard<std::mutex> lock(mutex); if (instance == nullptr) { // Second check instance = new Singleton(); } } return instance; }
1.2 Expose unnecessary details
-
question:
- The singleton pattern exposes the implementation details of "only one object" to users.
- Increased code coupling.
-
Example:
- Users need to explicitly call
Singleton::getInstance()
。
- Users need to explicitly call
1.3 Thread safety cannot be guaranteed
-
question:
- Even if the creation of a singleton object is thread-safe, the thread-safety of its member functions still requires additional guarantees.
-
Example:
void Singleton::doSomething() { std::lock_guard<std::mutex> lock(mutex); //Thread-safe operation }
1.4 The singleton model does not conform to the original design intention of the class
-
question:
- The original design intention of classes is to encapsulate data and behavior, but the singleton mode violates this principle by forcing only one object.
- The Singleton pattern is more like a global variable than a true class.
2. Alternatives to the Singleton Pattern
2.1 Using a C-like interface
-
describe:
- Encapsulate functionality in a set of global functions rather than forcing the use of a singleton object.
-
advantage:
- Avoids the complexity of the singleton pattern.
- Users do not need to care about the life cycle of objects.
-
Example:
namespace MyModule { void initialize(); void doSomething(); void cleanup(); }
2.2 Use static classes
-
describe:
- Encapsulate the functionality in a static class, and all member functions and variables are static.
-
advantage:
- Avoids the complexity of the singleton pattern.
- Users do not need to obtain the singleton object explicitly.
-
Example:
class MyModule { public: static void initialize(); static void doSomething(); static void cleanup(); private: static std::mutex mutex; static int sharedData; };
2.3 Dependency injection
-
describe:
- Pass the object to the consumer through dependency injection instead of letting the consumer get the singleton object directly.
-
advantage:
- Improved code testability and flexibility.
- Global state is avoided.
-
Example:
class MyService { public: void doSomething(); }; class MyClass { public: MyClass(MyService& service) : service(service) {} void useService() { (); } private: MyService& service; };
3. When to use static classes? When to use a C-like interface?
3.1 Scenarios for using static classes
-
The interface is fixed and the internal connection is strong:
- If a set of functions or methods are logically closely related and the interface (function signature) is relatively fixed, you can use a static class to encapsulate these functions.
- Example: MQ interaction class, configuration management class, logging tool class.
-
Need to share status:
- If multiple functions need to share some state (such as configuration, cache, connection, etc.), you can use static classes to manage these states.
-
Functional modularity:
- If a functional module needs to be encapsulated independently and does not need to instantiate objects, you can use a static class.
3.2 Scenarios using C-like interfaces
-
The interface is not fixed or the functions are scattered:
- If a set of functions are not logically closely related, or the interface is likely to change frequently, a C-like interface can be used.
- Example: String processing functions, mathematical tool functions.
-
No need to share state:
- If a set of functions does not need to share state, and each function is independent, a C-like interface can be used.
-
Cross-language compatibility:
- If your code needs to interact with other languages (such as C, Python), you can use a C-like interface, because C-style interfaces are easier to call from other languages.
4. Design principles and philosophy
4.1 Single Responsibility Principle (SRP)
-
describe:
- A class or module should have only one responsibility.
-
application:
- The singleton pattern often results in classes taking on too many responsibilities (such as object management, business logic, etc.), whereas static classes or C-like interfaces can provide a better separation of responsibilities.
4.2 Opening and closing principle (OCP)
-
describe:
- Software entities should be open for extension and closed for modification.
-
application:
- The singleton pattern is often difficult to extend, while dependency injection and C-like interfaces make it easier to extend functionality.
4.3 Dependency Inversion Principle (DIP)
-
describe:
- High-level modules should not depend on low-level modules, both should depend on abstractions.
-
application:
- The singleton pattern usually causes high-level modules to directly depend on specific singleton classes, while dependency injection can decouple dependencies through abstract interfaces.
4.4 Philosophical thinking
-
Disadvantages of Global State:
- The singleton pattern is essentially a global state, and global state reduces the testability and maintainability of the code.
- Through dependency injection or C-like interfaces, global state can be avoided, making the code more modular and testable.
-
Simplicity vs. Complexity:
- The singleton pattern seems simple, but in fact it hides complex thread safety issues and coupling issues.
- Using static classes or C-like interfaces can simplify the design and reduce complexity.
5. Summary
The singleton pattern has many problems in C++ development, especially in multi-threaded environments.
To avoid these problems, consider using alternatives such as static classes, C-like interfaces, or dependency injection.
These solutions not only simplify the code, but also improve flexibility and maintainability.
From a design principle and philosophy perspective, the Singleton pattern violates the Single Responsibility Principle, the Open-Closed Principle, and the Dependency Inversion Principle, while alternatives adhere to these principles better.
By avoiding global state and simplifying the design, we can write more robust and maintainable code.
Hope these arrangements and suggestions are helpful to you! If you have any other questions, please feel free to communicate! 😊🚀
Appendix: Reference Resources
-
C++ Core Guidelines
-
Google C++ Style Guide
-
Effective Modern C++
-
Design Patterns: Elements of Reusable Object-Oriented Software
-
ACE related references:
-
《C++ Network Programming, Volume 1: Mastering Complexity with ACE and Patterns》 by Douglas C. Schmidt and Stephen D. Huston
- Chapter 6: The ACE Singleton Class
- Chapter 7: The ACE Service Configurator Framework
-
《C++ Network Programming, Volume 2: Systematic Reuse with ACE and Frameworks》 by Douglas C. Schmidt and Stephen D. Huston
- Chapter 5: The ACE Reactor Framework
- Chapter 6: The ACE Task Framework
-