核心要点
虚拟代理这个名称听起来很花哨,但它可能是“面向接口编程”理念不仅仅是一个枯燥教条原则的最显着例子之一。虚拟代理基于多态性(动态多态性,而不是通过简单的方法覆盖实现的临时多态性),是一个简单而可靠的概念,它允许您延迟构建/加载代价高昂的对象图,而无需修改客户端代码。代理的一大优点是,它们在概念上可以设计为与单个对象或对象集合一起工作(或者两者兼而有之,尽管这样做可能会危及关注点分离,并且随着时间的推移难以管理)。为了从实践的角度演示如何利用虚拟代理提供的功能,在本系列的第一部分中,我介绍了几个示例的开发过程,展示了如何使用基本代理从数据库中提取聚合以满足简单的领域模型。虽然经验可能是有益的,而且幸运的是也很有趣,但它的另一方面有点具有欺骗性,因为它展示了虚拟代理的来龙去脉,但没有展示如何在更现实的场景中实现它们。在延迟加载存储中的领域对象集合方面,代理是无与伦比的。要了解这个概念,只需考虑一批博客文章,其中每一组相关评论都可以按需从数据库中提取;您一定会明白为什么代理在这种情况下能够出色地实现其承诺。
通常情况下,实践是最好的老师。在本部分中,我将展示如何将代理连接到特定领域对象的集合。我将在非常基础的层面上重现这个典型的场景,这样您就可以轻松地了解其驱动逻辑。
创建领域对象集合
如前所述,在从持久层获取与底层领域对象集合绑定的聚合根时,通常会创建虚拟代理。由于集合的本质特性,在许多情况下,集合的预先设置代价很高,这使得它们成为按需加载以减少数据库往返开销的良好候选对象。此外,考虑到博客文章与其对应评论之间的“一对多”关联在本用例中相当忠实地反映出来,在处理具体的代理之前,首先通过一些简单的领域类来建模这种关系将很有指导意义。为了使事情易于理解,我将在测试阶段添加的两个参与者将是一个隔离的接口和一个基本的实现者。这两个结合在一起,将定义泛型博客文章对象的契约和实现:
<code class="language-php"><?php namespace Model; use ModelCollectionCommentCollectionInterface; interface PostInterface { public function setId($id); public function getId(); public function setTitle($title); public function getTitle(); public function setContent($content); public function getContent(); public function setComments(CommentCollectionInterface $comments); public function getComments(); }</code>
<code class="language-php"><?php namespace Model; use ModelCollectionCommentCollectionInterface; class Post implements PostInterface { protected $id; protected $title; protected $content; protected $comments; public function __construct($title, $content, CommentCollectionInterface $comments = null) { $this->setTitle($title); $this->setContent($content); $this->comments = $comments; } // ... (Post class methods remain largely unchanged) ... }</code>
理解上述Post类的逻辑是一个简单的过程,不需要真正解释。尽管如此,这里有一个值得注意的相关细节:该类在构造函数中明确声明了对尚未定义的评论集合的依赖关系。现在让我们创建生成帖子评论的类:
<code class="language-php"><?php namespace Model; interface CommentInterface { public function setId($id); public function getId(); public function setContent($content); public function getContent(); public function setPoster($poster); public function getPoster(); }</code>
<code class="language-php"><?php namespace Model; class Comment implements CommentInterface { protected $id; protected $content; protected $poster; public function __construct($content, $poster) { $this->setContent($content); $this->setPoster($poster); } // ... (Comment class methods remain largely unchanged) ... }</code>
到目前为止,一切进展顺利。除了它们是基本领域模型的精简块之外,关于上述领域类没有什么好说的,其中每个博客文章对象都公开与相关评论的“一对多”关联。如果您愿意,可以称我为纯粹主义者,但在我看来,如果模型的当前实现没有用评论集合来增强,它看起来就显得不完整且笨拙。让我们通过向其中添加此额外组件的逻辑来使模型更丰富:
<code class="language-php"><?php namespace ModelCollection; interface CommentCollectionInterface extends Countable, IteratorAggregate { public function getComments(); }</code>
<code class="language-php"><?php namespace ModelCollection; use ModelCommentInterface; class CommentCollection implements CommentCollectionInterface { protected $comments = array(); public function __construct(array $comments = array()) { // ... (CommentCollection class methods remain largely unchanged) ... } }</code>
如果您观察仔细并浏览CommentCollection类,您首先会注意到它只不过是一个隐藏在华丽伪装背后的可迭代、可计数的数组包装器。事实上,数组集合有多种形式和风格,但大多数时候它们只是Iterator和ArrayAccess SPL类的简单用法。在本例中,我想让自己(和您)免于处理如此枯燥的任务,并将该类设为IteratorAggregate的实现者。有了评论集合之后,我们可以更进一步,让领域模型做它应该做的事情——在一些博客文章对象上进行操作,甚至将它们与从数据库中急切地获取的一批评论互连。但是这样做只会欺骗我们自己,而不会充分利用虚拟代理提供的功能。考虑到在典型的实现中,代理公开了与实际领域对象相同的API,与之前的CommentCollection类交互的代理也应该实现CommentCollectionInterface,以便在不引入一堆有问题的条件语句的情况下遵守与客户端代码的契约。
通过虚拟代理与领域对象的集合交互
坦率地说,像前面提到的那样包装数组的集合可以独立存在,而无需依赖任何其他依赖项。(如果您持怀疑态度,可以随意检查Doctrine中的集合在幕后如何工作。)尽管如此,请记住,我试图实现一个模拟真实评论集合行为的代理,但实际上它是一个轻量级的替代品。需要问的问题是:如何从数据库中提取评论并将其放入之前的集合中?有几种方法可以实现这一点,但我认为最吸引人的一种是通过数据映射器,因为它提高了持久性无关性。下面的映射器可以很好地从存储中获取评论集合。请查看:
<code class="language-php"><?php namespace Model; use ModelCollectionCommentCollectionInterface; interface PostInterface { public function setId($id); public function getId(); public function setTitle($title); public function getTitle(); public function setContent($content); public function getContent(); public function setComments(CommentCollectionInterface $comments); public function getComments(); }</code>
<code class="language-php"><?php namespace Model; use ModelCollectionCommentCollectionInterface; class Post implements PostInterface { protected $id; protected $title; protected $content; protected $comments; public function __construct($title, $content, CommentCollectionInterface $comments = null) { $this->setTitle($title); $this->setContent($content); $this->comments = $comments; } // ... (Post class methods remain largely unchanged) ... }</code>
虽然CommentMapper类公开的查找器通常坚持在标准数据映射器实现中可能期望的API,但到目前为止,fetchAll()方法是其中最引人注目的方法。它首先从存储中提取所有博客文章评论并将它们放入集合中,最后将集合返回给客户端代码。如果您像我一样,您的脑海中可能会响起警钟,因为集合是在方法内部直接实例化的。事实上,不必对在工厂之外偷偷摸摸的新运算符感到恐慌,至少在本例中是这样,因为集合实际上是一种通用的结构,属于“可创建”类别,而不是“可注入”类别。无论如何,如果您通过在映射器的构造函数中注入集合感到稍微不那么内疚,请随意这样做。有了评论映射器之后,是时候经历真正的顿悟时刻并构建与前面集合进行调解的代理类了:
<code class="language-php"><?php namespace Model; interface CommentInterface { public function setId($id); public function getId(); public function setContent($content); public function getContent(); public function setPoster($poster); public function getPoster(); }</code>
正如您可能预期的那样,CommentCollectionProxy实现了与真实评论集合相同的接口。但是,它的getComments()方法在幕后完成了实际工作,并通过构造函数中传递的映射器从数据库中延迟加载评论。这个简单而有效的方法允许您对评论进行各种巧妙的操作,而不会过度费力。您想看看哪些方法吗?假设您需要从数据库中获取与特定博客文章绑定的所有评论。以下代码片段可以完成这项工作:
<code class="language-php"><?php namespace Model; class Comment implements CommentInterface { protected $id; protected $content; protected $poster; public function __construct($content, $poster) { $this->setContent($content); $this->setPoster($poster); } // ... (Comment class methods remain largely unchanged) ... }</code>
这种方法的缺点是评论首先从存储中提取,然后注入到帖子对象的内部。如何反过来做,但这次是用代理“欺骗”客户端代码?
<code class="language-php"><?php namespace ModelCollection; interface CommentCollectionInterface extends Countable, IteratorAggregate { public function getComments(); }</code>
评论不仅在代理被放入foreach循环后从数据库中透明地延迟加载,而且向客户端代码公开的API在整个过程中都保持其原始结构不变。我们甚至敢要求更好的东西吗?除非您非常贪婪,否则我很难这么认为。无论哪种情况,此时您都应该了解虚拟代理的幕后实际情况,以及如何在提高领域对象与底层持久层之间交换的操作效率方面充分利用其功能。
结束语
虽然很简单,特别是如果您大胆地在生产环境中使用它,但前面的示例简要地展示了一些有趣的概念。首先,虚拟代理不仅易于设置和使用,而且在运行时混合不同的实现以延迟执行代价高昂的任务(例如,延迟加载存储层中的大量数据或创建重量级对象图)方面无与伦比。其次,它们是多态性如何成为减少许多面向对象应用程序遭受的常见刚性和脆弱性问题的有效疫苗的经典示例。此外,由于PHP的对象模型很简单并且支持闭包,因此可以巧妙地混合这些特性并构建其底层逻辑由闭包的优点驱动的代理。如果您想自己应对这一挑战,那么我提前祝您一切顺利。
(图片来自imredesiuk / Shutterstock)
以上是虚拟代理的介绍,第2部分的详细内容。更多信息请关注PHP中文网其他相关文章!