C ++ Справка по рефакторингу класса монстров

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

    class chef
    {
    public:
          void prep();
          void cook();
          void plate();

    private: 
          char name;
          char dish_responsible_for;
          int shift_working;
          etc...
    }

в псевдокоде это реализуется следующим образом:

   int main{
    chef my_chef;
    kitchen_class kitchen;
    for (day=0; day < 365; day++)
         {
         kitchen.opens();
         ....

         my_chef.prep();
         my_chef.cook();
         my_chef.plate();

         ....

         kitchen.closes();
         }
   }

Класс шеф-повара здесь кажется классом монстров и может им стать. chef также, похоже, нарушает принцип единой ответственности, поэтому вместо этого у нас должно быть что-то вроде:

  class employee
  {
  protected: 
        char name;
        int shift_working;
  }

  class kitchen_worker : employee
  {
  protected: 
        dish_responsible_for;
  }

  class cook_food : kitchen_worker
  {
  public:
        void cook();
        etc...
  }
  class prep_food : kitchen_worker
  {
  public:
        void prep();
        etc...
  }

и

     class plater : kitchen_worker
     {
     public:
         void plate();
     }

и т.д...

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

Кажется, это связано с более широким вопросом, который у меня есть: если один и тот же человек неизменно занимается приготовлением, приготовлением и сервировкой блюд в этом примере, каково реальное практическое преимущество наличия этой иерархии классов для моделирования того, что делает один шеф-повар? Я предполагаю, что это наталкивается на «боязнь добавления классов», но в то же время, прямо сейчас или в обозримом будущем я не думаю, что поддержание класса повара в целом ужасно обременительно. Я также думаю, что наивному читателю кода проще увидеть три разных метода в объекте chef и двигаться дальше.

Я понимаю, что это может грозить стать громоздким, когда / если мы добавим такие методы, как «cut_onions ()», «cut_carrots ()» и т. Д., Возможно, каждый со своими собственными данными, но кажется, что с ними можно справиться, сделав функция Prep (), скажем, более модульная. Более того, похоже, что SRP, доведенная до своего логического завершения, создаст класс "onion_cutters", "carrot_cutters" и т.д. один и тот же сотрудник режет лук и морковь, что помогает поддерживать одинаковую переменную состояния в разных методах (например, если сотрудник режет палец, режущий лук, он больше не имеет права резать морковь), тогда как в классе шеф-повара объекта-монстра кажется, что все, о чем позаботятся.

Конечно, я понимаю, что тогда речь идет не столько о содержательном «объектно-ориентированном дизайне», но мне кажется, что если мы должны иметь отдельные объекты для каждой из задач шеф-повара (что кажется неестественным, учитывая, что один и тот же человек является выполняет все три функции), то это, кажется, ставит во главу угла разработку программного обеспечения над концептуальной моделью. Я считаю, что объектно-ориентированный дизайн здесь полезен, если мы хотим иметь, скажем, "meat_chef", "sous_chef", "three_star_chef", которые, вероятно, представляют собой разные люди. Более того, проблема времени выполнения связана с накладными расходами, связанными со сложностью, при строгом применении принципа единой ответственности, который должен обеспечить изменение базовых данных, составляющих сотрудника базового класса, и что это изменение отражены в последующих временных шагах.

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


person user1790399    schedule 01.11.2012    source источник
comment
Иногда сопоставление реальных ролей / обязанностей / задач объектам в коде просто не работает. Может быть, вам нужна какая-то общая функция, которая принимает человека и действие. Эта функция заставляет человека применить действие.   -  person Adrian Cornish    schedule 01.11.2012
comment
модуль на каждом интерфейсе класса может дать вам больше подсказок? например, что умеет пластина? что может cook_food? им нужно наследовать или это просто навык (вызов функции)?   -  person billz    schedule 01.11.2012
comment
Посмотрите на метод композиции. Или, может быть, вам здесь нужен паттерн состояния?   -  person Denis Ermolin    schedule 01.11.2012
comment
Большое спасибо за предложения всем, они дают мне хорошие места для начала!   -  person user1790399    schedule 02.11.2012


Ответы (1)


Чтобы избежать злоупотребления иерархией классов сейчас и в будущем, вам действительно следует использовать его только при наличии отношения есть. Вы, как вы, "Cook_food a kitchen_worker". Очевидно, что это не имеет смысла в реальной жизни, и в коде тоже. "cook_food" - это действие, поэтому может иметь смысл создать класс действия и создать подкласс вместо него.

В любом случае наличие нового класса для добавления новых методов, таких как cook() и prep(), на самом деле не является улучшением исходной проблемы, поскольку все, что вы сделали, это обернули метод внутри класса. На самом деле вы хотели создать абстракцию для выполнения любого из этих действий - так что вернемся к классу действий.

class action {
    public:
        virtual void perform_action()=0;
}

class cook_food : public action {
    public:
        virtual void perform_action() {
            //do cooking;
        }
}

Затем повару может быть предоставлен список действий, которые необходимо выполнить в указанном вами порядке. Скажем, например, очередь.

class chef {
    ...
        perform_actions(queue<action>& actions) {
            for (action &a : actions) {
                a.perform_action();
            }
        }
    ...
}

Это более широко известно как шаблон стратегии. Он продвигает принцип открытого / закрытого, позволяя добавлять новые действия без изменения существующих классов.


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

class dish_maker {
    protected:
        virtual void prep() = 0;
        virtual void cook() = 0;
        virtual void plate() = 0;

    public:
        void make_dish() {
            prep();
            cook();
            plate();
        }
}

class onion_soup_dish_maker : public dish_maker {
    protected:
        virtual void prep() { ... }
        virtual void cook() { ... }
        virtual void plate() { ... }
}

Другой тесно связанный шаблон, который может быть подходящим для этого, - это шаблон Builder.

Эти шаблоны также могут уменьшить анти-шаблон Sequential Coupling, поскольку его слишком легко забыть вызывать некоторые методы или вызывать их в правильном порядке, особенно если вы делаете это несколько раз. Вы также можете подумать о том, чтобы поместить свои kitchen.opens () и closes () в аналогичный шаблонный метод, чем вам не нужно беспокоиться о вызове closes ().


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

Одним из шагов было бы создание абстракции, чтобы сказать, что что-то можно вырезать. Отношение is для подкласса является кандидатом, поскольку морковь разрезаема.

class cuttable {
    public:
        virtual void cut()=0;
}

class carrot : public cuttable {
    public:
      virtual void cut() {
          //specific code for cutting a carrot;
      }
}

Действие резки может принимать разрезаемый объект и выполнять любое обычное действие резки, которое применимо ко всем режущим столам, а также может применять определенное поведение разреза для каждого объекта.

class cutting_action : public action {
    private:
        cuttable* object;
    public:
        cutting_action(cuttable* obj) : object(obj) { }
        virtual void perform_action() {
            //common cutting code
            object->cut(); //specific cutting code
        }
}

person Mark H    schedule 01.11.2012
comment
Спасибо за это. Я хотел задать вопрос. Итак, если мы будем следовать шаблонному подходу, допустим, у каждого шеф-повара есть много элементов данных (например, tasting_spoon, salt_shaker и т. Д.), Используемых как в классе cook_food (), так и в plate_dish (). Кажется, тогда классному шеф-повару нужен cook_food (), а cook_food () нужен классный шеф. Как избежать циклической зависимости между, скажем, cook_food () и классным шеф-поваром, без необходимости передавать массу аргументов классу cook_food ()? С точки зрения дизайна, если классы сильно связаны, нет ли недостатка в их разделении? - person user1790399; 03.11.2012
comment
Один из способов избежать циклической зависимости - создать интерфейс Chef, который может использовать класс cook_food, а фактический класс Chef может реализовать. Пока в интерфейсе указаны только детали, необходимые для того, чтобы cook_food знала о шеф-поваре, проблем нет. (или вы могли бы сделать это наоборот и создать интерфейс для приготовления пищи для повара.) Не существует единственно правильного способа, но, безусловно, есть заслуга в том, чтобы разделить каждую задачу на отдельный класс. - person Mark H; 03.11.2012
comment
Извините, что значит для класса возможность использовать интерфейс? Означает ли это что-то вроде того, что мы используем интерфейс класса chef, например, cook_food (chef- ›interface_for_cooking ()), с interface_for_cooking (), состоящим из таких вызовов, как get_salt_shaker (), get_tasting_spoon () и т. Д. .? И, следуя этому примеру, было бы плохой идеей иметь два отдельных интерфейса для классов cook_food () и plate_dish ()? - person user1790399; 03.11.2012