开放的编程资料库

当前位置:我爱分享网 > Python教程 > 正文

关于弃用 ServiceLocatorAware

一两个月前,我们推出了一个新版本的zend-mvc,它提供了许多向前兼容的功能,以帮助用户为即将发布的v3版本准备他们的应用程序。

其中一个显然颇具争议:在v3中,zend-servicemanager不再定义ServiceLocatorAwareInterface,当您尝试将服务定位器注入应用程序时,这个特定版本的zend-mvc会引发弃用通知服务,或在您的控制器中拉取服务定位器。

争论是这样的:

  • “依赖注入太难理解了!”
  • “这个特性简化了开发!”
  • “如果这很糟糕,为什么它会出现在第一名?”

这些人通常会跟在后面:

  • 说他们会更换框架(好吧,我猜?);
  • 要求恢复该功能(嗯,否);
  • 要求删除弃用通知(为什么?这样您就可以延迟升级直到您要求恢复该功能时的痛苦?);或
  • 询问变更的理由。

所以,我决定做最后一个,证明更改的合理性,它解决了我们不做中间两个的原因,并解决了为什么关于ServiceLocatorAware的有用性的假设和断言是主要是被误导了。

最初发布在其他地方

这最初是作为对问题的评论发布的。我决定将它发布到我的博客上以吸引更多的观众,并提供更多的背景和细节。

最初发布在其他地方

这最初是作为对问题的评论发布的。我决定将它发布到我的博客上以吸引更多的观众,并提供更多的背景和细节。

zend-servicemanager的目的是用作控制反转容器。

它从来没有打算作为通用服务定位器(有趣的是,链接细节主要是模式的缺点!);这个角色是本着“快速应用程序开发”和“简化初始开发”的精神强加给它的,但其意图是,一旦类稳定下来,就应该重构以注入依赖项。(而且我们都知道忙碌的开发人员会发生什么:重构被推迟或永远不会发生。)

为什么不应该注入服务定位器?

谷歌“服务定位器反模式”以了解为什么不应该使用它。要点归结为:

  • 依赖隐藏。
  • 间接错误。
  • 类型安全。
  • 脆弱性。

让我们逐一分析。

依赖隐藏

“依赖隐藏”是什么意思?

看看这个类签名:

class Foo implements DispatchableInterface, ServiceLocatorAwareInterface
{
    /* Defined by DispatchableInterface */
    public function dispatch(Request $request, Response $response);

    /* Defined by ServiceLocatorAwareInterface */
    public function setServiceLocator(ServiceLocatorInterface $serviceLocator);
    public function getServiceLocator();
}

基于此,您会期望:

  • 您可以在没有依赖项的情况下实例化对象。
  • 如果您觉得有必要,您可以将服务定位器传递给实例。
  • 您应该能够通过向它传递一个请求和响应实例来执行dispatch(),它应该会成功返回。

服务定位器是模糊的;它的用途不明确,而且显然不是必需的依赖项,因为它在setter方法中。

因此,您为dispatch()方法编写了一个测试,您得到了一个ServiceNotFoundException。怎么了?

您深入了解dispatch()方法的代码:

public function dispatch(Request $request, Response $response)
{
    $authentication = $this->serviceLocator->get('authentication');

    if (! $authentication->hasIdentity()) {
        $response->setStatus(401);
        return $response;
    }

    $identity = $authentication->getIdentity();
    $response->setBody(
        $this->serviceLocator->get('renderer')->render(
            'foo',
            ['identity' => $identity]
        )
    );
    return $response;
}

ServiceNotFoundException可能被抛出的位置有两个:在方法的第一行,或在setBody()调用中。在这两种情况下,您都面临着一个难题:

  • 您现在知道服务定位器是必需的。从最初看班级来看,这并不明显;它似乎是一个可选依赖项。
  • 您不知道每个authenticationrenderer服务。

后者尤其令人不安。您现在必须了解应用程序中可能定义服务的所有不同位置,并开始搜索这些位置。很有可能,您会发现这些服务名称实际上可能是别名,这意味着您将确定它的别名是什么,但随后必须重新开始搜索以确定实际服务是什么。

简而言之,这就是依赖隐藏。类操作的要求隐藏在代码中,类型不一定能被推断。

(当然,您可以在get()调用上方添加注释以详细说明类型。但这是一个创可贴;您仍然需要查看代码本身以确定要求是什么。)

依赖隐藏的一个副作用是它使测试变得更加困难。我认为上面的例子说明了这一点;您不能仅查看签名来了解行为和要求,而是需要深入研究代码。此外,测试设置变得更加困难和脆弱,因为您现在需要添加对容器的依赖、填充容器,并希望您没有遗漏任何东西。稍后我会详细讲到这一点。重点是:任何使测试变得更加困难的事情都意味着开发人员将避免测试,从而降低代码质量。

让我们再分解一下:

  • 您需要一个特定的对象实例。
  • 您现在耦合到服务定位器以检索实例。
  • 您通过字符串名称,可以是任何内容,不一定表示目的或其功能
  • 检索可能会引发与正在使用的组件无关的异常,您需要在代码中或稍后调试时考虑到这一点。

您真正想要的只是对象实例。为什么不直接在构造函数中注入该实例?将需求定义为构造函数参数使它们显式化,并确保只看API的人理解操作所需的内容。

tl;dr:您想要的是您正在消耗的依赖性,而不是获得它所需的三个间接步骤。制作所需的所有依赖项,并将它们注入构造函数。

错误间接寻址

再次使用上面的示例,让我们检查一下我们得到了一个ServiceNotFoundException的事实。这发生在运行时。本质上,引导、路由、实例化控制器和预调度监听器的工作已经运行,只有在我们到达实际请求的逻辑时才会失败,因为缺少依赖项

在典型的PHP应用程序工作流中,这与直接注入依赖项没有太大区别。但是如果你考虑在像React这样的系统中使用,应用程序的引导可以发生一次,并且分派会一遍又一遍地发生,这是很有问题的;这根本不是运行时异常,由于配置错误。这很难追踪,而且不是您希望在生产中发生的事情。

类型安全

再次回到最初的例子:我们不知道期望的类型是什么,我们也不能保证我们从容器中提取的内容是正确的。

没有经验的开发人员,或者不熟悉容器中给定实例的所有用例的开发人员,可能会将服务映射到意想不到的类。直到运行时,在生产中,您才会知道这已经发生,当您突然出现“方法不存在”的致命PHP错误。这些很难追踪,因为您不知道类型是什么、期望的是什么,也不知道实例最初是在哪里定义的。调试器将采取几个步骤来确定这是由于容器配置错误造成的。

将其与构造函数中声明的依赖项进行比较:

class Foo implements DispatchableInterface
{
    public function __construct(
        AuthenticationService $authentication,
        RendererInterface $renderer
    );

    /* Defined by DispatchableInterface */
    public function dispatch(Request $request, Response $response);
}

您仍然会遇到致命错误,但您会知道该类从一开始就使用无效参数进行实例化,并且知道您需要检查您的映射和/或工厂。此类问题通常可以通过静态分析工具找到,为您提供了另一种轻松提高代码质量的方法。

另一方面,您的IDE现在也可以帮助您了解可用的方法。因为该属性被注入到构造函数中,所以当您在代码中访问它时,大多数IDE内置的静态分析(我再次使用这个词!)将能够推断出类型,并为您提供类型提示。对于服务定位器而言,这并非普遍适用(我知道PHPStorm在这方面取得了一些进展,但我也知道这是一项非常困难的任务,而且容易出错)。

脆性

依赖服务定位器会给您的设计带来脆弱性。

每次添加对get()的调用时,都会引入新的依赖项。这通常会破坏测试:

  • 如果您正在模拟服务定位器,您现在可以在测试过程中对其方法进行额外的调用,从而使模拟失败其断言。
  • 如果您是使用具体的定位器实例,并且预期存在一个实例,您现在会在测试执行过程中引发异常。

这种脆弱性导致开发人员不想进行测试,从而使代码更加脆弱,并且将来更有可能以意想不到的方式中断。任何使测试变得更加困难的做法都应该重新考虑。

此外,它会导致未记录的要求,使消费者不太清楚需要提供哪些服务才能使代码正常工作。当您跨团队工作时,这一点至关重要。

使用服务定位器的另一个方面是,您的班级很容易成长为承担太多职责。让我解释一下。

一个经常用于支持使用服务定位器的论据是促进可选依赖关系:仅在特定代码执行路径期间使用的依赖关系。如果依赖性特别重(Web服务、数据库访问等),则认为只有在它们即将被使用时才将它们从容器中拉出才有意义。

有两种方法可以解决这个问题:

  • zend-servicemanager(和其他几个IoC实现)已经提供了惰性服务,它通过创建一个代理类来解决这个问题,该代理类包装用于检索服务的工厂。您可以像与原始实例一样与它交互,但“大量实例化”会延迟到第一次使用时。
  • 将您的关注点分成多个类!无论如何,这是更好的解决方案;如果您知道某些依赖项仅存在于某些代码路径中,请为该路径创建一个新控制器,并专门路由到它。例如,如果您知道数据库访问只会发生在(a)向服务发出POST请求时,以及(b)发生验证时,则:
    • 创建一个专门映射到给定路径的POST请求的控制器,
    • 可选地,将数据库连接包装为惰性服务。不过,如果您将请求路由到该特定控制器,那么在性能方面准备好数据库访问可能是可以接受的。

tl;dr:对服务定位器的依赖会导致脆弱的设计和范围蔓延。当您关注依赖关系时,您最终会将关注点拆分为多个类,从而使它们更易于测试和维护。

个有效的用例

服务定位器有一些有效的用例。当你有很多相关的实例,并且在运行时拉取它们将基于输入时,服务定位器是理想的。此场景包括以下内容:

  • 插件和辅助系统
  • 策略模式
  • 路由系统

然而,在这些情况下,我们不处理一般的应用程序依赖性;我们正在处理特定的上下文,并且实例在该上下文中起作用。

在很多情况下,甚至这些都可以直接注入。如果您知道您的代码路径包含特定的插件或助手,您也可以注入它们。(我们已经使用多个Apigility控制器完成了此操作,因为它简化了测试!)

要点

作为获取依赖项的通用方式,服务定位器充其量是一种反模式,会导致质量下降和脆弱的架构。

我们引入了ServiceLocatorAwareInterface是由于来自需要“快速应用程序开发”功能并且不清楚依赖项注入的好处的用户的压力。当时,这似乎是个好主意;我们倾听并回应我们的用户。

然而,事后看来,我认为我们这样做是错误的,最终损害了我们的用户;对于那些依赖该模式的人来说,该实现助长了坏习惯并降低了代码质量。希望上面的讨论能更清楚地说明为什么我们最终决定删除它,以及我们认为删除将如何帮助您改进代码。

未经允许不得转载:我爱分享网 » 关于弃用 ServiceLocatorAware

感觉很棒!可以赞赏支持我哟~

赞(0) 打赏