Автор: Sergey Teplyakov
В обсуждении одного из моих ответов на ru.stackoverflow в G+ был поднят вопрос по поводу того, является ли оператор switch design или code smell-ом?
Тут нужно быстро вспомнить, откуда ноги вообще растут (ИМХО). В наших с вами разных языках программирования существует много разных способов решения одной и той же проблемы. Например, когда у нас есть определенная задача (нарисовать фигуру?!) и несколько разновидностей входных данных (круг, квадрат, прямоугольник?), то решить ее можно разными способами. Можно взять впихнуть тип в структурку и в методе draw перебрать все возможные варианты.
С точки зрения серьезного объектно-ориентированного программиста, такой подход считается негодным, нерасширяемым и вообще, неправильным. Ибо об этом писал дядюшка Боб Мартин, который будет потом приходить по ночам в страшных снах и мучить бедных программистов страшными вещами, типа добавлением новой фигуры каждые 42 минуты.
Чтобы следовать чьим-то советом правильно, а не как обычно, нужно понимать контекст, в котором этот самый совет дается.
Так, например, проблема с фигурками и их рисованием заключается в том, что в будущем *предполагается* добавление новых фигур и не предполагается добавление новых операций над этими фигурами. В этом случае, для уменьшения стоимости внесения изменений и дается совет по выделению иерархии наследования.
Но даже в этом случае нужно понимать, на какой компромисс мы идем: иерархия наследования позволяет легко добавить новый тип фигуры (минимум изменений), но усложняет добавление новых операций (много изменений во множестве типов). В этом плане подход на основе объектов упрощает изменение системы в одну сторону, а тот же структурный или функциональный подход в другую (в этом случае проще будет добавлять именно новую операцию, а не новый тип). Эта проблема широко известна под названием Expression Problem, а сравнение ФП и ОО подходов я делал в статье OCP: ФП vs. ООП.
Ну, ок, с фигурками мы разобрались, а как насчет других случаев использования оператора switch? Ведь он не расширяемый и нам, о ужас, придется изменять код при изменении требований?
Тут, как всегда, все зависит от контекста, решаемой задачи и того, что в этом switch-е происходит.
Вот, например, если у вас есть фабрика, которая создает экземпляр некоторого объекта по входным данным, например, некоторый парсер в зависимости от расширения файла:public class ParserFactory{public static IParser CreateParser(string fileName) {switch (Path.GetExtension(fileName)) {case '.txt':return new TextParser(),case '.xml':return new XmlParser(),default:throw new NotSupportedException(), } }}
Кто-то скажет, что решение #нерасширябельное, нарушает SRP и вообще, никуда не годится.
На самом деле, сделать такой вывод лишь по этому фрагменту нельзя. Если подобный switch лишь один, то с кодом все в порядке. Бертран Я могу и ФП тоже Мейер назвал это принципом единственного выбора, который заключается в следующем: если некоторый выбор находится в одном месте, то код зашибись. Если он начинает растекаться по коду, то это плохо и нужно что-то менять.
Но просто подумайте, а какие здесь альтернативы: прикрутить полиморфизм? Фабрику? А кто будет создавать фабрику? Что, MEF прикручивать и саморегистриуемые компоненты писать? Да, а крыша не поеедет это все сопровождать? Да, можно прикрутить словарик, но даже в этом случае он уместен, когда кода на каждый кейс много или самих кейсов сильно больше 2-х или 3-х.
Любые дополнительные уровни косвенности, они же хороши тогда, когда каждый случай сложен. А замена switch-а полиморфизмом уместна лишь тогда, когда иерархия наследования вырисовывается из домена и когда логики на каждый кейс становится сильно больше пары операторов.
Ну и самое главное, изменения требований приведут к изменению кода. Это нормально. Код, который будет расширяться как-то хитро путем дописывания наследников, передачей коллбеков или другим хитрым способом это усложнение, которое нужно вносить тогда, когда нужно, а не с самого начала.