核心要点
软件编程是艺术(有时是即兴创作的委婉说法)和许多行之有效的启发式方法的平衡组合,用于解决某些问题并以体面的方式解决它们。几乎没有人会不同意,艺术方面是迄今为止最难磨练和提炼的方面。另一方面,驾驭启发式方法背后的力量对于能够开发基于良好设计的软件至关重要。由于有如此多的启发式方法说明软件系统应该如何以及为什么应该坚持特定方法,因此在 PHP 世界中没有看到更广泛地实施它们,这令人相当失望。例如,迪米特法则可能是该语言领域中最被低估的法则之一。实际上,该法则的“只与你的密友交谈”的格言在 PHP 中似乎还处于相当不成熟的状态,这导致了几个面向对象代码库整体质量的下降。一些流行的框架正在积极推动它向前发展,试图更加遵守该法则的戒律。为违反迪米特法则而互相指责是没有意义的,因为减轻此类破坏的最佳方法是简单地采取务实态度,并了解该法则下的实际内容,从而在编写面向对象代码时有意识地应用它。为了加入正义事业,并从实践的角度更深入地研究该法则,在接下来的几行中,我将通过一些实践示例来演示,为什么像遵守该法则的原则这样简单的事情在设计松耦合软件模块时可以真正提升效率。
了解过多并非好事
通常被称为最少知识原则,迪米特法则所推崇的规则很容易理解。简单地说,假设您有一个精心设计的类,它实现了一种给定的方法,那么该方法应该被限制为调用属于以下对象的其他方法:
尽管该列表远非正式(对于更正式的列表,请查看维基百科),但这些要点很容易理解。在传统设计中,一个对象对另一个对象了解太多(这隐含地包括知道如何访问第三个对象)被认为是错误的,因为在某些情况下,对象必须不必要地从上到下遍历笨拙的中间体才能找到其需要按预期工作所需的实际依赖项。出于显而易见的原因,这是一个严重的设计缺陷。调用者对中间体的内部结构有相当广泛和详细的了解,即使这是通过几个 getter 访问的。此外,使用中间对象来获取调用者所需的对象本身就说明了一个问题。毕竟,如果可以通过直接注入依赖项来实现相同的结果,为什么还要使用如此复杂的路径来获取依赖项或调用其方法之一呢?这个过程根本没有任何意义。
让我们假设我们需要构建一个文件存储模块,该模块在内部使用多态编码器将数据拉入并保存到给定的目标文件中。如果我们故意马虎地将模块连接到可注入的服务定位器,则其实现将如下所示:
<?php namespace LibraryFile; use LibraryDependencyInjectionServiceLocatorInterface; class FileStorage { const DEFAULT_STORAGE_FILE = "data.dat"; private $locator; private $file; public function __construct(ServiceLocatorInterface $locator, $file = self::DEFAULT_STORAGE_FILE) { $this->locator = $locator; $this->setFile($file); } public function setFile($file) { if (!is_readable($file) || !is_writable($file)) { throw new InvalidArgumentException( "The target file is invalid."); } $this->file = $file; return $this; } public function write($data) { try { return file_put_contents($this->file, $this->locator->get("encoder")->encode($data), LOCK_EX); } catch (Exception $e) { throw new $e( "Error writing data to the target file: " . $e->getMessage()); } } public function read() { try { return $this->locator->get("encoder")->decode( @file_get_contents($this->file)); } catch(Exception $e) { throw new $e( "Error reading data from the target file: " . $e->getMessage()); } } }
省略一些不相关的实现细节,重点是 FileStorage 类的构造函数及其 write() 和 read() 方法。该类注入一个尚未定义的服务定位器的实例,稍后用于获取依赖项(前面提到的编码器),以便在目标文件中获取和存储数据。考虑到该类首先遍历定位器,然后到达编码器,这通常是违反迪米特法则的行为。调用者 FileStorage 对定位器的内部结构了解太多,包括如何访问编码器,这绝对不是我会赞扬的能力。它是一种内在地根植于服务定位器(这就是为什么有些人将其视为反模式)或任何其他类型的静态或动态注册表的本质的工件,这是我之前指出的。为了更全面地了解这个问题,让我们检查一下定位器的实现:
(此处省略了locator和encoder的代码,因为与上一个输出一致,为了避免重复,此处不再赘述。)
有了编码器,现在让我们一起使用所有示例类来启动:
(此处省略了使用示例代码,因为与上一个输出一致,为了避免重复,此处不再赘述。)
该法则的违反在这种情况下是一个相当隐蔽的问题,很难从表面上追踪到,除了使用定位器的 mutator,这表明在某些时候,编码器将以某种形式被 FileStorage 的实例访问和使用。无论如何,我们知道违规行为就在那里隐藏在外部世界之外,这一事实不仅揭示了太多关于定位器结构的信息,而且还将 FileStorage 类不必要地耦合到定位器本身。只需遵守该法则的规则并摆脱定位器,我们就可以消除耦合,同时为 FileStorage 提供其开展业务所需的实际协作者。途中不再有笨拙、暴露的中间体!幸运的是,所有这些废话都可以通过一点点努力轻松地转换为可工作的代码。只需在此处查看增强的、符合迪米特法则的 FileStorage 类版本:
(此处省略了重构后的FileStorage代码,因为与上一个输出一致,为了避免重复,此处不再赘述。)
这确实很容易重构。现在,该类直接使用 EncoderInterface 接口的任何实现者,避免遍历不必要的中间体的内部结构。该示例无疑是微不足道的,但它确实说明了一个有效点,并演示了为什么遵守迪米特法则的戒律是您可以做的最好的事情之一,以改进类的设计。但是,罗伯特·马丁的著作《代码整洁之道:敏捷软件开发手册》中深入探讨了该法则的一个特例,值得特别分析。请花一点时间仔细考虑一下:如果 FileStorage 被定义为通过数据传输对象 (DTO) 获取其协作者,会发生什么情况?
(此处省略了使用DTO的代码示例,因为与上一个输出一致,为了避免重复,此处不再赘述。)
这绝对是一种有趣的实现文件存储类的方法,因为它现在使用可注入的 DTO 来在内部传输和使用编码器。需要回答的问题是这种方法是否真的违反了该法则。从纯粹主义的角度来看,它确实违反了,因为 DTO 无疑是一个向调用者公开其整个结构的中间体。但是,DTO 只是一种普通的数据结构,与早期的服务定位器不同,它根本没有任何行为。而数据结构的目的正是……是的,公开其数据。这意味着,只要中间体不实现行为(这与常规类的行为完全相反,因为它公开行为而隐藏其数据),迪米特法则就会保持完整。以下代码片段显示了如何使用有问题的 DTO 来使用 FileStorage:
(此处省略了使用DTO的代码示例,因为与上一个输出一致,为了避免重复,此处不再赘述。)
这种方法比直接将编码器传递到文件存储类中要麻烦得多,但该示例表明,一些乍一看似乎是公然违反该法则的棘手实现,通常是相当无害的,只要它们使用没有任何附加行为的数据结构即可。
结束语
由于各种复杂、有时是深奥的启发式方法在 OOP 中流行,因此添加另一个显然对层组件的设计没有任何明显积极影响的原则似乎毫无意义。然而,迪米特法则绝不是一个在现实世界中几乎没有应用的原则。尽管名称华丽,但迪米特法则是一个强大的范例,其主要目标是通过消除任何不必要的中间体来促进高度解耦的应用程序组件的实现。只需遵循其戒律,当然不要盲目教条主义,您就会看到代码质量的提高。保证。
(此处省略了FAQs部分,因为与上一个输出一致,为了避免重复,此处不再赘述。)
以上是PHP主| Demeter法律简介的详细内容。更多信息请关注PHP中文网其他相关文章!