Home >Backend Development >PHP Tutorial >An Intro to Virtual Proxies, Part 2
Core points
The name of virtual agent sounds fancy, but it is probably one of the most prominent examples of the concept of "interface-oriented programming" that is more than just a boring dogmatic principle. Virtual proxy is based on polymorphism (dynamic polymorphism, rather than temporary polymorphism implemented through simple methods), and is a simple and reliable concept that allows you to delay building/loading costly object graphs without modifying client code. A big advantage of agents is that they can be conceptually designed to work with a single object or collection of objects (or both, although doing so can jeopardize separation of concerns and are difficult to manage over time). To demonstrate from a practical perspective how to take advantage of the functionality provided by a virtual proxy, in the first part of this series, I introduce several example development processes that show how to extract aggregates from a database using a basic proxy to satisfy a simple domain model. While the experience can be beneficial and, luckily, it's also fun, the other side of it is a bit deceptive as it shows the ins and outs of virtual agents, but does not show how to implement them in a more realistic scenario. The proxy is unparalleled in terms of the collection of domain objects in lazy loading storage. To understand this concept, just consider a batch of blog posts, where each set of related comments can be extracted from the database on demand; you will surely understand why the agent can deliver on its promises in this case.
Normally, practice is the best teacher. In this section, I will show how to connect a proxy to a collection of domain-specific objects. I will reproduce this typical scenario on a very basic level so you can easily understand its driving logic.
Create a collection of realm objects
As mentioned earlier, virtual agents are usually created when obtaining aggregate roots bound to the underlying realm object collection from the persistence layer. Due to the nature of collections, in many cases, pre-setting of collections is expensive, making them a good candidate for loading on demand to reduce database round trip overhead. Furthermore, considering that the "one-to-many" relationship between a blog post and its corresponding comments is quite faithfully reflected in this use case, it would be instructive to first model the relationship through some simple domain classes before dealing with specific agents. To make things easier to understand, the two participants I will add during the testing phase will be an isolated interface and a basic implementer. These two combine to define the contract and implementation of generic blog post objects:
<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>
Understanding the logic of the above Post class is a simple process and does not require real explanation. Nevertheless, here is a noteworthy related detail: the class explicitly declares dependencies on a collection of comments that have not been defined in the constructor. Now let's create the class that generates post comments:
<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>
So far, everything has been going well. Apart from their thin blocks of the basic domain model, there is nothing to say about the above domain classes, where each blog post object opens the "one-to-many" association with related comments. You can call me a purist if you want, but it seems to me that if the current implementation of the model is not enhanced with a collection of comments, it looks incomplete and clumsy. Let's make the model richer by adding this extra component to it:
<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>
If you look carefully and browse the CommentCollection class, you will first notice that it is nothing more than an iterable, countable array wrapper hidden behind the gorgeous disguise. In fact, array collections come in many forms and styles, but most of the time they are just simple usages of Iterator and ArrayAccess SPL classes. In this case, I want to save myself (and you) from such a boring task and make the class the implementer of IteratorAggregate. With the comment collection, we can go a step further and let the domain model do what it should do—operate on some blog post objects, or even interconnect them with a batch of comments eagerly fetched from the database. But doing so will only fool ourselves without taking full advantage of the capabilities provided by the virtual proxy. Considering that in a typical implementation, the proxy exposes the same API as the real domain object, the proxy interacting with the previous CommentCollection class should also implement the CommentCollectionInterface so as to adhere to the contract with the client code without introducing a bunch of problematic conditional statements.
Interaction with collections of domain objects through virtual agents
Frankly speaking, a collection of wrapping arrays, as mentioned earlier, can exist independently without relying on any other dependencies. (If you are skeptical, feel free to check how collections in Doctrine work behind the scenes.) Still, remember that I'm trying to implement a proxy that simulates the behavior of a real review collection, but it's actually a lightweight alternative. The question to be asked is: How do you extract comments from the database and put them into the previous collection? There are several ways to achieve this, but I think the most attractive one is through the data mapper because it improves persistence irrelevance. The mapper below makes it nice to get a collection of comments from storage. Please check:
<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>
While the finders exposed by the CommentMapper class usually stick to the API that might be expected in standard data mapper implementations, the fetchAll() method is by far the most compelling method. It first extracts all blog post comments from the storage and puts them into the collection, and finally returns the collection to the client code. If you are like me, you may have a wake-up call in your mind because the collection is instantiated directly inside the method. In fact, there is no need to panic about new operators sneaking outside the factory, at least in this case, because the collection is actually a general structure that falls under the "created" category, rather than the "injectable" category. Anyway, feel free to do it if you feel a little less guilty by injecting the collection into the constructor of the mapper. With the comment mapper, it's time to experience the real epiphany and build a proxy class that mediates with the previous set:
<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>
As you might expect, CommentCollectionProxy implements the same interface as a collection of real comments. However, its getComments() method does the actual work behind the scenes and delays the loading of comments from the database via the mapper passed in the constructor. This simple and effective method allows you to do all kinds of clever actions on your comments without overworking. Do you want to see what methods do you want to see? Suppose you need to get all the comments bound to a specific blog post from the database. The following code snippet can do this:
<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>
The disadvantage of this approach is that the comments are first extracted from the storage and then injected into the inside of the post object. How to do it the other way around, but this time it is to "spoof" the client code with a proxy?
<code class="language-php"><?php namespace ModelCollection; interface CommentCollectionInterface extends Countable, IteratorAggregate { public function getComments(); }</code>
Comments not only transparently delay loading from the database after the proxy is placed in the foreach loop, but the API exposed to the client code keeps its original structure unchanged throughout the process. Do we even dare to ask for something better? Unless you are very greedy, it's hard for me to think so. In either case, you should understand the reality behind the scenes of the virtual agent and how to make the most of its capabilities in improving the operational efficiency of the domain object and the underlying persistence layer.
Conclusion
While it is simple, especially if you are bold enough to use it in a production environment, the previous example briefly shows some interesting concepts. First, virtual agents are not only easy to set up and use, but are unparalleled in mixing different implementations at runtime to delay performing costly tasks (e.g., delaying loading large amounts of data in the storage tier or creating heavyweight object graphs). Second, they are classic examples of how polymorphisms can become effective vaccines that reduce common rigidity and vulnerability problems that many object-oriented applications suffer from. Furthermore, because PHP's object model is simple and supports closures, it is possible to cleverly mix these features and build a proxy whose underlying logic is driven by the advantages of closures. If you want to deal with this challenge yourself, I wish you all the best in advance.
(Picture from imredesiuk / Shutterstock)
The above is the detailed content of An Intro to Virtual Proxies, Part 2. For more information, please follow other related articles on the PHP Chinese website!