关于语言不可知:使用很多静态方法是件坏事吗?

Is using a lot of static methods a bad thing?

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

我阅读了各种建议(包括StackOverflow),不要过度使用静态方法,但我仍然无法理解上面的经验法则有什么问题。

这是不是一个合理的方法?


有两种常见的静态方法:

  • "安全"静态方法总是为相同的输入提供相同的输出。它不修改全局,也不调用任何类的任何"不安全"静态方法。本质上,您使用的是一种有限的函数式编程——不要害怕这些,它们很好。
  • 一个"不安全"的静态方法会改变全局状态、全局对象的代理或其他一些不可测试的行为。这些都是程序化编程的基础,如果可能的话应该重构。

"不安全"静态有几个常见的用法——例如,在单例模式中——但是请注意,尽管您称它们为漂亮的名称,但您只是在改变全局变量。在使用不安全的静电剂之前要仔细考虑。


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

通常,对象封装状态和行为。只封装行为的对象是奇数的。有时它是一个轻量或轻量级的例子。

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


这真的只是约翰米利金伟大答案的后续行动。

尽管可以安全地将无状态方法(相当多的函数)设置为静态,但有时会导致难以修改的耦合。假设您有一个静态方法,例如:

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

你称之为:

1
StaticClassVersionOne.doSomeFunkyThing(42);

这一切都很好,而且非常方便,直到您遇到一个必须修改静态方法行为的情况,并且发现您与StaticClassVersionOne紧密绑定。可能您可以修改代码,这样就可以了,但是如果有其他调用程序依赖于旧的行为,则需要在方法体中考虑它们。在某些情况下,如果方法体试图平衡所有这些行为,它可能会变得非常丑陋或无法维护。如果拆分方法,可能需要在多个地方修改代码才能考虑到它,或者调用新的类。

但是考虑一下,如果您已经创建了一个接口来提供该方法,并将其提供给调用方,那么现在当行为必须更改时,可以创建一个新的类来实现该接口,该接口更干净、更容易测试和更可维护,而该类是提供给调用方的。在这个场景中,调用类不需要修改甚至重新编译,并且更改是本地化的。

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


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

即:改变:

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

进入之内

1
2
3
4
public class Bar {
    ...
    public Foo transform() { ...}
}

然而,在许多情况下,这是不可能的(例如,从xsd/wsdl/etc生成常规类代码),否则它将使类非常长,而转换方法对于复杂对象来说通常是一种真正的痛苦,您只需要将它们放在自己的类中即可。所以是的,我在实用程序类中有静态方法。


静态类只要在正确的地方使用就可以。

即:"叶"方法(它们不修改状态,只是以某种方式转换输入)。例如path.combine。这些东西对terser语法很有用。

我对静态的问题有很多:

首先,如果您有静态类,依赖项是隐藏的。考虑以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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。现在放大这个依赖性噩梦,你可能会猜到会发生什么。想象一下,在没有明确初始化顺序的情况下,试图维护代码。与依赖注入和实例相比——在这种情况下,如果不满足依赖,代码甚至不会编译!

此外,如果您使用修改状态的静态数据,它就像是一个卡片库。你永远不知道谁有权得到什么,而且设计往往像一个意大利面怪物。

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

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

以下是一篇关于这些问题的好文章:http://gamearchitect.net/2008/09/13/an-anatomy-of-deadise-managers-and-context/


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

也就是说,如果您不使用内部数据,它们可以使用,并且执行速度稍快。不过,请确保不要接触其中的全局数据。

  • 有些语言还具有类级变量,允许封装数据和静态方法。


这似乎是一个合理的方法。您不想使用太多静态类/方法的原因是,您最终会从面向对象的编程转向结构化编程领域。

在您的情况下,如果您只是将a转换为b,那么我们所要做的就是将文本转换为

1
"hello" =>(transform)=>"Hello!"

那么静态方法就有意义了。

但是,如果您经常在一个对象上调用这些静态方法,并且它对于许多调用都是唯一的(例如,您使用它的方式取决于输入),或者它是对象固有行为的一部分,那么将它作为对象的一部分并保持其状态是明智的。实现这一点的一种方法是将其实现为一个接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Interface{
    method toHtml(){
        return transformed string (e.g."Hello!")
    }

    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:将函数式编程改为结构化编程(出于某种原因,我感到困惑),并向Torsten提供指出这一点的道具。


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

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


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


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

无论如何,singleton使您能够稍后将某个东西传递到工厂以获得不同的实例,从而在不重构的情况下改变整个程序的行为。将静态方法的全局类更改为具有不同的"支持"数据或稍有不同的行为(子类)的类是一个大麻烦。

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

所以是的,他们很坏。


当然,没有银弹。静态类对于小实用程序/帮助程序来说是可以的。但是使用静态方法进行业务逻辑编程当然是邪恶的。考虑以下代码

1
2
3
4
5
6
7
8
9
10
11
   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隔离(大多数测试工具不模拟静态类),这使得单元测试成为不可能。无单元测试==低质量


如果它是一个实用方法,最好使其静态化。瓜娃和阿帕奇公地是建立在这一原则之上的。

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

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

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

其中一个好处是我没有测试这种方法:—)


如果你知道你永远不需要使用C的内部状态,那就好了。不过,如果将来这种情况发生变化,您需要使方法非静态。如果开始时它是非静态的,那么如果不需要它,您可以忽略内部状态。


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


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