核心要点
虽然并非完全符合规范,但可以说正交性是基于“良好设计”原则的软件系统的精髓,其中各个模块彼此解耦,使系统不易出现僵化和脆弱的问题。当然,谈论正交系统的优点比实际运行生产系统更容易;这个过程通常是追求乌托邦。即使如此,在系统中实现高度解耦的组件绝非乌托邦的概念。多态性等多种编程概念允许设计灵活的程序,其各个部分可以在运行时切换,其依赖关系可以以抽象的形式表达,而不是具体的实现。我想说,无论我们是在实现基础设施还是应用程序逻辑,“面向接口编程”的旧格言随着时间的推移已经得到了普遍的采用。然而,当踏上领域模型的领域时,情况就大相径庭了。坦率地说,这是一个可预测的场景。毕竟,为什么一个相互关联的对象网络(其数据和行为受限于明确定义的业务规则)应该是多态的呢?它本身并没有多大意义。不过,此规则有一些例外,最终可能适用于这种情况。第一个例外是使用虚拟代理,它有效地与实际领域对象实现的接口相同。另一个例外是所谓的“空情况”,这是一种特殊情况,其中操作最终可能分配或返回空值,而不是填充良好的实体。在传统的非多态方法中,模型的使用者必须检查这些“有害的”空值并优雅地处理该条件,从而在整个代码中产生条件语句的爆炸。幸运的是,只需创建领域对象的多分支实现即可轻松解决这种看似混乱的情况,该实现将实现与所讨论对象相同的接口,只是其方法不会执行任何操作,因此将客户端代码从在执行操作时重复检查难看的空值中解脱出来。毫不奇怪,这种方法是一种称为空对象的设计模式,它将多态性的优势发挥到极致。在本文中,我将演示该模式在几种情况下的好处,并向您展示它们如何紧密地依附于多态方法。
处理非多态条件
正如人们所料,在展示空对象模式的优点时,有几种方法可以尝试。我发现一种特别直接的方法是实现一个数据映射器,它最终可能从通用查找器返回空值。假设我们已经成功创建了一个骨架领域模型,该模型仅由一个用户实体组成。接口及其类如下所示:
<code class="language-php"><?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(); }</code>
<code class="language-php"><?php namespace Model; class User implements UserInterface { private $id; private $name; private $email; public function __construct($name, $email) { $this->setName($name); $this->setEmail($email); } 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 throw new InvalidArgumentException( "The ID for this user is invalid."); } $this->id = $id; return $this; } public function getId() { return $this->id; } public function setName($name) { if (strlen($name) 30) { throw new InvalidArgumentException( "The user name is invalid."); } $this->name = $name; 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; } }</code>
User 类是一个反应式结构,它实现了一些 mutators/accessors 来定义一些用户的数据和行为。有了这个构造的领域类,我们现在可以更进一步,定义一个基本的数据映射器,它将我们的领域模型和数据访问层彼此隔离。
<code class="language-php"><?php namespace ModelMapper; use LibraryDatabaseDatabaseAdapterInterface, ModelUser; class UserMapper implements UserMapperInterface { private $adapter; public function __construct(DatabaseAdapterInterface $adapter) { $this->adapter = $adapter; } public function fetchById($id) { $this->adapter->select("users", array("id" => $id)); if (!$row = $this->adapter->fetch()) { return null; } return $this->createUser($row); } private function createUser(array $row) { $user = new User($row["name"], $row["email"]); $user->setId($row["id"]); return $user; } }</code>
首先应该出现的是映射器的 fetchById() 方法是块中的淘气鬼,因为它在数据库中没有用户与给定的 ID 匹配时有效地返回 null。出于显而易见的原因,这个笨拙的条件使得客户端代码每次调用映射器的查找器时都必须费力地检查空值。
<code class="language-php"><?php use LibraryLoaderAutoloader, LibraryDatabasePdoAdapter, ModelMapperUserMapper; require_once __DIR__ . "/Library/Loader/Autoloader.php"; $autoloader = new Autoloader; $autoloader->register(); $adapter = new PdoAdapter("mysql:dbname=test", "myusername", "mypassword"); $userMapper = new UserMapper($adapter); $user = $userMapper->fetchById(1); if ($user !== null) { echo $user->getName() . " " . $user->getEmail(); }</code>
乍一看,这不会有什么问题,只要在一个地方进行检查即可。但是,如果相同的行出现在多个页面控制器或服务层中,您会不会撞到砖墙上?在你意识到之前,映射器返回的看似无辜的 null 会产生大量重复的条件,这是设计不良的凶兆。
从客户端代码中删除条件语句
然而,无需焦虑,因为这正是空对象模式显示多态性为何是天赐之物的情况。如果我们想一劳永逸地摆脱那些讨厌的条件语句,我们可以实现前面 User 类的多态版本。
<code class="language-php"><?php namespace Model; class NullUser implements UserInterface { public function setId($id) { } public function getId() { } public function setName($name) { } public function getName() { } public function setEmail($email) { } public function getEmail() { } }</code>
如果您期望一个完整的实体类打包各种各样的装饰,那么恐怕您会非常失望。“null”版本的实体符合相应的接口,但方法是空包装器,没有实际的实现。虽然 NullUser 类的存在显然没有给我们带来任何值得称赞的有用东西,但它是一个简洁的结构,允许我们将所有之前的条件语句扔进垃圾桶。想看看它是如何实现的吗?首先,我们应该做一些前期工作并重构数据映射器,以便它的查找器返回一个空用户对象而不是空值。
<code class="language-php"><?php namespace ModelMapper; use LibraryDatabaseDatabaseAdapterInterface, ModelUser, ModelNullUser; class UserMapper implements UserMapperInterface { private $adapter; public function __construct(DatabaseAdapterInterface $adapter) { $this->adapter = $adapter; } public function fetchById($id) { $this->adapter->select("users", array("id" => $id)); return $this->createUser($this->adapter->fetch()); } private function createUser($row) { if (!$row) { return new NullUser; } $user = new User($row["name"], $row["email"]); $user->setId($row["id"]); return $user; } }</code>
映射器的 createUser() 方法隐藏了一个微小的条件,因为它现在负责在传递给查找器的 ID 没有返回有效用户时创建一个空用户。即便如此,这种细微的代价不仅可以节省客户端代码进行大量重复检查的工作,而且还可以将其变成一个宽松的使用者,当它必须处理空用户时不会抱怨。
<code class="language-php"><?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(); }</code>
这种多态方法的主要缺点是,任何使用它的应用程序都将变得过于宽松,因为它在处理无效实体时永远不会崩溃。在最坏的情况下,用户界面只会显示一些空白行,但没有任何真正嘈杂的东西让我们感到厌恶。当扫描早期 NullUser 类的当前实现时,这一点尤其明显。即使是可行的,更不用说推荐的了,在保持其多态性不变的同时,也可以在空对象中封装逻辑。我什至可以说,空对象非常适合封装默认数据和行为,这些数据和行为应该只在少数特殊情况下向客户端代码公开。如果您足够雄心勃勃并且想使用简单的空用户对象尝试这个概念,那么当前的 NullUser 类可以按以下方式重构:
<code class="language-php"><?php namespace Model; class User implements UserInterface { private $id; private $name; private $email; public function __construct($name, $email) { $this->setName($name); $this->setEmail($email); } 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 throw new InvalidArgumentException( "The ID for this user is invalid."); } $this->id = $id; return $this; } public function getId() { return $this->id; } public function setName($name) { if (strlen($name) 30) { throw new InvalidArgumentException( "The user name is invalid."); } $this->name = $name; 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; } }</code>
增强的 NullUser 版本比其安静的前身略微更具表达性,因为它的 getter 提供了一些基本的实现,以便在请求无效用户时返回一些默认消息。虽然微不足道,但这项更改对客户端代码处理空用户的方式产生了积极的影响,因为这次使用者至少清楚地知道当他们试图从存储中提取不存在的用户时出现了一些问题。这是一个不错的突破,它不仅展示了如何实现实际上根本不是空的空对象,还展示了根据特定需求在相关对象内部移动逻辑是多么容易。
结束语
有些人可能会说,实现空对象很麻烦,尤其是在 PHP 中,OOP 的核心概念(如多态性)被明显低估了。他们在某种程度上是对的。尽管如此,逐步采用值得信赖的编程原则和设计模式,以及该语言对象模型目前达到的成熟度水平,为稳步前进和开始使用一些不久前被认为是复杂、不切实际的概念的“奢侈品”提供了所有必要的基础。空对象模式属于此类别,但其实现非常简单和优雅,以至于在清除客户端代码中的重复空值检查时很难不觉得它很吸引人。 图片来自 Fotolia
(由于篇幅限制,此处省略了原文中的FAQ部分。)
以上是PHP主|零对象模式 - 域模型中的多态性的详细内容。更多信息请关注PHP中文网其他相关文章!