#ооп #классы #шаблоны_проектирования #solid
Недавно в процессе работы столкнулся с интересной проблемой имплементации принципов SOLID на практике, которой хочу поделиться с сообществом. Допустим, есть у нас функция, открытый член класса (в данном случае название класса и функции неважно), которой на вход передается единственный параметр (допустим целочисленный). В теле функции происходит проверка входного параметра с помощью блока switch. Внутри блока switch имеется несколько веток case (сейчас 5), где в случае совпадения происходят некоторые действия влияющие на возвращаемый результат. Казалось бы проще простого, но... Главная проблема - это каким образом я могу обеспечить возможность расширяемости функционала данного класса в случае, если в будущем возникнет необходимость обрабатывать большее количество веток case (то есть, еще несколько, помимо уже имеющихся 5-ти) без модификации тела уже имеющейся функции (SOLID, open/closed principle)?
Ответы
Ответ 1
Не вдаваясь в то, что там действительно могут быть проблемы с архитектурой class A public meth(int i) switch i case 1 ... class B extends A public meth(int i) switch i case 7 ... default return A.meth(i)Ответ 2
Не существует "абстрактного подхода" к применению принципов проектирования и уж тем более, нет такого понятия как "имплементация принципов на практике". Принципы проектирования были сформулированы для решения определенных проблем, в заданном наборе контрекстов. Вот, например, SRP возник в результате неспособности человеческого мозга справиться с наплывом сложности. OCP возник как необходимость параллельной разработки программных систем множеством людей. LSP возник для решения проблемы нанследования и формализации того, когда наследование корректно моделирует отношение "Является", а когда - нет. Не зная контекста задачи нельзя дать очень точного ответа, но исходя из описания я могу сделать следующий вывод: если сложность метода, который оперирует переданным аргументом относительно невысока (пол-экрана кода), код простой и понятный, то с ним все в порядке и никаких изменений вносить в него категорически не следует. Полиморфизмы и фабрики сделают дизайн сложным в понимании и сопровождении. Простой switch - это наиболее естественный способ решения проблемы, когда вся логика находится в одном месте. Не нужно пытаться предугадать изменения требований. Это все равно не получится сделать. Если внесение изменений будет продиктовано изменениями требований (и появится еще один case в блоке switch), то код менять все равно придется. Придется еще и тесты поправить, да и задеплоить изменения куда следует. OCP не подразумевает гибкость решения. Он говорит об изоляции изменения нужными местами. Бертран Мейер, автор OCP отдельно выделяет дополнительное правило, которое он называет "Принципом единственного выбора", который заключается в том, что система не нарушает OCP, если некоторое решение принимается в одном месте. Вывод: не нужно ничего менять. С методом все в порядке, пока подобных switch-ей больше нет в приложении. Никакие принципы не нарушаются, котята целы, а клиенты довольны. И не гонитесь за принципами, ради принципов! З.Ы. Довольно подробно я рассматривал принцип LSP и принцип единственного выбора в статье Liskov Substitution Principle. UPDATE: как раз нендавно был поднят вопрос о YAGNI и SOLID принципах в вопросе "Нарушает ли OCP и DIP (из SOLID) принцип YAGNI?".Ответ 3
Вам нужно заменить конструкцию switch на полиморфизм: наследование или стратегию. Я бы выбрал второй вариант ("Предпочитайте композицию наследованию"). Например, был у вас метод: void Foo(int i) { switch (i) { case 0: Console.WriteLine("Zero"); break; case 1: Console.WriteLine("One"); break; case 2: Console.WriteLine("Two"); break; default: Console.WriteLine("Unknown"); break; } } Введем интерфейс стратегии: interface INumberStrategy { void Do(); } И его наследники, по одному на каждую ветку switch: class ZeroNumberStrategy : INumberStrategy { void Do() { Console.WriteLine("Zero"); } } class OneNumberStrategy : INumberStrategy { void Do() { Console.WriteLine("One"); } } class TwoNumberStrategy : INumberStrategy { void Do() { Console.WriteLine("Two"); } } class UnknownNumberStrategy : INumberStrategy { void Do() { Console.WriteLine("Unknown"); } } Теперь наш метод превращается в: void Foo(INumberStrategy strategy) { numberStrategy.Do(); } Плюс вам понадобится место, где будут создаваться нужные стратегии: class NumberStrategyFactory { INumberStrategy CreateStrategy(int i) { switch (i) { case 0: return new ZeroNumberStrategy(); case 1: return new OneNumberStrategy(); case 2: return new TwoNumberStrategy(); default: return UnknownNumberStrategy(); } } } Т.о. при добавлении новой ветки алгоритма мы не будем менять исходный класс. Мы добавим новый класс стратегии, соответствующий новой ветке, и изменим фабрику (от добавления новой ветки в фабрике уже никуда не деться :)). Конечно, на таком простом примере это все выглядит излишним, но в сложной системе (я так полагаю, вы свой пример упростили) такие изменения оправдывают себя. Также важно помнить о том, что SOLID ради SOLID'а, шаблоны ради шаблонов и т.п. -- это неверный путь. Код писать нужно как можно проще, главное не в ущерб расширяемости и поддерживаемости. И во многих случаях может быть действительно проще остаться с изначальным switch'ем.Ответ 4
Вариация решения от Etki, которая гарантирует выполнение метода основного класса (но и запрещает изменять реализацию для описанных в нем значений) class A { protected methDelegate(int i) {} public final meth(int i) { switch i { case 1 ... default methDelegate(i); } } } class B extends A { @Override protected methDelegate(int i) { switch i { case 7 ... } } } Тут пока что остается проблема с блоком default из класса А. Если что-то должно выполняться, когда ни один вариант не подошел — как заставить B вызывать этот блок? Сделать метод defaultDelegate() и надеяться, что methDelegate() его вызовет? Костыль, в котором methDelegate возвращает false в default и true иначе?Ответ 5
Вот тут описана ваша проблема и её решение. Коротко: switch-case заменить полиморфизмом.Ответ 6
SOLID - это принципы проектирования. Их нельзя "имплементировать". Их можно только применять (или не применять). Каким образом я могу обеспечить возможность расширяемости функционала данного класса в случае, если в будущем возникнет необходимость обрабатывать большее количество веток case (то есть, еще несколько, помимо уже имеющихся 5-ти) без модификации тела уже имеющейся функции (SOLID, open/closed principle)? Ваша формулировка подразумевает следующие ограничения: код надо написать раз и навсегда в этом коде есть switch на вход кода подается целое число в зависимости от его значения нужно вызвать соответствующий код. Последние два ограничения подразумевают switch. Или его аналоги с регистрацией обработчиков для каждого значения (вроде Dictionary). Или еще какой-то финт, который на самом деле сводится к хорошо спрятанному switch. Единственный вариант с таким набором ограничений - это написать новый метод, который обработает новые ветки. А для старых вызовет старый метод (как в примере @Etki). Иначе вы нарушите Open/Closed. А это плохо (по крайней мере все так считают). На самом же деле у вас уже нарушены и Open/Closed и Single Responsibility Principle. Т.к. у вас есть явно больше чем одна причина для изменения кода: может поменяться логика для 1 может поменяться логика для 2 могут быть добавлены новые значения Open/Closed был нарушен в тот момент, когда вы написали switch. Как не нарушить уже нарушенный принцип? Никак. Надо сначала починить то, что есть. Вам нужно убирать switch - это как-то убрать целое число из параметров. Например, заменив его на набор классов с общим интерфейсом (как в решении от @andreycha) Для применения принципов проектирования (любых) надо осознавать какую проблему каждый из этих принципов решает. Open/Closed решает проблему "как не поломать старый код". Так вот, кроме фанатичного применения блестящей идеи 30-летней давности "а давайте старый код не менять!" есть куча современных альтернатив. Например, вместо того, чтобы думать как применить sOlid к коду со свитчем на 5 вариантов можно написать на него тесты! Еще лучше - написать тесты до того, как этот switch будет написан! И вообще писать тесты вне зависимости от того, умеете вы применять SOLID или нет. А Open/Closed применять не как бездумный "запрет на редактирование", а как принцип, предлагающий "дописывать/расширять" код вместо "переписывания".
Комментариев нет:
Отправить комментарий