Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

Liskov Substitution Principle

0 views
Skip to first unread message

mail...@googlemail.com

unread,
Oct 13, 2006, 6:06:32 AM10/13/06
to
Is this design well-formed? It contradicts the LSP and Design by
contract anyhow. LSP tells us that "In class hierarchies, it should be
possible to treat a specialized object as if it were a base class
object."

This is the design in question:

//////////////////////// Two abstract data types:

class Document
{
public:
virtual ~Document() {}
};

class Printer
{
public:
virtual void print(Document const&) const=0;
};

//////////////////////// Documents to be printed

class TextDocument: public Document {};
class Drawing: public Document {};

//////////////////////// Derived classes contradicting LSP

class Plotter: public Printer // a Plotter prints drawings only
{
public:
void print(Document const& d) const
{
/* use danymic_cast<Drawing const&>(d) */
}
};

class LinePrinter: public Printer // a LinePrinter prints text only
{
public:
void print(Document const& d) const
{
/* use danymic_cast<TextDocument const&>(d) */
}
};

//////////////////////// test function

void test(Printer const& p)
{
TextDocument td;
p->print(td);
}

//////////////////////// end

Now, if I were to pass a Plotter printer to the test function,
Plotter::print would throw a bad_cast exception, because it prints
drawings only.

What exactly went wrong here? Is a Plotter not a printer, after all?
(Same would happen to the LinePrinter.)

I guess Plotter is not a Printer. Well, it is, but it doesn't print
like a general "printer" (whatever that may be) anyway. So I either
remove the Printer::print method completely and use dynamic_cast a lot
to access Plotter::print, or I don't have Plotter inherited from
Printer (which I don't prefer, because I need a pointer pointing to
various printers).

I rather remove the Printer::print method and use dynamic_cast a lot,
which is ugly. Because this way it still throws exceptions if 'derived'
doesn't happen to be a Printer. Either way, there's always a
dynamic_cast throwing something unpleasant, isn't it?

Moonlit

unread,
Oct 13, 2006, 6:57:58 AM10/13/06
to

Hi,

Being more of a practical guy. I would say that the design is not very good.
You derive everything from document but since document is so generic that it
actually can't do anything (it has no methods) there is little or no
advantage.

Secondly a plotter can't do everything a printer can therefore it isn't a
specialized version of printer and shouldn't derive from it.

I would say a document is best at knowiing how to print itself. So I could
pass the document the printer object. If plotter and lineprinter have
something in common then that is the part that would go in the printer
object and additional stuff in plotter or lineprinter.

Now document can have a method to print on a printer (the common stuff) a
method to print on the lineprinter and a method to print on a plotter.

Regards, Ron AF Greve

http://moonlit.xs4all.nl

<mail...@googlemail.com> wrote in message
news:1160733992....@b28g2000cwb.googlegroups.com...

mail...@googlemail.com

unread,
Oct 13, 2006, 7:27:54 AM10/13/06
to

Moonlit wrote:
> Hi,
>
> Being more of a practical guy. I would say that the design is not very good.
I agree.

> You derive everything from document but since document is so generic that it
> actually can't do anything (it has no methods) there is little or no
> advantage.

The classes are simplified. There could, of course, be a method like
virtual std::vector<Pixel> get_content() const=0;
or something like that in the Document class.

> Secondly a plotter can't do everything a printer can therefore it isn't a
> specialized version of printer and shouldn't derive from it.

True.

> I would say a document is best at knowiing how to print itself. So I could
> pass the document the printer object. If plotter and lineprinter have
> something in common then that is the part that would go in the printer
> object and additional stuff in plotter or lineprinter.
>
> Now document can have a method to print on a printer (the common stuff) a
> method to print on the lineprinter and a method to print on a plotter.

And if I happen to have hundreds of printers? Or if I wanted to add
some, I would have to add methods to each class in the Document
hierarchy. I don't know...

Moonlit

unread,
Oct 13, 2006, 7:33:29 AM10/13/06
to
HI


<mail...@googlemail.com> wrote in message
news:1160738874.4...@m73g2000cwd.googlegroups.com...

The problem is. If your printers have things in common you only need one
print method. If you want some specialized thing only one printer can do (or
you might be able to group them i.e. make the derived tree a bit deeper)
then you do need more methods.

How are you going to used the specialized features from some printer only
using the generic printing routines, you can't. Object oriented programming
is not some miracle by which you don't have to code it only lets you reuse
code that's already there.

Moonlit

unread,
Oct 13, 2006, 7:43:31 AM10/13/06
to
Hi,


Okay there is another solution but it is a bit ugly. Add some or all
functions from all printers to the printer object but leave them empty. Then
in the derived classes overide the implemented ones.

Not very nice, makes debugging harder, I guess but less coding. I might or
might not work and can break when adding a new printer. For instance when a
printer hass less features than you expected in the initial design, it won't
give a compiler error, since all the (virtual) methods are there.

Regards, Ron AF Greve.


Stuart Golodetz

unread,
Oct 13, 2006, 8:33:56 AM10/13/06
to
<mail...@googlemail.com> wrote in message
news:1160733992....@b28g2000cwb.googlegroups.com...

I think the problem is your concept of what a Printer is here. Not all
printers in this scenario can print drawings and text. Why not do something
like this:

class DrawingPrinter
{
public:
virtual void print(const Drawing& d) = 0;
};

class TextPrinter
{
public:
virtual void print(const TextDocument& d) = 0;
};

class Plotter : public DrawingPrinter
{
public:
void print(const Drawing& d)
{
//...
}
};

class LinePrinter : public TextPrinter
{
public:
void print(const TextDocument& d)
{
//...
}
};

You might also have a general printer:

class GeneralPrinter : public DrawingPrinter, TextPrinter {};

class SuperFunkyPrinter : public GeneralPrinter
{
public:
void print(const Drawing& d)
{
//...
}

void print(const TextDocument& d)
{
//...
}
};

In your original code, the problem was that your Plotter isn't a Printer,
even though it represents something that is a printer, if you see the
distinction? In other words, Plotter may represent a real-life plotter,
which is a real-life printer, but not the sort of real-life printer
represented by Printer. You see my point?

HTH,
Stu


Daniel T.

unread,
Oct 13, 2006, 8:55:43 AM10/13/06
to
mail...@googlemail.com wrote:

> Is this design well-formed? It contradicts the LSP and Design by
> contract anyhow. LSP tells us that "In class hierarchies, it should be
> possible to treat a specialized object as if it were a base class
> object."
>
> This is the design in question:

[The example is much like the "animal eats food", "lion eats meat", "cow
eats grass" example.]

What you have is a blatant violation of OO principles (Tell Don't Ask, &
LSP) but I'll let the purists decide if that is poor design.

To make the design more OO would require a lot of rework and there
simply isn't enough information provided to decide how that rework
should look. However, when you are writing the code, *you* know which
type of Document you are working with in what parts of the code, and
which type of printer you are working with. So just put that explicitly
in the code and you should be better off.

So, you might end up with something like this:

class TextDocument {
LinePrinter* printer;
};

class Drawing {
Plotter* plotter;
};

The two classes above may, or may not, derive from Document, depending
on how Document is used in the code.

--
There are two things that simply cannot be doubted, logic and perception.
Doubt those, and you no longer have anyone to discuss your doubts with,
nor any ability to discuss them.

Noah Roberts

unread,
Oct 13, 2006, 11:48:51 AM10/13/06
to

mail...@googlemail.com wrote:
> Is this design well-formed? It contradicts the LSP and Design by
> contract anyhow.

Then no. Any time LSP is violated you have a poor design.

werasm

unread,
Oct 13, 2006, 12:04:18 PM10/13/06
to

mail...@googlemail.com wrote:

> And if I happen to have hundreds of printers? Or if I wanted to add
> some, I would have to add methods to each class in the Document
> hierarchy. I don't know...

struct Printable
{
virtual void print() = 0;
virtual ~Printable(){ }
}

struct Document : Printable
{
//...Additional interface
virtual void print();
};

struct Drawing : Printable
{
//... Additional interface
};

struct PrinterBase
{
virtual void print( Printable& printable ) = 0;
virtual ~PrinterBase(){ }
};

struct EpsonPrinter : PrinterBase
{
//To Be Implemented...
virtual void print( Printable& printable );
};

//etc...

No problem... Liskov substitutable. <Drawing> is not a <Document>

Regards,

Werner

Kirit Sælensminde

unread,
Oct 13, 2006, 10:33:42 PM10/13/06
to

mail...@googlemail.com wrote:
> Is this design well-formed? It contradicts the LSP and Design by
> contract anyhow. LSP tells us that "In class hierarchies, it should be
> possible to treat a specialized object as if it were a base class
> object."

LSP doesn't really say this at all, but it is one consequence. To
understand LSP properly we have to understand polymorphism a bit
better.

There are three types of polymorphism, and C++ supprts all three (in
these explanations a client is the function or method that makes use of
the 'polymorphic' type).

* Inclusional polymorphism is what you are talking about. It is where a
client is given a sub-class as a substitute for a superclass and is
what most people (at least in the OO world) think of when polymorphism
is raised.
* Operational polymorphism is where a client is written in terms of the
operations that the type supports. std::min is a good example of this.
Any type can be used so long as it supports <. Most of the STL is
written using this principle of substitutability and it is often called
genericity or in other languages 'duck typing'.
* Parametric polymorphism is where the client changes depending on the
type given it. An example of this is the specialisation of std::vector<
bool > to give a much more memory efficient representation.

In practice LSP talks about the substitution of one instances of one
type for an instances of another type and discusses the equivalence
*from the point of view of the client code*. This means that members of
a class hiearchy may or may not be subsitutable depending on how the
client uses the instances.

>
> This is the design in question:

[snip]

> Now, if I were to pass a Plotter printer to the test function,
> Plotter::print would throw a bad_cast exception, because it prints
> drawings only.
>
> What exactly went wrong here? Is a Plotter not a printer, after all?
> (Same would happen to the LinePrinter.)

There is a general OO question here, and there is the specific case for
the printers.

It often helps to consider responsibilties when thinking about class
designs. The document is clearly responsible for storing and
manipulating its content and the printer class is responsible for
controlling the printer. Your problem amounts to working out which of
these two classes should manage the translation between the
representations.

The correct answer is neither of them. For this situation you should
put a mediation class between them that handles the translation. This
mediation class is now responsible for turning the document's
representation into one suitable for a given printer.

For the specifics of the printers, we might identify three types of
printer - your line printer that can only print plain text, the plotter
which will print a vector representation of the document and a more
normal printer which prints a raster which is normally derived from
text, raster and vector forms.

For each of these outputs you might define a top level class that the
mediator can talk to. Different mediators will talk to different top
level classes. We can then use multiple inheritence on a given printer
implementation depending on which mediators it can accept input from.

Examples of mediators might be 'Strip non-text', 'Rasterise to a given
DPI', 'Rasterise images, keep text' etc

If you design it correctly then the mediators can also be chained which
means you can place 'Strip colour' as a mediator at the start of a
chain before connecting that into a second mediator just before the
printer at the end.

You will probably find that the same technique can give you export
capability to different file formats by suitably choosing the mediators
and 'printing' to a file, for example JPEG or HTML.

In more general programming terms what you're doing here is decoupling
the printer and the document. By doing this decoupling you'll find that
you have a design that is:

* easier to work with and understand because the scope of each class is
smaller (higher coherence);
* much more powerful because it is easy to extend (due to lower
coupling).

A bit short on C++, but hopefully useful nonetheless :)


K

Jacek Dziedzic

unread,
Oct 14, 2006, 5:20:10 AM10/14/06
to
mail...@googlemail.com wrote:
> Is this design well-formed? It contradicts the LSP and Design by
> contract anyhow. LSP tells us that "In class hierarchies, it should be
> possible to treat a specialized object as if it were a base class
> object."
> [snip].

I think it's the "is an ellipse a kind-of circle" problem
dealt in the FAQ:

http://www.parashift.com/c++-faq-lite/proper-inheritance.html#faq-21.6

Perhaps both Plotter and Printer should derive from
something fairly abstract like PrintingDevice?

HTH,
- J.

mail...@googlemail.com

unread,
Oct 14, 2006, 5:52:13 PM10/14/06
to
I like your suggested design, Stuart Golodetz. I like Kirit
Sælensminde's suggestion to use mediation classes, too.

I'll sure be using one of these. My decision will depend on complexity
I guess.

Again, thank you very much, folks.

0 new messages