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

时间:2009-04-15 17:12:38

标签: language-agnostic static-methods

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

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

这是否合理?

14 个答案:

答案 0 :(得分:140)

有两种常见的静态方法:

  • “安全”静态方法将始终为相同输入提供相同的输出。它不修改全局变量,也不会调用任何类的任何“不安全”静态方法。从本质上讲,你使用的是一种有限的函数式编程 - 不要害怕这些,它们很好。
  • “不安全”静态方法会将全局状态或代理转换为全局对象或其他一些不可测试的行为。这些是程序编程的回归,如果可能的话应该重构。

“不安全”静态的一些常见用法 - 例如,在Singleton模式中 - 但要注意尽管你称之为任何漂亮的名称,但你只是在改变全局变量。在使用不安全的静态之前要仔细考虑。

答案 1 :(得分:15)

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

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

其他时候,它是用对象语言完成的程序设计。

答案 2 :(得分:11)

这实际上只是John Millikin的最佳答案。


虽然将无状态方法(几乎是函数)静态化是安全的,但有时会导致很难修改的耦合。考虑一下你有一个静态方法:

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

你称之为:

StaticClassVersionOne.doSomeFunkyThing(42);

这一切都很好,非常方便,直到遇到必须修改静态方法行为的情况,并发现你与StaticClassVersionOne紧密绑定。可能你可以修改代码并且没问题,但如果有其他调用者依赖于旧行为,则需要在方法体中考虑它们。在某些情况下,如果方法体试图平衡所有这些行为,那么它可能变得非常难看或不可维护。如果你拆分方法,你可能需要在几个地方修改代码以考虑它,或者调用新类。

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

它可能或可能不是可能的情况,但我认为值得考虑。

答案 3 :(得分:6)

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

即改变:

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

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

然而在许多情况下这是不可能的(例如,从XSD / WSDL /等生成常规类代码),或者它会使类很长,并且转换方法通常对于复杂对象和您来说真的很痛苦只是希望他们在自己独立的课堂上。所以是的,我在实用程序类中有静态方法。

答案 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辅助方法。它们创建的html元素与对象的行为无关,因此是静态的。

编辑2:将函数式编程改为结构化编程(出于某种原因,我感到困惑),向托尔斯滕指出了这一点。

答案 5 :(得分:4)

您被警告远离静态方法的原因是使用它们会失去对象的一个​​优点。对象用于数据封装。这可以防止意外的副作用发生,避免了错误。静态方法没有封装数据*因此不能获得这种好处。

也就是说,如果你没有使用内部数据,它们可以使用并且执行起来稍快一些。但请确保您没有触及其中的全局数据。

  • 有些语言也有类级变量,可以封装数据和静态方法。

答案 6 :(得分:4)

静态类只要在正确的位置使用它们就可以了。

即:'leaf'方法的方法(它们不修改状态,它们只是以某种方式转换输入)。很好的例子就是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。现在扩大这种依赖性的噩梦,你可能会猜到会发生什么。想象一下,在没有明确的初始化顺序的情况下尝试维护代码。将此与依赖注入与实例进行对比 - 在这种情况下,如果未满足依赖关系,代码甚至不会编译

此外,如果你使用修改状态的静态,它就像一个纸牌屋。你永远不知道谁可以使用什么,而且设计往往像意大利面条怪物。

最后,同样重要的是,使用静态将程序与特定实现联系起来。静态代码是可测试性设计的对立面。测试充满静态的代码是一场噩梦。永远不能为测试double交换静态调用(除非您使用专门用于模拟静态类型的测试框架),因此静态系统会使用它的所有内容成为即时集成测试。

简而言之,静态可以用于某些东西,对于小工具或一次性代码我不会阻止它们的使用。然而,除此之外,它们是可维护性,良好设计和易于测试的血腥梦魇。

这是一篇关于问题的好文章:http://gamearchitect.net/2008/09/13/an-anatomy-of-despair-managers-and-contexts/

答案 7 :(得分:3)

我最近重构了一个应用程序来删除/修改最初作为静态类实现的一些类。随着时间的推移,这些类获得了很多,人们只是将新函数标记为静态,因为从来没有一个实例浮动。

所以,我的答案是静态类本身并不坏,但现在开始创建实例可能更容易,然后必须重构。

答案 8 :(得分:2)

我曾经在一个带有一堆静态方法和一个单例的类之间来回走动。两者都解决了问题,但单例可以更容易地被多个替换。 (程序员总是看起来很确定只会有一些东西,我发现自己错了很多时候完全放弃静态方法,除非在一些非常有限的情况下)。

无论如何,单例使您能够稍后将某些内容传递到工厂以获取不同的实例,并且无需重构即可更改整个程序的行为。将全局类静态方法更改为具有不同“后备”数据或略有不同的行为(子类)的内容是对接中的主要痛苦。

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

所以是的,他们很糟糕。

答案 9 :(得分:2)

我认为这是一种设计气味。如果您发现自己大多使用静态方法,那么您可能没有非常好的OO设计。它并不一定是坏的,但与所有气味一样,它会让我停下来重新评估。它暗示你可能能够做出更好的面向对象设计,或者你应该走向另一个方向并完全避免OO来解决这个问题。

答案 10 :(得分:1)

只要没有内部状态发挥作用,这很好。请注意,通常静态方法应该是线程安全的,因此如果使用帮助程序数据结构,请以线程安全的方式使用它们。

答案 11 :(得分:1)

如果你知道你从不需要使用C的内部状态,那很好。但是,如果将来发生变化,您需要使该方法非静态。如果它是非静态的,那么如果你不需要它,你可以忽略内部状态。

答案 12 :(得分:1)

如果它是一种实用方法,那么将其设为静态会很好。 Guava和Apache Commons建立在这个原则之上。

我对此的看法纯粹是务实的。如果它是您的应用程序代码,静态方法通常不是最好的方法。静态方法具有严重的单元测试限制 - 它们不容易被模拟:您不能将模拟的静态功能注入到其他测试中。您通常也无法将功能注入静态方法。

所以在我的应用程序逻辑中,我通常有一些类似静态实用程序的方法调用。即

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

其中一个好处是我不测试这样的方法: - )

答案 13 :(得分:0)

即使对于无状态代码,静态方法通常也是一个糟糕的选择。而是使用这些方法创建一个单例类,这些方法被实例化一次并注入到那些想要使用这些方法的类中。这些类更容易模拟和测试。它们更加面向对象。您可以在需要时使用代理包装它们。静力学让OO变得更加困难,我认为没有理由在几乎所有情况下都使用它们。不是100%,而是几乎全部。