Почему универсальные функции это плохо?

Рейтинг: 2Ответов: 1Опубликовано: 09.01.2023

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

Ответы

▲ 5Принят

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

Способов борьбы с этой проблемой по сути всего два и какой именно выбрать зависит от вашей конкретной ситуации: 1)наследование (общий код выталкивается в супер класс); 2)делигирование (общий код выносится в сторонний класс).

Наиболее простой и правильный способ понять, какой использовать - определиться с обязанностью класса. Поскольку SOLID в целом и принцип единственной ответственности в частности никто не отменял, надо задать себе вопрос, относится ли данная функциональность к ответственности класса, в который вы ее добавляете. Если да, то вас спасет наследование, если нет - делигирование. Поскольку вы говорите о дублирующем коде (т.е. данный код встречается в разных классах), то при наследовании разных классов от одного абстрактного класса/интерфейса (как и в принципе при любом наследовании) нужно помнить, что между классом-наследником и супер классом должно сохраняться отношение "is a" (наследник является супер классом). Простой пример: если у вас есть класс слон и зебра, а супер класс животное, то все ок, а если у вас класс слон и стул, то тут никакого наследования быть не может, вам нужно смотреть в сторону делигирования. Я думаю, что пример довольно наглядный и поможет сделать правильный выбор.

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

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