惰性初始化 - 如何使其成为干净的代码并消除硬依赖?

Lazy initialization - How to make it a clean code and remove hard dependency?

在书 "Clean Code: A Handbook of Agile Software Craftsmanship" 的第 11 章中,Bob 大叔说以下延迟初始化不是干净的代码。它承担两个职责,并且具有硬依赖性。

public Service getService() {
    if (service == null)
       service = new MyServiceImpl(...); // Good enough default for most cases?
    return service;
}

除了 IoC Container 和 Factory 之外,还有什么方法可以使代码干净并与依赖项分离?

这个例子的问题在于它违反了 Single Responsibility Principle and the Dependency Inversion Principle。 Robert Martin 已经在示例之后声明:

Having both of these responsibilities means that the method is doing more than one thing, so we are breaking the Single Responsibility Principle.

他还谈到了依赖倒置原则:

we now have a hard-coded dependency on MyServiceImpl and everything its constructor requires.

拥有这种硬编码依赖意味着打破 依赖倒置原则。

这个问题的解决方案不是使用 IoC 容器或工厂。这里的解决方案是应用依赖注入模式并:

have a global, consistent strategy for resolving our major dependencies.

如果我们应用 Dependency Injection pattern,我们的 class 将变得更简单,就像这样:

public class Consumer
{
    private Service service;

    public Consumer(Service service) {
        this.service = service;
    }

    public void SomeMethod() {
        // use service
    }
}

请注意,Consumer 现在不再通过其 public 方法公开 Service。这不需要,因为模块不应该共享其内部状态,如果其他组件需要使用我们的 Service,我们可以直接将其注入到其他组件中。

上面的例子似乎暗示我们在这里丢失了惰性初始化,但事实并非如此。我们只是将惰性初始化的责任转移到了“全局一致策略”,a.k.a。 Composition Root.

由于 Service 是一个抽象,我们可以创建一个代理来为我们的 MyServiceImpl 实现延迟初始化(延迟初始化将是它的单一职责)。这样的代理可以如下所示:

internal class LazyServiceProxy : Service
{
    // Here we make use of .NET's Lazy<T>. If your platform doesn't
    // have this, such type is easily created.
    private Lazy<Service> lazyService;

    public LazyServiceProxy(Lazy<Service> lazyService) {
        this.lazyService = lazyService;
    }

    public void ServiceMethod() {
        // Lazy initialization happens here.
        Service service = this.lazyService.Value;
        service.ServiceMethod();
    }
}

这里我们创建了一个LazyServiceProxy,它的唯一目的是推迟真正服务的创建。它甚至不需要“MyServiceImpl的硬编码依赖及其构造函数所需的一切”。

在我们的组合根中,我们可以轻松地将所有内容连接在一起,如下所示:

Service service = new LazyServiceProxy(
    new Lazy<Service>(() => new MyServiceImpl(...)));

Consumer consumer = new Consumer(service);

在这里,我们将应用任何惰性初始化的责任转移到我们应用程序的启动路径,并且我们保持 Consumer(可能还有许多其他组件)对 Service 实现是重量级对象。这甚至阻止我们让 Consumer 依赖于第二个 ServiceFactory 抽象。

不仅使这个额外的工厂抽象 Consumer 更加复杂,而且在这种特定情况下打破了依赖倒置原则,因为 MyServiceImpl 是一个重量级对象,是一个 实现细节 因此我们通过工厂抽象泄露了实现细节。这违反了依赖倒置原则,该原则规定:

Abstractions should not depend on details.

如您所见,此解决方案不需要 IoC 容器(尽管您仍然可以根据需要使用它)并且不需要工厂。虽然工厂设计模式在应用依赖注入时仍然有效,但您会看到正确应用 SOLID 和依赖注入将大大减少使用工厂的需要。