parshikovpavel/final-keyword

从habr.com上的文章中考察的示例

0.0.1 2019-12-16 12:03 UTC

This package is auto-updated.

Last update: 2024-09-09 08:17:42 UTC


README

该仓库包含从habr.com上的文章学习代码示例

使用以下composer命令获取源代码示例

composer create-project parshikovpavel/final-keyword dir/

根据文章结构,源代码已进行分隔。

为方便起见,以下提供了源代码文件链接以及使用示例,用于重现文章中的案例。这些信息也已根据文章结构进行分隔。

简介

CommentBlock类的初始版本。

继承问题

继承违反了信息隐藏原则

CommentBlock父类和CustomCommentBlock子类展示了违反信息隐藏原则。

香蕉猴丛林问题

BlockCommentBlockPopularCommentBlockCachedPopularCommentBlock类是深层继承层次结构的示例。

默认开启开放递归

CommentComment::getComment()CustomCommentBlock::getComment()有不同的行为实现。CommentBlock::getComments()方法通过调用$this->getComments()进行自我调用,并依赖于CommentBlock类中的行为实现。

CommentBlock::getComments()的实现,这是自动继承自CustomCommentBlock的,与CustomCommentBlock::getComment()方法的行为不兼容。因此,获取评论列表的行为不正确。您可以通过执行以下测试来查看此问题

$ vendor/bin/phpunit tests/InheritanceIssues/OpenRecursionByDefault/CustomCommentBlockTest.php --testdox

ppFinal\InheritanceIssues\OpenRecursionByDefault\CustomCommentBlock
 ✘ Returns correct list of comments
   │
   │ Failed asserting that two arrays are equal.
   │ --- Expected
   │ +++ Actual
   │ @@ @@
   │  Array (
   │ -    0 => ppFinal\Comment Object (...)
   │ -    1 => ppFinal\Comment Object (...)
   │ +    0 => null
   │ +    1 => null
   │  )

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

副作用控制

CountingCommentBlock 是一种特定的 CommentBlock 类型,用于在 PSR-16 兼容的缓存中统计特定评论的浏览量。由于 CountingCommentBlock::viewComment() 方法会在缓存中增加计数器值,因此具有副作用。而 CommentBlock::viewComments() 方法将评论浏览量合并为单个浏览量,并且其实现被 CountingCommentBlock 正确继承。然而,这个继承的实现并没有考虑到 CountingCommentBlock 在缓存中统计评论浏览量的责任。因此,在调用 CountingCommentBlock::viewComments() 时,视图计数器无法正确工作。下面的 测试结果 展示了这一点。

$ vendor/bin/phpunit tests/InheritanceIssues/ControlOfSideEffects/CountingCommentBlockTest.php --testdox

ppFinal\InheritanceIssues\ControlOfSideEffects\CountingCommentBlock
 ✘ Counts views of comment
   │
   │ Failed asserting that null matches expected 1.
   │

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

基类脆弱性

可继承的基类是“脆弱的”,因为对它的看似安全的修改可能会使派生类出现故障。程序员不能仅通过单独检查基类的方法来确定基类更改是否安全。因此,基类和派生类的实现细节变得紧密相关。

例如,在代码重构期间,程序员在 CommentBlock::viewComments() 方法中将 一行代码 进行了更改,以简化代码并避免未来的代码重复。

$view .= $comment->view();   ------>   $view .= $this->viewComment($key);

基类逻辑仍然有效,并且它继续成功通过测试。然而,基类并不完全隔离。因此,调用 CountingCommentBlock::viewComments() 会导致视图计数器值的双重增加。您可以通过研究 相应的测试 详细了解该问题。

$ vendor/bin/phpunit tests/InheritanceIssues/BaseClassFragility/CountingCommentBlockTest.php --testdox

ppFinal\InheritanceIssues\BaseClassFragility\CountingCommentBlock
 ✘ Counts views of comment
   │
   │ Failed asserting that 2 matches expected 1.
   │
   │

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

使用 final 关键字改进设计

模板方法模式

CommentBlock 是一个抽象超类,它定义了子类的骨架。

CommentBlock::viewComments() 是一个最终模板方法,它被子类继承,不能被它们覆盖。它调用抽象的 CommentBlock::viewComment() 方法,而具体的实现由子类提供。

最终的 SimpleCommentBlock 类实现了 SimpleCommentBlock::viewComment() 方法,它只是返回评论的字符串视图。

最终的CountingCommentBlock类在CountingCommentBlock::viewComment()中实现了不同的行为。除了返回注释的字符串视图外,此方法还会增加缓存中的计数器值。

优先实现接口而非继承

让我们通过实现细节避免任何类耦合。

CommentBlock是一个接口,它定义了契约并隐藏了实现细节。

SimpleCommentBlockCountingCommentBlock是实现了此接口的final类,但它们之间没有直接关联。作为缺点,这些类有相同的重复实现viewComments()方法。

优先使用聚合而非继承

聚合是最松散的关系类型。让我们使用装饰器模式的形式使用聚合来替换继承。

CommentBlock是一个接口,它定义了契约并隐藏了实现细节。

SimpleCommentBlock是一个基final类,它实现了提到的接口,并具有基行为。

CountingCommentBlock类似于子类。它存储了对装饰对象的引用,将所有调用转发给它,并实现了额外的行为。《CountingCommentBlock::viewComment()》和《CountingCommentBlock::viewComments()》增加了缓存功能。《CountingCommentBlock::getCommentKeys()》是一个简单的单行函数,它只是将执行责任传递给嵌套对象。

SimpleCommentBlockCountingCommentBlock通过实现《CommentBlock》接口保持了多态行为。客户端可以通过接口透明地与他们交互。

SimpleCommentBlockCountingCommentBlock 通过聚合耦合。因此,它们避免了继承的所有缺点:基类脆弱性问题、违反信息隐藏原则等。改变基类的实现细节不会影响派生类的行为和结构。如下所示,所有验证 CountingCommentBlock 行为的断言都成功。

$ vendor/bin/phpunit tests/ApplyingFinalKeyword/PreferAggregation/CountingCommentBlockTest.php --testdox

ppFinal\ApplyingFinalKeyword\PreferAggregation\CountingCommentBlock
 ✔ Counts views of comment

OK (1 test, 2 assertions)

一个类必须为继承做好准备

继承违反了信息隐藏原则。因此,在 PHPDoc 中不仅要记录公共接口,还要记录内部实现细节。

CommentBlock::viewComment() 是一个非最终方法,允许重写实现。因此,此方法的 PHPDoc 描述了参数的使用和现有的副作用。

CommentBlock::viewComments() 是一个最终方法,但它使用了上述非最终方法。在子类中重写非最终 CommentBlock::viewComment() 方法会影响最终继承的 CommentBlock::viewComments() 方法的行为。因此,它的 PHPDoc 揭示了使用所有非最终方法的模式。

一个类必须为聚合做好准备

创建松散耦合设计的通用方案包括以下步骤

  1. 引入一个初始类(SimpleCommentBlock),使用 final 关键字和继承限制。
  2. 为了扩展类功能,需要分析基类行为,形成其合约并以接口的形式正式描述它(CommentBlock)。
  3. 引入一个派生装饰器类(CountingCommentBlock),它扩展了基类的功能并实现了相同的接口。通过接口(CommentBlock)将基类的实例(SimpleCommentBlock)注入到派生类(CountingCommentBlock)的构造函数中。

在测试中使用 final 类

大多数单元测试库使用继承来构建测试双胞胎(存根、模拟等)。因此,尝试在 SimpleCommentBlock 最终类中创建一个 PHPUnit 测试

$mock = $this->createMock(SimpleCommentBlock::class)

将导致如下警告

$ vendor/bin/phpunit tests/ApplyingFinalKeyword/UsingFinalClassesInTests/SimpleCommentBlockTest.php --filter testCreatingTestDouble --testdox

ppFinal\ApplyingFinalKeyword\UsingFinalClassesInTests\SimpleCommentBlock
 ✘ Creating test double
   │
   │ Class "ppFinal\ApplyingFinalKeyword\UsingFinalClassesInTests\SimpleCommentBlock" is declared "final" and cannot be mocked.
   │

您可以使用两种方法来解决此问题。

  • 设计方法。 测试双胞胎是另一个简化的假合约实现。因此,您应该 构建一个实现 CommentBlock 接口的测试双胞胎,而不是扩展 SimpleCommentBlock 具体最终类。

    $mock = $this->createMock(CommentBlock::class);
  • 魔法方法。 如果没有必要的接口来创建测试双胞胎,或者因为业务任务没有提供通过接口使用这种行为,则使用此方法。在这种情况下,您别无选择,只能删除 final 继承限制。

    第一种方法是使用一个包含原始测试替身但没有 final 限制的代理双替身。您可以手动实现它,但最好使用MockeryPHPUnit 测试 中提供的现成实现。

    第二种方法是应用 PHP 魔法在加载文件时移除 final 关键字。现成的实现也可以在Bypass 库中找到。在加载类文件之前,只需在 PHPUnit 测试 中启用移除 final 关键字即可。

方便处理 final 类的工具

静态分析工具可以在不实际运行代码的情况下发现代码中的某些问题。然而,它们不仅可用于搜索典型错误,还可用于控制代码风格。最流行的分析器是 PHPStan。通过编写自定义规则,您可以很容易地扩展其功能。例如,您可以检查并使用来自非官方第三方扩展localheinz/phpstan-rules的现成 FinalRule。该规则必须在 phpstan.neon 配置文件中注册为服务。执行 analyse 命令后,PHPStan 将在非抽象类不是 final 时报告错误。

$ vendor/bin/phpstan -lmax analyse src tests

 ------ ------------------------------------------------------------------------
  Line   src\Introduction\CommentBlock.php
 ------ ------------------------------------------------------------------------
  10     Class ppFinal\Introduction\CommentBlock is neither abstract nor final.
 ------ ------------------------------------------------------------------------