parshikovpavel / final-keyword
Examples to examine from the article on habr.com
Installs: 5
Dependents: 0
Suggesters: 0
Security: 0
Stars: 2
Watchers: 0
Forks: 0
Open Issues: 0
Type:project
Requires
- php: ^7.1
- parshikovpavel/array-cache: *
Requires (Dev)
- dg/bypass-finals: ^1.1
- localheinz/phpstan-rules: ^0.13.0
- mockery/mockery: ^1.2
- phpstan/phpstan: ^0.11.19
- phpunit/phpunit: 7
This package is auto-updated.
Last update: 2025-03-09 09:25:26 UTC
README
The repository contains the code examples for learning from the article on habr.com
Use the following composer
command to fetch the source code examples:
composer create-project parshikovpavel/final-keyword dir/
The source code is separated according to the structure of the article.
For convenience, below are links to source code files and examples of their use to reproduce cases from the article. This information is also separated according to the structure of the article.
Introduction
The initial version of CommentBlock
class.
Inheritance issues
The inheritance violates the principle of the information hiding
CommentBlock
parent class and CustomCommentBlock
child class demonstrating violation of the principle of the information hiding.
Banana-monkey-jungle problem
Block
, CommentBlock
, PopularCommentBlock
, CachedPopularCommentBlock
classes are examples of a deep inheritance hierarchy.
Open recursion by default
CommentComment::getComment()
and CustomCommentBlock::getComment()
have different implementations of behavior. CommentBlock::getComments()
method makes
a self-call of $this->getComments()
and relies on the implementation of behavior in the CommentBlock
class.
CommentBlock::getComments()
implementations, which is automatically inherited by CustomCommentBlock
, is incompatible with the behavior of CustomCommentBlock::getComment()
method. So getting a list of comments doesn't work correctly. You can see this by executing the following test:
$ 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.
Control of side effects
CountingCommentBlock
is a specific type of CommentBlock
counting views of particular comments in a PSR-16 compatible cache. CountingCommentBlock::viewComment()
has a side effect since increments the counter value in the cache. CommentBlock::viewComments()
combines the comment views into a single view and its implementation is inherited by CountingCommentBlock
exactly as it is. However this inherited implementation doesn't take into account CountingCommentBlock
responsibility for counting comment views in the cache. As a result, view counters don't work correctly during calls to CountingCommentBlock::viewComments()
. The test result below demonstrates this thought:
$ 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.
Base class fragility
An inheritable base class are "fragile" because seemingly safe modifications to it, may cause the derived classes to malfunction. The programmer cannot determine whether a base class change is safe simply by examining in isolation the methods of the base class. So the implementation detail of the base and the derived classes become tightly related.
For example, during code refactoring the programmer change a single line in CommentBlock::viewComments()
method to simplify the code and to avoid code duplicate in the future.
$view .= $comment->view(); ------> $view .= $this->viewComment($key);
The base class logic remains valid and it continues to pass the tests successfully. However base class isn't completely isolated. As a result, calling CountingCommentBlock::viewComments()
causes double increment of a view counter value. You can explore the problem in detail by studying the corresponding test:
$ 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.
Applying the final keyword to improve design
Template method pattern
The CommentBlock
is an abstract superclass which defines the skeleton of subclasses.
The CommentBlock::viewComments()
is a final template method which is inherited by subclasses and can't be overridden by them. It calls the abstract CommentBlock::viewComment()
method concrete implementations of which are provided by subclasses.
The final SimpleCommentBlock
class implements the SimpleCommentBlock::viewComment()
method which just returns a string view of the comment.
The final CountingCommentBlock
class includes different behavior implemented in CountingCommentBlock::viewComment()
. In addition to returning a string view of the comment, this method increments the counter value in the cache.
Prefer interface implementation to inheritance
Let's avoid any class coupling by implementation details.
The CommentBlock
is an interface which defines the contract and hides implementation details.
The SimpleCommentBlock
and CountingCommentBlock
are final classes that implement this interface but don't have a direct association. As a disadvantage, this classes have the same duplicate implementation of viewComments()
method.
Prefer aggregation to inheritance
The aggregation is the loosest relationship type. Let's use the aggregation in the form of the decorator pattern to replace the inheritance.
The CommentBlock
is an interface which defines the contract and hides implementation details.
The SimpleCommentBlock
is a base final class with base behavior that implements the mentioned interface.
The CountingCommentBlock
is similar to a child class. It stores a reference to the decorated object, forwards all calls to it and implements additional behavior. CountingCommentBlock::viewComment()
и CountingCommentBlock::viewComments()
add cache capabilities. CountingCommentBlock::getCommentKeys()
is a simple single-line function that just transfers responsibility for the execution to the nested object.
SimpleCommentBlock
и CountingCommentBlock
maintain polymorphic behavior since both implement CommentBlock
interface. Clients interacts with them transparently through the interface.
SimpleCommentBlock
и CountingCommentBlock
are coupled through an aggregation. For this reason they are devoid of all disadvantages of the inheritance: the base class fragility problem, the information hiding principle violation, etc. Changing the implementation detail of the base class doesn't affect the behavior and the structure of the derived class. As shown below all assertions which verify the CountingCommentBlock
behavior are successful.
$ vendor/bin/phpunit tests/ApplyingFinalKeyword/PreferAggregation/CountingCommentBlockTest.php --testdox ppFinal\ApplyingFinalKeyword\PreferAggregation\CountingCommentBlock ✔ Counts views of comment OK (1 test, 2 assertions)
A class must be prepared for the inheritance
The inheritance violates the principle of the information hiding. So it's necessary to document in PHPDoc not only a public interface but also the internal implementation details.
The CommentBlock::viewComment()
is a non-final method that allows to override the implementation. Therefore this method's PHPDoc describes the use of parameters and the existing side effects.
The CommentBlock::viewComments()
is a final method however it uses the non-final method mentioned above. Overriding of the non-final CommentBlock::viewComment()
method in the subclasses affects the behavior of the final inherited CommentBlock::viewComments()
method. Therefore its PHPDoc reveals the schema of using all non-final methods.
A class must be prepared for the aggregation
The general schema to create a loosely coupled design consists of the following steps:
- An initial class (
SimpleCommentBlock
) is introduced into a design with thefinal
keyword and the inheritance restriction. - To expand the functionality of the class you need to analyze the base class behavior, form its contract and to formally describe it as an interface (
CommentBlock
). - Introduce into a design a derived decorator class (
CountingCommentBlock
) which expands the functionality of the base class and implements the same interface. An instance of the base class (SimpleCommentBlock
) is injected into the constructor of the derived class (CountingCommentBlock
) through the interface (CommentBlock
).
Using final classes in tests
Most unit test libraries use the inheritance to construct test doubles (stubs, mocks, etc). Therefore an attempt to mock the SimpleCommentBlock
final class in a PHPUnit test:
$mock = $this->createMock(SimpleCommentBlock::class)
results in a warning like this:
$ 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. │
You can use two approaches to solve this problem.
-
Design approach. The test double is another simplified dummy contract implementation. So you should construct the test double that implements the
CommentBlock
interface rather than extendingSimpleCommentBlock
concrete final class.$mock = $this->createMock(CommentBlock::class);
-
Magic approach. It's used if you don't have the necessary interface to create the test double as the use of such behavior through the interface isn't provided for by business tasks. In this case you have no choice but to remove the
final
inheritance restriction.The first approach is to use a proxy double which contains the original test double but has no
final
restriction. You can implement it manually, but it's better to use the ready-made Mockery implementation in the PHPUnit test.The second approach is to apply PHP magic to remove
final
keyword during loading files. Also the ready-made implementation is available as the Bypass library. It's enough to enable removingfinal
keywords in the PHPUnit test before loading a class file.
Tools for convenient work with final classes
Static analysis tools allow to find some problems in the code without actually running it. However, they can be used not only to search for typical errors, but also to control the code style. The most popular analyzer is PHPStan. It enables you to extend the functionality quite easily by writing custom rules. As an example you can examine and use the ready-made FinalRule
from the unofficial third-party localheinz/phpstan-rules
extension. The rule must be registered as a service in the phpstan.neon
configuration file. Issue the analyse
command and PHPStan will report an error when a non-abstract class is not final
.
$ vendor/bin/phpstan -lmax analyse src tests ------ ------------------------------------------------------------------------ Line src\Introduction\CommentBlock.php ------ ------------------------------------------------------------------------ 10 Class ppFinal\Introduction\CommentBlock is neither abstract nor final. ------ ------------------------------------------------------------------------