Заменить условное полиморфизмом - хорошо в теории, но не практично - PullRequest
25 голосов
/ 01 февраля 2011

«Заменить условное на полиморфизм» элегантно, только если тип объекта, который вы делаете, переключается /, если для вас уже выбран оператор. Например, у меня есть веб-приложение, которое читает параметр строки запроса, называемый «действие». Действие может иметь значения «view», «edit», «sort» и т. Д. Так как мне реализовать это с полиморфизмом? Что ж, я могу создать абстрактный класс с именем BaseAction и извлечь из него ViewAction, EditAction и SortAction. Но мне не нужно условное решение, чтобы решить, какой тип типа BaseAction создать? Я не понимаю, как можно полностью заменить условные выражения полиморфизмом. Во всяком случае, условные обозначения просто подталкиваются к вершине цепочки.

EDIT:

public abstract class BaseAction
{
    public abstract void doSomething();
}

public class ViewAction : BaseAction
{
    public override void doSomething() { // perform a view action here... }
}

public class EditAction : BaseAction
{
    public override void doSomething() { // perform an edit action here... }
}

public class SortAction : BaseAction
{
    public override void doSomething() { // perform a sort action here... }
}


string action = "view";  // suppose user can pass either "view", "edit", or "sort" strings to you.
BaseAction theAction = null;

switch (action)
{
    case "view":
        theAction = new ViewAction();
        break;

    case "edit":
        theAction = new EditAction();
        break;

    case "sort":
        theAction = new SortAction();
        break;
}

theAction.doSomething();    // So I don't need conditionals here, but I still need it to decide which BaseAction type to instantiate first. There's no way to completely get rid of the conditionals.

Ответы [ 9 ]

28 голосов
/ 01 февраля 2011

Вы правы - «условности выталкиваются на вершину цепочки» - но в этом нет «просто».Это очень мощно.Как говорит @thkala, вы просто делаете выбор один раз ;с этого момента объект знает, как заниматься своим делом.Подход, который вы описываете - BaseAction, ViewAction и все остальное, - это хороший способ сделать это.Попробуйте и посмотрите, насколько чистым становится ваш код.

Когда у вас есть один фабричный метод, который принимает строку типа «Вид» и возвращает действие, и вы вызываете его, вы изолируете свои условия.Замечательно.И вы не сможете правильно оценить силу, пока не попробуете - так что попробуйте!

9 голосов
/ 15 февраля 2014

Хотя последний ответ был год назад, я хотел бы сделать несколько отзывов / комментариев по этой теме.

Ответы на обзор

Я согласен с @CarlManaster относительно однократного кодирования оператора switch, чтобы избежать всех хорошо известных проблем работы с дублированным кодом, в данном случае с использованием условий (некоторые из них упомянуты @thkala).

Я не верю, что подход, предложенный @ KonradSzałwiński или @AlexanderKogtenkov, подходит к этому сценарию по двум причинам:

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

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

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

Есть способ избавиться от всех условных выражений, включая оператор switch:

Удаление оператора switch (вообще не использовать условия)

Как создать правильный объект действия из имени действия?

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

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

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

public class BaseAction {

    //I'm using this notation to write a class method
    public static handlingByName(anActionName) {
        subclasses = this.concreteSubclasses()

        handlingClass = subclasses.detect(x => x.handlesByName(anActionName));

        return new handlingClass();
    }
}

Итак, что делает этот метод?

Сначала извлекаются все конкретные подклассы этого (что указывает на BaseAction). В вашем примере вы получите коллекцию с ViewAction, EditAction и SortAction.

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

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

public static class ViewAction {

    public static bool handlesByName(anActionName) {
        return anActionName == 'view'
    }

}

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

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

Заключение

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

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

Да, это может звучать чрезмерно. Пожалуйста, будьте непредвзяты и осознайте, что решение о том, является ли решение чрезмерным или нет, должно зависеть, помимо прочего, от культуры разработки конкретного языка программирования, который вы используете. Например, парни .NET, вероятно, не будут использовать его, потому что .NET не позволяет вам рассматривать классы как реальные объекты, в то время как, с другой стороны, это решение используется в культурах Smalltalk / Ruby.

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

7 голосов
/ 01 февраля 2011

Несколько вещей для рассмотрения:

  • Вы создаете экземпляр каждого объекта только один раз .Как только вы это сделаете, больше не нужно больше условий относительно его типа.

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

  • Что происходит, когда вам требуется foo Значение действия в будущем?Сколько мест вам нужно изменить?

  • Что если вам нужен bar, который немного отличается от foo?С классами вы просто наследуете BarAction от FooAction, переопределяя одну вещь, которую вам нужно изменить.

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

4 голосов
/ 01 февраля 2011

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

Таблица соответствия может использоваться для сопоставления входных строк с необходимыми объектами:

action = table.lookup (action_name); // Retrieve an action by its name
if (action == null) ...              // No matching action is found

Код инициализации позаботится о создании требуемогообъекты, например

table ["edit"] = new EditAction ();
table ["view"] = new ViewAction ();
...

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

3 голосов
/ 01 февраля 2011

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

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

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

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

2 голосов
/ 14 июня 2012

Я думал об этой проблеме, вероятно, больше, чем остальные разработчики, которых я встречал. Большинство из них совершенно не осознают стоимость поддержки длинных вложенных операторов if-else или переключений. Я полностью понимаю вашу проблему в применении решения под названием «Заменить условное полиморфизмом» в вашем случае. Вы успешно заметили, что полиморфизм работает, пока объект уже выбран. В этой статье также сказано, что эта проблема может быть сведена к ассоциации [ключ] -> [класс]. Вот, например, реализация решения AS3.

private var _mapping:Dictionary;
private function map():void
{
   _mapping["view"] = new ViewAction();
   _mapping["edit"] = new EditAction();
   _mapping["sort"] = new SortAction();
}

private function getAction(key:String):BaseAction
{
    return _mapping[key] as BaseAction;
} 

Бег, который вы хотели бы:

public function run(action:String):void
{
   var selectedAction:BaseAction = _mapping[action];
   selectedAction.apply();
}

В ActionScript3 есть глобальная функция getDefinitionByName (ключ: String): Class. Идея состоит в том, чтобы использовать значения вашего ключа для соответствия именам классов, которые представляют решение для вашего состояния. В вашем случае вам нужно изменить «view» на «ViewAction», «edit» на «EditAction» и «sort» на «SortAtion». Нет необходимости запоминать что-либо, используя таблицы поиска. Запуск функции будет выглядеть так:

public function run(action:Script):void
{
   var class:Class = getDefintionByName(action);
   var selectedAction:BaseAction = new class();
   selectedAction.apply();
}

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

Пожалуйста, оставьте комментарий, даже если вы не согласны со мной.

1 голос
/ 01 февраля 2011
public abstract class BaseAction
{
    public abstract void doSomething();
}

public class ViewAction : BaseAction
{
    public override void doSomething() { // perform a view action here... }
}

public class EditAction : BaseAction
{
    public override void doSomething() { // perform an edit action here... }
}

public class SortAction : BaseAction
{
    public override void doSomething() { // perform a sort action here... }
}


string action = "view"; // suppose user can pass either
                        // "view", "edit", or "sort" strings to you.
BaseAction theAction = null;

switch (action)
{
    case "view":
        theAction = new ViewAction();
        break;

    case "edit":
        theAction = new EditAction();
        break;

    case "sort":
        theAction = new SortAction();
        break;
}

theAction.doSomething();

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

0 голосов
/ 21 марта 2016

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

public abstract class BaseAction
{
    public abstract void doSomething();
}

public class ViewAction : BaseAction
{
    public override void doSomething() { // perform a view action here... }
}

public class EditAction : BaseAction
{
    public override void doSomething() { // perform an edit action here... }
}

public class SortAction : BaseAction
{
    public override void doSomething() { // perform a sort action here... }
}


string action = "view"; // suppose user can pass either
                        // "view", "edit", or "sort" strings to you.
BaseAction theAction = null;

theAction = actionMap.get(action); // decide at runtime, no conditions
theAction.doSomething();
0 голосов
/ 02 июня 2012

Полиморфизм - это метод связывания. Это особый случай, известный как «объектная модель». Объектные модели используются для управления сложными системами, такими как схемы или чертежи. Рассмотрим что-то сохраненное / маршализованное в текстовом формате: элемент «A», связанный с элементом «B» и «C». Теперь вам нужно знать, что связано с A. Парень может сказать, что я не собираюсь создавать объектную модель для этого, потому что я могу сосчитать ее при синтаксическом анализе в один проход. В этом случае вы можете быть правы, вы можете уйти без объектной модели. Но что, если вам нужно сделать много сложных манипуляций с импортированным дизайном? Будете ли вы манипулировать им в текстовом формате или отправлять сообщения, вызывая java-методы и делая ссылки на java-объекты более удобными? Вот почему упоминалось, что перевод нужно выполнять только один раз.

...