【问题标题】:Converting from service-locator to dependency injection从服务定位器转换为依赖注入
【发布时间】:2015-09-25 16:36:40
【问题描述】:

我正在清理一些代码。我决定这样做是因为很可能在不久的将来会请求新功能并且代码很难理解(不仅因为它使用依赖容器作为服务定位器)。

在一定程度上,我理解为什么 SL 对依赖管理很糟糕,甚至在我知道 OOP 是什么之前,我就已经在操作的签名中声明了方法的所有依赖项。

使用 AngularJS 等框架时,您可以免费获得依赖注入。我当前的项目不是这种情况。我能想到的“正确方式”实例化应用程序的唯一方法是通过服务定位 main 方法中的所有内容。请为我指明正确的方向,实现更优雅的依赖注入。

这是我目前的做法:

function main()
    container = new Dic
    container->set(A, lazyNew(foo/bar/A)
    container->set(B, lazyNew(A), depends-on:[A])
    container->set(App, lazyNew(App), depends-on:[A, B])
    // more configuration code
    app = container->get(App)
    app.main()

这里的缺陷是我仍在使用容器作为服务定位器。唯一的好处是依赖关系图是“自动解决的”(繁琐的配置和每个实例的依赖关系声明)。另一个优点是实例化发生在一个地方。请帮助我了解如何将其提升到新的水平。

【问题讨论】:

    标签: design-patterns dependency-injection


    【解决方案1】:

    由于您仅在 main 方法中使用容器,因此您将容器用作服务定位器。

    仅当您在类(而非主类)中使用容器时,才会将其视为服务位置。

    实际上,在进行依赖注入时,我们必须在 main 方法(或其他类型应用程序的其他入口点)中创建对象图。这个地方叫做Composition Root

    问题:您是否在类中使用构造函数注入?我假设你会这样做。

    在我看来,更优雅的 DI(或 Composition Root)实现是不使用 DI 容器,而是使用 Pure DI。请参阅我的文章here 了解原因。

    更新

    这是一个如何使用构造函数注入的示例。

    我会使用 C# 语言。

    public interface IDependencyA {}
    public interface IDependencyB {}
    public interface IDependencyC {}
    public interface IBackEnd {}
    
    public class DependencyC : IDependencyC {}
    public class DependencyA : IDependencyA {}
    
    public class DependencyB : IDependencyB
    {
        private readonly IBackEnd m_BackEnd;
        public DependencyB(IBackEnd back_end)
        {
            m_BackEnd = back_end;
        }
    }
    
    public class BackEnd : IBackEnd
    {
        private readonly IDependencyC m_DependencyC;
        public BackEnd(IDependencyC dependency_c)
        {
            m_DependencyC = dependency_c;
        }
    }
    
    public class App
    {
        private readonly IDependencyA m_DependencyA;
        private readonly IDependencyB m_DependencyB;
        public App(IDependencyA dependency_a, IDependencyB dependency_b)
        {
            m_DependencyA = dependency_a;
            m_DependencyB = dependency_b;
        }
    }
    

    如何使用 Unity 容器构建对象图:

    UnityContainer container = new UnityContainer();
    
    container.RegisterType<IDependencyA, DependencyA>();
    container.RegisterType<IDependencyB, DependencyB>();
    container.RegisterType<IDependencyC, DependencyC>();
    container.RegisterType<IBackEnd, BackEnd>();
    
    App application = container.Resolve<App>();
    

    以下是使用 Pure DI 创建对象图的方法:

    App application = new App(
        new DependencyA(),
        new DependencyB(
            new BackEnd(
                new DependencyC())));
    

    【讨论】:

    • 我不同意第一句话,因为即使我使用容器在一个地方定位“App”,它仍然在那个地方用作服务定位器。除了 App 的预配置依赖项外,没有注入任何内容。如果我要向这个项目添加一个 Backend(dep, dep, dep, ...),我需要从容器中获取(定位服务)它。我不明白如何概括将依赖项注入构造函数的过程。谢谢你的链接。
    • 您不应该从容器中获取BackEnd,而只能从您的情况中获取App 的根类。您在容器中注册BackEnd,然后在对象图中的某个位置,某些类通过构造函数注入依赖于BackEnd,然后容器会将BackEnd 注入对象图中的适当位置。
    【解决方案2】:

    一般而言,您需要设备某种基础设施来声明依赖关系并抽象声明的服务或模块的实例化。

    鉴于问题中的 Angular 示例,如果您退后一步思考一下,一切都是通过 Angular 中的 angular.module 函数发生的。

    直观地说,这必须是如何在底层依赖注入容器中声明和注册“应用程序”以及应用程序的较小组件(指令、服务、控制器)。毕竟,所有这些都将一组依赖项作为第二个参数:

    var injectable3, injectable2, injectable;
    
    injectable  =    angular.module(    'myApp', [dependency, ..., fn(d1, ...){}]);
    injectable2 = injectable.controller('foo',   [dependency, ..., fn(d2, ...){}]);
    injectable3 = injectable.service(   'bar',   [dependency, ..., fn(d3, ...){}]);
    

    看起来很神奇,但实际上并非如此。他们都通过某种依赖注入容器进行对话。这实际上是在cmet中说明的:loader.js

    检查angular中的一些核心方法:

    /**
     * @name angular.bootstrap
     * @returns {auto.$injector} Returns the newly created injector for app.
     */
    function bootstrap(element, modules, config) {
        // (...)
        var doBootstrap = function(element, modules, config) {
            modules = modules || [];
            modules.unshift(['$provide', function ($provide) {
                $provide.value('$rootElement', element);
            }]);
            if (config.debugInfoEnabled) {
                modules.push(['$compileProvider', function ($compileProvider) {
                    $compileProvider.debugInfoEnabled(true);
                }]);
            }
            // (...)
            modules.unshift('ng');
            var injector = createInjector(modules, config.strictDi);
            injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector',
                    function bootstrapApply(scope, element, compile, injector) {
                        scope.$apply(function () {
                            element.data('$injector', injector);
                            compile(element)(scope);
                        });
                    }]
            );
            return injector;
        };
        // (...)
        if (window && !NG_DEFER_BOOTSTRAP.test(window.name)) {
            return doBootstrap();
        }
        // (...)
    }
    

    如果您不记得,引导方法负责初始化应用程序,例如&lt;body ng-app="coffeeShop"&gt;。上面的摘录说明了为每个应用程序声明了额外的依赖项。下一步 (createInjector) 是找出依赖项的创建位置和方式。

    总而言之,一种方法是声明提供者,使用定位器注册它们,然后声明依赖于提供者的模块,最后在稍后阶段引导整个事情。

    我想这个星期天早上我已经吃饱了。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2013-01-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-08-29
      • 2013-06-28
      相关资源
      最近更新 更多