核心要点
许多人可能怀疑继承和多态性在面向对象设计中的相关性吗?可能很少,大多数可能是由于无知或思维狭隘。但这里有一个小问题不容忽视。虽然理解继承的逻辑很简单,但在深入研究多态性的细节时,事情就变得更加困难了。“多态性”这个术语本身就令人望而生畏,其学术定义充满了多种不同的观点,这使得更难以理解其背后的实际内容。诸如参数多态性和特设多态性之类的周边概念(通常通过方法覆盖/重载实现)在某些编程语言中确实有其显著的应用领域,但在设计能够消费特定契约(读取抽象)的可扩展系统时,应该放弃最后一种情况,而无需检查实现者是否属于预期的类型。简而言之,大多数时候,在面向对象编程中对多态性的任何通用引用都被隐含地认为是系统公开的一种能力,该能力用于定义一组契约或接口,而这些契约或接口又由不同的实现来遵守。这种“规范的”多态性通常被称为子类型多态性,因为接口的实现者被认为是它们的子类型,无论是否存在实际的层次结构。正如人们可能预期的那样,理解多态性的本质只是学习过程的一半;另一半当然是演示如何设计多态系统,使其能够适应相当现实的情况,而不会陷入仅仅展示“一些漂亮的教学代码”(在许多情况下,这是玩具代码的廉价委婉说法)的陷阱。在本文中,我将向您展示如何通过开发一个可插入的缓存组件来利用多态性提供的优点。核心功能以后可以扩展以满足您的需求,方法是开发额外的缓存驱动程序。
定义组件的接口和实现
构建可扩展缓存组件时,可供选择的选项菜单绝非匮乏(如果您对此表示怀疑,只需看看一些流行框架背后的情况)。但是,在这里,我提供的组件具有在运行时交换不同缓存驱动程序的巧妙能力,而无需修改任何客户端代码。那么,如何在开发过程中不费太多力气就能做到这一点呢?嗯,第一步应该是……是的,定义一个隔离的缓存契约,稍后将由不同的实现来遵守,从而利用多态性的好处。在其最基本的层面上,上述契约如下所示:
<code class="language-php"><?php namespace LibraryCache; interface CacheInterface { public function set($id, $data); public function get($id); public function delete($id); public function exists($id); }</code>
CacheInterface
接口是一个骨架契约,它抽象了通用缓存元素的行为。有了接口,就可以轻松创建一些符合其契约的具体缓存实现。由于我想保持简洁易懂,我设置的缓存驱动程序将只是一个精简的二重奏:第一个使用文件系统作为缓存/获取数据的底层后端,而第二个在幕后使用 APC 扩展。以下是基于文件的缓存实现:
<code class="language-php"><?php namespace LibraryCache; class FileCache implements CacheInterface { const DEFAULT_CACHE_DIRECTORY = 'Cache/'; private $cacheDir; public function __construct($cacheDir = self::DEFAULT_CACHE_DIRECTORY) { $this->setCacheDir($cacheDir); } public function setCacheDir($cacheDir) { if (!is_dir($cacheDir)) { if (!mkdir($cacheDir, 0644)) { throw InvalidArgumentException('The cache directory is invalid.'); } } $this->cacheDir = $cacheDir; return $this; } public function set($id, $data) { if (!file_put_contents($this->cacheDir . $id, serialize($data), LOCK_EX)) { throw new RuntimeException("Unable to cache the data with ID '$id'."); } return $this; } public function get($id) { if (!$data = unserialize(@file_get_contents($this->cacheDir . $id, false))) { throw new RuntimeException("Unable to get the data with ID '$id'."); } return $data; } public function delete($id) { if (!@unlink($this->cacheDir . $id)) { throw new RuntimeException("Unable to delete the data with ID '$id'."); } return $this; } public function exists($id) { return file_exists($this->cacheDir . $id); } }</code>
FileCache
类的驱动逻辑应该很容易理解。到目前为止,这里最相关的事情是它公开了一种整洁的多态行为,因为它忠实地实现了早期的 CacheInterface
。虽然这种能力很甜蜜迷人,但就其本身而言,考虑到这里的目标是创建一个能够在运行时切换后端的缓存组件,我不会为此大加赞赏。让我们为了教学目的而付出额外的努力,并使 CacheInterface
的另一个精简实现栩栩如生。下面的实现遵守接口的契约,但这次是通过使用 APC 扩展捆绑的方法:
<code class="language-php"><?php namespace LibraryCache; class ApcCache implements CacheInterface { public function set($id, $data, $lifeTime = 0) { if (!apc_store($id, $data, (int) $lifeTime)) { throw new RuntimeException("Unable to cache the data with ID '$id'."); } } public function get($id) { if (!$data = apc_fetch($id)) { throw new RuntimeException("Unable to get the data with ID '$id'."); } return $data; } public function delete($id) { if (!apc_delete($id)) { throw new RuntimeException("Unable to delete the data with ID '$id'."); } } public function exists($id) { return apc_exists($id); } }</code>
ApcCache
类不是您在职业生涯中见过的最炫的 APC 包装器,它打包了从内存中保存、检索和删除数据所需的所有功能。让我们为我们自己鼓掌,因为我们已经成功地实现了一个轻量级缓存模块,其具体的后台不仅可以由于其多态性而在运行时轻松交换,而且以后添加更多后台也极其简单。只需编写另一个符合 CacheInterface
的实现即可。但是,我应该强调的是,实际的子类型多态性是通过实现通过接口构造定义的契约来实现的,这是一种非常普遍的方法。但是,没有什么可以阻止您不那么正统,并通过切换一个声明为一组抽象方法的接口(位于抽象类中)来获得相同的结果。如果您感觉冒险并想走那条旁路,则可以如下重构契约和相应的实现:
<code class="language-php"><?php namespace LibraryCache; interface CacheInterface { public function set($id, $data); public function get($id); public function delete($id); public function exists($id); }</code>
<code class="language-php"><?php namespace LibraryCache; class FileCache implements CacheInterface { const DEFAULT_CACHE_DIRECTORY = 'Cache/'; private $cacheDir; public function __construct($cacheDir = self::DEFAULT_CACHE_DIRECTORY) { $this->setCacheDir($cacheDir); } public function setCacheDir($cacheDir) { if (!is_dir($cacheDir)) { if (!mkdir($cacheDir, 0644)) { throw InvalidArgumentException('The cache directory is invalid.'); } } $this->cacheDir = $cacheDir; return $this; } public function set($id, $data) { if (!file_put_contents($this->cacheDir . $id, serialize($data), LOCK_EX)) { throw new RuntimeException("Unable to cache the data with ID '$id'."); } return $this; } public function get($id) { if (!$data = unserialize(@file_get_contents($this->cacheDir . $id, false))) { throw new RuntimeException("Unable to get the data with ID '$id'."); } return $data; } public function delete($id) { if (!@unlink($this->cacheDir . $id)) { throw new RuntimeException("Unable to delete the data with ID '$id'."); } return $this; } public function exists($id) { return file_exists($this->cacheDir . $id); } }</code>
<code class="language-php"><?php namespace LibraryCache; class ApcCache implements CacheInterface { public function set($id, $data, $lifeTime = 0) { if (!apc_store($id, $data, (int) $lifeTime)) { throw new RuntimeException("Unable to cache the data with ID '$id'."); } } public function get($id) { if (!$data = apc_fetch($id)) { throw new RuntimeException("Unable to get the data with ID '$id'."); } return $data; } public function delete($id) { if (!apc_delete($id)) { throw new RuntimeException("Unable to delete the data with ID '$id'."); } } public function exists($id) { return apc_exists($id); } }</code>
从上到下,这确实是一种多态方法,它与之前讨论的方法针锋相对。就我个人而言,这只是我个人的说法,我更喜欢使用接口构造来定义契约,并且只在封装几个子类型共享的样板实现时才使用抽象类。您可以选择最适合您需求的方法。在这一点上,我可以放下帷幕,写一些花哨的结束评论,夸夸我们令人印象深刻的编码技巧,并吹嘘我们缓存组件的灵活性,但这将是对我们的怠慢。当存在一些能够消费多个实现的客户端代码时,多态性会展现出其最具诱惑力的方面,而无需检查这些实现是否是某种类型的实例,只要它们符合预期的契约即可。因此,让我们通过将缓存组件连接到一个基本的客户端视图类来揭示该方面,这将允许我们毫不费力地进行一些整洁的 HTML 缓存。
将缓存驱动程序投入使用
通过我们的示例缓存模块缓存 HTML 输出非常简单,我将在其他时间保存任何冗长的解释。整个缓存过程可以简化为一个简单的视图类,类似于以下这个:
<code class="language-php"><?php namespace LibraryCache; abstract class AbstractCache { abstract public function set($id, $data); abstract public function get($id); abstract public function delete($id); abstract public function exists($id); }</code>
<code class="language-php"><?php namespace LibraryCache; class FileCache extends AbstractCache { // the same implementation goes here }</code>
最炫的家伙是类的构造函数,它使用早期的 CacheInterface
的实现者,以及 render()
方法。由于最后一个方法的职责是在视图的模板被推送到输出缓冲区后缓存它,因此利用此能力并缓存整个 HTML 文档会非常不错。假设视图的默认模板具有以下结构:
<code class="language-php"><?php namespace LibraryCache; class ApcCache extends AbstractCache { // the same implementation goes here }</code>
现在,让我们玩得开心一些,通过向视图提供 ApcCache
类的实例来缓存文档:
<code class="language-php"><?php namespace LibraryView; interface ViewInterface { public function setTemplate($template); public function __set($field, $value); public function __get($field); public function render(); }</code>
很不错,对吧?但是等等!我太兴奋了,忘记提到上面的代码片段会在任何未安装 APC 扩展的系统上爆炸(调皮的系统管理员!)。这是否意味着精心制作的缓存模块不再可重用?这正是基于文件的驱动程序发挥作用的地方,它可以放入客户端代码中而不会收到任何投诉:
<code class="language-php"><?php namespace LibraryView; use LibraryCacheCacheInterface; class View implements ViewInterface { const DEFAULT_TEMPLATE = 'default'; private $template; private $fields = array(); private $cache; public function __construct(CacheInterface $cache, $template = self::DEFAULT_TEMPLATE) { $this->cache = $cache; $this->setTemplate($template); } public function setTemplate($template) { $template = $template . '.php'; if (!is_file($template) || !is_readable($template)) { throw new InvalidArgumentException( "The template '$template' is invalid."); } $this->template = $template; return $this; } public function __set($name, $value) { $this->fields[$name] = $value; return $this; } public function __get($name) { if (!isset($this->fields[$name])) { throw new InvalidArgumentException( "Unable to get the field '$field'."); } return $this->fields[$name]; } public function render() { try { if (!$this->cache->exists($this->template)) { extract($this->fields); ob_start(); include $this->template; $this->cache->set($this->template, ob_get_clean()); } return $this->cache->get($this->template); } catch (RuntimeException $e) { throw new Exception($e->getMessage()); } } }</code>
上面的单行代码明确声明视图将使用文件系统而不是共享内存来缓存其输出。这种动态切换缓存后端简明地说明了为什么多态性在设计高度解耦的模块时如此重要。它允许我们在运行时轻松地重新连接事物,而不会将脆弱性/刚性相关的伪影传播到我们系统的其他部分。
结束语
在使理解该概念变得难以捉摸的大量正式定义的压迫下,多态性确实是生活中那些美好的事物之一,一旦您理解了它,就会让您想知道您如何在没有它的情况下继续这么长时间。多态系统本质上更正交、更容易扩展,并且不太容易违反开放/封闭原则和明智的“面向接口编程”原则等核心范例。尽管相当原始,但我们的缓存模块是这些优点的突出示例。如果您尚未重构您的应用程序以利用多态性带来的好处,那么您最好快点,因为您错过了大奖!图片来自 Fotolia
关于子类型多态性的常见问题解答 (FAQ)
子类型多态性,也称为包含多态性,是一种多态性形式,其中一个名称表示许多不同类别的实例,这些类别通过某个公共超类相关联。另一方面,参数多态性允许函数或数据类型以相同的方式处理值,而无需依赖其类型。参数多态性是一种使语言更具表达力同时保持完全静态类型安全性的方法。
在 Java 中,子类型多态性是通过使用继承和接口来实现的。超类引用变量可以指向子类对象。这允许 Java 在运行时决定调用哪个方法,这被称为动态方法调度。它是 Java 的强大功能之一,使它能够支持动态多态性。
当然,让我们考虑一下 Java 中的一个简单示例。假设我们有一个名为“Animal”的超类和两个子类“Dog”和“Cat”。“Dog”和“Cat”类都重写了“Animal”类的“sound”方法。现在,如果我们创建一个“Animal”引用指向“Dog”或“Cat”对象并调用“sound”方法,Java 将在运行时决定调用哪个类的“sound”方法。这是一个子类型多态性的例子。
子类型多态性是面向对象编程的一个基本方面。它允许代码的灵活性和可重用性。使用子类型多态性,您可以为一组类设计一个通用接口,然后使用此接口以统一的方式与这些类的对象交互。这将导致更简洁、更直观和更易于维护的代码。
Liskov 替换原则 (LSP) 是面向对象设计的一个原则,它指出,如果程序正在使用基类,则它应该能够使用任何其子类,而无需程序知道它。换句话说,超类的对象应该能够被子类的对象替换,而不会影响程序的正确性。子类型多态性是 LSP 的直接应用。
不,并非所有编程语言都支持子类型多态性。它主要是静态类型面向对象编程语言(如 Java、C 和 C#)的一个特性。像 Python 和 JavaScript 这样的动态类型语言具有不同形式的多态性,称为鸭子类型。
静态多态性,也称为编译时多态性,是通过方法重载实现的。关于调用哪个方法的决定是在编译时做出的。另一方面,动态多态性,也称为运行时多态性,是通过方法重写实现的。关于调用哪个方法的决定是在运行时做出的。子类型多态性是一种动态多态性。
向上转换是将派生类对象视为基类对象的过程。它是子类型多态性的一个关键方面。当您向上转换派生类对象时,您可以调用基类中定义的任何方法。但是,如果该方法在派生类中被重写,则将调用重写版本。
向下转换与向上转换相反。它是将超类对象转换为子类的过程。当您需要访问仅存在于子类中的方法时,可以使用向下转换。但是,向下转换可能很危险,因为它如果被转换的对象实际上不具有您要转换到的类型,则可能导致 ClassCastException。
子类型多态性允许我们编写更通用和可重用的代码。通过使用超类引用来与子类对象交互,我们可以编写适用于各种对象的代码,只要它们都属于同一个超类的子类即可。这意味着我们可以添加新的子类,而无需更改使用超类的代码,这使得我们的代码更灵活、更容易维护。
以上是亚型多态性 - 运行时交换实现的详细内容。更多信息请关注PHP中文网其他相关文章!