Shout-out to brilliant developer (and my co-worker) David Potter, who--through hours of discussion--helped me arrive at this 'thought'.
FYI: I use a lot of WPF examples in here. Apologies to the non-WPF devs.
Back in June, I wrote a post titled: Don't subclass a Panel, unless you're making a Panel.
In that post, I outlined the 'great' reason to use subclassing (read, inheritance):
I read a great internal paper at MS once--written by one of those brilliant old-guard devs with decades of perspective. The point the author pushed: use inheritance only for polymorphism. Translated: when making MyGrid, only subclass Grid if you mean for people to treat your new MyGrid just like another Grid. Code (and a user, for the most part) that deals with Grid should be able to "deal" with MyGrid without any issues.
I use a strong word there: only.
Now with time and experience comes perspective.
I'm going to make this philosophy a bit more nuanced by introducing categories of subclassing: great, good, and okay.
My argument: there are times to use each, but the further you get away from 'great', the more trouble you can get in and the more careful and thoughtful you should be.
Great subclassing: for polymorphism.
Examples here: Stream (from mscorlib) and UIElement (from WPF).
Stream is worthless by itself. It's meant to be the base class for something else. But there are innumerable features of the CLR that deal with Stream generically, without much concern for any subclass.
UIElement is similar. Basically worthless by itself. Designed to be a baseclass. But things like WPF layout (Measure/Arrange) deal explicitly with UIElement, ignoring any details of any subclass.
If you create a Foo class and have other classes that take a Foo in a constructor or method argument, even though Foo is abstract or rarely used by itself you are playing in the 'great' end of the subclassing spectrum.
Good subclassing: cognitive simplicity
If 'great' subclassing is for software components, 'good' subclassing is for humans.
ContentControl (WPF) is a pretty good example. I can't think of any constructor or method that takes ContentControl as a parameter.
We could imagine Button and Label and ScrollViewer all re-exposing the common properties (Content, ContentTemplate, ContentTemplateSelector) without any changes in functionality.
But! It's really nice that users can look at Label and say "oh, that's a ContentControl" and immediately have a valid mental model of how to use it.
The baseclass, in this respect, provides a grouping paradigm for a user to understand classes of types.
While this is a nice feature, it has its issues.
ButtonBase is a good example.
ButtonBase is the 'clickable' widget in WPF.
Almost everything that is clickable subclasses ButtonBase.
ButtonBase subclasses ContentControl.
ContentControl is the 'content' widget in WPF.
Almost everything in WPF that has content is a content control. Well, almost. ContentPresenter has the same properties that ContentControl has regarding content (Content, ContentTemplate, ContentTemplateSelector).
But they don't share a baseclass. Why?
Well, because ContentControl is a Control.
Control is the 'template-able' widget in WPF. It lets you swap out the chrome.
ContentPresenter doesn't let you swap out the chrome, just the template for the data.
So, we could really imagine three separate classes, all of which derive from FrameworkElement.
- ClickElement: exposes the click properties/methods/events.
- ContentElement: exposes the content properties/methods/events (Yes, this class name already exists for text stuff. Play along...)
- TemplateElement: exposes the template properties/methods/events (this is what Control is now).
The fact that ButtonBase subclasses in a specific order is kinda arbitrary.
One could imagine wanting a template-able, click-able element that doesn't have content.
One could imagine wanting a click-able, element that holds content that isn't template-able.
Single inheritance forces framework authors to choose. This can be annoying if you want a re-mix of the available features.
A great example of this: TransitionPresenter (from the bag-o-tricks).
It has all of the content properties, but it's not a ContentPresenter or a ContentControl. (The way it does its visual tree is different than both of those controls.)
It turns out to be impossible to subclass ContentPresenter and it's confusing to subclass ContentControl (because TransitionPresenter doesn't use a ControlTemplate).
A really smart dev who was doing code clean-up actually changed the baseclass from FrameworkElement to ContentControl, thinking he was making things simpler.
This is bad! A user will then expect to be able to use the Template property to change the chrome. This is the same problem with making MyGrid from Grid unless you want people treating it like any other Grid.
This trap is usually because of the third type...
Okay subclassing: tactical code reuse
If you have 15 classes that all have a Foo, Bar, and Baz property, having a common baseclass just saves code. Less to document. Less to test. Less to mess up.
But if sharing some common state is your only goal, just make sure you acknowledge (along with the rest of your team) that is why you're doing it. Put it in the code comments of the class.
In some respects this is less dangerous than the 'good' reason to subclass because the FooBarBaz class probably doesn't have much meaning or use beyond your tactical reasons. As soon as it does, though, you can fall into the MyGrid trap.
This was a really long write-up.
I guess my final advice is be careful.
As with anything in life, if you try to be clever for clever's sake, you'll get burned.
If you try to use every feature of a language or a framework just to add spice to your afternoon of coding, you'll get burned.
As I discussed in my post about patters and guidelines, always try to understand the implications of the tools/tricks you use when coding.
And that's all I have to say about that.
Did you find this interesting? Let me know. I'd hate to think I was riffing all this out just to make my tendinitis worse.