惰性初始化 - 如何使其成为干净的代码并消除硬依赖?
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 和依赖注入将大大减少使用工厂的需要。
在书 "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 和依赖注入将大大减少使用工厂的需要。