在软件设计领域,服务定位器模式常常被误解为一种反模式。本文旨在澄清这种误解,并探讨服务定位器模式在何种情况下是合理的设计选择。
首先,让明确一点:服务定位器模式并不是用于依赖管理的最佳实践。然而,这并不意味着它在所有情况下都是不可取的。
许多博客文章指出服务定位器模式隐藏了类的依赖关系,导致运行时错误而不是编译时错误,并且使代码更难维护,因为引入破坏性变更时变得不清晰。
例如,以下是一个常见的示例:
public class SomeService {
public void DoSomething() {
ServiceLocator.Resolve<ISomeRepository>().Save("kdd");
}
}
完全同意,服务定位器在这种情况下并不适用。强烈不推荐滥用定位器。应该始终通过构造函数注入所需的依赖/信息。
一个更好的解决方案是:
public class SomeService {
ISomeRepository _repos;
public SomeService(ISomeRepository repos) {
if (repos == null) throw new ArgumentNullException("repos");
_repos = repos;
}
public void DoSomething() {
_repos.Save("kdd");
}
}
已经确定了服务定位器模式不适用的使用案例。但这是否意味着它是一种反模式呢?当然不是。让来探讨一下它在何时是完全有效的。
首先,让看看服务定位器的定义(来自维基百科):
服务定位器模式是一种在软件开发中使用的模式,用于封装获取服务的过程,并通过强大的抽象层。这种模式使用一个称为“服务定位器”的中央注册表,根据请求返回执行特定任务所需的信息。
这意味着服务定位器基本上是抽象了请求类型和某些东西的实现之间的映射。也就是说,当请求一个服务时,不需要关心实际的实现。
这听起来是不是很像控制反转容器?是的,因为IoC容器无非就是一个配置了所有注册后的服务定位器,具有生命周期管理功能。
让以SimpleInjector文档中的一个示例为例:
public partial class User : BasePage {
private readonly IUserRepository repository;
private readonly ILogger logger;
public User() {
// 5. Retrieve instances from the container (resolve)
this.repository = Global.Container.GetInstance<IUserRepository>();
this.logger = Global.Container.GetInstance<ILogger>();
}
protected void Page_Load(object sender, EventArgs e) {
// Use repository and logger here.
}
}
他确实警告说不要将容器用作服务定位器。但重点是,任何容器都可以用作服务定位器。这是为什么呢?因为这是允许其他人利用容器管理的所有服务的最简单方式。
所以,当使用任何具有IoC支持的框架(如ASP.NET MVC)时,可以安全地假设它们使用了最喜欢的容器的服务定位功能。
意思是,可以滥用任何模式,但这并不意味着它是一种反模式。Singleton和Service Locator之所以名声不好,是因为它们易于理解、实现和使用。
问题是,实现者/用户没有完全理解这些模式试图解决的问题。