You can answer questions like this by going back to first principles and asking "what is the Liskov substitution principle intended to accomplish?" And the answer to that is that code that works correctly with the superclass should also work correctly with all subclasses. (For CS theory purposes there is a more specific mathematical definition but for the workaday programmer that's likely to only be concern if you are creating a programming language).
So, why would an abstract class be easier? There really is no situation where using a concrete class means you can't possibly follow the Liskov substitution principle in your subclasses. After all, the superclass defines a contract for the subclasses either way. The fact that it also contains one possible implementation of the contract is neither here nor there.
However, one might make the argument that psychologically it's easier to focus on a contract when making an abstract class, and perhaps a concrete class might take an implementation detail and accidentally make that part of the contract. It seems sort of plausible without being really convincing.
As far as never using a concrete class - perhaps a good rule of thumb is to be a bit skeptical of any design philosophy that says you should never use a standard, not totally broken feature of a language. In this case, the chance that you might, maybe, be more likely to make a mistake designing the contract of a concrete class vs. an abstract one - well that would be at best a minor factor in making that decision.
My favorite example of inheriting and adding nothing new is this:
public class LoginException : System.ApplicationException{}
Why? Because I've said all I need to say with that wonderfully descriptive class name. Sure if I want to add a dynamic message I'll need to add a constructor. But if I don't, why should I?
Your example lacks this clarity because the semantic issues are hidden with names like Prop1
. I love replacing inheritance with composition and delegation but in this case, when I do that, I end up looking at this:
SaleRequest sr = new SaleRequest(new ReturnRequest());
Now I gotta ask, does that make semantic sense? I can't request a sale without requesting a return?
Sure, mechanically it makes sense because the 'Props' are 'correct' but those are just datatypes. Would it still make sense if they had meaningful names?
Your attempt to fix this problem looks like this:
SaleRequest sr = new SaleRequest(new BaseRequest());
Which again is semantically anemic. Base? That doesn't tell me what belongs in this class and what doesn't belong in it. That's not a good name.
If BaseRequest
had a name that made it clear that Prop1
and Prop2
(whatever those are) belong in it and BaseRequest
had a name for something that is clearly part of, or all of, the other two requests then I'd be fine with this.
As it stands, I don't know what is supposed to be going on when I look at this code. That's not a good thing.
I have no problem with empty classes. But please, give them good names. It's really the only thing they have to offer.
Best Answer
Yes, it is generally a bad design; however, I am sure they may be a case or two where it is appropriate, but I haven't seen one.
In fact, use of abstract base classes, even one, is generally bad. Inheritance is for polymorphism, not code resuse; and composition should be favored over inheritance to begin with.
With that said, we work in the real world, and tools and are here to make working software, not to be used in a exercise of spiritual purity. In some cases, it just makes practical sense to use an abstract base class for code reuse; however, it should be done with great reservation. Here are my personal rules for abstract base class use:
I do not think your particular use case warrants even one abstract class, never the less two.
If I were you, I would just declare several interfaces, one for each shared method, then implement everything individually in the sink and datawrite nodes. This will give you much more flexibility in the future.
Let me give you an example as to why you should not be using abstract base classes and inheritance haphazardly:
Let's say your boss tells you to make a game, the player class needs to be able to shoot any of three types of enemies: goblins, trolls, and elves. Since they act largely the same, you define a abstract Enemy class which they all inherit from. Now you make your player class which has a shoot method taking the abstract Enemy class as an argument.
This looks great, and you've saved a lot of time and code... until your boss adds a requirement that if you shoot a golden box, your health refills... obviously your golden box cannot inherit from your your Enemy class, it shares nothing in common with them except the ability to be shot. Now you have to implement two versions of the Shoot method... there goes code reuse.