谈谈static和util/helper类
今天先聊聊static方法的那些事儿,然后再引出static使用的“极致” util/helper类,说说它们的问题。
static方法什么时候用?
简单说在面向对象的场景下,绝大多数情况都不应使用static方法。这里有一些关于static使用场景的讨论: When should I use static methods?
Java: when to use static methods
上面讨论总结一下可以归纳为两点:
- 逻辑上不和某个类实例绑定,如Singleton中的GetInstance()
- 方法比较轻量,不涉及类变量,最好是一个“纯函数”,调用之后没有副作用
其中第一点是最值得注意之处。
下面两个例子可以使用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。当不确定是否要用时,不用。