Подтвердить что ты не робот

Полиморфизм или условные выражения способствуют лучшему дизайну?

Недавно я наткнулся на

4b9b3361

Ответ 1

На самом деле это упрощает процесс тестирования и кода.

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

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

class Animal
{
    public:
       Noise warningNoise();
       Noise pleasureNoise();
    private:
       AnimalType type;
};

Noise Animal::warningNoise()
{
    switch(type)
    {
        case Cat: return Hiss;
        case Dog: return Bark;
    }
}
Noise Animal::pleasureNoise()
{
    switch(type)
    {
        case Cat: return Purr;
        case Dog: return Bark;
    }
}

В этом простом случае все новые причины для животных требуют обновления обоих операторов switch.
Вы забыли его? Что такое значение по умолчанию? BANG!!

Использование полиморфизма

class Animal
{
    public:
       virtual Noise warningNoise() = 0;
       virtual Noise pleasureNoise() = 0;
};

class Cat: public Animal
{
   // Compiler forces you to define both method.
   // Otherwise you can't have a Cat object

   // All code local to the cat belongs to the cat.

};

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

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

Ответ 2

Не бойтесь...

Я предполагаю, что ваша проблема связана с знакомством, а не с технологией. Ознакомьтесь с С++ OOP.

С++ - язык OOP

Среди своих множественных парадигм он имеет функции ООП и более чем способен поддерживать сравнение с самым чистым языком OO.

Не позволяйте "C-части внутри С++" заставлять вас думать, что С++ не может справиться с другими парадигмами. С++ может справиться с множеством программных парадигм довольно любезно. И среди них OOP С++ является наиболее зрелой парадигмой С++ после процедурной парадигмы (т.е. Вышеупомянутой "части C" ).

Полиморфизм Ok для производства

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

переключатель и полиморфизм [почти] похожи...

... Но полиморфизм устранил большинство ошибок.

Разница заключается в том, что вы должны обрабатывать ключи вручную, тогда как полиморфизм более естественен, как только вы привыкнете с переопределением метода наследования.

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

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

В целом, две функции касаются выбора. Но полиморфизм позволяет вам сделать более сложным и в то же время более естественным и, следовательно, более простым выбором.

Избегайте использования RTTI для поиска типа объекта

RTTI - интересная концепция и может быть полезна. Но большую часть времени (то есть 95% времени), переопределение и наследование метода будет более чем достаточно, и большая часть вашего кода не должна даже знать точный тип обработанного объекта, но доверять ему, чтобы он поступал правильно.

Если вы используете RTTI в качестве прославленного коммутатора, вам не хватает точки.

(Отказ от ответственности: я большой поклонник концепции RTTI и dynamic_casts. Но нужно использовать правильный инструмент для этой задачи, и большую часть времени RTTI используется в качестве прославленного коммутатора, что неверно)

Сравнение динамического и статического полиморфизма

Если ваш код не знает точный тип объекта во время компиляции, используйте динамический полиморфизм (например, классическое наследование, переопределение виртуальных методов и т.д.)

Если ваш код знает тип во время компиляции, возможно, вы можете использовать статический полиморфизм, то есть шаблон CRTP http://en.wikipedia.org/wiki/Curiously_Recurring_Template_Pattern

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

Пример производственного кода

Для производства используется код, похожий на этот (из памяти).

Более легкое решение вращалось вокруг процедуры, вызванной конвейером сообщений (WinProc в Win32, но для упрощения я написал более упрощенную версию). Итак, суммируем, это было что-то вроде:

void MyProcedure(int p_iCommand, void *p_vParam)
{
   // A LOT OF CODE ???
   // each case has a lot of code, with both similarities
   // and differences, and of course, casting p_vParam
   // into something, depending on hoping no one
   // did a mistake, associating the wrong command with
   // the wrong data type in p_vParam

   switch(p_iCommand)
   {
      case COMMAND_AAA: { /* A LOT OF CODE (see above) */ } break ;
      case COMMAND_BBB: { /* A LOT OF CODE (see above) */ } break ;
      // etc.
      case COMMAND_XXX: { /* A LOT OF CODE (see above) */ } break ;
      case COMMAND_ZZZ: { /* A LOT OF CODE (see above) */ } break ;
      default: { /* call default procedure */} break ;
   }
}

Каждое добавление команды добавило случай.

Проблема в том, что некоторые команды там, где они аналогичны, и частично разделяют их реализацию.

Таким образом, смешивание случаев было риском для эволюции.

Я решил проблему, используя шаблон Command, то есть создав базовый объект Command, с помощью одного метода process().

Итак, я перезаписал процедуру сообщения, сведя к минимуму опасный код (т.е. играя с void * и т.д.) до минимума и написал его, чтобы убедиться, что мне больше не понадобится его касаться:

void MyProcedure(int p_iCommand, void *p_vParam)
{
   switch(p_iCommand)
   {
      // Only one case. Isn't it cool?
      case COMMAND:
         {
           Command * c = static_cast<Command *>(p_vParam) ;
           c->process() ;
         }
         break ;
      default: { /* call default procedure */} break ;
   }
}

И затем для каждой возможной команды вместо добавления кода в процедуру и смешивания (или, что еще хуже, копирования/вставки) кода из похожих команд я создал новую команду и вывел ее либо из объекта Command, или один из его производных объектов:

Это привело к иерархии (представленной как дерево):

[+] Command
 |
 +--[+] CommandServer
 |   |
 |   +--[+] CommandServerInitialize
 |   |
 |   +--[+] CommandServerInsert
 |   |
 |   +--[+] CommandServerUpdate
 |   |
 |   +--[+] CommandServerDelete
 |
 +--[+] CommandAction
 |   |
 |   +--[+] CommandActionStart
 |   |
 |   +--[+] CommandActionPause
 |   |
 |   +--[+] CommandActionEnd
 |
 +--[+] CommandMessage

Теперь все, что мне нужно было сделать, это переопределить процесс для каждого объекта.

Простой и удобный.

Например, предположим, что CommandAction должен был выполнить свой процесс в три этапа: "до", "while" и "after". Его код будет примерно таким:

class CommandAction : public Command
{
   // etc.
   virtual void process() // overriding Command::process pure virtual method
   {
      this->processBefore() ;
      this->processWhile() ;
      this->processAfter() ;
   }

   virtual void processBefore() = 0 ; // To be overriden

   virtual void processWhile()
   {
      // Do something common for all CommandAction objects
   }

   virtual void processAfter()  = 0 ; // To be overriden

} ;

И, например, CommandActionStart может быть закодирован как:

class CommandActionStart : public CommandAction
{
   // etc.
   virtual void processBefore()
   {
      // Do something common for all CommandActionStart objects
   }

   virtual void processAfter()
   {
      // Do something common for all CommandActionStart objects
   }
} ;

Как я уже сказал: легко понять (если правильно прокомментировать) и очень легко расширить.

Коммутатор сводится к минимальному минимуму (т.е. if-like, потому что нам все еще необходимо делегировать команды Windows для процедуры Windows по умолчанию), и нет необходимости в RTTI (или, что еще хуже, в собственном RTTI).

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

Ответ 3

Единичное тестирование программы OO означает тестирование каждого класса в качестве единицы. Принцип, который вы хотите изучить, - "Открыто для расширения, закрыто для модификации". Я получил это от Head First Design Patterns. Но в основном говорится, что вы хотите иметь возможность легко расширять свой код без изменения существующего проверенного кода.

Полиморфизм делает это возможным, устраняя эти условные утверждения. Рассмотрим этот пример:

Предположим, что у вас есть объект Character, который несет Оружие. Вы можете написать метод атаки следующим образом:

If (weapon is a rifle) then //Code to attack with rifle else
If (weapon is a plasma gun) //Then code to attack with plasma gun

и др.

С полиморфизмом персонаж не должен "знать" тип оружия, просто

weapon.attack()

будет работать. Что произойдет, если будет изобретено новое оружие? Без полиморфизма вам придется изменить условное утверждение. При полиморфизме вам нужно будет добавить новый класс и оставить тестовый класс символов самостоятельно.

Ответ 4

Я немного скептически: я считаю, что наследование часто добавляет больше сложности, чем удаляет.

Я думаю, что вы задаете хороший вопрос, и я считаю, что это:

Разделяете ли вы на несколько классов, потому что вы имеете дело с разными вещами? Или это действительно одно и то же, действуя по-другому?

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

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

Ответ 5

Не эксперт в отношении последствий для тестовых случаев, а с точки зрения разработки программного обеспечения:

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

  • Не повторяйте себя. Важной частью руководства является "такое же условие". Это указывает на то, что ваш класс имеет некоторые различные режимы работы, которые могут быть учтены в классе. Затем это условие появляется в одном месте вашего кода - когда вы создаете экземпляр объекта для этого режима. И снова, если приходит новый, вам нужно всего лишь изменить один кусок кода.

Ответ 6

Полиморфизм является одним из краеугольных камней ОО и, безусловно, очень полезен. Разделяя проблемы по нескольким классам, вы создаете изолированные и проверяемые единицы. Поэтому вместо того, чтобы делать переключатель... случай, когда вы вызываете методы на нескольких разных типах или реализациях, вы создаете единый интерфейс, имеющий несколько реализаций. Когда вам нужно добавить реализацию, вам не нужно изменять клиентов, как в случае с switch... case. Очень важно, так как это помогает избежать регрессии.

Вы также можете упростить свой клиентский алгоритм, имея дело только с одним типом: интерфейс.

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

Полиморфизм - это основа, на которой построена наша кодовая база, поэтому я считаю ее очень практичной.

Ответ 7

Переключение и полиморфизм делают то же самое.

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

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

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

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

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

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

Переключатели могут быть выполнены правильно:

switch (type)
{
    case T_FOO: doFoo(); break;
    case T_BAR: doBar(); break;
    default:
        fprintf(stderr, "You, who are reading this, add a new case for %d to the FooBar function ASAP!\n", type);
        assert(0);
}

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

Конечно, если конкретный коммутатор будет использоваться более чем в одном месте, он вырезается в функцию (не повторяйте себя).

Если вы хотите расширить эти ключи, просто выполните grep 'case[ ]*T_BAR' rn . (в Linux), и он выплюнет места, на которые стоит посмотреть. Поскольку вам нужно посмотреть на код, вы увидите контекст, который поможет вам правильно добавить новый случай. Когда вы используете полиморфизм, сайты вызовов скрыты внутри системы, и вы зависите от правильности документации, если она вообще существует.

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

Выключатели также помогают следующему парню, пытающемуся привыкнуть и понимать код:

  • Возможные случаи перед глазами. Это хорошо, когда читаете код (меньше прыгаете).
  • Но вызовы виртуальных методов аналогичны вызовам обычных методов. Невозможно узнать, является ли вызов виртуальным или нормальным (без поиска класса). Это плохо.
  • Но если вызов виртуальный, возможные случаи не очевидны (не найдя все производные классы). Это также плохо.

Когда вы предоставляете интерфейс третьей стороне, чтобы они могли добавлять поведение и пользовательские данные в систему, то это другое дело. (Они могут устанавливать обратные вызовы и указатели на пользовательские данные, и вы даете им ручки)

Дальнейшие обсуждения можно найти здесь: http://c2.com/cgi/wiki?SwitchStatementsSmell

Я боюсь, что мой "синдром C-хакера" и анти-OOPism в конечном итоге сожгут всю мою репутацию здесь. Но всякий раз, когда мне было нужно или нужно было взломать что-либо в процедурной системе С, я нашел это довольно легко, отсутствие ограничений, принудительное инкапсулирование и меньшие уровни абстракции заставляют меня "просто делать это". Но в системе С++/С#/Java, где десятки слоев абстракции укладываются друг на друга в течение всего срока службы программного обеспечения, мне нужно потратить много часов на несколько дней, чтобы выяснить, как правильно работать со всеми ограничениями и ограничениями, которые другие программисты встроенные в их систему, чтобы другие не "возились со своим классом".

Ответ 8

Это в основном связано с инкапсуляцией знаний. Начнем с действительно очевидного примера - toString(). Это Java, но легко переносится на С++. Предположим, вы хотите напечатать дружественную для человека версию объекта для целей отладки. Вы можете сделать:

switch(obj.type): {
case 1: cout << "Type 1" << obj.foo <<...; break;   
case 2: cout << "Type 2" << ...
Тем не менее, это было бы глупо. Почему один метод где-то знает, как печатать все. Для самого объекта часто будет лучше знать, как печатать себя, например:
cout << object.toString();

Таким образом, toString() может получить доступ к полям-членам без необходимости приведения. Их можно тестировать самостоятельно. Их можно легко изменить.

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

Ответ 9

Он отлично работает , если вы его понимаете.

Существует также 2 варианта полиморфизма. Первое очень легко понять в java-esque:

interface A{

   int foo();

}

final class B implements A{

   int foo(){ print("B"); }

}

final class C implements A{

   int foo(){ print("C"); }

}

B и C имеют общий интерфейс. B и C в этом случае не могут быть расширены, поэтому вы всегда уверены, какие foo() вы вызываете. То же самое касается С++, просто сделайте A:: foo чистым виртуальным.

Во-вторых, и сложнее полиморфизм во время выполнения. Это плохо выглядит в псевдокоде.

class A{

   int foo(){print("A");}

}

class B extends A{

   int foo(){print("B");}

}

class C extends B{

  int foo(){print("C");}

}

...

class Z extends Y{

   int foo(){print("Z");

}

main(){

   F* f = new Z();
   A* a = f;
   a->foo();
   f->foo();

}

Но это намного сложнее. Особенно, если вы работаете на С++, где некоторые из деклараций foo могут быть виртуальными, а некоторые из наследования могут быть виртуальными. Также ответ на этот вопрос:

A* a  = new Z;
A  a2 = *a;
a->foo();
a2.foo();

может не быть тем, что вы ожидаете.

Просто внимательно следите за тем, что вы делаете, и не знаете, используете ли вы полиморфизм во время выполнения. Не становитесь слишком самоуверенными, и если вы не уверены, что что-то будет делать во время выполнения, проверьте его.

Ответ 10

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

Ответ 11

Я должен повторить итерацию, что поиск всех статусов коммутатора может быть нетривиальным процессом в зрелой кодовой базе. Если вы пропустите любой, приложение может сбой из-за несогласованного оператора case, если вы не установили по умолчанию.

Также ознакомьтесь с книгой "Мартин Фаулерс" на тему "Рефакторинг"
Использование коммутатора вместо полиморфизма - это запах кода.

Ответ 12

Это действительно зависит от вашего стиля программирования. Хотя это может быть правильно в Java или С#, я не согласен с тем, что автоматическое решение использовать полиморфизм является правильным. Например, вы можете разделить свой код на множество небольших функций и выполнить поиск массива с указателями функций (инициализированными во время компиляции). В С++ полиморфизм и классы часто злоупотребляют - вероятно, самая большая ошибка дизайна, сделанная людьми из сильных языков ООП в С++, состоит в том, что все идет в класс - это не так. Класс должен содержать только минимальный набор вещей, которые заставляют его работать в целом. Если подкласс или друг необходимы, пусть будет так, но они не должны быть нормой. Любые другие операции над классом должны быть свободными функциями в одном и том же пространстве имен; ADL позволит использовать эти функции без поиска.

С++ не является языком ООП, не делайте его одним. Это так же плохо, как программирование C на С++.