Liskov Substitution Principle

M

mailforpr

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?
 
M

Moonlit

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
 
M

mailforpr

Moonlit said:
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
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...
 
M

Moonlit

HI


I agree.

The classes are simplified. There could, of course, be a method like


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

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.


Regards, Ron AF Greve

http://moonlit.xs4all.nl
 
M

Moonlit

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

Stuart Golodetz

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?

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
 
D

Daniel T.

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

werasm

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
 
?

=?iso-8859-1?q?Kirit_S=E6lensminde?=

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
 
J

Jacek Dziedzic

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

mailforpr

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.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,768
Messages
2,569,574
Members
45,048
Latest member
verona

Latest Threads

Top