Статьи

Использование полиморфизма вместо условных


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

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

Вопрос прост. «Учитывая математическое выражение, такое как 2 + 3 * 5, которое может быть представлено в виде двоичного дерева, как бы вы спроектировали классы и закодировали методы, чтобы я мог вызывать valual () и toString () на любом узле дерева? и получить правильное значение. » , Конечно, я хотел бы уточнить, что заполнение дерева выходит за рамки проблемы, поэтому у них было заполненное дерево для работы. Это также дает мне возможность выяснить, как думает кандидат, спрашивает ли он, является ли заполнение дерева его проблемой, или же он просто принимает вещи. Вы могли бы задать этот вопрос другим вопросом о дереве и обходе, чтобы проверить знания кандидата и определить, будет ли этот вопрос пустой тратой времени или нет.

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

class Node {
char operator;
int lhsValue, rhsValue;
Node left, right;

public int evaluate() {
int leftVal = left == null ?
lhsValue : left.evaluate();
int rightVal = right == null ?
rhsValue : right.evaluate();
if (operator == '+') {
return leftVal + rightVal;
} else if (operator == '-') {
// So on and so forth.
// Same for toString()
}
}
}

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

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

Идеальный ответ будет, на этот вопрос, в том , что узел представляет собой интерфейс с оценкой () и ToString (). Затем у нас есть различные реализации Node , такие как ValueNode , AdditionOperationNode , и так далее, и так далее. Реализации будут выглядеть следующим образом:

interface Node {
int evaluate();
String toString();
}

public class ValueNode
implements Node {
private int value;
public int evaluate() {
return value;
}
public String toString() {
return value + "";
}
}

public class AdditionOperationNode
implements Node {
Node left, right;
public int evaluate() {
return left.evaluate()
+ right.evaluate();
}
public String toString() {
return left.toString() + " + "
+ right.toString();
}
}

 

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

Теперь обычно есть два типа условных выражений, которые вы не можете заменить полиморфизмом. Это сравнительные (>, <) (или работа с примитивами, как правило), а иногда и граничные случаи. И эти два также зависят от языка, как и в Java. Некоторые другие языки позволяют обходить замыкания, что скрывает необходимость в условных выражениях.

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

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