我一直在阅读/观看许多推荐材料,最近的一篇是针对高级PHP开发人员的MVC。出现的一件事是,Singleton很糟糕,它们在类之间创建依赖关系,而依赖注入很好,因为它允许单元测试和解耦。
在我写程序之前,一切都很好。让我们以eshop中的产品页面为例。首先,我有我的页面:
class Page {
public $html;
public function __construct() {
}
public function createPage() {
// do something to generate the page
}
public function showPage() {
echo $this->html;
}
}
到目前为止一切都很好,但页面需要一个产品,所以让我们传入一个:
class Page {
public $html;
private $product;
public function __construct(Product $product) {
$this->product = $product;
}
public function createPage() {
// do something to generate the page
}
public function showPage() {
echo $this->html;
}
}
我使用了依赖项注入来避免使我的页面类依赖于产品。但若页面有几个公共变量,那个么调试时我想看看这些变量中有什么。没问题,我只是var_dump()
页面实例。它给了我页面中的所有变量,包括product对象,所以我也得到了product中所有的变量。
但产品不仅实例化了包含产品所有细节的所有变量,它还拥有一个数据库连接来获取这些产品细节。所以现在我的var_dump()
中也有数据库对象。现在,它开始变得更长,更难阅读,即使在<pre>
标签中也是如此。
产品也属于一个或多个类别。为了论证起见,让我们说它属于两类。它们加载在构造函数中,并存储在包含数组的类变量中。现在,我不仅有了产品和数据库连接中的所有变量,还有类别类的两个实例。当然,类别信息也必须从数据库加载,所以每个类别实例也有一个数据库私有变量。
所以现在,当我var_dump()
我的页面时,我有所有的页面变量,所有的产品变量,数组中类别变量的倍数,以及数据库变量的3个副本(一个来自产品实例,一个来自每个类别实例)。我的作品现在很大,很难阅读。
现在单身汉怎么样?让我们看看我的页面类使用singleton。
class Page {
public $html;
public function __construct() {
}
public function createPage() {
$prodId = Url::getProdId();
$productInfo = Product::instance($prodId)->info();
// do something to generate the page
}
public function showPage() {
echo $this->html;
}
}
我在Product类中也使用了类似的singleton。现在,当我var_dump()
我的Page实例时,我只得到我想要的变量,那些属于页面的变量,而没有其他变量。
当然,这在我的类之间产生了依赖关系。在单元测试中,没有办法不调用产品类,这使得单元测试变得困难。
如何既能获得依赖注入的所有好处,又能使用var_dump()
轻松调试类?如何避免将所有这些实例作为变量存储在类中?
我将尝试在这里写几件事。
关于var_dump()
:
我使用Symfony2作为默认框架,有时var_dump()
是快速调试的最佳选择。然而,它可以输出如此多的信息,以至于你不可能阅读所有信息,对吧?比如,转储Symfony的AppKernel.php
,或者,更接近您的情况,一些具有EntityManager
依赖关系的服务。IMHO,var_dump()
在调试小代码时很好,但大而复杂的产品会使var_dump()
无效。对我来说,另一种选择是使用"真正的"调试器,与IDE集成。有了PhpStorm下的xDebug,我就不再真正需要var_dump()
了。
这里有关于"为什么?"answers"如何?"的有用链接。
关于DI容器:
它的忠实粉丝。它很简单,使代码更稳定;它在现代应用中很常见。但我同意你的看法,这背后有一个真正的问题:嵌套依赖。这是过度抽象的,它会通过添加有时不必要的层来增加复杂性。
通过使用依赖注入容器来掩盖痛苦您的应用程序更加复杂。
如果您想从应用程序中删除DIC,并且您实际上可以做到,那么您根本不需要DIC。如果你想要DIC的替代品,那么Singleton被认为是不可测试的代码和应用程序的巨大状态空间的糟糕做法服务定位器对我来说根本没有任何好处。看来只有一种方法,那就是正确使用DI。
关于您的示例:
我立刻看到了一件事——通过construct()
注入。这很酷,但与需要它的方法相比,我更喜欢可选的传递依赖关系,例如通过服务config.yml
中的setter。
class Page
{
public $html;
protected $em;
protected $product;
public function __construct(EntityManager $em) {
$this->em = $em;
}
//I suppose it's not from DB, because in this case EM handles this for you
protected function setProduct(Product $product)
{
$this->product = $product;
}
public function createPage()
{
//$this->product can be used here ONLY when you really need it
// do something to generate the page
}
public function showPage()
{
echo $this->html;
}
}
我认为,当您在执行过程中只需要一些对象时,它提供了所需的灵活性,并且在给定的时刻,您可以在类内部看到您只需要的属性。
结论
对不起,我的回答过于宽泛,有些肤浅。我真的认为你的问题没有直接的答案,任何解决方案都是基于意见的。我只是希望你能发现DIC确实是最好的解决方案,缺点有限,还有集成调试器,而不是转储整个类(构造函数、服务等)。
我完全知道可以达到你想要的结果,不要使用极端的解决方案。
我不确定我的例子对你来说是否足够好,但它已经做到了:di和它很容易被单元测试覆盖,var_dump将完全显示你想要的,我认为它鼓励SRP。
<?php
class Url
{
public static function getProdId()
{
return 'Category1';
}
}
class Product
{
public static $name = 'Car';
public static function instance($prodId)
{
if ($prodId === 'Category1') {
return new Category1();
}
}
}
class Category1 extends Product
{
public $model = 'DB9';
public function info()
{
return 'Aston Martin DB9 v12';
}
}
class Page
{
public $html;
public function createPage(Product $product)
{
// Here you can do something more to generate the page.
$this->html = $product->info() . PHP_EOL;
}
public function showPage()
{
echo $this->html;
}
}
$page = new Page();
$page->createPage(Product::instance(Url::getProdId()));
$page->showPage();
var_export($page);
结果:
Aston Martin DB9 v12
Page::__set_state(array(
'html' => 'Aston Martin DB9 v12
',
))
也许这会对您有所帮助:
class Potatoe {
public $skin;
protected $meat;
private $roots;
function __construct ( $s, $m, $r ) {
$this->skin = $s;
$this->meat = $m;
$this->roots = $r;
}
}
$Obj = new Potatoe ( 1, 2, 3 );
echo "<pre>'n";
echo "Using get_object_vars:'n";
$vars = get_object_vars ( $Obj );
print_r ( $vars );
echo "'n'nUsing array cast:'n";
$Arr = (array)$Obj;
print_r ( $Arr );
这将返回:
Using get_object_vars:
Array
(
[skin] => 1
)
Using array cast:
Array
(
[skin] => 1
[ * meat] => 2
[ Potatoe roots] => 3
)
在此处查看其余内容http://php.net/manual/en/function.get-object-vars.php
简单的答案是,是的,您可以避免使用许多私有变量和依赖注入。但是(这是一个很大的问题)你必须使用ServiceContainer或其原理
简而言之:
class A
{
protected $services = array();
public function setService($name, $instance)
{
$this->services[$name] = $instance;
}
public function getService($name)
{
if (array_key_exists($name, $this->services)) {
return $this->services[$name];
}
return null;
}
private function log($message, $logLevel)
{
if (null === $this->getService('logger')) {
// Default behaviour is to log to php error log if $logLevel is critical
if ('critical' === $logLevel) {
error_log($message);
}
return;
}
$this->getService('logger')->log($message, $logLevel);
}
public function actionOne()
{
echo 'Action on was called';
$this->log('Action on was called', 0);
}
}
$a = new A();
// Logs to error log
$a->actionOne();
$a->setService('logger', new Logger());
// using the logger service
$a->actionOne();
使用该类,您只有一个受保护的变量,并且只需添加一个服务就可以向该类添加任何功能。
ServiceContainer的一个更复杂的例子可能是类似的东西
<?php
/**
* Class ServiceContainer
* Manage our services
*/
class ServiceContainer
{
private $serviceDefinition = array();
private $services = array();
public function addService($name, $class)
{
$this->serviceDefinition[$name] = $class;
}
public function getService($name)
{
if (!array_key_exists($name, $this->services)) {
if (!array_key_exists($name, $this->serviceDefinition)) {
throw new 'RuntimeException(
sprintf(
'Unkown service "%s". Known services are %s.',
$name,
implode(', ', array_keys($this->serviceDefinition))
)
);
}
$this->services[$name] = new $this->serviceDefinition[$name];
}
return $this->services[$name];
}
}
/**
* Class Product
* Part of the Model. Nothing too complex
*/
class Product
{
public $id;
public $info;
/**
* Get info
*
* @return mixed
*/
public function getInfo()
{
return $this->info;
}
}
/**
* Class ProductManager
*
*/
class ProductManager
{
public function find($id)
{
$p = new Product();
$p->id = $id;
$p->info = 'Product info of product with id ' . $id;
return $p;
}
}
class UnusedBadService
{
public function _construct()
{
ThisWillProduceAnErrorOnExecution();
}
}
/**
* Class Page
* Handle this request.
*/
class Page
{
protected $container;
/**
* Set container
*
* @param ServiceContainer $container
*
* @return ContainerAware
*/
public function setContainer(ServiceContainer $container)
{
$this->container = $container;
return $this;
}
public function get($name)
{
return $this->container->getService($name);
}
public function createPage($productId)
{
$pm = $this->get('product_manager');
$productInfo = $pm->find($productId)->getInfo();
// do something to generate the page
return sprintf('<html><head></head><body><h1>%s</h1></body></html>', $productInfo);
}
}
$serviceContainer = new ServiceContainer();
// Add some services
$serviceContainer->addService('product_manager', 'ProductManager');
$serviceContainer->addService('unused_bad_service', 'UnusedBadService');
$page = new Page();
$page->setContainer($serviceContainer);
echo $page->createPage(1);
var_dump($page);
如果查看var_dump输出,您可以看到输出中只有您调用的服务。所以这是小,快和性感;)