Thoughts on sealed classes

Guriy Samarin
8 min readApr 15, 2022

--

Recently in .NET community theme of optimization became one of the hottest and making classed sealed by default was on of the suggested ways. We already had some third party Roslyn analyzers to deal with this and it seems like the .NET team will finally provide Roslyn analyzer of her own in .NET 7.

But lets leave optimization aside. We have a lot of famous developers advocating for defaulting sealed (performance arguments omitted)

I summarized their arguments, but you can read them all beneath:

  1. Design — if you are planning for your class to be inherited it’s one thing and if you are not another. So, you have to be explicit about enabling inheritance of your class. Otherwise, your internal class logic will be broken. It’s considered common place to use the most strict access modifier for any class, property, method etc. The same thing seems reasonable for sealing classes.
  2. Versioning — going from sealed to not seal is not breaking change.
  3. Security — potential hostile implementations overriding the base class behavior.
  4. Inheritance — brings more problems than it solves. Extend — don not inherit!

And strangely enough all the argument I managed to find in favor of current .NET design seems a little vague:

  1. You cannot think about all extensions point upfront — and I don’t need to. The consumer of my class will find it even more difficult to find all the problems overriding of base class functionality may bring. So, I will kindly advice not to inherit unless you have to
  2. What if someone wants to reuse? — you can copy it if you absolutely have to. The thing with best practices: they don't necessarily provide easier solution — they provide the right solution in the long run.
  3. Inheritance is a foundational principle of OO, so disallowing it by default wouldn’t be intuitive — no one disallows inheritance. It's just not the thing you need in most of the cases. And trying to make developers think about it a little bit more — seems to be a good thing.

The question is: if it is all so obvious to you, what is a point writing about it? The answer is: I’m not sure. We have Kotlin with final by default classes. Take a look at the proposal to bring open back. It seems like a lot of developers was made uncomfortable by this decision. But all their arguments sounds like: “We used to hacking, please, let us”. Still, we need better data. And if new Roslyn analyzers will bring us into the “Sealed Land” — we’ll see.

And here is experts opinions the matter, as I have promised:

Jeffrey Richter

There are three reasons why a sealed class is better than an unsealed class:

Versioning: When a class is originally sealed, it can change to unsealed in the future without breaking compatibility. However, once a class is unsealed, you can never change it to sealed in the future as this would break all derived classes. In addition, if the unsealed class defines any unsealed virtual methods, ordering of the virtual method calls must be maintained with new versions or there is the potential of breaking derived types in the future.

Performance: …

Security: and predictability A class must protect its own state and not allow itself to ever become corrupted. When a class is unsealed, a derived class can access and manipulate the base class’s state if any data fields or methods that internally manipulate fields are accessible and not private. In addition, a virtual method can be overridden by a derived class, and the derived class can decide whether to call the base class’s implementation. By making a method, property, or event virtual, the base class is giving up some control over its behavior and its state. Unless carefully thought out, this can cause the object to behave unpredictably, and it opens up potential security holes.

Eric Lippert

Well, every public class that my team produces is sealed if possible. If it is not possible to seal a class then, if possible, it has an inheritance demand on it so that only someone with the MSFT private key can subclass it. My reasons for insisting upon this policy boil down to one overriding principle:

Good code does exactly what it was designed to do, no more, no less.

Let me expand upon that in four ways.

1) Philosophical. OOP design includes subclassing to represent the polymorphic “is a” relationship between two things. A Giraffe IS AN Ungulate IS A Mammal IS AN Animal… Unless I can think of a clear case where a customer would need to express an IS A relationship with some code that I produce, I don’t allow for such cases.

2) Practical. Designing classes so that they can be effectively extended by third parties is HARD. (Look at the collection base classes for example.) You have to get the design right — what is protected? You have to implement that design correctly. The test matrix grows enormously because you have to think about what weird things people are going to do. You have to document the protected methods and write documentation on how to properly subclass the thing.

This is all expensive and time consuming — that is time that we could be spending looking for bugs in more important user scenarios, planning future versions, fixing security holes, whatever. There is only a finite amount of developer time we can spend on designing and implementing code, so we have to spend it the way that benefits customers most. If the class is not designed to be extended, I’m going to avoid all that expense by sealing it. I am not going to release half-baked classes that look extensible but in fact are not quite there.

3) Compatible. If in the future I discover that I should have sealed a class, I’m stuck. Sealing a class is a breaking change. If I discover that I should have left a class unsealed, unsealing in a future version is a non-breaking change. Sealing classes helps maintain compatibility.

4) Secure. the whole point of polymorphism is that you can pass around objects that look like Animals but are in fact Giraffes. There are potential security issues here.

Every time you implement a method which takes an instance of an unsealed type, you MUST write that method to be robust in the face of potentially hostile instances of that type. You cannot rely upon any invariants which you know to be true of YOUR implementations, because some hostile web page might subclass your implementation, override the virtual methods to do stuff that messes up your logic, and passes it in. Every time I seal a class, I can write methods that use that class with the confidence that I know what that class does.

Now, I recognize that developers are highly practical people who just want to get stuff done. Being able to extend any class is convenient, sure. Typical developers say “IS-A-SHMIZ-A, I just want to slap a Confusticator into the Froboznicator class”. That developer could write up a hash table to map one to the other, but then you have to worry about when to remove the items, etc, etc, etc — it’s not rocket science, but it is work.

Obviously there is a tradeoff here. The tradeoff is between letting developers save a little time by allowing them to treat any old object as a property bag on the one hand, and developing a well-designed, OOPtacular, fully-featured, robust, secure, predictable, testable framework in a reasonable amount of time — and I’m going to lean heavily towards the latter. Because you know what? Those same developers are going to complain bitterly if the framework we give them slows them down because it is half-baked, brittle, insecure, and not fully tested!

John Skeet

Designing for inheritance is hard, and can make your implementation less flexible, especially if you have virtual methods, one of which calls the other. Maybe they’re overloads, maybe they’re not. The fact that one calls the other must be documented otherwise you can’t override either method safely — you don’t know when it’ll be called, or whether you’re safe to call the other method without risking a stack overflow.

Now if you later want to change which method calls which in a later version, you can’t — you’ll potentially break subclasses. So in the name of “flexibility” you’ve actually made the implementation less flexible, and had to document your implementation details more closely. That doesn’t sound like a great idea to me.

Next up is immutability — I like immutable types. I find them easier to reason about than mutable types. It’s one reason why the Joda Time API is nicer than using Date and Calendar in Java. But an unsealed class can never be known to be immutable. If I accept a parameter of type Foo, I may be able to rely on the properties declared in Foo not to be changed over time, but I can't rely on the object itself not being modified - there could be a mutable property in the subclass. Heaven help me if that property is also used by an override of some virtual method. Wave goodbye to many of the benefits of immutability. (Ironically, Joda Time has very large inheritance hierarchies - often with things saying "subclasses should be immutable. The large inheritance hierarchy of Chronology made it hard to understand when porting to C#.)

Finally, there’s the aspect of overuse of inheritance. Personally I favour composition over inheritance where feasible. I love polymorphism for interfaces, and occasionally I use inheritance of implementation — but it’s rarely a great fit in my experience. Making classes sealed avoids them being inappropriately derived from where composition would be a better fit.

Mark Seemann

You don’t need inheritance #

Base classes imply class inheritance as a reuse and extensibility mechanism. We’ve known since 1994, though, that inheritance probably isn’t the best design principle.

“Favor object composition over class inheritance.” Design Patterns

In single-inheritance languages like C# and Java, inheritance is just evil. Once you decide to inherit from a base class, you exclude all other base classes. Inheritance signifies a single ‘yes’ and an infinity of ‘noes’. This is particularly problematic if you rely on inheritance for reuse. You can only ‘reuse’ a single base class, which again leads to duplication or bloated base classes.

It’s been years (probably more than a decade) since I stopped relying on base classes for anything. You don’t need inheritance. Haskell doesn’t have it at all, and I only use it in C# when a framework forces me to derive from some base class.

There’s little you can do with an abstract class that you can’t do in some other way. Abstract classes are isomorphic with Dependency Injection up to accessibility.

Seal #

If I already follow a design principle of not relying on inheritance, then why keep classes unsealed? Explicit is better than implicit, so why not make that principle visible? Seal classes.

It doesn’t have any immediate impact on the code, but it might make it clearer to other programmers that an explicit decision was made.

References:

--

--

Guriy Samarin
Guriy Samarin

Written by Guriy Samarin

Software developer at Amazon. Web (mostly backend) development now. My stack — .NET (APS.NET Core MVC).

No responses yet