Re: LSP and subtype



"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.

.



Relevant Pages

  • Re: Design issue with constructor arguments
    ... seem to keep getting but I dont know if it has a name or something. ... I am writing a library which is to be used by some client code. ... special processing class which invokes callback function supplied by ... I instantiate the processor and then pass a reference to it ...
    (comp.object)
  • Design issue with constructor arguments
    ... seem to keep getting but I dont know if it has a name or something. ... I am writing a library which is to be used by some client code. ... special processing class which invokes callback function supplied by ... I instantiate the processor and then pass a reference to it ...
    (comp.object)
  • Re: What about CAO?
    ... One possible solution to this is to extend your shared assembly with an interface for a SAO object. ... It needs a single method which returns a new instance of the CAO, ... Then get a reference of this object on the client via GetObject (you do not instantiate the object, you get a proxy of the existing one from the server - so it will work with the interface only on the client). ...
    (microsoft.public.dotnet.framework.remoting)
  • Re: Connecting Out of Process Servers via COM+
    ... it would create a Dispatcher-Object first and registers ... how would I instantiate the teh object at the client? ... If two Clients at the same time are requesting the COM+-Server, the requests ...
    (microsoft.public.vb.com)
  • Re: question refined - Copy file from client to server
    ... You are not using a FileStream to actually get the data from the client's ... the user browsed to the file so your page could upload it. ... doing something with it on the server. ... just manipulate the files on a client machine at will. ...
    (microsoft.public.dotnet.general)