15

Final classes by default, why?

 5 years ago
source link: https://www.tuicool.com/articles/hit/JriuUnq
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

I recently wrote about when to add an interface to a class . After explaining good reasons for adding an interface, I claim that if none of those reasons apply in your situation, you should just use a class and declare it "final".

PHP 5 introduces the final keyword, which prevents child classes from overriding a method by prefixing the definition with final. If the class itself is being defined final then it cannot be extended.

— PHP Manual, "Final Keyword"

An illustration of the final syntax for class declarations:

final class CanNotBeExtended
{
    // ...
}

class CanStillBeExtended
{
     // ...
}

class Subclass extends CanStillBeExtended
{
    // override methods here
}

// Produces a Fatal error:

class Subclass extends CanNotBeExtended
{
    // ...
}

For a couple of years now I've been using the final keyword everywhere (thanks to Marco Pivetta for getting me on track !). When I see a class that's not final, it feels to me like it's a very vulnerable class. Its internals are out in the open; people can do with it what they want, not only what its creator has imagined.

Still, I also remember my initial resistance to adding final to every class definition, and I often have to defend myself during workshops, so I thought it would help if I explained all about it here.

The alternative: non-final classes

Omitting the final keyword is the standard for many developers, who reason that they should allow others to reuse the class and change just a part of its behavior by extending it. At first this may seem like a very considerate thing to do, but there are several downsides to it.

You are the initial designer of the class, and you had one particular use case in mind when you created it. Using the class for something else by overriding part of its behavior may jeopardize its integrity. The state of the extended object may become unpredictable or inconsistent with the state of the original object. Or the behavior may change in such a way that the subclass can no longer be considered a proper substitute for the base class.

Furthermore, the developer who creates a subclass of your class to override its behavior, probably doesn't need all of the internals of the parent class. Still it inherits those internals (data structures, private methods), and will soon arrive at the point that it has to work around them.

In terms of future development there's another issue: the class that has now been subclassed, still has a life on its own. Its clients may have different needs over time, so changes will be made to it. These changes may affect the subclasses too, since they likely rely on a particular implementation detail of the parent class. When this detail changes, the subclass will break.

From all these issues we have to conclude that by allowing subclassing, we will end up with an undesirable situation. The future of the subclass and the parent class will become intertwined. Refactoring the parent class will be quite hard, because who knows which classes are relying on a particular implementation detail. And since the subclass doesn't need all of the parent class's data and behaviors, they may act like they are the same kind of thing, but in reality, they aren't.

Replacing is better than overriding

So changing the behavior of a class shouldn't be done by subclassing that class and overriding methods. But what else can we do? In most cases, actually modifying the code of the original class isn't possible. Either because the class is used and relied on elsewhere in the project, or it's not physically an option to modify its code because it's part of a vendor package.

I'd even say that changing code shouldn't be the preferred way of changing behavior in the first place. If you can, change the behavior of an object by reconfiguring it with, that is, by swapping out constructor arguments, you should do it.

This reminds us of the Dependency inversion principle , according to which we should depend on abstractions, and the Open/closed principle , which helps us design objects to be reconfigurable, without touching its code.

What about the Template Method pattern?

There's an alternative approach for changing the behavior of a class. It's a behavioral pattern described in the classic "Design Patterns: Elements of Reusable Object-Oriented Software". The pattern is called "Template Method":

Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm’s structure.

This solution is slightly better than allowing subclasses to override a parent class's behavior, since it limits what can be changed. It renders the parent class itself useless by marking it as abstract . This prevents it from having a life on its own. In the Symfony Security component we find an excellent example of the Template Method pattern in the abstract Voter class. It helps users implement their own authorization voter, by providing the bulk of the algorithm that is supposed to be the same for all voters. The user only needs to implement supports() and voteOnAttribute() . which are both smaller parts of the overall algorithm. Symfony wouldn't be able to guess these implementations, since they are project-specific.

abstract class Voter implements VoterInterface
{
    public function vote(TokenInterface $token, $subject, array $attributes)
    {
        $vote = self::ACCESS_ABSTAIN;

        foreach ($attributes as $attribute) {
            if (!$this->supports($attribute, $subject)) {
                continue;
            }

            $vote = self::ACCESS_DENIED;
            if ($this->voteOnAttribute($attribute, $subject, $token)) {
                return self::ACCESS_GRANTED;
            }
        }

        return $vote;
    }

    abstract protected function supports($attribute, $subject);

    abstract protected function voteOnAttribute($attribute, $subject, TokenInterface $token);
}

It's really nice, but still, there's nothing in here that couldn't have been solved with composition instead of inheritance. An equivalent solution would've been:

// The user needs to implement this interface, instead of extending from Voter:

interface AttributeVoter
{
    public function supports($attribute, $subject);

    public function voteOnAttribute($attribute, $subject, TokenInterface $token);
}

// The Security package provides this standard Voter implementation:

final class Voter implements VoterInterface
{
    private $attributeVoter;

    public function __construct(AttributeVoter $attributeVoter)
    {
        $this->attributeVoter = $attributeVoter;
    }

    public function vote(TokenInterface $token, $subject, array $attributes)
    {
        $vote = self::ACCESS_ABSTAIN;

        foreach ($attributes as $attribute) {
            // We delegate calls to the injected AttributeVoter

            if (!$this->attributeVoter->supports($attribute, $subject)) {
                continue;
            }

            $vote = self::ACCESS_DENIED;
            if ($this->attributeVoter->voteOnAttribute($attribute, $subject, $token)) {
                return self::ACCESS_GRANTED;
            }
        }

        return $vote;
    }
}

Following the reasoning from "When to add an interface to a class" we might even reconsider the presence of a VoterInterface in the example above. If all the voting is done in the same way, that is, the voting algorithm is the same in all situations, we don't actually want users to write their own implementations of VoterInterface . So we also might provide only the Voter class, not the interface (I'm not actually sure it's the case for the Symfony Security component, but it's a good design choice to consider in your own projects).

The proposed solution doesn't involve inheritance anymore. It leaves the internals of all the involved classes to themselves. That is, these classes can all be final , and can be safely refactored now. Another advantage is that we could reuse each class in a different scenario. Final classes are more like building blocks ready for use, instead of templates that still need to be made concrete.

Composition over inheritance

You may have heard the phrase "Composition over inheritance" before; this is what is meant by that. In most cases, you should prefer a solution that involves composition over a solution that involves inheritance. To be more specific, if you feel the need to reconfigure an object, to change parts of an algorithm, to rewrite part of the implementation, consider creating a new class instead of overriding an existing class. If you need to represent a hierarchy of classes, where subclasses are proper substitutes for their parent classes, this would be the classic situation where you may still consider inheritance. However, the result may still be better if you don't inherit from concrete parent classes but from abstract interfaces. Still, this is very tricky to get right (I personally regret most of my previous work involving inheritance), so if you can, you should still prefer composition.

Extension should be a distinct use case

As a class designer you create a class in such a way that it provides a number of useful things its users can do with it. For example, they can:

  • instantiate it,
  • call a method on it,
  • get some information from it,
  • or change something about it.

"Reconfiguring it to change part of its behavior" should also be on this list, as a separate item. And so should "allowing it to be extended to override part of its behavior". It should be a deliberate decision to allow the user to do that with your class. Allowing a class to be extended has (often limiting) consequences for its design, and its future. So at least you shouldn't allow it by default.

"Final" pushes everyone in the right direction

So if allowing users to subclass a class shouldn't be the standard, then not allowing it should be. In other words: adding final to a class declaration should be your default. This simple trick will lead everyone in the right direction: towards classes that are smaller, have fewer maintenance issues, are easier to refactor, and act more like building blocks that can be reused in different parts of the project.

"You can always remove final if you want to"

I've often heard one argument for using final that I wanted to address here: "You can always remove final if you want to." In a sense, this is right; you can always allow extending a class whenever you want. But the same seems to be true for adding "final" - it's always just a few keystrokes away. As discussed in this article, I don't think it's the right attitude; it's better to close down, instead of open up. Opening up after having been closed is asking for all the trouble of inheritance described above. Make sure that instead of removing "final" from the class declaration, you will always aim for a solution that replaces part of the existing solution, and uses composition to allow for reconfiguration.

"My mocking tool doesn't work with final classes"

One objection against final classes that I've often heard is: "I like what you say, but my mocking tool doesn't work with final classes". Indeed, most don't. It makes sense, because the test doubles that such a tool generates usually look something like this:

final class CanNotBeExtended
{
}

// This produces a Fatal error:

class TestDoubleForCanNotBeExtended32789473246324369823903 extends CanNotBeExtended
{
    // override implementations of parent class here...
}

It's unfortunate, but the tools are not to blame. They'd have to do some sneaky things like hooking into the class loader to make the class non-final (in fact, that's quite doable). But they don't, so the truth gets slapped in our face. All that the tool is saying is that a final class can't be extended - there's nothing problematic about that fatal error. Instead, whenever we feel the need to replace a final class with a test double, we should consider two options:

  1. Maybe we shouldn't want to replace the real thing, i.e. the class.
  2. Maybe we should introduce something we can replace, i.e. an interface.

Option 1 is often useful when you're trying to mock things like entities and value objects. Just don't do it.

Option 2 should be applied in most other cases. When the class should've had an interface , add one, and create a test double for the interface instead.

Conclusion

In conclusion: make your classes final by default. One trick to help you with that is to modify the IDE template for new classes to automatically add final for you. Also, make the "final" discussion part of your technical code review. Ask: why is this class not final ? The burden of proof should be on the author of the class (and if they don't agree, talk to them about the merits of using "final").


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK