Why I don't like the sealed classes RFC

So I've just seen yesterday a new draft RFC on PHP internals for adding sealed classes and after some discussion, thought it'd be a good topic for me to explore in a blog post.

What is a sealed class?

Different languages which have a concept of "sealed" mean slightly different things by it, but in respect of the PHP RFC, a "sealed" class (or interface) is one which is closed to inheritance except by any explicitly named exceptions. In short, it's like a final class except you can permit (and this is proposed as a new keyword for this feature) some named children.

sealed class Foo permits Bar {
    ...
}

// This is okay
class Bar extends Foo {
    ...
}

// This is an error
class Baz extends Foo {
    ...
}

What's the rationale?

Ostensibly (and to its advocates), restricting inheritance in this way declares your intent about the usage of an API (that you have not specifically designed it with overridable behaviours in mind) and protects consumers of it from accidentally breaking what should be wholly encapsulated behaviours, in particular in the manner of the fragile base class problem. It can also, as a corollary of point one there, be used to indicate you have defined a data model where - in theory at least - the domain boundaries are well understood, finite and immutable.

And these reasons are true enough; language constructs certainly do enable you to declare your intentions about how something should be used, you most definitely can't break a subclass with changes to its parent if you are prevented from subclassing it in the first place and you may sometimes be dealing with models where it makes more sense to describe them in terms of their total sum (e.g. the sum of all Colours is the sum of all Red, Green and Blue colours).

My argument, however, would be that no new keyword, language construct or indeed enforcement of additional inheritance control is needed to meet the first (and primary) benefit - we have existing constructs which are designed very well to express intent in a way which can be understood and used by automated tools and IDEs. These are annotations and, from PHP 8, attributes.

Further, I would argue problems like the fragile base class are problems of the consumer of some code, not the code being consumed and are better addressed through improving your software design than artificial, syntactically imposed constraints on what you can do with code.

A note about inheritance

I've never been a fundamentalist about any aspect of programming; there are things I advocate (or caution) more readily than not, but as far as I'm concerned, programming will always be as much of an art as it is a science.

I don't therefore worship at the "composition over inheritance" altar or other such absolute positions. As far as I'm concerned, there are different, equally valid (and context-dependent) ways of modeling your domains and concerns in software, has-a vs. is-a relationships being two of them.

But I would say broadly inheritance can lead to a mess and that I will try to avoid subclassing concrete types - as a general principle I would say inheritance is better left to interfaces and abstract classes, so if I can create an instance of it, I'd at the very least consider whether it could be a design smell if I'm thinking about subclassing it.

Issues like the fragile base class can be easily avoided, where there is doubt about suitability, by using constructs like interfaces and composition.

Are you an enabler or a director?

Martin Fowler defines two types of approach to programming, which I broadly agree with. There are those of us who believe other programmers need to be protected from themselves and provided with as many robust safety nets as is practical (Martin refers to this as "Directing Attitude") and there are those of us who believe we can either trust other programmers to be responsible, or at the very least leave them to clean up their own mess if they choose not to be. My tendencies usually fall more in to the latter group, which Martin refers to as "Enabling Attitude".

Should we add sealed classes to PHP?

When I see any RFC proposing a new language feature, the first and most important question I ask myself is simple; "Who or what will benefit from this change?"

In this case, the change being proposed is specifically to throw a fatal error in the event a user tries to extend a sealed class outside its defined boundaries.

There is one "what" and two "whos" which might benefit from any change to a programming language; the computer system running it, the author of some code and the user (consumer) of some code. So I look at the RFC for sealed classes and apply the benchmark of my question to each of these stakeholders; who will benefit?

  1. It can't be the PHP engine, because unless someone can correct me on this (and I've seen no implementation thusfar), the opcodes generated by PHP for a class declared sealed will not be any different, at the very least not in any way which provides some quantifiable gain (be it performance or whatever else).

  2. It can't be the author of some code, because they will definitely only use and support their own code in the manner they intended. And while it's certainly very useful to express your intentions to other users through constructs like documentation, annotations or attributes, if someone else comes along and misuses/abuses your code in a way you didn't intend, you owe them nothing, your obligation to them is nil.

  3. It can't be the consumer of some code, because best case scenario they didn't want to extend your class anyway (or perhaps they did at first, then saw your documentation/annotations stating your intention that inheritance was not designed for in the API you're exposing and realised it wasn't a good idea) and in this case, sealing the class has therefore made no difference. But worst case scenario, they have a legitimate, justified reason to extend your class - and you cannot possibly have foreseen or predicted every possible situation such as to say they don't - and now they can't do it (without forking), which is distinctly and only a disadvantage in their case.

Personally, although I accept my objections are contentious to some (as evident from the internals discussion), I am forced to come to the conclusion there is no benefit to adding a concept of sealed classes to PHP. Languages like Java can at least claim a point for (1) in the above list, because they need to reason about virtual vs. non-virtual methods, it makes some difference to the bytecode they produce.

In respect of the argument from data modelling, I say the same sort of thing for (2) and (3) still applies - you may not understand your data models at first and they may turn out to have more variants than you initially envisioned (perhaps you thought the set of all colours was the summation of red, green and blue, but now it turns out you, or someone using your code, also needs to reason about purple).

In PHP's case, I argue the only thing throwing a fatal error when your boundaries of intent are violated is giving you (above and beyond what you could do with a simple #[Sealed] attribute understood by your IDE) is a false, flimsy pretence of security1; an idea that you've magically prevented other people from using your code in a way you think they shouldn't. You haven't, though - and I get it, I get why on the surface, sealed classes might seem like a good addition to the language - but as what Martin Fowler might call an enabler, trust me when I say if that other programmer you're trying to protect wants to wriggle out of your straightjacket, they will easily find a way to do it.

The added safety is only an illusion and I do not believe in forfeiting freedoms for illusions of security.

Notes on the internals discussion

In response to one of my messages, one person said that the same arguments I make about sealed classes could be applied to type-hinting. I disagree, this is a false analogy. There is a huge difference between the safety boundaries created by type-hinting - which can directly prevent logical errors already present which might otherwise go unnoticed - and what is mere metadata enforced at syntax level, such as sealed.

Put simply, if you add a proper type hint to the correct behaviour of a function and my code suddenly breaks with a TypeError, I definitely have a bug somewhere - the best case scenario is that I've got some unwise, ambiguous code. If you add sealed and my code suddenly starts throwing a fatal error, sure, there might be a subtle bug a la fragile base class in my design, equally it could be working perfectly were it not for an arbitrary violation of what you decided isn't sensible for my code to do (and without knowing about it, no less).

I agree sealed classes are one way of preventing me from introducing new bugs through poor design, but they do not prevent or catch bugs in and of themselves.

It is, however, much more comparable to the final construct - but I have never been a fan declaring classes final as a syntactic construct2, either.

Ocramius notes "the main use-case of sealed types is being able to declare total functions around them", with which I agree but stand by my contention this is also adequately achieved with a non-functional #[Sealed] attribute.

A couple of contributors say in response to a point I raised that their support for a sealed classes feature is not about [unrestricted] inheritance being dangerous. Obviously I can't speak for their minds, but in mine - when the only real, tangible benefit you can cite for adding a feature amounts to "it will put up at least some barrier to people using my code in a way I don't think they should", it smells to me like you've decided what's dangerous for them.

Another couple raised an idea - which I have to admit I don't understand - that it's somehow better for the author of some library or component to mark their classes as sealed because it will stop people....I'm not sure? Who've misused their code from bugging them to patch for and fix problems they as consumers have introduced themselves?

If they have to reason about every way a method could be overridden, every change in the library would become more risky, require more thought and more frequent major version updates

Not to put too fine a point on it, but how about you just tell those people you've made it adequately clear in your library what your APIs do and don't support and no, you won't be fixing problems they've created by subclassing something they probably shouldn't? Someone correct me if I read those bits of the thread wrong, but again I'm not seeing what this achieves as a language feature a simple attribute doesn't.

References

1 And security is not the point of encapsulation boundaries; they do not exist to "secure" your code against mis-use, they exist to delineate what is intended to be directly used and what isn't. That is all. The reason compiled languages need to enforce those boundaries is because it makes a difference to how they reason about the program and the bytecode they generate.

2 Note I say "as a syntactic construct"; I do not have any issue at all with a developer marking their classes or methods as final, this is expression of intent and is a good thing. What I don't see is any benefit to an interpreted language like PHP enforcing this at runtime and thereby best-case scenario preventing a responsible developer from doing something useful.

Sealed classes RFC
Discussion on internals
"Enabling attitude" definition


Comments

Add a comment

All comments are pre-moderated and will not be published until approval.
Moderation policy: no abuse, no spam, no problem.

You can write in _italics_ or **bold** like this.

Recent posts


Sunday 01 December 2024, 18:37

Re-examining this famous puzzle of probability and explaining why our intuitions aren't correct.

musings

Sunday 17 November 2024, 22:53

Keep your database data secure by selectively encrypting fields using this free bundle.

php

SPONSORED AD

Buy this advertising space. Your product, your logo, your promotional text, your call to action, visible on every page. Space available for 3, 6 or 12 months.

Get in touch

Sunday 27 October 2024, 19:02

Learn how to build an extensible plugin system for a Symfony application

php

Saturday 10 February 2024, 17:18

The difference between failure and success isn't whether you make mistakes, it's whether you learn from them.

musings coding

Monday 22 January 2024, 20:15

Recalling the time I turned down a job offer because the company's interview technique sucked.

musings