HiveBrain v1.2.0
Get Started
← Back to all entries
principlecppModerate

Design dilemma: extensibility vs simplicity

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
simplicitydesigndilemmaextensibility

Problem

Here is my problem abstracted to Bird classes. I know that number of Birds will increase on the future and new behaviors might be needed. With 10 Birds first design might not look so simple and lead to lots of duplicated code. On the other hand, second design can be perceived as "class explosion". Which of these two designs would be considered best-practice?

Classic:

#include 

class Bird
{
  public:
    virtual void Fly() const = 0;
    virtual void Speak() const = 0;
};

class Eagle : public Bird
{
  public:
    virtual void Fly() const
    {
      std::cout Fly();
  bird->Speak();
  delete bird; bird = NULL;

  bird = new Penguin();
  bird->Fly();
  bird->Speak();
  delete bird; bird = NULL;

  return 0;
}


"Better?

#include 
#include 

class FlyStyle
{
  public:
    virtual void Fly() const = 0;
};

class FlyHigh : public FlyStyle
{
  virtual void Fly() const
  {
    std::cout Fly();
    }
    virtual void Speak() const
    {
      speakstyle->Speak();
    }
  protected:
    FlyStyle* flystyle;
    SpeakStyle* speakstyle;
};

class SuperBirdFactory
{
  public:
    static SuperBird* createEagle()
    {
      return new SuperBird(new FlyHigh(), new SpeakLoud()); 
    }
    static SuperBird* createPenguin()
    {
      return new SuperBird(new NoFly(), new NoSpeak()); 
    }
};

int main()
{
  SuperBird* bird = NULL;

  bird = SuperBirdFactory::createEagle();
  bird->Fly();
  bird->Speak();
  delete bird; bird = NULL;

  bird = SuperBirdFactory::createPenguin();
  bird->Fly();
  bird->Speak();
  delete bird; bird = NULL;

  return 0;
}

Solution

In my opinion, a better approach (along similar lines) is to avoid abstract base classes and instead use generic "policy" classes. You informally define an interface for each behaviour, and mix them into the class as template parameters. This avoids the need for dynamic memory allocation, and removes the overhead of virtual function calls; everything is resolved at compile time. Your example could be something like this:

#include 

// Flying styles must have interfaces compatible with this
// struct FlyStyle
// {
//    void Fly() const;
// };

struct FlyHigh
{
  void Fly() const
  {
    std::cout 
class SuperBird
{
  public:
    void Fly() const
    {
      flystyle.Fly();
    }
    void Speak() const
    {
      speakstyle.Speak();
    }
  private:
    FlyStyle flystyle;
    SpeakStyle speakstyle;
};

typedef SuperBird Eagle;
typedef SuperBird Penguin;

int main()
{
  Eagle eagle;
  eagle.Fly();
  eagle.Speak();

  Penguin penguin;
  penguin.Fly();
  penguin.Speak();
}

Code Snippets

#include <iostream>

// Flying styles must have interfaces compatible with this
// struct FlyStyle
// {
//    void Fly() const;
// };

struct FlyHigh
{
  void Fly() const
  {
    std::cout << "Fly high!" << std::endl;
  }
};

struct NoFly
{
  void Fly() const
  {
    std::cout << "No fly!" << std::endl;
  }
};

// Speaking styles must have interfaces compatible with this
// struct SpeakStyle
// {
//     void Speak() const;
// };

struct SpeakLoud
{
  void Speak() const
  {
    std::cout << "Speak LAUD!!!!" << std::endl;
  }
};

struct NoSpeak
{
  void Speak() const
  {
    std::cout << "No speaking!" << std::endl;
  }
};

template <class FlyStyle, class SpeakStyle>
class SuperBird
{
  public:
    void Fly() const
    {
      flystyle.Fly();
    }
    void Speak() const
    {
      speakstyle.Speak();
    }
  private:
    FlyStyle flystyle;
    SpeakStyle speakstyle;
};

typedef SuperBird<FlyHigh, SpeakLoud> Eagle;
typedef SuperBird<NoFly, NoSpeak> Penguin;

int main()
{
  Eagle eagle;
  eagle.Fly();
  eagle.Speak();

  Penguin penguin;
  penguin.Fly();
  penguin.Speak();
}

Context

StackExchange Code Review Q#1022, answer score: 17

Revisions (0)

No revisions yet.