持久化模型

tech2023-11-29  29

持久化模型

With so much water flowing under the Domain Models bridge over the last few years, it’s rather hard to dig deep into the key concepts without rippling even more confusion in the already agitated creek. Moreover, with tons of MVC implementations proliferating like hungry ants, the acronym’s “M” continues to suffer from the symptoms of an ad-hoc layer, usually known as a Database Model, which can pollute with total impunity the domain logic with code for database access (hence infrastructure), or with other type of underlying storage.

在过去的几年里,由于在“ 领域模型”桥下流动的水如此之多,要想深入挖掘关键概念而又不会在已经动荡的小溪中造成更大的混乱,就很难了。 此外,随着成千上万的MVC实现像饥饿的蚂蚁一样激增,首字母缩略词“ M”继续遭受临时层的症状,通常称为数据库模型,该模型可能完全不受惩罚地污染了带有数据库代码的域逻辑访问(因此具有基础架构),或具有其他类型的基础存储。

What makes a Database Model so appealing in many cases is that it performs fairly well from the perspective of client code. After all, it’s easily consumable, as it hides a lot of complexity behind an apparently harmless API (for example, something like $user->save()). The downside is that it clashes noisily when it comes to sticking to good object-oriented design practices, not to mention the plethora of scalability and testability issues that eventually bubble up to the surface.

在许多情况下,使数据库模型如此吸引人的原因是,从客户端代码的角度来看,它的性能相当好。 毕竟,它很容易使用,因为它在看似无害的API后面隐藏了很多复杂性(例如,类似$user->save() )。 不利的一面是,在坚持良好的面向对象设计实践时,它会产生很大的冲突,更不用说最终会浮出水面的大量可扩展性和可测试性问题。

From this standpoint, it would seem that popular data source architectural patterns, such as Active Record and Table Data Gateway should be considered potentially harmful intruders, when they’re coupled to domain logic. But throwing blame to the patterns for what they’re intended to do is nothing but a weak excuse for not embracing a Domain Model according to the purpose it was conceived in the first place: an independent, persistence-agnostic layer responsible for defining clearly the interactions between the entities of a system through data and behavior.

从这个角度来看,当将流行的数据源体系结构模式(例如Active Record和Table Data Gateway)与域逻辑耦合时,似乎应该被认为是潜在有害的入侵者。 但是,将模式的意图归咎于模式,只是为根据最初设想的目的不拥抱域模型的一个弱借口: 一个独立的,与持久性无关的层,负责清楚地定义域模型。 系统实体之间通过数据和行为进行的交互。

Of course, the above definition is a world away from being formal. Still, it highlights a few important ideas. First off, creating a rich Domain Model, where multiple domain objects with well-defined constraints and rules interact, can be a daunting task. Second, not only is it necessary to define from top to bottom the model itself, but it’s also necessary to implement from scratch or reuse a mapping layer in order to move data back and forward between the persistence layer and the model in question. The process requires at least one extra layer to get things sorted out, therefore moving away from more pragmatic approaches.

当然,以上定义是一个远离形式的世界。 尽管如此,它仍然突出了一些重要的想法。 首先,创建一个丰富的域模型,其中多个具有明确定义的约束和规则的域对象将相互作用,这可能是一项艰巨的任务。 其次,不仅有必要自上而下定义模型本身,而且还必须从头开始实现或重用映射层,以便在持久层和相关模型之间来回移动数据。 这个过程至少需要一层额外的层来整理事物,因此要脱离更为实用的方法。

The payback, though, can be surprisingly gratifying in the end, especially when considering the model in its entirety can be ported without much hassle from one infrastructure to another. A big bonus, indeed.

但是,最终的回报可能令人惊讶地令人满足,尤其是当考虑到可以将模型的整体从一个基础架构移植到另一个基础架构时,尤其如此。 确实是一大笔钱。

You’re probably wondering if a Domain Model gets along with PHP. Well, in fact it does, albeit at the expenses of having to tackle the aforementioned issues. But claims like this must be always backed up with a decent proof of concept, so, allow me to provide a few examples.

您可能想知道域模型是否与PHP兼容。 嗯,实际上确实如此,尽管必须解决上述问题。 但是,必须始终以像样的概念证明来支持此类主张,因此,请允许我提供一些示例。

建立基本的Blog域模型 (Building a Basic Blog Domain Model)

Unquestionably the nuts and bolts of a Domain Model is the strong focus put on the relationship between data and the behavior of domain objects while leaving any trace of infrastructure out of the picture. The beauty of this approach (and why I’m a big fan of it as well) is that the goal is achieved with elegance and simplicity. Quite often, simple PHP Domain Models are composed of a few POPOs (Plain Old PHP Objects), which encapsulate rich business logic, like validation and strategy, behind a clean API.

毫无疑问,领域模型的基本要素是将重点放在数据与领域对象行为之间的关系上,同时将基础架构的任何痕迹都排除在外。 这种方法的优点(以及为什么我也是它的忠实拥护者)是通过优雅和简单来实现目标。 通常,简单PHP域模型由一些POPO (普通的旧PHP对象)组成,这些POPO在干净的API之后封装了丰富的业务逻辑,例如验证和策略。

With that said, let’s see how to translate the conceptual stuff to tangible PHP code. So, say we need to build a pretty contrived blog program which must be capable of handling posts, comments, and users in a standard fashion. A good start to achieve this would be modeling the blog’s domain objects like POPOs.

如此说来,让我们看看如何将概念性内容转换为有形PHP代码。 因此,说我们需要构建一个非常人为设计的博客程序,该程序必须能够以标准方式处理帖子,评论和用户。 要实现这一目标,一个好的开始就是对博客的域对象(如POPO)进行建模。

Here’s the first one:

这是第一个:

<?php namespace Model; 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(array $comments); public function getComments(); }

The above interface is anything but complicated. Indeed, it’s clear to see that the purpose of PostInterface is to define a narrow contract for generic post objects which should have a one-to-many relationship with the related comments.

上面的界面并不复杂。 确实,可以很清楚地看到PostInterface的目的是为通用post对象定义一个狭窄的契约,该契约应与相关注释具有一对多的关系。

Since the interface’s code speaks for itself, let’s go one step further and create another interface to specify the contract for the corresponding comments:

既然接口的代码说明了一切,那么让我们更进一步,创建另一个接口来为相应的注释指定协定:

<?php namespace Model; interface CommentInterface { public function setId($id); public function getId(); public function setContent($content); public function getContent(); public function setUser(UserInterface $user); public function getUser(); }

Similar to PostInterface, there’s not much to be said about CommentInterface. It drops into the model a simple contract for blog comment objects. Quite possibly the only detail worth noting in this case is the signature of its setUser() method, which appeals to the whip of Interface Injection for binding a user to a specific comment.

类似PostInterface ,没有太多的约说CommentInterface 。 它为博客评论对象添加了一个简单的合同模型。 在这种情况下,唯一值得注意的细节可能是它的setUser()方法的签名,该方法吸引了Interface Injection的鞭策,将用户绑定到特定注释。

We’re almost done with creating the model’s interfaces. But before we can throw a well-deserved party and start toasting, it’s necessary to create one more which must outline the behavior of blog users. Here’s how this final interface looks:

创建模型的接口几乎完成了。 但是,在我们举行当之无愧的聚会并开始敬酒之前,有必要再创建一个聚会,概述博客用户的行为。 这是最终界面的外观:

<?php namespace Model; interface UserInterface { public function setId($id); public function getId(); public function setName($name); public function getName(); public function setEmail($email); public function getEmail(); public function setUrl($url); public function getUrl(); }

With this batch of interfaces, we’ve managed to define in a jiffy a simple – yet efficient – set of granular contracts which allow you to swap out concrete domain object implementations, as if we were using Lego blocks.

借助这批接口,我们设法轻松定义了一组简单但有效的粒度合同,这些合同使您可以换出具体的域对象实现,就像我们在使用Lego块一样。

Next we need to hydrate our model with some interface implementers; let’s see now how to accomplish this in a fairly painless way.

接下来,我们需要使用一些接口实现程序来充实我们的模型。 现在让我们看看如何以一种非常轻松的方式来完成此任务。

使用POPO为博客帖子,评论和用户建模 (Modeling Blog Posts, Comments, and Users with POPOs)

Since we’re rather lazy developers who want to save ourselves the hassles of calling mutators and accessors every time we need to work with the fields of a domain object, we can use some boilerplate PHP magic for mapping client code references from nonexistent properties to the corresponding domain objects methods. This is a pretty common and pragmatic approach which is usually encapsulated inside the boundaries of an abstract class, like the one shown below:

由于我们是比较懒惰的开发人员,他们想在每次需要处理域对象的字段时避免自己调用更改器和访问器的麻烦,因此我们可以使用一些样板PHP魔术来将客户端代码引用从不存在的属性映射到相应的领域对象方法。 这是一种非常常见且实用的方法,通常封装在一个抽象类的边界内,如下所示:

<?php namespace Model; abstract class AbstractEntity { /** * Map the setting of non-existing fields to a mutator when * possible, otherwise use the matching field */ public function __set($name, $value) { $field = "_" . strtolower($name); if (!property_exists($this, $field)) { throw new InvalidArgumentException( "Setting the field '$field' is not valid for this entity."); } $mutator = "set" . ucfirst(strtolower($name)); if (method_exists($this, $mutator) && is_callable(array($this, $mutator))) { $this->$mutator($value) } else { $this->$field = $value; } return $this; } /** * Map the getting of non-existing properties to an accessor when * possible, otherwise use the matching field */ public function __get($name) { $field = "_" . strtolower($name); if (!property_exists($this, $field)) { throw new InvalidArgumentException( "Getting the field '$field' is not valid for this entity."); } $accessor = "get" . ucfirst(strtolower($name)); return (method_exists($this, $accessor) && is_callable(array($this, $accessor))) ? $this->$accessor() : $this->field; } /** * Get the entity fields */ public function toArray() { return get_object_vars($this); } }

I’m not a strong advocate of relying heavily on PHP’s magic methods, but in this case the __set() and __get() methods come in handy for shortening calls to setters and getters without cluttering too much of the model’s API. With the previous parent class doing the leg work behind the scenes when it comes to working with domain object fields, the creation of concrete implementations for blog post, comment, and user objects boils down to subclassing a parent, as follows:

我并不是强烈依赖PHP的魔术方法的坚决拥护者,但是在这种情况下, __set()和__get()方法在缩短对setter和getter的调用而又不会使模型的API过于混乱的情况下派上用场。 上一个父类在处理域对象字段时会在幕后工作,因此为博客文章,评论和用户对象创建具体实现的过程归结为父类的子类,如下所示:

<?php namespace Model; class Post extends AbstractEntity implements PostInterface { protected $_id; protected $_title; protected $_content; protected $_comments; public function __construct($title, $content, array $comments = array()) { // map post fields to the corresponding mutators $this->setTitle($title); $this->setContent($content); if ($comments) { $this->setComments($comments); } } public function setId($id) { if ($this->_id !== null) { throw new BadMethodCallException( "The ID for this post has been set already."); } if (!is_int($id) || $id < 1) { throw new InvalidArgumentException("The post ID is invalid."); } $this->_id = $id; return $this; } public function setTitle($title) { if (!is_string($title) || strlen($title) < 2 || strlen($title) > 100) { throw new InvalidArgumentException("The post title is invalid."); } $this->_title = htmlspecialchars(trim($title), ENT_QUOTES); return $this; } public function getTitle() { return $this->_title; } public function setContent($content) { if (!is_string($content) || strlen($content) < 2) { throw new InvalidArgumentException("The post content is invalid."); } $this->_content = htmlspecialchars(trim($content), ENT_QUOTES); return $this; } public function getContent() { return $this->_content; } public function setComments(array $comments) { foreach ($comments as $comment) { if (!$comment instanceof CommentInterface) { throw new InvalidArgumentException( "One or more comments are invalid."); } } $this->_comments = $comments; return $this; } public function getComments() { return $this->_comments; } }

As you might expect, modelling a blog post as a POPO is a very straightforward process, reduced to 1) implementing the methods defined by its associated interface, and 2) optionally extending the functionality of the base entity class. What’s more, since in this case the post is capable of validating itself through its mutators, thus carrying both data and behavior, there’s no need to pollute application logic with scattered validation blocks. This vaccinates the whole model against anemic issues and makes it much cleaner and DRYer.

如您所料,将博客帖子建模为POPO是一个非常简单的过程,简化为1)实现由其关联接口定义的方法,以及2)可选地扩展基础实体类的功能。 而且,由于在这种情况下,该帖子能够通过其更改器进行验证,因此可以承载数据和行为,因此无需使用分散的验证块来污染应用程序逻辑。 这为整个模型接种了针对贫血的疫苗,并使其更加清洁和干燥。

Considering that the previous approach delivers what it promises at face value, let’s reuse it for modeling blog comments and users as well. Here are the subclasses that wrap these additional domain objects:

考虑到以前的方法可以实现其期望的面值,因此,我们也可以将其重用于建模博客评论和用户。 以下是包装这些其他域对象的子类:

<?php namespace Model; class Comment extends AbstractEntity implements CommentInterface { protected $_id; protected $_content; protected $_user; public function __construct($content, UserInterface $user) { $this->setContent($content); $this->setUser($user); } public function setId($id) { if ($this->_id !== null) { throw new BadMethodCallException( "The ID for this comment has been set already."); } if (!is_int($id) || $id < 1) { throw new InvalidArgumentException("The comment ID is invalid."); } $this->_id = $id; return $this; } public function getId() { return $this->_id; } public function setContent($content) { if (!is_string($content) || strlen($content) < 2) { throw new InvalidArgumentException( "The content of the comment is invalid."); } $this->_content = htmlspecialchars(trim($content), ENT_QUOTES); return $this; } public function getContent() { return $this->_content; } public function setUser(UserInterface $user) { $this->_user = $user; return $this; } public function getuser() { return $this->_user; } } <?php namespace Model; class User extends AbstractEntity implements UserInterface { protected $_id; protected $_name; protected $_email; protected $_url; public function __construct($name, $email, $url = null) { // map user fields to the corresponding mutators $this->setName($name); $this->setEmail($email); if ($url !== null) { $this->setUrl($url); } } public function setId($id) { if ($this->_id !== null) { throw new BadMethodCallException( "The ID for this user has been set already."); } if (!is_int($id) || $id < 1) { throw new InvalidArgumentException("The user ID is invalid."); } $this->_id = $id; return $this; } public function getId() { return $this->_id; } public function setName($name) { if (strlen($name) < 2 || strlen($name) > 30) { throw new InvalidArgumentException("The user name is invalid."); } $this->_name = htmlspecialchars(trim($name), ENT_QUOTES); return $this; } public function getName() { return $this->_name; } public function setEmail($email) { if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { throw new InvalidArgumentException("The user email is invalid."); } $this->_email = $email; return $this; } public function getEmail() { return $this->_email; } public function setUrl($url) { if (!filter_var($url, FILTER_VALIDATE_URL)) { throw new InvalidArgumentException("The user URL is invalid."); } $this->_url = $url; return $this; } public function getUrl() { return $this->_url; } }

Mission accomplished!

任务完成!

Even at the risk of sounding somewhat verbose, at this moment it’s safe to say that the blog domain model is finally set. The underlying interfaces and classes are there, living in happy ignorance about the existence of any type of persistence mechanism that may be implemented down the line, be it a database, a web service, or anything else.

即使冒着听起来有些冗长的风险,但现在可以肯定地说,博客域模型已最终确定。 底层的接口和类在那里,对可以在行下实现的任何类型的持久性机制(无论是数据库,Web服务还是其他任何东西)的存在一无所知。

Furthermore, not only do they define a network of rules and rich relationships with each other, but the current domain object implementations can be replaced with custom ones without much fuss.

此外,它们不仅定义了规则和彼此之间丰富的关系的网络,而且当前的域对象实现也可以用自定义实现替换,而不必大惊小怪。

Now the question becomes how to consume the model. Well, that’s an easy one, indeed! If we were going to build a naive blog application, where all of its object graphs reside all the time in memory and don’t need to be persisted, the process would be a breeze to tackle as there would be no need to create a mapping layer at all. To elaborate a bit further this concept, however, let’s try out some concrete code samples.

现在的问题变成了如何使用模型。 好吧,的确很简单! 如果我们要构建一个幼稚的博客应用程序,该应用程序的所有对象图始终都驻留在内存中,并且不需要持久化,那么该过程将很容易解决,因为无需创建映射层。 为了进一步阐述该概念,让我们尝试一些具体的代码示例。

运用领域模型 (Putting the Domain Model to Work)

If we plan to use a multi-tier perspective in the design of the blog, the application could be easily broken down into well-defined layers. The first layer would be the typical bootstrap, which would simply include and initialize an autoloader, like this:

如果我们打算在博客设计中使用多层视角,则可以轻松地将应用程序分解为定义明确的层。 第一层是典型的引导程序,它仅包含并初始化自动加载器,如下所示:

<?php require_once __DIR__ . "/Autoloader.php"; $autoloader = new Autoloader(); $autoloader->register();

The bootstrap doesn’t doing anything weird; it just loads and registers a PSR-0 compliant autoloader.

引导程序不会做任何奇怪的事情; 它只是加载并注册符合PSR-0的自动加载器。

The second layer would be the home to our domain model, which at its most basic level could be implemented as follows:

第二层将是我们的域模型的所在地,在最基本的层次上,它可以按以下方式实现:

<?php use ModelPost, ModelComment, ModelUser; // create some posts $postOne = new Post( "Welcome to SitePoint", "To become yourself a true PHP master, yeap you must first master PHP."); $postTwo = new Post( "Welcome to SitePoint (Reprise)", "To become yourself a PHP Master, yeap you must first master... Wait! Did I post that already?"); // create a user $user = new User( "Everchanging Joe", "joe@hisdomain.com"); // add some comments to the first post $postOne->comments = array( new Comment( "I just love this post! Looking forward to seeing more of this stuff.", $user), new Comment( "I just changed my mind and dislike this post! Hope not seeing more of this stuff.", $user)); // add another comment to the second post $postTwo->comments = array( new Comment( "Not quite sure if I like this post or not, so I cannot say anything for now.", $user));

Even though model performs only a few simple tasks, such as creating a couple of posts and binding some comments made by a rather irresolute guy, it comes in handy for showing how to wire the domain objects together and put them to work. In this case each object graph is spawned by using plain Dependency Injection, which is sufficient for demonstrative purposes.

尽管模型仅执行一些简单的任务,例如创建几个帖子并绑定一个相当不拘泥的家伙发表的一些评论,但是它对于显示如何将域对象连接在一起并使其工作变得非常有用。 在这种情况下,通过使用简单的依赖注入就可以生成每个对象图,这足以用于演示目的。

If the situation warrants, however, object graph creation should be delegated to more versatile structures, such as a Dependency Injection Container or a Service Locator. In either case, at this point the model is already doing its business as expected.

但是,如果情况允许,则应将对象图的创建委托给更多通用的结构,例如依赖注入容器或服务定位器。 无论哪种情况,在这一点上模型都已经按照预期进行了工作。

Let’s move on and create the blog’s application layer (the controllers in an MVC stack) which is responsible for pulling in model data and passing it to the presentational layer.

让我们继续并创建博客的应用程序层(MVC堆栈中的控制器),该层负责引入模型数据并将其传递到表示层。

Here’s how this tier looks:

这是此层的外观:

<?php $posts = array($postOne, $postTwo);

Could this layer be any simpler or shorter? I don’t think so.

这层可以更简单或更短吗? 我不这么认为。

Leaving all mockery aside, it demonstrates in a nutshell that a Domain Model is, quite possibly, the most glaring example of the Fat Models/Skinny Controllers mantra in action. Having all the emphasis placed on business logic, controllers are naturally diminished to the realm of simple mediators between the model and the user interface.

撇开所有嘲弄,它简而言之证明了领域模型很可能是实际中Fat模型/ Skinny Controllers口头禅的最明显例子。 由于所有重点都放在业务逻辑上,因此控制器自然会减少到模型和用户界面之间的简单中介者的领域。

Now that our blog’s application layer is rolling smoothly, let’s make create the layer that dumps the previous blog posts to screen. As you might expect, this is a dull HTML template containing just a few PHP loops:

现在我们博客的应用程序层正在平稳滚动,让我们创建一个将以前的博客文章转储到屏幕上的层。 如您所料,这是一个枯燥HTML模板,仅包含几个PHP循环:

<!doctype html> <html> <head> <meta charset="utf-8"> <title>Building a Domain Model in PHP</title> </head> <body> <header> <h1>SitePoint.com</h1> </header> <section> <ul> <?php foreach ($posts as $post) { ?> <li> <h2><?php echo $post->title;?></h2> <p><?php echo $post->content;?></p> <?php if ($post->comments) { ?> <ul> <?php foreach ($post->comments as $comment) { ?> <li> <h3><?php echo $comment->user->name;?> says:</h3> <p><?php echo $comment->content;?></p> </li> <? } ?> </ul> <?php } ?> </li> <?php } ?> </ul> </section> </body> </html>

Given the banal responsibility of the blog’s presentation layer, I decided to implement it by using just PHP’s native templating capabilities. Regardless, the most relevant thing to stress here is the fact that hooking up our domain model to different layers was indeed a breeze to achieve. Best of all, the entire implementation didn’t require to tie up the model in question to any form of persistence infrastructure, therefore turning it into an easily portable and scalable creature!

考虑到博客表示层的日常责任,我决定仅使用PHP的本机模板功能来实现它。 无论如何,最需要强调的是,将我们的领域模型连接到不同的层确实很容易实现。 最重要的是,整个实现不需要将模型与任何形式的持久性基础设施捆绑在一起,因此可以将其转变为易于移植和可扩展的生物!

Does this mean that a Domain Model is the panacea for all the flaws that a Database Model exposes behind the scenes? Well, in a sense it is, even with some caveats. As noted at the beginning, the biggest caveat is the hassle of having to map domain objects back and forward to the persistence layer, something that can’t be accomplished in a heartbeat unless we appeal to the goodies of a third-party ORM like Doctrine, RedBeanPHP, or something along those lines.

这是否意味着域模型是数据库模型在幕后暴露的所有缺陷的灵丹妙药? 好吧,从某种意义上讲,即使有一些警告也是如此。 如开始时所指出的,最大的警告是必须将域对象前后映射到持久层的麻烦,除非我们吸引第三方Doctor(如Doctrine)的好礼,否则这是无法实现的, RedBeanPHP或类似的东西。

As usual, choosing between a prepackaged data mapper or a custom one is just a matter of personal requirements and taste. As the French philosopher Jean Paul Sartre said once, “men are condemned to be free”. So, use your freedom consciously and pick up the mapping library that suits your needs best.

像往常一样,在预包装的数据映射器或自定义的数据映射器之间进行选择只是个人需求和品味的问题。 正如法国哲学家让·保罗·萨特(Jean Paul Sartre)曾经说过的那样,“人们被谴责为自由”。 因此,有意识地使用您的自由并选择最适合您需求的映射库。

最后的想法 (Final Thoughts)

With a huge number of HTTP frameworks gaining momentum in the PHP world (like Symfony 2.x, Aura, and even Zend Framework) which don’t provide users with a base Model up front (or worse, provide the infamous Database Model), hopefully we’ll see in the near future more advocates of rich Domain Models. In the interim, it’s pretty healthy to take an in-depth look at them and see how to implement a trivial one from scratch, as we just did before.

随着大量的HTTP框架在PHP世界中蓬勃发展(例如Symfony 2.x , Aura甚至Zend Framework ),这些框架没有预先为用户提供基础模型(或更糟糕的是,提供了臭名昭著的数据库模型),希望在不久的将来,我们会看到更多的支持丰富域模型的人。 在此期间,像我们之前所做的那样,深入研究它们并了解如何从头实现一个琐碎的事情是非常健康的。

In a future article, I hope to dig even deeper into the nitty-gritty of Domain Models and developing a custom mapping layer to demonstrate how to transfer data between the previous model and MySQL. Stay tuned!

在以后的文章中,我希望进一步深入研究“领域模型”的本质,并开发一个自定义映射层,以演示如何在先前模型和MySQL之间传输数据。 敬请关注!

Image via kentoh/ Shutterstock

图片来自kentoh / Shutterstock

翻译自: https://www.sitepoint.com/building-a-domain-model/

持久化模型

相关资源:25个经典网站源代码
最新回复(0)