谈谈static和util/helper类

今天先聊聊static方法的那些事儿,然后再引出static使用的“极致” util/helper类,说说它们的问题。

static方法什么时候用?

简单说在面向对象的场景下,绝大多数情况都不应使用static方法。这里有一些关于static使用场景的讨论: When should I use static methods?

Java: when to use static methods

上面讨论总结一下可以归纳为两点:

  1. 逻辑上不和某个类实例绑定,如Singleton中的GetInstance()
  2. 方法比较轻量,不涉及类变量,最好是一个“纯函数”,调用之后没有副作用

其中第一点是最值得注意之处。

下面两个例子可以使用static方法:

public static double Sigmoid(double x, double upper = 10.0, double lower = -10.0)
{
    if (x >= upper)
    {
        return 0.999999;
    }

    if (x <= lower)
    {
        return 0.000001;
    }

    return 1.0 / (1.0 + Math.Exp(-x));
}

public static long UnixTimeStamp(DateTime time)
{
    return (long)time.Subtract(new DateTime(1970, 1, 1)).TotalSeconds;
}

还有一些情况必须使用static方法的,比如C#中的extention method实现 Extension Methods (C# Programming Guide):

public static class ListShuffle
{
    public static IList<T> Shuffle<T>(this IList<T> list)
    {
        return list.OrderBy(x => new Random().Next()).ToList();
    }
}

不该用static方法

static方法一定程度上与面向对象的思想是对立的,设想一种极端情况,如果一个程序中全是static方法,那么它将退化成面向过程的程序,且很有可能组织不得当。比如下例:

public class QueryValidation
{
    private static readonly HttpClient Client = new HttpClient();
    private const string Url = "http://xxx/api";

    public static async Task<bool> IsValidQueryAsync(string query)
    {
        var requestMessage = new HttpRequestMessage(HttpMethod.Post, Url)
        {
            Content = new StringContent(JsonConvert.SerializeObject(query))
        };

        try
        {
            using (var cancelTokenSource = new CancellationTokenSource(timeoutInMs))
            {
                var result = await Client.SendAsync(requestMessage, cancelTokenSource.Token).ConfigureAwait(false);
                ...
                return true;
            }
        }
        catch (Exception e)
        {
            ...
            return false;
        }
    }
}

IsValidQueryAsync()就不应使用static方法,首先逻辑上也看不出该方法应属于类的全体而非类的实例。其次方法不轻量,远程调用,且用到了静态类变量。

因此,不用static方法,将类名改为QueryValidator,其他类聚合了它并用QueryValidator.IsValidQueryAsync()方法来验证Query更为合理。

关于性能

99.999%以上的情况下可以忽略。Speed Test: Static vs Instance Methods,这里有前人做过测试,二者之间的性能差异微乎其微,不应作为是否static的首要考虑因素。

避免使用Util/Helper类

为什么聊static方法会谈到Util/Helper类呢?因为曾在代码库见过一些所谓Util,把各种很复杂的方法都堆在里面,都写为static方法“方便”调用。一个类堆了上千行,里面什么东西都有。

这种Util美其名曰“代码复用”,实际上是偷懒,没有仔细思考类之间的逻辑抽象。面向对象的代码复用有许多方式,继承、聚合都可以,用static方法复用的方式最省脑子,但上千行的Util基本可以判断为anti-pattern。

这么做的思想很朴素:A和B需要复用一段代码,于是就将共用的代码变成static放在公用的Util类中,然后C和D又要利用一段代码,算了,不再新建一个类了,也static后放在公用的Util类中。再看编译器报错了,在static方法中使用了非static的变量,得,把这变量也static了……

这种Util有许多问题,第一,多个类调用Util类中的静态方法,那么这些类都增加了额外依赖,与面向对象希望达到的解耦目的不符。第二,Util类完全不内聚,代码垃圾场,违反了SOLID原则中的单一职责原则。

我个人认为Util Class并非绝对禁用,只是大部分情况下使用者没有想清楚为什么要用,是否有更优雅的替代方案。还有一个问题就是命名,如果整个项目中只有一个Util类,即使里面所有函数都是static纯函数,那它也不符合单一职责原则。解决方案:1. 将这些方法放进使用它的类中 2. 将方法分类,分拆成更内聚的具体类并起一个合适的名称,比如DateTimeConverter之类。

这里也有一些关于Util Class的讨论: Avoid Utility Classes

Anti-Patterns and Worst Practices – Utils Class

结论

在面向对象程序中尽量不用static。当不确定是否要用时,不用。