您现在已经听说过PHP命名空间。您很可能听说过并可能参与了围绕选择命名空间分隔符的自行车脱落。
无论您对名称空间分隔符有何看法,或者名称空间在其他语言中如何工作或可能无法工作,我向您提供几个理由,说明为什么我认为PHP中的名称空间是该语言的积极补充。
代码组织
在PHP5.3之前,我们有许多关于如何命名类以及将类文件放在文件系统中的位置的标准。这些范围从完全任意到基于约定(library/abc/some
中的abcSomeClass
),再到类PEAR(类名和文件系统位置之间的1:1相关性)。
虽然名称空间不强制执行任何特定范例,但它们适合PEAR风格的约定。为什么?
考虑:
namespace my\Component; class Gateway {}
您希望在哪里找到该文件?你是说“在my/Component/Gateway.php
”吗?我的猜测是超过90%的读者都这样做了。为什么?因为命名空间分隔符让我们想起了目录分隔符。简单明了。这种约定很有意义。
因此,命名空间有助于高效和简单的命名约定。
接口
在我看来,接口在PHP中经常未得到充分利用。许多人会争辩说,“嘿,他们什么都不做,需要加载更多文件,而且我可以在抽象类或具体类上同样轻松地键入提示。”这些都是真的。然而,接口为我们提供了我们为应用程序定义的契约的简单表示,并为我们提供了扩展和修改系统所需的蓝图。
我在使用PHP5.3之前的代码时遇到的一个问题是如何命名接口。由于我们没有真正的命名空间,我们(即PHP开发人员)经常使用诸如My_Component_Adapter_Interface
之类的名称。考虑到在从伪命名空间到PHP5.3命名空间的文字1:1转换时,这变成了My\Component\Adapter\Interface
,我遇到了几个问题:
- 首先,由于PHP词法分析器的工作方式,由于
interfaceInterface
的声明,您会得到一个E_FATAL
。 - 其次,结构现在感觉很奇怪:我们最终描述的是一个适配器,但为什么我们要把它放在命名空间层次结构的更深层次?
我发现一个运作良好的组织如下所示:
library/ |-- mwop/ | |-- Component/ | | |-- ClassConsumingAdapters.php | | |-- Adapter.php | | |-- Adapter/ | | | |-- AbstractAdapter.php | | | |-- SomeConcreteAdapter.php
在上面,我们声明了一个mwop\Component
命名空间。在那个命名空间中有一个使用适配器的具体类,以及实际的适配器接口本身——简单地命名为Adapter
。这会将适配器定义置于使用它的同一级别。
具体的适配器随后位于子命名空间mwop\Component\Adapter
中。我们将基本实现放在AbstractAdapter
类中,具体适配器通常会扩展它。抽象适配器声明如下所示:
namespace mwop\Component\Adapter; use mwop\Component\Adapter; abstract class AbstractAdapter implements Adapter { ... }
这看起来很奇怪,而且在我第一次尝试时它似乎不起作用,但它确实是合法的语法。我特别喜欢它的一点是,它很清楚类是什么(它是一个适配器),而且很清楚我将在这个命名空间中找到兄弟类。
在我的ClassConsumingAdapters
中,我只引用了Adapter
:
namespace mwop\Component; class ClassConsumingAdapters { protected $adapter; public function __construct(Adapter $adapter) { $this->adapter = $adapter; } public function doSomething() { $data = $this->adapter->someMethodCall(); // do some work return $data; } }
我只是担心拥有一个适配器并使用它,而不是特定的实现——这就是使用接口编程的意义所在。将接口置于同一级别使代码非常易读和易于理解。
可读性
首先拥有命名空间的一个论点是代码的可读性。诚然,这主要来自我们PEAR阵营的那些人,我们试图在语义上将代码组织成层次结构和依赖关系,并以长名称结尾,例如Foo_Component_Decorator_View_Helper
—当我们真正的意思是“辅助对象”时。然而,由于使用伪命名空间来组织我们的代码,而且我们只能使用类名这一事实,我们陷入了冗长的困境。
有了命名空间,我们有两个工具可以使用。
首先,名称空间本身。如果我们正在编写新代码,我们可以创建命名空间,并且我们命名空间内的所有代码立即可用,根本不需要前缀。上面的一个示例,其中ClassConsumingAdapters
只是引用Adapter
—因为它们在同一个命名空间中,所以不需要前缀。
我们的第二个工具是导入和别名的能力。例如,让我们考虑一下:
library/ |-- mwop/ | `-- Component/ | |-- ClassConsumingAdapters.php | |-- Adapter.php | `-- Adapter/ | |-- AbstractAdapter.php | `-- SomeConcreteAdapter.php |-- Zend/ | `-- EventManager/ | |-- EventCollection.php | |-- EventManager.php | `-- StaticEventManager.php
假设ClassConsumingAdapters
想要使用新的Zend\EventManager
组件。有几种方法可以做到这一点。首先,它可以简单地使用全局分辨率:
namespace mwop\Component; class ClassConsumingAdapters { protected $events; public function events(\Zend\EventManager\EventCollection $events = null) { if (null !== $events) { $this->events = $events; } elseif (null === $this->events) { $this->events = new \Zend\EventManager\EventManager(__CLASS__); } return $this->events; } }
这非常丑陋,可以说比命名空间之前的代码更糟糕。那么,让我们尝试导入一些类和接口。在PHP中,我们使用use
关键字将类导入当前范围:
namespace mwop\Component; use Zend\EventManager\EventCollection, Zend\EventManager\EventManager; class ClassConsumingAdapters { protected $events; public function events(EventCollection $events = null) { if (null !== $events) { $this->events = $events; } elseif (null === $this->events) { $this->events = new EventManager(__CLASS__); } return $this->events; } }
这样更容易阅读!我们现在有了可以更好地表明我们正在使用的类的目的的引用,这使得我们更容易理解我们正在做的事情。
第三个选项是别名。别名是您在导入类时所做的事情;在导入时,您指定了一个备用名称,您希望通过该名称来引用类或接口。插图将有所帮助:
namespace mwop\Component; use Zend\EventManager\EventCollection as Events, Zend\EventManager\EventManager; class ClassConsumingAdapters { protected $events; public function events(Events $events = null) { if (null !== $events) { $this->events = $events; } elseif (null === $this->events) { $this->events = new EventManager(__CLASS__); } return $this->events; } }
在上面的例子中,我们别名Zend\EventManager\EventCollection
到简单的Events
(复数通常表示一个集合)。
既然我们知道了别名,这里有一个提示:您不需要重写所有漂亮、干净、pre-PHP5.3的库代码来使用命名空间!您可以简单地在您的消费者代码中使用别名:
namespace Application; use Zend_Controller_Action as ActionController; class FooController extends ActionController { }
(自去年春天以来,我一直在我的演示文稿中使用上述技巧,因为它通常有助于提高代码示例的可读性!)
识别依赖
既然您了解了导入和别名,还有另一点需要介绍:导入可以帮助您明确依赖关系。
声明一个import语句不会立即加载一个类——它只是提示PHP解释器在遇到某些符号时如何理解它们。
事实上,您不仅可以导入和别名化类和接口,还可以导入命名空间本身——尽管在导入命名空间时,您可以在该命名空间下为类添加前缀:
namespace Application; use Foo\Exception; // ... // Foo\Exception\InvalidArgumentException: throw Exception\InvalidArgumentException();
导入的一个副作用是您在代码级别记录您对来自其他名称空间的组件的依赖性。这允许你做一些事情,比如使用静态分析工具来识别依赖关系。例如,我创建了一个scanDeps工具,该工具将分析PHP文件树中的导入语句,并创建引用的唯一组件列表。
这种自动化是无价的;它可以帮助您确定在给定组件中更改代码时可能希望运行的测试,允许您创建引用适当依赖项的代码的PEAR包,等等。
结论
组织。可读性。依赖跟踪。所有这些都是有价值的目标,它们本身就令人印象深刻。所有这些都来自一个功能:命名空间。
当然,我们都可以讨论名称空间分隔符。然而,归根结底,重点是:无论语法如何,名称空间都给我带来了什么?希望我的论点让您相信它们对PHP开发的普遍效用。
如果您还没有使用过命名空间,请安装PHP5.3并开始试验—让我知道您找到了哪些使用模式!