18

I encountered a question today, found here, which raised this question for me.

Here's a pseudo-code example of what I'm getting at:

class Car{
public:
    virtual int goFast() = 0;
};


class FordFocus : public Car {
public:
    int goFast(){
        return 35;
    };
};


class Lamborghini : public Car {
    bool roof;
public:
    int goFast(){
        return -1/0;  // crash 
    };
    void retractTheRoof(){
        roof = 0;
    };
};



class RichGuy {
    vector<Car *> cars;
public:
    void goDrive() {

        for(int i = 0; i < cars.size(); ++i) {
            if(Lamborghini* lambo = dynamic_cast<Lamborghini*>(cars[i])) {
                lambo->retractTheRoof();
            };
            goFast();
        };
    };
};

In the example, there is a RichGuy class. Richguy only keeps track of his Cars in a single vector. Because he has so many Cars it would be too troublesome to keep track of them based on if they're a FordFocus or a Lamborghini. However, the only type of car he has with a retractable roof is the Lambo. In order to retractTheRoof(), RichGuy must now determine if the Car he has is indeed a Lamboghini, and then downcast to execute this function of it.

Based on this example, was the choice to downcast in good design? Or did it violate the purpose of polymorphism, assuming that purpose is to allow derived classes to define their own behavior, and provide a common interface for classes like RichGuy? And if so, is there a better way to allow for functions like retractTheRoof() (or at least it's effect) to be available to RichGuy to use?

Community
  • 1
  • 1
Anne Quinn
  • 12,609
  • 8
  • 54
  • 101
  • 1
    If you care about the retractability of the roof, you could have a `ConvertibleCar` abstract base class between `Car` and `Lamborghini` in the inheritance chain. – Sander De Dycker Aug 26 '11 at 05:52
  • 2
    @Sander - `RichGuy` is very vain and owns many types of `Car`. He can't keep track of if they're convertables, jeeps, suvs, trucks, etc., in the same way he can't keep track of what models they are. The middle abstraction wouldn't simplify it enough for him. – Anne Quinn Aug 26 '11 at 06:03
  • 1
    @Sander De Dycker: There are a few different approaches that people commonly use to deal with this situation; DeadMG's & littleadv's are two of these, and yours is another. It should probably be added as a proper Answer, IMO :) – SSJ_GZ Aug 26 '11 at 06:03
  • @Clairvoire: He wouldn't need to, though: each individual Car knows whether it is a ConvertibleCar or not. RichGuy then, for each of the cars he owns, dynamic_casts to ConvertibleCar and, if successful, tells it to retractTheRoof. It fits in pretty well with OO principles: the dynamic_cast is not to see if it is a particular concrete type of Car (e.g. Lamborghini) (which would be un-OO), but to see if it can *respond to* a given message. If it can, the car handles the message according to type via polymorphism. In my subjective opinion, this is the better of the 3 solutions presented. – SSJ_GZ Aug 26 '11 at 06:19
  • @MrLunchtime : added as an answer. – Sander De Dycker Aug 26 '11 at 06:21
  • @MrLunchtime - Oh, okay. I was thinking of the added layer of abstraction from the RichGuy's perspective, and not the Car's. Looking at the answer as it's fleshed out, the added base class is well used afterall. – Anne Quinn Aug 26 '11 at 06:41

4 Answers4

14

Now if there are more than one type of cars which is retractable, say such cars are CarA, CarB, and CarC (in addition to Lamborghini), then are you going to write this:

if(Lamborghini* lambo = dynamic_cast<Lamborghini*>(cars[i])) {
    lambo->retractTheRoof();
}
else if(CarA * pCarA = dynamic_cast<CarA*>(cars[i])) {
    pCarA->retractTheRoof();
}
else if(CarB * pCarB = dynamic_cast<CarB*>(cars[i])) {
    pCarB->retractTheRoof();
}
else if(CarC * pCarC = dynamic_cast<CarC*>(cars[i])) {
    pCarC->retractTheRoof();
}

So a better design in such cases would be this: add an interface called IRetractable and derive from it as well:

struct IRetractable 
{
   virtual void retractTheRoof() = 0;
};

class Lamborghini : public Car, public IRetractable {
   //...
};

class CarA : public Car, public IRetractable {
   //...
};
class CarB : public Car, public IRetractable { 
   //...
};
class CarC : public Car, public IRetractable {
   //...
}; 

Then you can simply write this:

if(IRetractable *retractable =  dynamic_cast<IRetractable *>(cars[i])) 
{
    retractable->retractTheRoof(); //Call polymorphically!
}

Cool? Isn't it?

Online demo : http://www.ideone.com/1vVId

Of course, this still uses dynamic_cast, but the important point here is that you're playing with interfaces only, no need to mention concrete class anywhere. In other words, the design still makes use of runtime-polymorphism as much as possible. This is one of the principle of Design Patterns:

"Program to an 'interface', not an 'implementation'." (Gang of Four 1995:18)

Also, see this:


Other important point is that you must make the destructor of Car (base class) virtual:

class Car{
public:
    virtual ~Car() {} //important : virtual destructor
    virtual int goFast() = 0;
};

Its imporant because you're maintaining a vector of Car*, that means, later on you would like to delete the instances through the base class pointer, for which you need to make ~Car() a virtual destructor, otherwise delete car[i] would invoke undefined behaviour.

Community
  • 1
  • 1
Nawaz
  • 353,942
  • 115
  • 666
  • 851
  • 2
    Thank you! Aha, to not slight the other answers, but yours was the first to directly answer the question posed in the title. – Anne Quinn Aug 26 '11 at 06:38
  • @Clairvoire: Exactly. Added one comment in the code `retractable->retractTheRoof(); //Call polymorphically!` and one sentence to make this more clear: `In other words, the design still makes use of runtime-polymorphism as much as possible.` – Nawaz Aug 26 '11 at 06:45
7

Yes, this is usually the better design case. You should only introduce a virtual function into the hierarchy if it makes sense for all derived classes.

However, your MODEL enum is completely worthless- that's what dynamic_cast is actually for.

if(Lamborghini* lambo = dynamic_cast<Lamborghini*>(cars[i])) {
    lambo->retractTheRoof();
}
Puppy
  • 144,682
  • 38
  • 256
  • 465
  • Ah, right you are. Not sure why I put both of those in there, probably a force of habit, since I rarely use dynamic casts. Editing the example to fit – Anne Quinn Aug 26 '11 at 05:50
  • 2
    And when Ford becomes a convertible you should of course go everywhere you do a dynamic cast to lambo, and add the Ford there too. Nice. Very OOP. Not downvoting because your point was the correct usage of `dynamic_cast`, but it shouldn't have been an answer. – littleadv Aug 26 '11 at 06:06
  • @littleadv: YAGNI. More importantly, if that ever happens, you can just factor into a Convertible interface. – Puppy Aug 26 '11 at 12:51
4

If you care about the retractability of the roof, you could have a ConvertibleCar abstract base class between Car and Lamborghini in the inheritance chain :

class Car {
  public :
    virtual int goFast() = 0;
};

class ConvertibleCar : public virtual Car {
  public :
    virtual void retractTheRoof() = 0;
};

class FordFocus : public Car {
  public :
    int goFast() { return 35; };
};

class Lamborghini : public ConvertibleCar {
    bool roof;
  public :
    int goFast() { return -1/0; /* crash */ };
    void retractTheRoof() { roof = 0; };
};

If the RichGuy class can't keep track of all different kinds of cars separately, you can still use dynamic_cast to figure out if a certain car is of a certain type :

ConvertibleCar* convertible = dynamic_cast<ConvertibleCar*>(cars[i]);
if (convertible) {
    convertible->retractTheRoof();
};

Note that this scales quite well with different car types (ConvertibleCar, AllTerrainCar, SportsCar, ...), where the same car can inherit from 0 or more of these types. A Lamborghini would probably derive from both ConvertibleCar and SportsCar :

class Lamborghini : public SportsCar, public ConvertibleCar {
    // ...
};
Sander De Dycker
  • 16,053
  • 1
  • 35
  • 40
  • That looks like a reasonable solution! It doesn't do away with the downcast, but it makes it much more reasonable. – Anne Quinn Aug 26 '11 at 06:32
  • Thanks for putting it as an answer, this does indeed solves the scalability issue without introducing *fake* methods in the base class. – Matthieu M. Aug 26 '11 at 06:35
3

Instead of checking for "Lamborghini" in this module, because I know for a fact that Lamborghini is not the only car maker that makes cars with retractable roofs.

Then you don't need dynamic-casting at all. That's how it should have been done.

class Car{
public:
    virtual int goFast() = 0;
    virtual void retractTheRoof(){ /*no-op*/}; // default
};

class Lamborghini : public Car {
    bool roof;
public:
    int goFast(){
        return -1/0;  // crash 
    };
    void retractTheRoof(){ roof = 0;}; // specific
};

and then in the code instead of

if(Lamborghini* lambo = dynamic_cast<Lamborghini*>(cars[i])) {
    lambo->retractTheRoof();
}

do

cars[i]->retractTheRoof();

That's it.

littleadv
  • 20,100
  • 2
  • 36
  • 50
  • How is `isRetractable` different to `dynamic_cast`? Except that all the other derived classes will have a retractTheRoof function that doesn't mean anything to them. – Puppy Aug 26 '11 at 05:57
  • @DeadMG As I mentioned - it's not even necessary in my example, so I'll edit the answer and remove it entirely to avoid confusion – littleadv Aug 26 '11 at 05:59
  • No, I mean, how is it actually functionally different? All you've achieved is polluting the base interface. – Puppy Aug 26 '11 at 06:00
  • @DeadMG - well, "polluting" is a harsh word. Retractable roof is a reasonably common car characteristic. Take a use case of a car wash. You can downcast to every single module and call "Put the roof back up" for each, or you can just create the "Put the roof back up" method for all the cars, and let it be no-op for those that represent non-convertibles. If in 5 years, Ford becomes a convertible - you don't want to go and add it to the list of the downcasted types with regards to the roof, you just override the no-op implementation. You call it pollution, I call it an expandable design. – littleadv Aug 26 '11 at 06:05
  • 2
    retractable roofs are just a single function, in reality there may be many functions like it, in that they're unique to specific breeds of `Car`. If the base class contains empty functions for all these unique functions, it will get clogged pretty fast. – Anne Quinn Aug 26 '11 at 06:06
  • @littleadv: YAGNI. Really. There's no sense in a regular car having a retractTheRoof method, especially when the exact same principle can be achieved without having to do so. – Puppy Aug 26 '11 at 06:06
  • @Clairvoire - then you should have more base classes. If you go from `Car` to `Lamborghini` with nothing in between - then the retractable roof functionality has to be in `Car`. – littleadv Aug 26 '11 at 06:08
  • @DeadMG - YAGNI is a nice acronym, but I can't see why you're using it here. Neither you nor I know anything about the target system. You're assuming it's not going to change, I'm assuming it is, because most systems do, but the only one who really knows is the OP, even that not for sure. – littleadv Aug 26 '11 at 06:10
  • @littleadv - I should've specified, it would be changing. I feel like my Car example is beginning to fall short, 5 minutes in, haha – Anne Quinn Aug 26 '11 at 06:15
  • 1
    @Clairvoire - I think Nawaz's answer solves the dilemma DeadMG had with my solution for you:-) – littleadv Aug 26 '11 at 07:23
  • @littleadv: I'm throwing in YAGNI because the OP has like three lines of code and going back to refactor them later will be significantly less effort and less punishing than your suggested solution. – Puppy Aug 26 '11 at 12:52
  • @DeadMG - you do understand that the three lines of code are only here to illustrate the issue, don't you? – littleadv Aug 26 '11 at 19:53