parshikovpavel/final-keyword

Examples to examine from the article on habr.com

0.0.1 2019-12-16 12:03 UTC

This package is auto-updated.

Last update: 2024-04-09 07:31:09 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:

  1. An initial class (SimpleCommentBlock) is introduced into a design with the final keyword and the inheritance restriction.
  2. 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).
  3. 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 extending SimpleCommentBlock 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 removing final 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.
 ------ ------------------------------------------------------------------------