How to handle a change in the published abstract base class?

Viewed 75

I have developed a "Kennel" application which takes care of various kinds of Dogs. My clients are expected to admit their Dogs to my application to avail the services.

So, I had defined a generic "Dog" interface. The clients need to implement the interface to create a concrete Dog type (Say Labrador, Poodle, etc), instantiate them and admit them to my kennel application(using say, kennel::admitDog(dog *dog).

Here is the Dog abstract base class:

class Dog {
public:
    Dog()
    {

    }

    virtual ~Dog()
    {

    }

    virtual void eatFood() = 0;
    virtual void takeBath() = 0;
    virtual void play() = 0;
    virtual void sleep() = 0;
};

I published this Interface and my clients already started using it to create their own concrete Dog types. In the next version of the application, I am planning to support robotic dogs in the kennel.

Here comes the problem. The robotic dogs needs Dog::rechargeBattery() in the above abstract base class. And, it doesnt need the existing Dog::eatFood() function. Adding the Dog::rechargeBattery() to the above abstract base class would affect all the existing clients who are already using this interface. They will be forced to implement the Dog::rechargeBattery() and recompile. This may not be desirable.

  1. What is the solution at this point?
  2. What should I have been done in the initial design to have this problem prevented?
3 Answers

Both questions can be answered the same, i.e. you still can implement a design to solve the problem now and prevent it for the future. In order to handle different classes (dogs in your example) which implement different subsets of operations, simply add an API to find out what they can/need. The handler/client (kennel in your example) is then able to find out what they can/need and call accordingly.

This is a more transparent design than determining the needs/capabilities based on being aware of all possible derived classes and what each of them can/need.

Consider this approach. "I can tell that you are a robot (because you reek of oil), so I will charge your battery. You (other) have salivated all over me, so you seem to be a biological dog, that is why I will feed you."
Compare it to "Do you want food? Nice sausage? Ah, you beg, so obviously you want it. Here, nice doggy." and "Your low battery indicator flashes, so I will show you the power outlet. Nice robby."
The point is that if something begs for a sausage AND has a low battery indicator, then you can charge AND feed it - without first being told that there are not only biological dogs and robodogs, but newly invented cyborgdogs.
(Sorry if this is scary, but it is for illustrating.)

The indicators of whether food or electricity is needed can be implemented in the base class, avoiding any changes to existing dog code. The API is meaningful for anything you can hold in a kennel (and can be abstracted to any class hierarchy with sets of potentially meaningful/-less operations).

To implement this concept, you can add virtual checker methods to the base class, which allow the kennel to find out what the dog needs. With a default implementation of those methods, all existing biological dogs will learn to give the right info on their needs, without the necessity to change their implementation.

For robots (which have not yet been implemented by anybody) you can require behavior of the needs-checkers which is different from the default.

class Dog {
public:
    Dog()
    {

    }

    virtual ~Dog()
    {

    }

    virtual void eatFood() = 0;
    virtual void takeBath() = 0;
    virtual void play() = 0;
    virtual void sleep() = 0;

    virtual bool boNeedsFood(void)
    {return true; /* standard dog */
    }

    virtual bool boNeedsElectricity(void)
    {return false; /* standard dog */
    }

    virtual void rechargeBattery(void)
    {    /* optional exception handling, in case kennel is malfunctioning */;
         /* sorry for the mental image... */
    }
};

class robodog
: public dog
{

public:

    /* ... */ 

    virtual bool boNeedsFood(void)
    {    return false; /* standard dog */
    }

    virtual bool boNeedsElectricity(void)
    {    return true; /* standard dog */
    }

    virtual void eatFood(void)
    {/* optional exception handling, in case kennel is malfunctioning */;}

    virtual void rechargeBattery(void)
    {
        /* actual code */
    }

}

/* ... somewhere in kennel ... */

if (doginstance.needsFood())
{doginstance.eatFood();
} /* intentionally no "else", could be cyborg dog, which needs both */
if (doginstance.needsElectricity())
{ doginstance.rechargeBattery();
}

Here's what I'd do:

#include <iostream>

using namespace std;

class Dog {
public:
    virtual ~Dog()
    {
    }

    virtual void eatFood() = 0;
    virtual void takeBath() = 0;
    virtual void play() = 0;
    virtual void sleep() = 0;
};

class RoboticDog : public Dog {
public:
    virtual void rechargeBattery() = 0;
};

class Pug : public Dog {
private:
    void eatFood()
    {
    }

    void takeBath()
    {
    }

    void play()
    {
        cout << "Pug::play()" << endl;
    }

    void sleep()
    {
    }
};

class Robo1 : public RoboticDog {
private:
    void eatFood()
    {
    }

    void takeBath()
    {
    }

    void play()
    {
        cout << "Robo1::play()" << endl;
    }

    void sleep()
    {
    }

    void rechargeBattery()
    {
        cout << "Robo1::rechargeBattery()" << endl;
    }
};

int main()
{
    Pug pug;
    Robo1 robo1;

    Dog *dogs[] = { &pug, &robo1 };

    for(unsigned i = 0; i < sizeof(dogs) / sizeof(Dog *); ++i) {
        dogs[i]->play();

        RoboticDog *robo = dynamic_cast<RoboticDog *>(dogs[i]);
        if(robo) // If dynamic_cast<> returned != nullptr, this is a RoboticDog
            robo->rechargeBattery();
    }
}

This code allows for binary compatibility with your existing clients. Their code will continue to work as-is, and new code can implement the RoboticDog interface, which is backwards-compatible with Dog.

dynamic_cast<> safely converts pointers and references to classes up, down, and sideways along the inheritance hierarchy. What that means is that if the object you have an interface pointer to implements another interface as well, dynamic_cast<SecondInterface *>(pointerToFirstInterface) will return nullptr if the underlying object does not implement SecondInterface.

So you can simply modify your code to check if the Dog * pointers you were previously working with happen to point to objects that are really RoboticDogs, and if they are, then you're free to use the full RoboticDog interface with them.

That's pretty much how they're extending interfaces in COM+ (where you can see a bunch of SomeInterfaceEx and ThatInterface2 abstract classes).

This:

class Dog {
    virtual void eatFood() = 0;
    virtual void takeBath() = 0;
    virtual void play() = 0;
    virtual void sleep() = 0;
};

class Kennel {
    void admit (Dog*);
}

Becomes this:

class DogLike {
    // virtual void eatFood() = 0; <-- removed
    virtual void takeBath() = 0;
    virtual void play() = 0;
    virtual void sleep() = 0;
};

class Dog : public DogLike {
    virtual void eatFood() = 0; // <-- added
};

class RoboticDog : public DogLike {
    virtual void rechargeBatteries() = 0;
};

class Kennel {
    void admit (DogLike*);
};

Existing clients should be source-compatible (but almost certainly not binary-compatible) with your modified library. Achieving binary compatibility may or may not be possible, but that's the case with any change to the published Dog class.

Since clients supposedly know what kind of dog they are handing to the kennel, it is possible to modify the kennel interface to separate live dogs from robotic dogs:

class Kennel {
  public:
    void admit (Dog*);
    void admit (RoboticDog*);
};

Other kinds of dogs will not be admitted.

What's next? Supposedly the kennel has facilities to serve all who answer the DogLike interface, and separate facilities for live dogs and for robotic dogs.

void Kennel::admit (Dog* dog) {
    commonFacilities.admit(dog);
    messHall.admit(dog);
}

void Kennel::admit (RoboticDog* dog) {
    commonFacilities.admit(dog);
    chargingStation.admit(dog);
}

Still no cast in sight. If it is undesirable to have two public admit methods, one can hide them behind a façade that inspects dynamic types behind the scenes.

class Kennel {
    void admit (Dog*);
    void admit (RoboticDog*);
  public:
    void admit (DogLike* dog) {
      if (auto d = dynamic_cast<Dog*>(dog)) 
        admit (d);
      else if (auto d = dynamic_cast<RoboticDog*>(dog)) 
        admit (d);
      else
        throw UnknownDogTypeError;
    }
};
Related