延迟初始化 - 如何使其成为一个干净的代码并删除硬依赖?

use*_*794 2 lazy-loading ioc-container inversion-of-control code-cleanup factory-pattern

在"清洁代码:敏捷软件工艺手册"一书的第11章中,Bob叔叔说下面的Lazy-Initialization不是一个干净的代码.它需要两个职责,并且具有很强的依赖性.

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

除了IoC容器和工厂,有没有办法让代码干净并与依赖关系分开?

Ste*_*ven 6

这个例子的结果是它违反了单一责任原则依赖性倒置原则.罗伯特马丁刚刚在这个例子后说:

拥有这两个职责意味着该方法不止一件事,所以我们打破了单一责任原则.

他还通过陈述来触及依赖性倒置原则:

我们现在有一个硬编码的依赖项MyServiceImpl及其构造函数所需的一切.

具有这种硬编码依赖性意味着打破依赖性倒置原则.

解决此问题的方法与使用IoC容器或工厂无关.这里的解决方案是应用依赖注入模式并:

有一个全球一致的策略来解决我们的主要依赖关系.

如果我们应用依赖注入模式,我们的类将变得更简单,就像这样:

public class Consumer
{
    private Service service;

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

    public void SomeMethod() {
        // use service
    }
}
Run Code Online (Sandbox Code Playgroud)

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

上面的例子似乎暗示我们在这里丢失了延迟初始化,但事实并非如此.我们只是将延迟初始化的责任转移到了" 全局,一致的策略 ",即组合根.

既然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();
    }
}
Run Code Online (Sandbox Code Playgroud)

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

在我们的合成根中,我们可以轻松地将所有内容连接在一起

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

Consumer consumer = new Consumer(service);
Run Code Online (Sandbox Code Playgroud)

在这里,我们将应用任何延迟初始化的责任转移到我们的应用程序的启动路径,并且我们保持或Consumer(可能还有许多其他组件)清除了解Service实现作为重量级对象的任何内容.这甚至阻止我们让我们Consumer依赖于第二次ServiceFactory抽象.

这不仅使得这个额外的工厂抽象Consumer变得更加复杂,它在这个特定情况下打破了依赖性倒置原则,因为MyServiceImpl作为重量级对象的事实是一个实现细节,因此我们通过工厂抽象泄漏实现细节.这违反了依赖性倒置原则,该原则指出:

抽象不应该依赖于细节.

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