Чем плохи static методы
Статические члены класса. Не дай им загубить твой код
Давно хотел написать на эту тему. Первым толчком послужила статья Miško Hevery «Static Methods are Death to Testability». Я написал ответную статью, но так и не опубликовал ее. А вот недавно увидел нечто, что можно назвать «Классо-Ориентированное Программирование». Это освежило мой интерес к теме и вот результат.
Зависимости
Обычно, код зависит от другого кода. Например:
Теперь, такой пример:
normalizer_normalize — это функция пакета Intl, который интегрирован в PHP начиная с версии 5.3 и может быть установлен отдельно для более старых версий. Здесь уже немного сложнее — работоспособность кода зависит от наличия конкретного пакета.
Теперь, такой вариант:
Что-то знакомое.
Неправда ли, похоже на процедурный подход? Давайте попробуем переписать этот пример в процедурном стиле:
Классо-ориентированное программирование — это как покупка машины, для того чтобы сидеть в ней, периодически открывать и закрывать двери, прыгать на сидениях, случайно заставляя срабатывать подушки безопасности, но никогда не поворачивать ключ зажигания и не сдвинуться с места. Это полное непонимание сути.
Поворачиваем ключ зажигания
Теперь, давайте попробуем ООП. Начнем с реализации Foo :
Вы попросту не сможете использовать код, если хоть одно условие не удовлетворено.
Инъекция
Написать эту строчку можно в любом месте и она выполнится. Ее поведение зависит от глобального состояния подключения к БД. Хотя из кода это не очевидно. Добавим обработку ошибок:
Для сравнения, вот классо-ориентированная реализация:
Никакой разницы. Обработка ошибок — идентична, т.е. все также сложно найти источник проблем. Это все потому, что вызов статического метода — это просто вызов функции, который ничем не отличается от любого другого вызова функции.
Теперь мы удовлетворили все зависимости, которые обещали, все готово к запуску.
В процедурном коде, Вы создаете много жестких зависимостей и спутываете стальной проволокой разные участки кода. Все зависит от всего. Вы создаете монолитный кусок софта. Я не хочу сказать, что оно не будет работать. Я хочу сказать, что это очень жесткая конструкция, которую очень сложно разобрать. Для маленьких приложений это может работать хорошо. Для больших это превращается в ужас хитросплетений, который невозможно тестировать, расширять и отлаживать:
Классо-ориентированный подход выглядит обманчиво просто, но намертво приколачивает код гвоздями зависимостей. Объектно-ориентированный подход оставляет все гибким и изолированым до момента использования, что может выглядеть более сложным, но это более управляемо.
Статические члены
Зачем же нужны статические свойства и методы? Они полезны для статических данных. Например, данные от которых зависит экземпляр, но которые никогда не меняются. Полностью гипотетический пример:
Статические свойства также могут быть полезны, чтобы закешировать некоторые данные, которые идентичны для всех экземпляров. Статические свойства существуют, по большей части, как техника оптимизации, они не должны рассматриваться как философия программирования. А статические методы полезны в качестве вспомогательных методов и альтернативных конструкторов.
Использование статических методов допустимо при следующих обстоятельствах:
Это не создает никаких дополнительных зависимостей. Класс зависит сам от себя.
Слово об абстракции
Зачем вся эта возня с зависимостями? Возможность абстрагировать! С ростом Вашего продукта, растет его сложность. И абстракция — ключ к управлению сложностью.
Если код внутри этой функции будет выполнен — это значит, что экземпляр Database был успешно передан, что значит, что экземпляр объекта Database был успешно создан. Если класс Database спроектирован верно, то можно быть уверенным, что наличие экземпляра этого класса означает возможность выполнять запросы к БД. Если экземпляра класса не будет, то тело функции не будет выполнено. Это значит, что функция не должна заботиться о состоянии БД, класс Database это сделает сам. Такой подход позволяет забыть о зависимостях и сконцентрироваться на решении задач.
Классо-ориентированное программирование — глупость. Учитесь использовать ООП.
Когда использовать статические методы
В обсуждениях к посту (перевод) о именованных конструкторах прозвучало мнение, что статические методы плохи и их вообще не стоит использовать. На мой взгляд, это слишком большое обобщение.
Статические методы по сути своей просто способ организации глобальных функций в пространства имен. Использование пространств имен, я думаю, вы согласитесь — хороший тон. Что касается глобальных функций — мы используем их всегда, встроенные функции PHP составляют основу нашего кода.
Основная проблема здесь — отсутствие совместно используемого глобального состояния. Вот пример из прошлого поста:
В данном примере возвращаемый результат свободен от побочных эффектов и вполне предсказуем, т.к. зависит только от аргументов, подаваемых на вход. Каждый раз при вызове метода вам будет возвращен идентичный результат (объект Time со значением 11:45), вне зависимости от состояния системы, контекста или чего-либо еще.
Другой пример:
И снова — результат предсказуем, Calculator::sum(1, 2); предоставляет нам сервис, не имеющий состояния, и не зависящий ни от чего, кроме аргументов. Более того, эта реализация не может быть полиморфной или иметь различные имплементации, т.к. любой результат кроме 3 будет ошибкой. Да, вы можете изменить внутреннюю реализацию метода, улучшив алгоритм сложения чисел, но это не должно никак отражаться на результате его использования.
Возьмем обратный пример, на этот раз с состоянием:
Пример элементарный, но в более сложных ситуациях это может быть не столь доходчиво. Представьте, что два разработчика используют в своем коде счетчики. Когда они тестируют свое решение изолированно — нет никаких проблем. Но после интеграции их решений счетчик начинает работать не так, как ожидалось, потому что используется глобальное состояние, вместо того, чтобы воспользоваться отдельным экземпляром счетчика.
Абстракция
Это все может показаться избыточным, но не стоит забывать про функции и статические методы, ведь они могут быть очень полезны при правильном их применении.
Часть 1: Как использовать именованные конструкторы в PHP
Плохо ли использовать много статических методов?
Я склонен объявлять статическими все методы в классе, когда этот класс не требует отслеживания внутренних состояний. Например, если мне нужно преобразовать A в B и не полагаться на какое-то внутреннее состояние C, которое может изменяться, я создаю статическое преобразование. Если есть внутреннее состояние C, которое я хочу настроить, я добавляю конструктор для установки C и не использую статическое преобразование.
Я читал различные рекомендации (в том числе по StackOverflow), чтобы не злоупотреблять статикой но я все еще не понимаю, что не так с эмпирическим правилом выше.
Это разумный подход или нет?
15 ответов
существует два вида общих статических методов:
объект без какого-либо внутреннего состояния-это подозрительная вещь.
обычно объекты инкапсулируют состояние и поведение. Объект, который только инкапсулирует поведение, является нечетным. Иногда это пример легкий или Flyweight.
в других случаях это процедурный дизайн, выполненный на языке объектов.
это действительно только продолжение Великого ответа Джона Милликина.
хотя можно безопасно сделать методы без состояния (которые в значительной степени являются функциями) статическими, иногда это может привести к соединению, которое трудно изменить. Рассмотрим, что у вас есть статический метод как таковой:
но подумайте, если вы создали интерфейс для предоставления метода и дали его вызывающим, теперь, когда поведение должно измениться, можно создать новый класс для реализации интерфейса, который является более чистым, более легко тестируемым и более доступным для обслуживания, и который вместо этого предоставляется вызывающим. В этом случае вызывающие классы не нужно изменять или даже перекомпилировать, и изменения локализованы.
это может быть или не может быть вероятной ситуацией, но я думаю, это стоит рассмотреть.
другой вариант-добавить их в качестве нестатических методов в исходный объект:
статические классы в порядке, если они используются в нужных местах.
а именно: методы, которые являются «листовыми» методами (они не изменяют состояние, они просто каким-то образом преобразуют вход). Хорошими примерами этого являются такие вещи, как пути.Объединять. Такие вещи полезны и делают для синтаксиса terser.
на проблемы у меня со статикой многочисленны:
во-первых, если у вас есть статические классы, зависимостей скрыты. Рассмотрим следующий:
кроме того, если вы используете статику, которая изменяет состояние, это похоже на карточный домик. Вы никогда не знаете, у кого есть доступ к чему, и дизайн имеет тенденцию напоминать монстра спагетти.
короче говоря, статика хороша для некоторых вещей, а для небольших инструментов или одноразового кода я бы не препятствовал их использованию. Однако, помимо этого, они являются кровавыми кошмар для ремонтопригодности, хорошего дизайна и простоты тестирования.
причина, по которой вас предостерегают от статических методов, заключается в том, что их использование теряет одно из преимуществ объектов. Объекты предназначены для инкапсуляции данных. Это предотвращает непредвиденные побочные эффекты от случаться который избегает ошибок. Статические методы не имеют инкапсулированных данных* и поэтому не получают этого преимущества.
тем не менее, если вы не используете внутренние данные, они хорошо использовать и немного быстрее выполнять. Убедитесь, что вы не касаетесь глобальных данных в них хотя.
это кажется разумным подходом. Почему вы не хотите использовать слишком много статических классов/методов, которые вы в конечном итоге отходит от объектно-ориентированного программирования и в области структурного программирования.
в вашем случае, когда вы просто преобразуете A В B, скажите, что все, что мы делаем, это преобразуем текст, чтобы перейти от
тогда статический метод имел бы смысл.
однако, если вы вызываете эти статические методы объект часто, и он имеет тенденцию быть уникальным для многих вызовов (например, способ его использования зависит от ввода), или это часть неотъемлемого поведения объекта, было бы разумно сделать его частью объекта и поддерживать его состояние. Один из способов сделать это-реализовать его как интерфейс.
Edit: одним из хороших примеров большого использования статических методов являются вспомогательные методы html в Asp.Net MVC или Ruby. Они создают html-элементы, которые не привязаны к поведению объект, и, следовательно, статический.
Edit 2: изменено функциональное программирование на структурированное Программирование (по какой-то причине я запутался), реквизит Торстену для указания на это.
недавно я переработал приложение для удаления / изменения некоторых классов, которые были первоначально реализованы как статические классы. Со временем эти классы приобрели так много, и люди просто продолжали помечать новые функции как статические, так как никогда не было экземпляра, плавающего вокруг.
Итак, мой ответ заключается в том, что статические классы не обязательно плохо, но это может быть проще начать создавать экземпляры сейчас, то есть рефакторинг позже.
Я бы счел это дизайнерским запахом. Если вы используете в основном статические методы, у вас, вероятно, не очень хороший дизайн OO. Это не обязательно плохо, но, как и все запахи, это заставит меня остановиться и переоценить. Это намекает на то, что вы можете сделать лучший дизайн OO или, возможно, вам следует пойти в другом направлении и полностью избегать OO для этой проблемы.
пока не внутреннее состояние приходит в игру, это нормально. Обратите внимание, что обычно статические методы должны быть потокобезопасными, поэтому при использовании вспомогательных структур данных используйте их потокобезопасным образом.
Я обычно ходил туда и обратно между классом с кучей статических методов и синглтоном. Оба решают проблему, но синглтон может быть гораздо легче заменен более чем одним. (Программисты всегда кажутся настолько уверенными, что будет только 1 чего-то, и я обнаружил, что ошибался достаточно раз, чтобы полностью отказаться от статических методов, за исключением некоторых очень ограниченных случаев).
и статические методы не имеют подобных преимуществ.
Так что да, они плохие.
Если это метод утилиты, приятно сделать его статическим. Guava и Apache Commons построены на этом принципе.
Так в моей логике приложения у меня обычно есть небольшие статические вызовы методов, подобные утилите. Т. е.
одним из преимуществ является то, что я не испытываю таких методов 🙂
ну, серебряной пули, конечно, нет. Статические классы подходят для небольших утилит / помощников. Но использование статических методов для программирования бизнес-логики, безусловно, зло. Рассмотрим следующий код
вы видите вызов статического метода ItemsProcessor.SplitItem(newItem); пахнет делу
Если вы знаете, вы будете никогда нужно использовать внутреннее состояние C, все в порядке. Если это когда-либо изменится в будущем, вам нужно будет сделать метод нестатическим. Если он нестатичен для начала, вы можете просто игнорировать внутреннее состояние, если оно вам не нужно.
статические методы, как правило, являются плохим выбором даже для кода без состояния. Вместо этого создайте одноэлементный класс с этими методами, который создается один раз и вводится в те классы, которые хотят использовать методы. Такие занятия легче поддразнивать и тестировать. Они гораздо более объектно-ориентированным. При необходимости вы можете обернуть их прокси-сервером. Статика делает OO намного сложнее, и я не вижу причин использовать их почти во всех случаях. Не 100%, но почти все.
Есть ли какие-либо недостатки у статических методов?
Узнал от(значительно) более опытного коллеги, что использование статических методов считается нарушением принципов ООП. Хотелось бы уточнить, о каких именно принципах идёт речь и имеют ли они под собой какой-то рациональный фундамент.
Простой 1 комментарий
Значит так, берем толстую тетрадь, ручку и пишем фразу «Статические методы не имеют отношения к ООП» до тех пор, пока не запомним это на всю жизнь.
Суть объектно ориентированного программирование, как понятно из названия, заключается в том, что должен существовать объект. Статика существует не в контексте объекта, а в контексте класса! Из этого вытекает то, что на протяжении всего жизненного цикла вашего кода будет существовать лишь одно глобальное состояние статических членов класса.
Есть ли у статического варианта какие-то подводные камни
Узнал от(значительно) более опытного коллеги, что использование статических методов считается нарушением принципов ООП.
В некоторых случаях такой подход уместен, например при создании классов-хелперов. В остальных случаях нет.
Что будет занимать больше памяти и как вообще в обоих случаях будет работать сборщик мусора?
Какая разница? Это экономия на спичках в которой нет смысла
Все зависит от компилятора
Есть ли у статического варианта какие-то подводные камни, например, при вызове из нескольких потоков?
Речь идёт именно о создании хелпера. У меня есть общий метод, который я хочу использовать в 20 разных местах и который очень неудобно(кажется) наследовать классом, который его использует. Я выношу такой метод, или даже целую группу методов, в отдельный класс.
Я что-то выигрываю, создавая объект хелпера? Не создавая объект хелпера?
но реализуется он только статическими методами, и только в статических классах
всем этим приемам есть свое место и время, просто надо в этом разобраться
и у вас будет один экземпляр класса. далеко не всегда это уместно. большинство прикладных задач, требует множества экземпляров различных классов
тем не менее, бвают ситуации, которые решаются гораздо элегантнее, именно статикой. а без нее получаются жутко корявыми
Есть ли у статического варианта какие-то подводные камни, например, при вызове из нескольких потоков?
Если возникает необходимость сделать статические методы с логикой посерьезней фильтра списка, например, доступ к БД, то есть пара вариантов в этом случае. Первый, сделать синглтон, если хотите, чтобы была одна точка входа для доступа к БД и Вы хотите эту точку и попытки получения данных из БД контролировать.
Второй, это иньекция зависимостей и контейнеры для них, всевозможные IoC. В настройках IoC указываете, что выдавать единственный экземпляр и он по сути сам станет singlton, фабрика контейнера не будет на каждое обращение создавать новый экземпляр.
Лично я статическими методами и даже полями пользуюсь. Иметь хелперы, это удобно, но, конечно, без фанатизма.
Я, например, нажегся в ASP.NET.
Разные юзеры генерили отчеты, у отчета были статические свойства. Я полагал, что значение свойств у каждого отчета для каждого юзера будет разное, ан нет. IIS, судя по всему, выдавал разным юзерам отчет из одного потока. Пришлось переписать, создавать отчет экземпляром класса и присваивать ему свойства.
Статические методы использую, например, чтобы преобразовать/форматировать что-то во что-то, если алгоритм преобразования достаточно хитрый. В статические свойства складываю константы или настройки какие-нибудь.
Хотя скорее всего есть и более элегантные решения в с# 😀
Статика в C#
Волею судьбы в последние годы у меня появилось ещё одно очень увлекательное хобби – учить. Я занимаюсь обучением людей, которые хотят стать программистами C#. Люди приходят разные: технари, гуманитарии, кто-то по своей воле, кого-то направляют от организаций. Не смотря на различные уровни, мне нужно их обучать. Поэтому я стараюсь постоянно обновлять и улучшать свои обучающие материалы. В связи с чем, пришёл к выводу: «А не плохо было бы оформить материалы в текстовом виде, чтобы ими было удобно пользоваться». Под катом я выложил как пример одну из недавно оформленных лекций.
Общая концепция
Статика подразумевает, что вам не нужно создавать экземпляр класса. Все приведённые выше составляющие класса, доступны посредством указания его имени.
Следует отметить, что необязательно делать весь класс статическим. Иногда достаточно применить статику для отдельных его членов.
Больше деталей
Выше мы не рассматривали такую конструкцию, как статический конструктор. Один из достаточно интересных вопросов, на мой взгляд, когда происходит вызов статического конструктор у классов?
Я думаю вы уже обратили внимание, что для статического конструктора не используется спецификатор доступа. Всё очень очевидно, создание статики вы не контролируете. Если попробовать выполнить приведённый ниже код, то можно убедиться в верности следующего утверждения: статический конструктор вызывается перед доступом к любому члену класса.
Можно поиграться, закомментировав любую из строк в которых происходит обращение к классу Box. Теперь немного изменим код, и подправим наше утверждение
В данном случае вызов статического конструктора не происходит. Итак: статический конструктор вызывается перед доступом к любому члену класса, за исключением констант. Я не зря использовал слово класс в данном определении. Со структурами очень много “приколов”. Вы должно быть знаете, что в C# нельзя переопределить конструктор по-умолчанию, но можно определить статический конструктор без параметров. Однако он будет вызываться не всегда, так например не произойдёт его вызов, если вы, например, попытаетесь создать массив структур.
Общие рассуждения об использовании статики
Полиморфизм
Как упоминалось выше, статические классы не поддерживают наследование, т.е. вы не можете наследоваться от интерфейса или другого класса и таким образом расширить функциональность.
Тестирование
При использование статики тестирование достаточно затруднено. Нельзя оперативно подменять код, основываясь на интерфейсах. Если нужно менять, то серьёзно, переписывая значительные куски кода.