Re: LSP and subtype
- From: "Andreas Huber" <ahd6974-spamgroupstrap@xxxxxxxxx>
- Date: Fri, 14 Oct 2005 21:32:02 +0200
"H. S. Lahman" <h.lahman@xxxxxxxxxxx> wrote in message news:imR3f.4$f02.3@xxxxxxxxxxx
However, there are some potential pitfalls here. For example, some poorly formed OOPLs like C++ allow one to instantiate a superclass without specifying a subclass.
These "poorly formed" OOPLS almost invariably provide means to disallow the instantiation of a superclass without specifying a subclass.
To achieve this in C++, one usually makes all constructors protected, so that they are only accessible from a deriving class ...
class Base
{
// ...
protected:
Base() {}
};.... so the following is flagged with an error at compile time:
int main()
{
Base b = new Base();
return 0;
}There is the abstract keyword in C# (to my knowledge the same works in Java) ...
abstract class Base
{
// ...
}.... with similar effects as in C++.
So if a designer feels that he'd rather not deal with any of the "problems" you describe then he can easily enforce the proper use of his base class.
BTW, I'm not disputing that base classes should *usually* be abstract (most of mine are). The problem arises with libraries that provide classes that can be used out-of-the box and as base classes. If you follow the base-classes-must-be-abstract rule then you end up with essentially double the amount of classes, see below.
That can create problems for the client who might be more particular about what actual implementation is provided. For example:
[Base] + foo() A | +---------+--------+ | | [Sub1] [Sub2] + foo() + foo()
where [Sub1] and [Sub2] provide different overrides. Now suppose the Client is indifferent to [Sub1] vs. [Sub2] but does not want to invoke foo() if the instance in hand is just a Base (i.e., no subclass specified).
Well, you can easily avoid this problem by making Base abstract (see above). Moreover, even if Base is not abstract, the client only sees the interface of Base, so why should the client ever care about the runtime type of the object he deals with? Example: .NET provides the FileStream class http://msdn2.microsoft.com/en-us/library/y0bs3w9t which is concrete and can therefore be used on its own. Now suppose you have some code that is written for and tested with FileStream objects ...
class Whatever
{
void DoIt( FileStream fs )
{
fs.Lock( /* ... */ );
int i = fs.ReadByte();
// ...
}
}.... and you have the urge to write your own FileStream class that behaves exactly like a FileStream class but does some other things behind the scenes, like e.g. write to locations that are not accessible to the ordinary FileStream. You would derive a new class from FileStream and could then pass objects of that class wherever a FileStream is accepted as parameter:
class SuperStream : FileStream { /* ... */ }Whatever whatever = new Whatever(); whatever.DoIt( new SuperStream( /*... */ ) );
So the question are:
- Why would Whatever.DoIt *ever* care or notice that it does not deal with a FileStream but a SuperStream?
- Why would Whatever.DoIt *ever* want to distinguish between FileStream and SuperStream?
Sure, Microsoft could easily have followed the base-classes-must-be-abstract rule and provide two classes instead of one:
abstract class AbstractFileStream { /* ... */ }
sealed class FileStream : AbstractFileStream { /* ... */ }In such a world one would write his code in terms of AbstractFileStream ....
class Whatever
{
void DoIt( AbstractFileStream fs )
{
fs.Lock( /* ... */ );
int i = fs.ReadByte();
// ...
}
}.... and derive your own classes from it:
sealed class SuperStream : AbstractFileStream { /* ... */ }but, as mentioned above, this essentially doubles the amount of classes. For what gain, *exactly*?
That segues to the notion that the client has to understand _the whole tree_ to access it properly. For example, suppose we can't instantiate Base on a standalone basis and we have:
[Base] + foo() A | +---------+--------+ | | [Sub1] [Sub2] + foo()
where [Sub2] gets the Base.foo() implementation. Suppose a Client invoking Base.foo() is happy with either implementation so all is well.
Why would the client ever care about what implementation he gets? All Sub1.foo() must respect are the invariants, pre- & postconditions that were established by Base. If Sub1.foo() does so then the client will never notice a difference. If Sub1.foo() fails to do so then the LSP is violated and Sub1 is faulty.
Now suppose somebody comes along doing maintenance and decides we need a third implementation:
[Base] + foo() A | +---------+--------+--------------+ | | | [Sub1] [Sub2] [Sub3] + foo() + foo()
This may break the original Client if the Sub3.Foo() implementation is unacceptable (i.e., an LSP violation from the Client perspective) even though the original Client and its context was not touched.
This is just an example that derived classes must observe the LSP or else there's a bug in the software. I don't see how this backs your assertion that one should not be able to instantiate Base classes.
To fix this problem we would need to do further surgery on the tree:
[Base] + foo() A | +---------+--------+ | | [SubA] [Sub3] A + foo() | +-----+------+ | | [Sub1] [Sub2] + foo() + foo()
Not only do we have to perform more surgery on the tree, we have to change the calling context in Client to invoke SubA.foo() rather than Base.foo(). Thus the Client has to understand the tree to invoke the correct property implementations
How could this ever fix the problem above? Sub3.foo() is buggy, no amount of rearranging the inheritance tree will fix that.
Bottom line: overriding implementations in subclassing was just one of those things that seemed like a good idea at the time but turned out to be a real Bad Idea.
Reality disagrees with this statement. Most of the real work is done with languages that allow exactly that (C++, Java, C#, VB, etc.). BTW, would you care to entertain us with a name of a language that, in your view, is not poorly formed?
-- Andreas Huber
When replying by private email, please remove the words spam and trap from the address shown in the header.
.
- Follow-Ups:
- Re: LSP and subtype
- From: H. S. Lahman
- Re: LSP and subtype
- References:
- LSP and subtype
- From: Tony Johansson
- Re: LSP and subtype
- From: H. S. Lahman
- LSP and subtype
- Prev by Date: Re: What is the point in using argument contravariance
- Next by Date: Data Access Objects?
- Previous by thread: Re: LSP and subtype
- Next by thread: Re: LSP and subtype
- Index(es):
Relevant Pages
|