在Java中创建无状态实用类的最佳实践是什么?

29

在Java中创建不持有任何状态的工具类的最佳实践是什么。

在大多数情况下,我们会为这些任务创建静态方法。另一个可能的方法是“创建单例对象”来执行此操作。

当要求代码易于单元测试时,应考虑哪些设计因素?


9
当你在Java或其他OO语言中看到一个实用类时,这很可能是某些东西被忽略了的指示。也就是说,你的模型中需要存在某个对象。这是一个机会去考虑可能需要哪些对象,以便让你的系统更可测试和灵活。 - Don Branson
5个回答

56

如果你能容忍我使用一个比喻……

你可能以前见过这样的东西:

Device

注意我们称其为烤面包机,而不是“BreadUtil”。

同样,实用方法可以放在一个命名为特定功能的类中,而不是“与面包相关的杂项”。

大多数时候,静态方法应该放在相关的类中;例如,Integer.parseInt 是 Integer 类的静态方法,而不是理论上的 IntegerUtil 或 NumberUtil 类的成员。

过去,创建单独的实用程序类的一个案例是感兴趣的主要类是一个接口。一个例子是 java.util.Collections。然而,从 Java 8 开始,这不再是一个借口,因为接口可以有静态方法和默认方法。事实上,Collections.sort(List)已经迁移到了 List.sort

如果您有很多实用程序方法,并且感觉它们会混乱相关的类,那么将它们放入单独的类中是可以的,但不是“BreadUtil”类。在类名中加入“util”(或“utils”,“utilities”,“misc”,“miscellaneous”,“general”,“shared”,“common”或“framework”)是绝对不可接受的。给这个类起一个有意义的名字来描述这些方法的用途。如果这些方法太多样化而无法允许这样的类名称,则可能需要将其拆分为多个类。(只有几个方法的小类也是完全可以接受的;很多人甚至认为这是好的设计。)

回到 Integer 的例子,如果你觉得这些方法给类带来了混乱,你可以创建像这样的新类:

public class IntegerMath {
    private IntegerMath() { }

    public static int compare(int x, int y) { /* ... */ }
    public static int compareUnsigned(int x, int y) { /* ... */ }
    public static int divideUnsigned(int dividend, int divisor) { /* ... */ }
    public static int min(int a, int b) { /* ... */ }
    public static int max(int a, int b) { /* ... */ }
    public static int remainderUnsigned(int dividend, int divisor) { /* ... */ }
    public static int signum(int i) { /* ... */ }
    public static int sum(int a, int b) { /* ... */ }
    public static long toUnsignedLong(int i) { /* ... */ }
}

public class IntegerBits {
    private IntegerBits() { }

    public static int bitCount(int i) { /* ... */ }
    public static int highestOneBit(int i) { /* ... */ }
    public static int lowestOneBit(int i) { /* ... */ }
    public static int numberOfLeadingZeros(int i) { /* ... */ }
    public static int numberOfTrailingZeros(int i) { /* ... */ }
    public static int reverse(int i) { /* ... */ }
    public static int reverseBytes(int i) { /* ... */ }
    public static int rotateLeft(int i, int distance) { /* ... */ }
    public static int rotateRight(int i, int distance) { /* ... */ }
}

public class IntegerParser {
    private IntegerParser() { }

    public static int parseInt(String s) { /* ... */ }
    public static int parseInt(String s, int radix) { /* ... */ }
    public static int parseUnsignedInt(String s) { /* ... */ }
    public static int parseUnsignedInt(String s, int radix) { /* ... */ }
}

最后一个例子可能没有静态方法会更好:

public class IntegerParser {
    public IntegerParser() { this(10); }
    public IntegerParser(int radix) { /* ... */ }

    public int parseInt(String s) { /* ... */ }
    public int parseUnsignedInt(String s) { /* ... */ }
}

1
大部分情况下,我真的很欣赏这个答案,但是这里有一个大胆的声明,即在类名中永远不要使用单词“Util”。 我猜我的问题是@VGR,您认为充满非常有用的实用程序类的Apache commons库怎么样?(https://mvnrepository.com/search?q=apache+commons) - aaiezza
1
@aaiezza 我对Apache Commons没有很高的评价。每个名字中带有“util”的类都可以被赋予更好的名称,并且在许多情况下还可以进行更好的设计。 - VGR

8

我认为最常见的方法是创建静态方法。例如,可以参考Apache Commons Lang中的StringUtilsGuava中的Strings或者甚至是JDK中的Arrays

此外,类应该被声明为final,并且应该有一个私有构造函数以避免继承或实例化它。

无论是使用静态方法还是单例模式,对于单元测试来说应该是相同的工作量。在后一种情况下,你可能需要编写更多的代码(字符)。

我知道OO纯粹主义者会质疑这些类的存在,我倾向于同意他们的看法,但这些类仅仅是为了简化而添加的,你应该限制这类类的数量。


随着Java 8引入了接口中的默认方法和静态方法,我们是否应该使用接口来提供实用方法? - Udaya Shankara Gandhi Thalabat
我会坚持使用类,以保持与JDK和其他知名库的一致性。你有使用接口的理由吗? - Adrian Ber

2
如果您使用Spring等框架,您可以创建一个带有@Service注解的实用类。这将确保它是单例,并且轻松地将其注入到任何其他需要其方法的类中。
在任何其他情况下,我建议使用工厂模式将其设置为单例,或者相反,仅使用静态方法。

1
在Java中创建实用程序(不持有任何状态)类的最佳实践是什么?
在我看来,最好的方法是尽可能省略实用程序类。
实用程序类颠覆了面向对象编程的思想。当您需要一个新方法时,通常将其添加到具有最高内聚力的类中。这意味着将大部分信息保存在该类中,该方法需要。其他信息将作为参数传递给该方法。如果参数列表过长,则通常表示方法放错位置。
很少有情况下,您确实需要实用程序类来为某些类型提供方法。例如:
- 如果无法将该方法添加到源代码中,因为您不拥有它(但您可以创建包装器或子类) - 如果需要附加方法的类是final且您不拥有源代码(例如StringUtility)
在大多数情况下,我们最终会为此类任务创建静态方法。执行此操作的另一种可能方式是“创建单例对象”。
当要求代码易于进行单元测试时,应考虑哪些设计因素?
如果你从单元测试的角度来看待实用类,并且想要能够模拟实用类,那么应该使用单例模式,因为对象引用可以被替换。可以通过更改单例实例引用(静态变量)或在客户端使用对象字段来实现。例如:
 private SomeUtility someUtility = SomeUtiltiy.INSTANCE;

 public void someMethod(...){
    // Can be replaced by changing the someUtility reference
    someUtility.doSomething(....); 

    // A static call like 
    // SomeUtility.doSomething(...);
    // can not be easily replaced.
 }

静态方法调用很难替换。一些测试框架(如powermock)通过重写客户端字节码来支持模拟静态调用。但我认为这些框架是为了支持对糟糕的遗留代码进行单元测试而设计的。如果您需要为新代码使用powermock,您应该重新考虑您的设计。


如果您创建一个通用字符串或数组操作库,会怎样呢?这通常是实用类的目的。对简单类型或对象进行原始(或不那么原始)操作。 - Vladislav Rastrusny

0

静态是单例的。只有当您需要多个具有不同属性/设置值的实用程序类变体时,才需要具有非静态方法的单例实例。例如,当您的实用程序具有某些属性(即customProperty),并且您需要同时使用两种不同情况时。

  1. 当实用程序需要使用customProperty = value1时

  2. 当实用程序需要使用customProperty = value2时...

但这很奇怪,笨拙而不好...实用程序调用者可以将所需的属性值提供给静态方法。因此,请不要固守那个理论模式...使实用程序方法始终为静态,并且不必关心“理论”模式... :)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接