This article is a follow-up to my talk about “Magento 2: PHP Development Best Practices“, which I presented at the Magento Developer’s Paradise conference in April 2016.

After the conference, a Magento 2 core architect approached me to talk about the use of final classes and the possible consequences of using them for Magento 2.

That’s how this article was born. The purpose is to compare the benefits and drawbacks of using final classes in Magento 2, to see if there’s a clear indication of whether doing so could be considered a best-practice in the context of components that are released to the public (freely or commercially).

The Use of Final Classes

Following defensive programming principles, I usually make all classes final unless there’s a compelling reason NOT to. So it seems natural to start the analysis from this perspective.

final class MyObserver implements ObserverInterface
{
    // etc
}

A few benefits of this approach:

  1. It helps me to protect my code from BC breaks related to third party code extending my classes, which in turn makes my code easier to maintain while following Semantic Versioning principles.
  2. Closely related to the the first point, it benefits anyone who uses my code because BC breaks would happen less often.
  3. It­ encourages me to design better: closely related concepts like SRP (single responsibility principle) and DI become very important when it comes to efficiently unit-testing classes that are marked final.

There are more benefits, and they’re very well documented in other articles on the subject of defensive programming.

Magento 2 vs. Final Classes

Despite all the general benefits of making classes final, such classes are incompatible with several Magento 2 features.

Proxies

One of the most obvious is the Object Manager’s (OM) Proxy feature: the proxy pattern requires that the proxy class extends the class that’s being proxied. Whether this is done manually or by the OM code generation tool, a “final” class just can’t be proxied (in this particular sense).

Interceptors

Classes that are “final” cannot be intercepted. This was not obvious to me at first, but the interception feature of the OM apparently also needs generated classes to extend the intercepted class.

Dependency Analysis

The Magento 2 core team is building a tool that will allow component developers to examine the use of dependencies in their classes (e.g., to find any declared dependencies that are never actually used). This tool relies on classes being “extendable”.

Once the tool is released, it should be able to gracefully skip final classes from its reports (possibly with a warning). But skipping them will come at the cost of incomplete results for those classes.

Possible Solutions

So how do we deal with this situation? As a component developer, you never know which classes consumers may need to intercept; does this mean you can never use final classes in your Magento 2 components?

Approach #1: Service Contracts

The core Magento 2 developer argued that the Service Contracts approach in Magento 2 is better than using final classes. For public component developers, the best practice can be summarized as follows:

  1. Create interfaces in your component for anything that may be a point of communication between your code and third party code.
  2. Group all those interfaces together in an “Api” folder in your component. This group of interfaces is called your module’s “Service Contracts”.
  3. Implementations of those interfaces exist within your module as non-final classes.
  4. Client code would, by convention, have to stick to interacting with your component through interfaces only.

Pros

  1. Anyone can use the full range of OM tools and patterns with any class of your component, without limitations.
  2. You design your component with interfaces in mind, which may help you create better abstractions.
  3. New interfaces can be easily added when requested by third parties that need to interact with your module through an interface that doesn’t exist yet—without introducing changes that break backwards compatibility.

Cons

  1. Anyone can abuse your API using the full range of OM tools and patterns.
  2. In practice, the convention of interacting with a component’s Service Contracts only is not enforceable in any reliable way. There’s no real guarantee that third parties would stick to the convention.
  3. Even if you use interfaces for every class in your system, you still can’t work with the protected scope in your classes (for example, removing protected functions) without potentially breaking backwards compatibility. A workaround for this is applying Semantic Versioning only at the Service Contract level, but that probably means entering a grey area where you’re doing Semantic Versioning with only a portion of your code.

Approach #2: Be Stubborn

Another approach would be to stick to using final classes. This really needs no further explanation. Let’s weigh the pros and cons.

Pros

  1. All the pros of final classes: better encapsulation, easier maintenance, fewer BC breaks.
  2. It’s still always easy to make a class non-final whenever necessary.

Cons

  1. Several OM features and some tools won’t be available to those classes.

Best of Both Worlds?

After thinking about it for a while, I realized that it’s possible to combine these approaches. Let’s see how this would work out:

  1. Use final and create as many interfaces as you think may be useful.
  2. If you need to analyze a class’ dependencies with the Magento 2 tool I mentioned, simply remove the “final” keyword during the analysis, then add it back before releasing the module to the public.
  3. Similarly, remove the final keyword from classes if you know that they must be part of a Proxy pattern or were meant to be intercepted.
  4. Let people create pull requests or issues on your repositories in order to request that certain classes be non-final so they can be intercepted. If possible, review each use case with them (ask questions such as “why do you need to intercept this class?”). Document the outcomes of each of these conversations publicly to avoid having to repeat yourself in the future.

Another thing: if at some point you feel like you should not make a class final just so that it can be intercepted, consider firing events instead. Events are a great way of “breaking out” of strict encapsulation constraints without having to make classes non-final.

Pros

It seems that this approach combines the best benefits of the other approaches.

Cons

The fact that the community would have to request that classes be made non-final on a case-by-case basis could slow down their work. In addition, answering those requests may create a lot of work for you.

Conclusion

Find the right balance— so many things come down to that! Making classes final is all about managing encapsulation in your code and making your life easier in the future. Personally, I’ll continue to use final classes whenever possible and make them non-final only on a strictly “as-needed” basis.

If you have any insights or questions about this topic, you’re more than welcome to leave a comment. Sharing would also be greatly appreciated!

Leave a Reply