单例模式如何隐藏依赖?附代码示例解析
Great question—this is a super common point of confusion when first wrapping your head around why singletons get labeled as anti-patterns. Let’s break this down with concrete examples to make it crystal clear.
What Does "Hiding Dependencies" Actually Mean?
When a component uses a singleton directly (like calling Singleton.getInstance() inside its methods), it doesn’t declare that dependency anywhere in its public interface—think constructor parameters or method signatures. Anyone looking at the component from the outside has no way of knowing it relies on that singleton unless they dig through every line of implementation code.
This is the opposite of explicit dependency injection, where dependencies are passed in upfront (usually via constructor) and are immediately visible to anyone using the component.
Example 1: Singleton with Hidden Dependency
Let’s start with a classic singleton logger:
// Singleton Logger public class Logger { private static Logger instance; private String logLevel = "INFO"; private Logger() {} public static Logger getInstance() { if (instance == null) { instance = new Logger(); } return instance; } public void log(String message) { System.out.printf("[%s] %s%n", logLevel, message); } public void setLogLevel(String logLevel) { this.logLevel = logLevel; } }
Now a service that uses this singleton directly:
public class UserService { public void createUser(String username) { // Hidden dependency: You'd never know UserService needs Logger from its constructor/methods Logger.getInstance().log("Creating user: " + username); // Rest of user creation logic... } }
The Problems Here:
- Testing becomes a nightmare: If I want to test
UserService, I can’t easily replace the realLoggerwith a mock or stub. The singleton is hardcoded into the service. - Global state contamination: If someone changes the
logLevelon the singleton elsewhere in the codebase, it affects all uses ofLogger—includingUserService—leading to unexpected behavior. - Poor readability: A new developer looking at
UserServicehas no clue it depends onLoggeruntil they read the implementation details. This makes the code harder to reason about.
Example 2: Explicit Dependency Injection (No Singleton)
Now let’s rewrite this with clear, explicit dependencies:
// No singleton—just a plain, configurable Logger class public class Logger { private String logLevel = "INFO"; public void log(String message) { System.out.printf("[%s] %s%n", logLevel, message); } public void setLogLevel(String logLevel) { this.logLevel = logLevel; } }
public class UserService { private final Logger logger; // Explicit dependency: The constructor tells you exactly what UserService needs public UserService(Logger logger) { this.logger = logger; } public void createUser(String username) { logger.log("Creating user: " + username); // Rest of user creation logic... } }
The Benefits Here:
- Testing is trivial: I can pass a mock
LoggertoUserServicein tests to verify logs are written without cluttering the console. - Dependencies are obvious at a glance: Anyone using
UserServiceknows they need to provide aLoggerinstance before they can use it. - No global state mess: Each
UserServicecan have its ownLoggerwith unique configuration if needed, avoiding cross-contamination between components.
Why Setter Injection Doesn’t Fix the Singleton Problem
You mentioned setter injection—and while you can add a setter to UserService to inject the singleton, this doesn’t solve the core issues:
- Dependency is still optional: If someone forgets to call the setter,
UserServicewill still fall back to the singleton, leading to hidden, unpredictable behavior. - Readability still suffers: The dependency isn’t declared in the constructor, so it’s still not obvious
UserServiceneeds aLoggerunless you check the setter and implementation code. - Singleton state issues remain: Even if you inject the singleton via setter, it’s still a global instance. Changes to its state elsewhere affect all components using it.
The "Giant Vine" Problem
As the blog noted, singletons tend to spread through codebases like vines. Once one class uses a singleton directly, other classes start copying that pattern. Before you know it, half your codebase is tightly coupled to that singleton, making it impossible to replace or test components in isolation.
With explicit injection, dependencies stay contained—you only pass them to the components that need them, keeping the codebase modular and flexible.
内容的提问来源于stack exchange,提问作者Artem

