Still, there is value in quick and dirty solutions to Get Things Done, as long as it’s blatantly clear that it’s a temporary hack (so it could be rejected easily from clean code bases). I’d like to have an annotation named
@disregardSourceRestrictions
that allows one to extendfinal
andsealed
classes or methods and to accessprivate
members, perhaps only enabled under a compiler flag.
I like the general idea. In the previous thread Make concrete classes final by default, it was stated that one can distinguish three possible states when writing a class.
-
You do intend that the class can be extended. This means you have to carefully work out the internal contract for each overridable method. You communicate this by making the class
open
. -
You forbid that the class is extended. You communicate this by making the class
final
. -
You have not made a firm decision. The class is not a priori intended for extensions, but if others find it useful to extend, let them go ahead. However, they are on their own in this case. There is no documented internal contract, and future versions of the class might break the extensions (by rearranging internal call patterns, for instance).
Right now, (1) and (3) are not distinguished. The current SIP proposal could change this by using open
for (1), final
for (2) and no modifier for (3).
Now, the question is, what should happen on the user side if somebody does extend a class of the third category? There should be some form of indication that this is a risky operation from the standpoint of software evolution. It strikes me that we could use a language import for that. E.g.
import language.adhocExtensions
class C extends otherPackage.D
This achieves several things:
- Library users can still write ad-hoc extensions, like they do today, but they have to opt-in with the
language import. - Mocking works without warnings. Just set the language import in your mocks.
- A simple
grep
checks whether a codebase is “clean”, i.e. that it does not use ad-hoc extensions. - Library writers are protected: If they don’t make a class
open
, no internal contract is assumed and they are free to change implementations. - Migration headaches go away. If a library chooses to keep a class C non-open then code extending C has to add a language import. That’s a simple user-side operation. No coordination between different code bases is needed. If C is actually intended to allow extensions, the library writers will be lobbied to make it
open
, which would also be a good opportunity to make sure the internal contract is well documented.
So, it looks to me we have something which would be very easy to implement and would likely produce a helpful social dynamic for the interactions of library writers and users. The scheme also follows Scala’s philosophy to always provide an escape-hatch for the cases where static checking is too restrictive.
With this revised proposal, what is the role of sealed
? sealed
is still needed because it makes it an error to extend a class in another compilation unit instead of just requiring a language annotation to do it. But as far as pattern matching is concerned, default classes should be treated like sealed classes: we know all their planned extensions so we can do exhaustivity analysis based on this.