一、为什么静态工具类和Singleton类都不合适连接资源

对于行为会被底层资源以参数化方式影响的类而言,静态工具类和Singleton类都不合适。

1.明确几个概念

(1)什么是“行为被底层资源以参数化方式影响”?

指的是一个类做的事情(行为),不是固定的,而是由某个外部资源决定,而这个资源是通过参数传进来的。

书中例子:一个拼写检查工具依赖词典,每种语言都有自己词典,特殊词汇还要使用特殊词典。这里的 “词典” 就是底层资源,它会影响检查工具的具体行为。

(2)静态工具类(static utility class)

详见条目4,它的特点是无状态、不依赖实例字段、构造器私有,通常被声明为final

public final class MathUtils {
    private MathUtils() {}

    public static int add(int a, int b) {
        return a + b;
    }
}

✅ 适合:真正无状态、行为固定的工具

❌ 不适合:行为随参数变化、依赖外部资源

(3)Singleton(单例)

详见条目3,它的特斯按是全局唯一实例(或线程唯一实例)、生命周期几乎等于JVM

public enum EvlisSingleton{
	INSTANC;
    public void sing(){
        //...
    }
}

✅ 适合:天然只有一个实例、状态稳定

❌ 不适合:行为会因参数不同而不同

2.为什么这两者“都不合适”?

核心原因一句话:参数化行为的类,本质上是“多变的、可配置的”,而静态工具类和单例都是“全局固定的”。

(1)静态工具类的问题

参数固定,满足不了参数变化要求。

public class SpellChecker{
    private static final Lexicon dictionary=new ChineseLexicon(); // 只有一个中文词典 
    
    private SpellChecker(){}                                       // 不可实例化    
   
    public static boolean isValid(String word){                    // 检测方法
        return dictionary.contains(word);
    }
}

如果行为被参数化,那么工具类只能强行“假装自己是无状态的”,通过这种方式保存实例化字段,已经不是工具类了。

public class SpellChecker{
    private static Lexicon dictionary;   
   
    private SpellChecker(){}                        // 不可实例化    
   
    public static void init(Lexicon dictionary){    // 多线程很危险
        SpellChecker.dictionary=dictionary;
    }
    
    public static boolean isValid(String word){
        return dictionary.contains(word);
    }
}

(2)Singleton 的问题

参数固定,满足不了参数变化要求。

public class SpellChecker{
    
    private static final Lexicon dictionary=new ChineseLexicon(); // 只有一个中文词典
    
    public static SpellChecker INSTANCE=new SpellChecker();        // Singleton
    
    private SpellChecker(){}  
    
    public static boolean isValid(String word){
        return dictionary.contains(word);
    }
}

Singleton ≠ 参数化实例。Singleton 的本质是:“不管谁、不管什么时候、不管用什么参数,都只能拿到同一个实例。”

但如果行为是由参数决定的:

SpellChecker s1 = new SpellChecker(ChineseLexicon);
SpellChecker s2 = new SpellCheckerr(EnglishLexicon);

这两个对象理应不同,如果你强行用Singleton,要么覆盖已有配置,要么违反单例。

二、为什么推荐使用依赖注入?

静态工具类和 Singleton 适合 “行为固定、全局一致” 的场景;一旦类的“行为要由外部资源参数化决定”,就说明它应该是普通类,通过构造函数 注入依赖

依赖注入(Dependency Injection,DI)就是:一个对象所需要的“依赖”,不由自己创建,而是由外部“传进来”。 换句话说: “别在自己内部 new,别人给你。” 依赖注入解决了强耦合(自己new耦合度高)、可测试的问题(Mock不能使用)。

(一)依赖注入常见方式

1.构造函数注入(强烈推荐)
private final Lexicon dictionary;
public SpellChecker(Lexicon dictionary){
    this.dictionary=dictionary;
}

具有依赖不可变(final)、线程安全、对象创建完整优点。

2.Setter 注入(可选依赖)
private Lexicon dictionary;
public void setDictionary(Lexicon dictionary){
    this.dictionary=dictionary;
}

依赖是可选的,对象可能处于“半吊子”状态。

3.字段注入(不推荐,常见Spring
@Autowired
private Lexicon dictionary;

隐藏依赖路径,除非用Spring,否则不建议用。

(二)依赖注入传参形式

1.基础版:资源直接传递

在创建新实例时将资源传入构造器。

词典是拼写检查工具的一个依赖项,在创建该工具的实例时将词典注入其中。

public class SpellChecker{
    private final Lexicon dictionary; 

    private SpellChecker(Lexicon dictionary){
        this.dictionary=Object.requireNonNull(dictionary);
    }  
    
    public static boolean isValid(String word){
        return dictionary.contains(word);
    }
}
2.升级版:传递资源工厂

(1)什么是资源工厂?

书中说的“资源工厂”,指的是:一个对象,它的唯一职责就是“生产另一种对象”。这通常表现为:

  • 一个接口
  • 一个 get()/ create()/ make()方法

Java8引入的Supplier<T>接口就非常适合表示工厂。它的定义:

public interface Supplier<T> {
    T get();
}

(2)如何用资源工厂?

我们还以拼写检查工具为例,升级为资源工厂模式。

首先,定义“字典工厂”

// 用 Supplier
Supplier<Dictionary> dictionaryFactory;
// 或自己定义个接口
interface DictionaryFactory {
    Dictionary getDictionary();
}

然后,SpellChecker 接收的是“工厂”

class SpellChecker {

    private final Supplier<Dictionary> dictionaryFactory;

    SpellChecker(Supplier<Dictionary> dictionaryFactory) {
        this.dictionaryFactory = dictionaryFactory;
    }

    public boolean isValid(String word) {
        Dictionary dictionary = dictionaryFactory.get();
        return dictionary.contains(word);
    }
}

最后,测试示例

public static void main(String[] args){
    // 每次创建一个新字典
    SpellChecker checker =new SpellChecker(() -> new EnglishDictionary());
    // 每次返回同一个(相当于单例)
    Dictionary dict = new ChineseDictionary();
    SpellChecker checker = new SpellChecker(() -> dict);
    // 线程级、请求级资源
    SpellChecker checker = new SpellChecker(ThreadLocalDictionary::get);
}

SpellChecker 不再持有具体字典,只要知道可以从工厂拿字典用就行,至于什么字典什么时候创建、如何创建,与他无关。

(3)资源工厂有什么优点?

  • 延迟创建(Lazy Creation),字典只有在真正检查时才创建,当然,用SpellChecker不会不创建字典。
  • SpellChecker 的行为由:“你怎么给我字典”决定,而不是:“你给我哪个字典”。
  • 极高的灵活性,工厂每次new、返回同一个实例、线程级实例等等。
3.高级版:依赖注入框架

Springboot开发过程中,通过注解@Autowired就是框架依赖注入的一种使用形!大型项目包含成百上千个依赖项,如没有Spring容器管理,那将是灾难性的难度。

总结一条设计原则:如果一个类的行为由外部资源“参数化决定”,那么它就应该是一个普通的、可被实例化的类,而不是全局共享的结构。

更多推荐