如何解决对可测试控制器的依赖关系?
工作原理:URI被路由到Controller,Controller可能具有依赖关系来执行特定任务。
<?php
require 'vendor/autoload.php';
/*
* Registry
* Singleton
* Tight coupling
* Testable?
*/
$request = new Example\Http\Request();
Example\Dependency\Registry::getInstance()->set('request', $request);
$controller = new Example\Controller\RegistryController();
$controller->indexAction();
/*
* Service Locator
*
* Testable? Hard!
*
*/
$request = new Example\Http\Request();
$serviceLocator = new Example\Dependency\ServiceLocator();
$serviceLocator->set('request', $request);
$controller = new Example\Controller\ServiceLocatorController($serviceLocator);
$controller->indexAction();
/*
* Poor Man
*
* Testable? Yes!
* Pain in the ass to create with many dependencies, and how do we know specifically what dependencies a controller needs
* during creation?
* A solution is the Factory, but you would still need to manually add every dependencies a specific controller needs
* etc.
*
*/
$request = new Example\Http\Request();
$controller = new Example\Controller\PoorManController($request);
$controller->indexAction();
这是我对设计模式示例的解释
注册表:
服务定位器
可怜的人
登记处
<?php
namespace Example\Dependency;
class Registry
{
protected $items;
public static function getInstance()
{
static $instance = null;
if (null === $instance) {
$instance = new static();
}
return $instance;
}
public function set($name, $item)
{
$this->items[$name] = $item;
}
public function get($name)
{
return $this->items[$name];
}
}
服务定位器
<?php
namespace Example\Dependency;
class ServiceLocator
{
protected $items;
public function set($name, $item)
{
$this->items[$name] = $item;
}
public function get($name)
{
return $this->items[$name];
}
}
如何解决对可测试控制器的依赖关系?
您在控制器中谈论的依赖关系是什么?
到主要解决方案是:
我将尝试分别详细描述这两种方法。
注意:所有示例都将省略与视图的交互,授权的处理,处理服务工厂的依赖关系以及其他细节
引导阶段的简化部分,涉及启动控制器的工作,看起来像这样
$request = //... we do something to initialize and route this
$resource = $request->getParameter('controller');
$command = $request->getMethod() . $request->getParameter('action');
$factory = new ServiceFactory;
if ( class_exists( $resource ) ) {
$controller = new $resource( $factory );
$controller->{$command}( $request );
} else {
// do something, because requesting non-existing thing
}
该方法提供了一种简单的方法,只需通过传入不同的工厂作为依赖项,即可扩展和/或替换与模型层相关的代码。在控制器中,它看起来像这样:
public function __construct( $factory )
{
$this->serviceFactory = $factory;
}
public function postLogin( $request )
{
$authentication = $this->serviceFactory->create( 'Authentication' );
$authentication->login(
$request->getParameter('username'),
$request->getParameter('password')
);
}
这意味着,要测试该控制器的方法,您将必须编写一个单元测试,该单元测试可以模拟的内容$this->serviceFactory
,创建的实例以及传入的值$request
。所述模拟将需要返回一个实例,该实例可以接受两个参数。
注意:对用户的响应应完全由视图实例处理,因为创建响应是UI逻辑的一部分。请记住,HTTP Location标头也是一种响应形式。
此类控制器的单元测试如下所示:
public function test_if_Posting_of_Login_Works()
{
// setting up mocks for the seam
$service = $this->getMock( 'Services\Authentication', ['login']);
$service->expects( $this->once() )
->method( 'login' )
->with( $this->equalTo('foo'),
$this->equalTo('bar') );
$factory = $this->getMock( 'ServiceFactory', ['create']);
$factory->expects( $this->once() )
->method( 'create' )
->with( $this->equalTo('Authentication'))
->will( $this->returnValue( $service ) );
$request = $this->getMock( 'Request', ['getParameter']);
$request->expects( $this->exactly(2) )
->method( 'getParameter' )
->will( $this->onConsecutiveCalls( 'foo', 'bar' ) );
// test itself
$instance = new SomeController( $factory );
$instance->postLogin( $request );
// done
}
控制器应该是应用程序中最薄的部分。控制器的职责是:接受用户输入,并基于该输入更改模型层的状态(在极少数情况下为当前视图)。而已。
这另一种方法是..好..基本上,这是一种复杂性的交易(在一处减去,在另一处添加更多)。它还会继续使用真正的DI容器,而不是像Pimple这样的美化服务定位器。
我的建议:查看Auryn。
DI容器的作用是,使用配置文件或反射,它确定要创建的实例的依赖关系。收集所说的依赖关系。并传入实例的构造函数。
$request = //... we do something to initialize and route this
$resource = $request->getParameter('controller');
$command = $request->getMethod() . $request->getParameter('action');
$container = new DIContainer;
try {
$controller = $container->create( $resource );
$controller->{$command}( $request );
} catch ( FubarException $e ) {
// do something, because requesting non-existing thing
}
因此,除了引发异常的能力外,控制器的引导过程几乎保持不变。
同样,在这一点上您应该已经意识到,从一种方法切换到另一种方法通常需要完全重写控制器(以及相关的单元测试)。
在这种情况下,控制器的方法如下所示:
private $authenticationService;
#IMPORTANT: if you are using reflection-based DI container,
#then the type-hinting would be MANDATORY
public function __construct( Service\Authentication $authenticationService )
{
$this->authenticationService = $authenticationService;
}
public function postLogin( $request )
{
$this->authenticatioService->login(
$request->getParameter('username'),
$request->getParameter('password')
);
}
至于编写测试,在这种情况下,您要做的就是再次提供一些隔离模拟并简单地进行验证。但是,在这种情况下,单元测试更加简单:
public function test_if_Posting_of_Login_Works()
{
// setting up mocks for the seam
$service = $this->getMock( 'Services\Authentication', ['login']);
$service->expects( $this->once() )
->method( 'login' )
->with( $this->equalTo('foo'),
$this->equalTo('bar') );
$request = $this->getMock( 'Request', ['getParameter']);
$request->expects( $this->exactly(2) )
->method( 'getParameter' )
->will( $this->onConsecutiveCalls( 'foo', 'bar' ) );
// test itself
$instance = new SomeController( $service );
$instance->postLogin( $request );
// done
}
如您所见,在这种情况下,您可以少模拟一个类。
耦合到名称(在示例中为“身份验证”):
您可能已经注意到,在两个示例中,您的代码都将与所使用的服务名称耦合在一起。即使使用基于配置的DI容器(如symfony中可能的那样),您仍然最终将定义特定类的名称。
DI容器不是魔术:
在过去的几年中,DI容器的使用已经大肆宣传。这不是灵丹妙药。我什至可以说:DI容器与SOLID不兼容。特别是因为它们不适用于接口。您不能在代码中真正使用多态行为,该行为将由DI容器初始化。
然后是基于配置的DI的问题。好吧..它只是项目很小的时候的美丽。但是随着项目的增长,配置文件也会增长。您可以得到光荣的xml / yaml配置WALL,项目中只有一个人可以理解。
第三个问题是复杂性。好的DI容器并不容易制造。而且,如果您使用第三方工具,则会带来其他风险。
依赖性过多:
如果您的类具有过多的依赖关系,那么按照惯例,这并不是DI的失败。相反,这是一个明确的指示,表明您的课程在做太多事情。这违反了单一责任原则。
控制器实际上具有(某些)逻辑:
上面使用的示例非常简单,并且可以通过单个服务与模型层进行交互。在现实世界中,您的控制器方法将包含控制结构(循环,条件,填充)。
最基本的用例是使用“主题”下拉列表处理联系表单的控制器。大多数消息将被定向到与某些CRM进行通信的服务。但是,如果用户选择“报告错误”,则该消息应传递到差异服务,该服务将在错误跟踪器中自动创建票证并发送一些通知。
这是PHP单元:
单元测试的示例是使用PHPUnit框架编写的。如果您使用其他框架或手动编写测试,则必须进行一些基本更改
您将进行更多测试:
单元测试示例不是控制器方法要进行的全部测试。特别是当您拥有不平凡的控制器时。
有一些.. emm ...切线主题。
Brace for: shameless self-promotion
一些框架具有在控制器中推送授权检查的讨厌习惯(不要与“ authentication” ..不同的主题混淆)。除了要做完全愚蠢的事情之外,它还引入了控制器中的其他依赖项(通常-全局范围)。
还有另一篇文章使用类似的方法介绍了非侵入式测井
它是针对想要学习MVC的人的,但是实际上那里有面向OOP和开发实践的通识教育材料。这个想法是,当您完成该列表时,MVC和其他SoC实现将只会使您走到“哦,这有个名字吗?我认为这只是常识。”
解释上面描述中的那些神奇的“服务”。
本文收集自互联网,转载请注明来源。
如有侵权,请联系[email protected] 删除。
我来说两句