You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

单例模式如何隐藏依赖?附代码示例解析

Why Singletons Hide Dependencies (And Why Setter Injection Isn't a Fix)

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 real Logger with a mock or stub. The singleton is hardcoded into the service.
  • Global state contamination: If someone changes the logLevel on the singleton elsewhere in the codebase, it affects all uses of Logger—including UserService—leading to unexpected behavior.
  • Poor readability: A new developer looking at UserService has no clue it depends on Logger until 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 Logger to UserService in tests to verify logs are written without cluttering the console.
  • Dependencies are obvious at a glance: Anyone using UserService knows they need to provide a Logger instance before they can use it.
  • No global state mess: Each UserService can have its own Logger with 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:

  1. Dependency is still optional: If someone forgets to call the setter, UserService will still fall back to the singleton, leading to hidden, unpredictable behavior.
  2. Readability still suffers: The dependency isn’t declared in the constructor, so it’s still not obvious UserService needs a Logger unless you check the setter and implementation code.
  3. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 03:36:37