避免在不适用IOC的传统应用程序中使用服务定位器反模式

12

我经常看到IOC中的服务定位器是一种反模式

去年我们在工作中引入了IOC(具体是Ninject)到我们的应用程序中。这个应用程序是遗留的,非常庞大而且分散。有很多方式可以创建一个类或者一系列的类。一些是由Web框架(自定义的),一些是由nHibernate创建的。许多类只是散布在奇怪的地方。

我们如何处理不同的情况,不采用至少类似于Service Locator的方法,并且不会在不同的位置上得到不同的内核(如单例、HttpRequest和线程等范围很重要)。

编辑 我将为我们目前的SL模式添加更多细节。

我们实际上不希望有多个内核。我们只想要一个(确实因为SL我们只有一个静态的内核)。下面是我们的设置:

1) 我们在7-8个不同的项目/程序集中有Ninject模块。当我们的应用程序(一个Web应用程序)启动时,通过程序集扫描收集这些模块,加载到一个内核中并放置在服务定位器中。所以这一切都相当昂贵。

2) 我们有一个定制的UI框架,它很善于构造。想想大约有120个选项卡形式的表单,每个表单在其生命周期中构造1-10个选项卡页。SL在5-6个地方被策略性地使用,覆盖了所有这些内容,要么作为纯分辨率,要么仅用于属性的实例化后注入。

3) UI下的任何内容都不包含在这些顶级调用范围内,如果这些类想要使用IOC,他们需要想出自己的策略。有各种不同的小框架,它们都是自己的小世界。

所以,根据我所读到的,理想的方法是在需要访问IOC时注入一个核心...好吧,这很好;我们确实尽量少使用SL。

但是,我从哪里获取这个核心?我不想到处构建和注册一个新的核心。似乎我必须从静态上下文或工厂中获取它,以确保我正在使用的核心持有与其他人使用的相同作用域对象,并避免注册所有模块的开销。此时,那个东西似乎很像服务定位器,对吧?

请记住,这个应用程序非常庞大且紧密耦合。我们没有花几个月时间进行重构的奢侈。SL似乎是我们可以随着时间推移慢慢将IOC引入其中的好方法。


2
根据您提供的细节,我只能建议您阅读@Mark Seeman的最佳答案,然后回来说明为什么您觉得需要多个内核(除了重构过程中的中间阶段)以及您拥有各种组合根。目前这个问题太开放了,任何人都无法回答并感到自己做出了有价值的贡献,而且已经涵盖在上述帖子中的内容。 - Ruben Bartelink
1个回答

12
所以从我读过的内容来看,最理想的做法是在需要访问IOC时注入内核...好吧,这很好;我们确实将SL的使用保持在最低限度。
不,将内核注入业务类并不是最佳选择。更好的方法是创建一个工厂,例如IFooFactory { IFoo Create(); }Func<IFoo>,并让它创建新实例。
该接口的实现放在组合根中,获取内核的实例并使用内核进行解析。这样可以使您的类不受特定容器的限制,并且可以在使用不同容器的另一个项目中重用它们。对于Func,您可以使用以下模块:Ninject是否支持Func(自动生成的工厂)? Ninject 2.4将原生支持此功能。
就重构而言,如果不了解您应用程序的源代码,很难告诉您最佳方法。我可以提供一种可能可行的方法。
我想您可能希望在长期内对整个应用程序进行适当的 DI 重构。我曾经为一个相当大的项目(30-40人年)做过以下工作:
从组合根开始,自上而下遍历对象树,并一个接一个地更改类以使用适当的 DI。一旦到达所有叶子节点,请开始重构所有不依赖于其他服务的服务,并使用相同的方法向其叶子节点工作。之后,继续处理仅依赖于已经重构的服务的服务,并重复此过程,直到所有服务都被重构。所有这些步骤都可以依次完成,以便在新功能仍然可以添加的同时持续完善代码。与此同时,只要专注尽快做好,ServiceLocation 就是可以接受的。
伪代码示例:
Foo{ ServiceLocator.Get<Service1>(), new Bar() }
Bar{ ServiceLocator.Get<IService1>(), ServiceLocator.Get<IService2>(), new Baz() }
Baz{ ServiceLocator.Get<IService3>() }
Service1 { ServiceLocator.Get<Service3>() }
Service2 { ServiceLocator.Get<Service3>() }
Service3 { new SomeClass()}

步骤1 - 更改根目录(Foo)

Foo{ ctor(IService1, IBar) }
Bar{ ServiceLocator.Get<IService1>(), ServiceLocator.Get<IService2>(), new Baz() }
Baz{ ServiceLocator.Get<IService3>() }
Service1 { ServiceLocator.Get<IService2>() }
Service2 { ServiceLocator.Get<IService3>() }
Service3 { new SomeClass()}

Bind<IBar>().To<Bar>();
Bind<IService1>().ToMethod(ctx => ServiceLocator.Get<IService1>());

步骤2 - 更改根目录的依赖关系

Foo{ ctor(IService1, IBar) }
Bar{ ctor(IService1, IService2, IBaz) }
Baz{ ServiceLocator.Get<IService3>() }
Service1 { ServiceLocator.Get<Service2>() }
Service2 { ServiceLocator.Get<Service3>() }
Service3 { new SomeClass()}

Bind<IBar>().To<Bar>();
Bind<IBaz>().To<Baz>();
Bind<IService1>().ToMethod(ctx => ServiceLocator.Get<IService1>());
Bind<IService2>().ToMethod(ctx => ServiceLocator.Get<IService2>());

步骤三 - 更改它们的依赖关系并继续,直到到达叶子节点

Foo{ ctor(IService1, IBar) }
Bar{ ctor(IService1, IService2, IBaz) }
Baz{ ctor(IService3) }
Service1 { ServiceLocator.Get<Service2>() }
Service2 { ServiceLocator.Get<Service3>() }
Service3 { new SomeClass() }

Bind<IBar>().To<Bar>();
Bind<IBaz>().To<Baz>();
Bind<IService1>().ToMethod(ctx => ServiceLocator.Get<IService1>());
Bind<IService2>().ToMethod(ctx => ServiceLocator.Get<IService2>());
Bind<IService3>().ToMethod(ctx => ServiceLocator.Get<IService3>());

第四步 - 重构不依赖于其他服务的服务

Foo{ ctor(IService1, IBar) }
Bar{ ctor(IService1, IService2, IBaz) }
Baz{ ctor(IService3) }
Service1 { ServiceLocator.Get<Service2>() }
Service2 { ServiceLocator.Get<Service3>() }
Service3 { ctor(ISomeClass) }

Bind<IBar>().To<Bar>();
Bind<IBaz>().To<Baz>();
Bind<ISomeClass>().To<SomeClass>();
Bind<IService1>().ToMethod(ctx => ServiceLocator.Get<IService1>());
Bind<IService2>().ToMethod(ctx => ServiceLocator.Get<IService2>());
Bind<IService3>().To<Service3>().InSingletonScope();

步骤5 - 接下来重构那些依赖于仅重构服务作为依赖项的服务。

Foo{ ctor(IService1, IBar) }
Bar{ ctor(IService1, IService2, IBaz) }
Baz{ ctor(IService3) }
Service1 { ServiceLocator.Get<Service2>() }
Service2 { ctor(IService3) }
Service3 { ctor(ISomeClass) }

Bind<IBar>().To<Bar>();
Bind<IBaz>().To<Baz>();
Bind<ISomeClass>().To<SomeClass>();
Bind<IService1>().ToMethod(ctx => ServiceLocator.Get<IService1>());
Bind<IService2>().To<Service2>().InSingletonScope();
Bind<IService3>().To<Service3>().InSingletonScope();

第六步 - 重复操作,直到每个服务都被重构。

Foo{ ctor(IService1, IBar) }
Bar{ ctor(IService1, IService2, IBaz) }
Baz{ ctor(IService3) }
Service1 { ctor(IService2) }
Service2 { ctor(IService3) }
Service3 { ctor(ISomeClass) }

Bind<IBar>().To<Bar>();
Bind<IBaz>().To<Baz>();
Bind<ISomeClass>().To<SomeClass>();
Bind<IService1>().To<Service1>().InSingletonScope();
Bind<IService2>().To<Service2>().InSingletonScope();
Bind<IService3>().To<Service3>().InSingletonScope();

也许您想与重构一起切换到基于约定的容器配置。在这种情况下,我会向所有重构类添加一个属性来标记它们,并在完成所有重构后再将其删除。在约定中,您可以使用此属性过滤所有已重构的类。


谢谢,实际上这与我们正在进行的工作并不相距太远。我们一段时间前改变了我们的服务定位器,内部使用一个包含内核的工厂(具有接口),这在一些测试场景中非常有帮助,因为我们可以替换不同的模拟对象。直接与该工厂一起工作只需要一点点努力,而不是使用静态的服务定位器调用。 - ryber

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接