使用很多静态方法是一件坏事吗?


97

当该类不需要跟踪内部状态时,我倾向于将该类中的所有方法声明为静态的。例如,如果我需要将A转换为B并且不依赖于可能变化的某些内部状态C,则可以创建一个静态转换。如果存在一个我希望能够调整的内部状态C,那么我将添加一个构造函数来设置C,并且不要使用静态转换。

我阅读了各种建议(包括关于StackOverflow的建议),不要过度使用静态方法,但是我仍然无法理解上述经验法则的错误之处。

那是合理的方法吗?

Answers:


152

有两种常见的静态方法:

  • “安全”静态方法将始终为相同的输入提供相同的输出。它不会修改全局变量,并且不会调用任何类的任何“不安全”静态方法。本质上,您使用的是有限的函数式编程-不用担心,它们很好。
  • 一种“不安全”的静态方法会改变全局状态或一个全局对象的代理或某些其他不可测试的行为。这些都是过程编程的遗漏,如果可能的话,应该将其重构。

“不安全”静态变量有几种常见用法(例如,在Singleton模式中),但请注意,尽管您称呼它们为漂亮名字,但您只是在对全局变量进行突变。在使用不安全的静电之前,请仔细考虑。


这正是我必须解决的问题-Singleton对象的使用或滥用。
2009年

感谢您提供的最佳答案。我的问题是,如果将单例作为参数传递给静态方法,这会使静态方法不安全吗?
Tony D

1
术语“纯函数”和“不纯函数”是功能编程中给您所谓的“安全”和“不安全”静态变量的名称。
Omnimike

19

没有任何内部状态的对象是可疑的东西。

通常,对象封装状态和行为。仅封装行为的对象是奇怪的。有时,这是“ 轻量级”或“ 轻量级”的示例。

其他时候,它是用目标语言完成的过程设计。


6
我听到您在说什么,但是像Math对象这样的东西如何封装除了行为之外的任何东西?
JonoW

10
他只是说可疑,没有错,他是绝对正确的。
比尔K

2
@JonoW:数学是非常特殊的情况,其中有许多无状态函数。当然,如果您使用Java进行函数式编程,则将有许多无状态函数。
S.Lott

13

这实际上只是John Millikin的出色回答的后续措施。


尽管将无状态方法(几乎是函数)静态化是安全的,但有时可能导致难以修改的耦合。考虑一下您有一个静态方法:

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

您称之为:

StaticClassVersionOne.doSomeFunkyThing(42);

一切都很好,也很方便,直到遇到必须修改静态方法的行为并发现与它紧密联系的情况StaticClassVersionOne。可能您可以修改代码,这会很好,但是如果还有其他调用者依赖于旧的行为,则需要在方法的主体中加以考虑。在某些情况下,如果该方法主体试图平衡所有这些行为,则可能会变得难看或难以维护。如果拆分方法,则可能必须在几个地方修改代码以考虑到它,或者调用新类。

但是考虑一下您是否已经创建了一个接口来提供该方法,并将其提供给调用方,现在当必须更改行为时,可以创建一个新类来实现该接口,该类更干净,更易于测试并且更易于维护,而是提供给呼叫者。在这种情况下,不需要更改甚至不需要重新编译调用类,并且更改已本地化。

可能出现或可能不会出现这种情况,但我认为值得考虑。


5
我认为这不仅是一种可能的情况,而且会使静态方法成为最后的选择。静电也使TDD成为噩梦。无论在何处使用静态方法,都无法进行模拟,您必须知道输入和输出是什么以测试不相关的类。现在,如果您更改了静态对象的行为,则使用该静态对象的不相关类的测试将被破坏。而且,它成为隐藏的依赖项,您不能将其传递给构造函数以通知开发人员潜在的重要依赖项。
DanCaveman'1

6

另一个选择是将它们作为非静态方法添加到原始对象上:

即,更改:

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

进入

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

但是,在许多情况下这是不可能的(例如,从XSD / WSDL / etc生成常规的类代码),否则会使类非常长,并且转换方法对于复杂对象常常是真正的痛苦,而您只需要它们在他们自己的单独班级中。是的,我在实用工具类中有静态方法。


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()
}

编辑:大量使用静态方法的一个很好的例子是Asp.Net MVC或Ruby中的html helper方法。它们创建的HTML元素与对象的行为无关,因此是静态的。

编辑2:将函数式编程更改为结构化编程(出于某种原因,我感到困惑),这是Torsten指出的支持。


2
我认为使用静态方法不等同于函数式编程,因此我猜您的意思是结构化编程。
Torsten Marek

3

我最近重构了一个应用程序,以删除/修改最初实现为静态类的某些类。随着时间的流逝,这些类变得越来越重要,人们一直将新功能标记为静态,因为从来没有任何实例在周围徘徊。

因此,我的回答是,静态类并不是天生就不好,但是现在开始创建实例,然后再进行重构可能会更容易。


3

我认为这是一种设计气味。如果您发现自己大多使用静态方法,则可能没有很好的OO设计。不一定坏,但是所有气味都会使我停下来重新评估。它暗示您可能能够做出更好的OO设计,或者您应该朝另一个方向发展,并完全避免OO来解决此问题。


2

我曾经在带有一堆静态方法的类和单例之间来回切换。两者都解决了问题,但是单身人士可以很容易地被一个以上的人取代。(程序员似乎总是很确定,只有一种东西,我发现自己错了很多次,完全放弃了静态方法,除了在某些非常有限的情况下)。

无论如何,单例功能使您能够稍后将某些东西传递给工厂以获取不同的实例,并且无需更改即可更改整个程序的行为。将静态方法的全局类更改为具有不同“后备”数据或行为稍有不同的行为(子类)是一种麻烦。

和静态方法没有类似的优势。

所以是的,它们很糟糕。


1

只要内部状态不起作用,就可以了。请注意,通常静态方法应该是线程安全的,因此,如果您使用辅助数据结构,请以线程安全的方式使用它们。


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

即使对于无状态代码,静态方法通常也是一个不好的选择。而是使用这些方法实例化一次,然后将其注入要使用这些方法的类中。这样的类更容易模拟和测试。它们更加面向对象。您可以在需要时用代理包装它们。静电使OO变得困难得多,我认为没有理由在几乎所有情况下都使用它们。不是100%,而是几乎所有。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.