Плохо ли использовать множество статических методов?

97

Я склонен объявлять статическими все методы в классе, когда этому классу не требуется отслеживать внутренние состояния. Например, если мне нужно преобразовать A в B и не полагаться на какое-то внутреннее состояние C, которое может отличаться, я создаю статическое преобразование. Если есть внутреннее состояние C, которое я хочу настроить, я добавляю конструктор для установки C и не использую статическое преобразование.

Я читал различные рекомендации (в том числе о StackOverflow) НЕ злоупотреблять статическими методами, но я все еще не понимаю, что не так с приведенным выше эмпирическим правилом.

Это разумный подход или нет?

Лоло
источник

Ответы:

153

Есть два типа общих статических методов:

  • «Безопасный» статический метод всегда будет давать одинаковый результат для одних и тех же входов. Он не изменяет глобальные объекты и не вызывает никаких «небезопасных» статических методов любого класса. По сути, вы используете ограниченный вид функционального программирования - не бойтесь этого, они в порядке.
  • «Небезопасный» статический метод изменяет глобальное состояние или прокси в глобальный объект или какое-либо другое поведение, не подлежащее тестированию. Это возврат к процедурному программированию, и по возможности следует провести рефакторинг.

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

Джон Милликин
источник
Это была именно та проблема, которую я должен был решить - использование или, скорее, неправильное использование объектов Singleton.
overslack
Спасибо за отличный ответ. У меня вопрос: если синглтоны передаются в качестве параметров статическим методам, делает ли это небезопасным статический метод?
Tony D
1
Термины «чистая функция» и «нечистая функция» - это названия, данные в функциональном программировании тем, что вы называете «безопасной» и «небезопасной» статикой.
Omnimike
19

Объект без внутреннего состояния - вещь подозрительная.

Обычно объекты инкапсулируют состояние и поведение. Странный объект, который только инкапсулирует поведение. Иногда это пример Легкого или Наилегчайшего веса .

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

С.Лотт
источник
6
Я слышу, что вы говорите, но как объект Math может инкапсулировать что-либо, кроме поведения?
JonoW
10
Он просто сказал, что это подозрительно, а не неправильно, и он абсолютно прав.
Bill K
2
@JonoW: Математика - это особый случай, когда существует множество функций без сохранения состояния. Конечно, если вы занимаетесь функциональным программированием на Java, у вас будет много функций без сохранения состояния.
S.Lott
14

На самом деле это только продолжение великого ответа Джона Милликина.


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

public class StaticClassVersionOne {
    public static void doSomeFunkyThing(int arg);
}

Что вы называете:

StaticClassVersionOne.doSomeFunkyThing(42);

Это хорошо, хорошо и очень удобно, пока вы не столкнетесь со случаем, когда вам нужно изменить поведение статического метода, и обнаружите, что вы жестко привязаны к StaticClassVersionOne . Возможно, вы могли бы изменить код, и это было бы хорошо, но если бы были другие вызывающие объекты, зависящие от старого поведения, их нужно будет учитывать в теле метода. В некоторых случаях это тело метода может стать довольно некрасивым или неподдерживаемым, если оно пытается сбалансировать все эти поведения. Если вы разделите методы, вам, возможно, придется изменить код в нескольких местах, чтобы учесть это, или выполнить вызовы новых классов.

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

Это может быть, а может и нет, но я думаю, что стоит подумать.

Grundlefleck
источник
5
Я утверждаю, что это не только вероятный сценарий, но и статика - последнее средство. Статика также превращает TDD в кошмар. Где бы вы ни использовали статику, вы не можете создать макет, вы должны знать, что это за ввод и вывод, чтобы протестировать несвязанный класс. Теперь, если вы измените поведение статики, ваши тесты на несвязанных классах, которые используют эту статику, будут сломаны. Кроме того, это становится скрытой зависимостью, которую вы не можете передать конструктору, чтобы уведомить разработчиков о потенциально важной зависимости.
DanCaveman
6

Другой вариант - добавить их как нестатические методы к исходному объекту:

т.е. изменение:

public class BarUtil {
    public static Foo transform(Bar toFoo) { ... }
}

в

public class Bar {
    ...
    public Foo transform() { ...}
}

однако во многих ситуациях это невозможно (например, генерация кода обычного класса из XSD / WSDL / и т. д.), или это сделает класс очень длинным, а методы преобразования часто могут быть настоящей проблемой для сложных объектов, и вы просто хотите, чтобы они в своем отдельном классе. Так что да, у меня есть статические методы в служебных классах.

JeeBee
источник
5

Статические классы хороши, если они используются в нужных местах.

А именно: методы, которые являются «листовыми» методами (они не изменяют состояние, они просто каким-то образом преобразуют ввод). Хорошим примером этого являются такие вещи, как Path.Combine. Подобные вещи полезны и позволяют сократить синтаксис.

У меня много проблем со статикой:

Во-первых, если у вас есть статические классы, зависимости скрыты. Учтите следующее:

public static class ResourceLoader
{
    public static void Init(string _rootPath) { ... etc. }
    public static void GetResource(string _resourceName)  { ... etc. }
    public static void Quit() { ... etc. }
}

public static class TextureManager
{
    private static Dictionary<string, Texture> m_textures;

    public static Init(IEnumerable<GraphicsFormat> _formats) 
    {
        m_textures = new Dictionary<string, Texture>();

        foreach(var graphicsFormat in _formats)
        {
              // do something to create loading classes for all 
              // supported formats or some other contrived example!
        }
    }

    public static Texture GetTexture(string _path) 
    {
        if(m_textures.ContainsKey(_path))
            return m_textures[_path];

        // How do we know that ResourceLoader is valid at this point?
        var texture = ResourceLoader.LoadResource(_path);
        m_textures.Add(_path, texture);
        return texture; 
    }

    public static Quit() { ... cleanup code }       
}

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

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

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

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

Вот хорошая статья о проблемах: http://gamearchitect.net/2008/09/13/an-anatomy-of-despair-managers-and-contexts/

Марк Симпсон
источник
4

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

Тем не менее, если вы не используете внутренние данные, их можно использовать и выполнять немного быстрее. Убедитесь, что вы не касаетесь глобальных данных в них.

  • В некоторых языках также есть переменные уровня класса, которые позволяют инкапсулировать данные и статические методы.
Стив Роу
источник
4

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

В вашем случае, когда вы просто преобразуете A в B, скажем, все, что мы делаем, это преобразовываем текст, чтобы он

"hello" =>(transform)=> "<b>Hello!</b>"

Тогда имел бы смысл статический метод.

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

class Interface{
    method toHtml(){
        return transformed string (e.g. "<b>Hello!</b>")
    }

    method toConsole(){
        return transformed string (e.g. "printf Hello!")
    }
}


class Object implements Interface {
    mystring = "hello"

    //the implementations of the interface would yield the necessary 
    //functionality, and it is reusable across the board since it 
    //is an interface so... you can make it specific to the object

   method toHtml()
   method toConsole()
}

Изменить: одним из хороших примеров использования статических методов являются вспомогательные методы html в Asp.Net MVC или Ruby. Они создают элементы HTML, которые не привязаны к поведению объекта и поэтому являются статическими.

Изменить 2: изменено функциональное программирование на структурированное программирование (по какой-то причине я запутался), спасибо Торстену за указание на это.

MunkiPhD
источник
2
Я не думаю, что использование статических методов квалифицируется как функциональное программирование, поэтому я предполагаю, что вы имеете в виду структурированное программирование.
Торстен Марек
3

Недавно я провел рефакторинг приложения, чтобы удалить / изменить некоторые классы, которые изначально были реализованы как статические классы. Со временем эти классы приобрели так много, и люди просто продолжали отмечать новые функции как статические, поскольку никогда не было экземпляра, плавающего вокруг.

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

лишний
источник
3

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

Патрос
источник
2

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

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

И статические методы не имеют подобного преимущества.

Так что да, они плохие.

Билл К
источник
1

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

Lucero
источник
1

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

Адам Крум
источник
1

Если это служебный метод, неплохо сделать его статическим. Guava и Apache Commons построены на этом принципе.

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

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

static cutNotNull(String s, int length){
  return s == null ? null : s.substring(0, length);
}

одно из преимуществ - я не тестирую такие методы :-)

Андрей Чащев
источник
1

Ну, серебряной пули, конечно, нет. Статические классы подходят для небольших утилит / помощников. Но использование статических методов для программирования бизнес-логики - определенно зло. Рассмотрим следующий код

   public class BusinessService
   {

        public Guid CreateItem(Item newItem, Guid userID, Guid ownerID)
        {
            var newItemId = itemsRepository.Create(createItem, userID, ownerID);
            **var searchItem = ItemsProcessor.SplitItem(newItem);**
            searchRepository.Add(searchItem);
            return newItemId;
        }
    }

Вы видите статический вызов метода, вызывающего ItemsProcessor.SplitItem(newItem);запах, причина

  • У вас не объявлена ​​явная зависимость, и если вы не вникаете в код, вы можете пропустить связь между вашим классом и контейнером статического метода
  • Вы не можете тестировать, BusinessServiceизолировав его ItemsProcessor(большинство инструментов тестирования не имитируют статические классы), и это делает невозможным модульное тестирование. Нет модульных тестов == низкое качество
Константин Чернов
источник
0

Статические методы обычно плохой выбор даже для кода без состояния. Вместо этого создайте одноэлементный класс с этими методами, который создается один раз и внедряется в те классы, которые хотят использовать эти методы. Такие классы легче смоделировать и протестировать. Они гораздо более объектно-ориентированы. При необходимости вы можете обернуть их прокси. Статика значительно усложняет ОО, и я не вижу смысла использовать их почти во всех случаях. Не 100%, но почти все.

Джон
источник