Дилемма дизайн: расширяемость против простоты


Вот моя проблема, отведенную для птиц - классы. Я знаю, что количество птицбудет увеличиваться в будущем и новые варианты поведения могут быть необходимы. С 10 птиц первый дизайн не может выглядеть так просто и привести к много повторяющегося кода. С другой стороны, второй дизайн, может быть воспринято как "взрыв класса". Какой из этих двух проектов будут рассмотрены лучшие практики?

Классика:

#include <iostream>

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

class Eagle : public Bird
{
  public:
    virtual void Fly() const
    {
      std::cout << "Eagle shall fly now!" << std::endl;
    }
    virtual void Speak() const
    {
      std::cout << "Eagle speaking!" << std::endl;
    }
};


class Penguin : public Bird
{
  public:
    virtual void Fly() const
    {
      std::cout << "Penguin shall fly now!" << std::endl;
    }
    virtual void Speak() const
    {
      std::cout << "Penquin speaking!" << std::endl;
    }
};

int main()
{
  std::cout << "..." << std::endl;
  Bird* bird = NULL;

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

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

  return 0;
}

"Лучше?

#include <iostream>
#include <cassert>

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

class FlyHigh : public FlyStyle
{
  virtual void Fly() const
  {
    std::cout << "Fly high!" << std::endl;
  }
};

class NoFly : public FlyStyle
{
  virtual void Fly() const
  {
    std::cout << "No fly!" << std::endl;
  }
};

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

class SpeakLoud : public SpeakStyle
{
  virtual void Speak() const
  {
    std::cout << "Speak LAUD!!!!" << std::endl;
  }
};

class NoSpeak : public SpeakStyle
{
  virtual void Speak() const
  {
    std::cout << "No speaking!" << std::endl;
  }
};

class SuperBird
{
  public:
    SuperBird(FlyStyle* fly, SpeakStyle* speak)
      : flystyle(fly),
      speakstyle(speak)
  {
    assert(NULL != flystyle);
    assert(NULL != speakstyle);
  }

    ~SuperBird() { delete flystyle; delete speakstyle;}

    virtual void Fly() const
    {
      flystyle->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;
}


1387
14
задан 27 февраля 2011 в 03:02 Источник Поделиться
Комментарии
10 ответов

На мой взгляд, лучшим подходом (по аналогии), чтобы избежать абстрактных базовых классов, и вместо того, чтобы использовать общие "политики" классов. Вы неформально определить интерфейс для каждого поведения, и смешивать их в класс в качестве параметров шаблона. Это позволяет избежать динамического выделения памяти, и удаляет накладные расходы на вызовы виртуальных функций; все решается во время компиляции. Ваш пример может быть что-то вроде этого:

#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();
}

17
ответ дан 28 февраля 2011 в 01:02 Источник Поделиться

Тот факт, что у вас есть две совершенно разные выходы из этих двух примеров должно быть огромным ключ к ответу.

Eagle shall fly now!
Eagle speaking!
Penguin shall fly now!
Penguin speaking!

против

Fly high!
Speak LAUD!!!!
No fly!
No speaking!

Если вы хотите, чтобы ваш класс Пингвин летать как пингвин, что бы это ни значило, то ты, наверное, права, чтобы пойти с первым примером. Если вы хотите, чтобы все нелетающих птиц, действовать точно так же, как друг с другом, Когда зовет летать(), второй, наверное, лучше.

Что сказал, я сомневаюсь, что я бы пойти так далеко, как с помощью фабрики класса. Я бы переименовал Суперптицы как птица и впредь извлекать Орла и пингвина от этого, передав соответствующие FlyStyle и SpeakStyle на суперкласс конструктор, например

class Penguin : public Bird
{
public:
Penguin() : Bird(new NoFly(), new NoSpeak()) {}
}

Это попытка следовать один принцип ответственности, говоря "если я хочу изменить действие всех птиц, которые не летают, я должен изменить NoFly класса соответственно, а если я хочу сделать пингвина летать, я изменю Пингвин класс".

Шаблона "Фабрика" больше подходит к противной случае использовать, где ваш вызывающий код не знает или не заботится, какой тип птички вы хотите создать но не знаете достаточно информации для класса фабрики, чтобы принять это решение, как

bird = BirdFactory::create(CAN_FLY, MAKES_NOISE);
bird.Fly();
bird.Speak();

8
ответ дан 27 февраля 2011 в 07:02 Источник Поделиться

Если там будут разные виды птиц, которые разделяют летающий или стилю речи ("стратегия", то "банда четырех" называет это), то второй дизайн имеет смысл. Если все птицы будут иметь уникальные летающие и говорящие стили, придерживаться первого.

Если вы не знаете ответа, придерживаться первого дизайна. В отсутствие информации, всегда предпочитают самое простое решение. Это легче выразить простую конструкцию позже, чем это должно упростить напрасно расширяемый один.

5
ответ дан 28 февраля 2011 в 05:02 Источник Поделиться

Давайте посмотрим: вы получили голоса за первое и за второе. Хм...как я могу спорить с обоими. О, я понял: обе конструкции должно быть не так!

С абстрактной точки зрения, вы не имеете ничего, чтобы оправдать птица/орел/Пингвин классы как классы вообще. В частности, даже если вы определили несколько виртуальных функций, они действительно не делают ничего вообще. Во всех случаях, они делают ровно то же самое-выводит строку.

Таким образом, вы должны, вероятно, просто создать птиц, и при создании птица необходимо указать "flyingstyle" и "speakingstyle" в качестве аргументов конструктора:

#include <string>
#include <iostream>

class bird {
std::string movingstyle;
std::string speakingstyle;
public:
bird(std::string const &ms, std::string const &ss)
: movingstyle(ms), speakingstyle(ss)
{}

void speak() { std::cout << speakingstyle << "\n"; }
void move() { std::cout << movingstyle << "\n"; }
};

int main() {
bird birds[2] = {
bird("Eagle Flying", "Eagle speaking"),
bird("Penguin swimming", "Penguin speaking")
};

for (int i=0; i<2; i++) {
birds[i].speak();
birds[i].move();
}
return 0;
}

Отдельные классы резерва для объектов, которые действительно ведут себя по-разному, а не одинаково с разными значениями.

Я также отмечу, что я наполовину-ремонт основной недостаток в исходной иерархии. Включая "летать" в свой базовый класс, ты утверждал, что все птицы могут летать -- откровенная ложь. Здесь я изменил, что более абстрактное понятие "движение" вместо. При этом, вы можете иметь пингвинов, которые плавают, страусы, которые бегают, и орлы, которые летают.

Нечто подобное, вероятно, следует поступить с "говорить". Некоторые птицы не издают звуков, так что вы должны рассмотреть, хотите ли вы что-нибудь вроде:

class bird {};

class speaking_bird : public bird {
public:
virtual void speak() = 0;
};

class silent_bird : public bird {};

Это позволяет корректно моделировать тот факт, что некоторые птицы "говорят", а другие нет. Она также имеет принципиальной разницы в поведении между "говорящие птицы" и "тихая птица", которая оправдывает использование наследования.

Кроме того, можно утверждать в дизайне, что все птицы могут говорить, но в осуществлении сказать, что некоторые птицы (что я могу знать, не могу сказать) просто никогда не попросят говорить:

class bird {
bool silent;
public:
void speak() { if (silent) throw runtime_error("Cannot speak"); }
};

Есть справедливое основание для говорю, что это ляп, но в некоторых случаях стоит избегать создания дополнительных уровней наследования (и таких), чтобы покрыть непонятных случаях, которые вы, вероятно, не все равно в реальном использовании. В то же время, это может привести к захламлению другого кода с проверками для тех случаев, чтобы избежать вызова функции, не будет работать, и вы бы вообще предпочел избежать этого. Таким образом, вы должны смотреть на конкретную ситуацию и решить, какой бы из существующих зол.

5
ответ дан 1 марта 2011 в 04:03 Источник Поделиться

Начнем с первого, рефакторинг ко второму.

Интерфейс одинаково в обоих случаях. Весь код, который использует эти классы не должны заботиться, какой метод вы выберете для реализации.

Вы должны реализовать первый, более простой метод. Когда это становится полезным, необходимо применить другие методы. Например, если вы найдете себя много нелетающих птиц можно реализовать класс FlightlessBird, который имеет общую логику. При разговоре становится слишком сложным, вы, возможно, захотите, чтобы разбить его в свой собственный объект. Другими словами, реализовать частями второй версии, как они становятся необходимыми.

Ваша вторая версия пытается иметь максимальную гибкость. Попытка сделать, что имеет странную тенденцию приводить к много тупого кода и по-прежнему не хватает гибкости, что вам нужно. Поэтому в большинстве случаев лучше подождать, пока у вас есть лучшее представление о том, что вам действительно нужно, а не гадать раньше времени.

Нет существенной выгоды в реализации сейчас гибкий вариант. Есть определенные преимущества в реализации сейчас простую версию. Вы только основная идея, что код необходимо будет сделать позже. Пытаясь реализовать что сейчас будут производить только беспорядок.

4
ответ дан 28 февраля 2011 в 04:02 Источник Поделиться

Первый определенно лучше. Вы должны попробовать использовать некоторые классы как получит работу, а также за множеством мелких методов за несколько больших методов. Это позволит сделать код более удобным для чтения, понимания и поддержки. Я думаю, что вы могли бы улучшить на ваш первый немного:

#include <iostream>
using namespace std;

class Bird {
public:
void print(const char* str) {
cout << str << endl;
}
};

class Eagle : public Bird {
public:
void fly() {
print("Eagle shall fly now!");
}
void speak() {
print("Eagle speaking!");
}
};

int main() {
Eagle* eagle = new Eagle();
eagle->fly();
eagle->speak();
return 0;
}

В основном, я говорю 'Орел-птица, и будет делать то же самое с другими птицами. Я оставлю определенные части (например. что нужно сделать для Fly() и говорить()) в занятиях с детьми (например, Орел) и общие детали (например. возможность печати на stdout) в родительском классе (птица).

3
ответ дан 27 февраля 2011 в 07:02 Источник Поделиться

Первый вариант лучше. Читаем и поцелуй-совместимыми. Если вы когда-нибудь понадобится, чтобы разделить осуществления полета, вы можете написать миксин. Кроме того, несвязанные, вы должны смотреть на РАИИ и избавиться от этих утверждать/новый/удалить.

3
ответ дан 27 февраля 2011 в 09:02 Источник Поделиться

Я действительно предпочитаю первый, так как это проще. Вы всегда можете подмешать позже другие интерфейсы:

class Predator {
public:
virtual void attack();
}

class Eagle: public Bird, public Predator {
}

2
ответ дан 27 февраля 2011 в 07:02 Источник Поделиться

Это первое проще, и вы, возможно, захотите использовать его, если вам не нужно много гибкости. Если вы задаете себе этот вопрос, я думаю, это означает, что вы, возможно, потребуется гибкость.

В этом маленьком примере, первый действительно способ иметь меньше видов, но в целом, если вам нужно много разных комбинаций может быть больше. Возьмем второй пример, если ваш "Суперптицы" продлен быть смесь из 5 разных объектов и для каждого из них определены 5 абстрактные типы: делать то же самое для первого примера нужно 3000 классов с большим количеством стереотипных повторяющихся во многих классах.

Ознакомиться с Банда четырех принципу: пользу состав объекта наследования классов/

0
ответ дан 28 февраля 2011 в 09:02 Источник Поделиться

Это гораздо более важно, чтобы иметь возможность изменить дизайн в будущем, что выбрать "правильный дизайн" с самого начала.

Простой способ, чтобы иметь эту способность, чтобы как можно больше скрыть.

Пользователи довольны один класс с виртуальной функцией? Скрывать знание того, что разные птички и не отдельные классы.

Пользователи ОК с делегируя создание птицу в свой модуль? Скрыть все конструкторы из птицы (или его реферат).

Внутренне - использовать все, что хорошо для текущих целей, и изменить внутренний дизайн, как только он станет неудобен.

Таким образом, интерфейс может быть следующим:

class Bird
{
public:
virtual void Fly()=0;
virtual void Speak()=0;
};
class BirdFactory
{
public:
static Bird *CreateEagle();
...
};

Все остальные должны быть скрыты либо в качестве личных вещей определенного класса или в окружении других намеков о том, внутреннее (пространство имен "деталь", каталога "внутренних", и т. д.).

0
ответ дан 2 марта 2011 в 08:03 Источник Поделиться