什么是效用类?“任何人都能写出计算机能读懂的代码,好程序员应该写出人类能读懂的代码。”马丁·福尔
在软件开发中,经常会遇到包含各种静态方法的工具类,这些静态方法旨在解决日常问题,例如CPF验证(巴西的纳税人识别号)、字符串操作或数学计算。虽然它们看起来很方便,但这些类在长期来看可能会带来麻烦,尤其是在那些旨在遵循SOLID和领域驱动设计(DDD)等原则的项目中。本文中,我们将探讨为什么应该避免使用这些工具类以及如何用符合最佳设计实践的解决方案来替换它们。
关于工具类的问题效用类本质上是一组静态方法的集合,这些方法被归入一个单一的类中。这些方法没有状态,也不代表领域中的任何有意义的概念或实体。常见的例子包括 StringUtils
、MathUtils
或甚至 CPFUtils
。虽然看似有帮助,这些类带来了一些问题:
- 违反单一职责原则(SRP)
工具类往往会积累多个无关的责任。例如,在一个CPFUtils
类中,你可能会发现用于CPF验证、格式化甚至是CPF号生成的方法。这导致了低内聚和增加维护难度。 - 破坏封装性
工具类不会将行为封装在领域上下文中。相反,它们提供了一种通用但与领域无关的功能。这违反了DDD原则,其中每个代码片段都应反映应用程序领域中的有意义概念,如DDD原则所述。 - 使单元测试更难
工具类中的静态方法难以模拟和注入为依赖。这使得测试变得复杂,并可能导致测试环境中出现不必要的依赖。 - 低上下文重用
工具类既不可扩展也不具多态性。例如,如果你希望在不同的上下文中处理CPF,你可能需要重新实现逻辑或新增静态方法,这会增加冗余和不一致的风险。
为了说明背景,CPF(Cadastro de Pessoas Físicas) 是巴西的个人税号,是一个11位数,用于在金融和法律事务中进行身份确认。
下面是一个使用工具类验证CPF的示例。
public class CPFUtils {
public static boolean isValid(String cpf) {
// 验证CPF的有效性
return cpf != null && cpf.matches("\\d{11}") && validateDigits(cpf);
}
public static String format(String cpf) {
// 将CPF格式化为xxx.xxx.xxx-xx
return cpf.replaceAll("(\\d{3})(\\d{3})(\\d{3})(\\d{2})", "$1.$2.$3-$4");
}
private static boolean validateDigits(String cpf) {
// 验证CPF的校验位
return true; // 仅用于演示目的
}
}
这个类可能工作,但它有几个问题。
- 累积的责任性:验证、格式化和数字验证混杂在同一个类中。
- 缺乏领域概念:CPF被视为仅仅是字符串,而不是具有领域意义的对象(CPF可以考虑保留英文或提供明确解释)。
- 测试限制:在测试中模拟或修改静态方法的行为很麻烦。
我们可以创建一个值对象(Value Object)来表示CPF,作为领域的一部分,而不是使用工具类。
import java.util.Objects;
public class CPF {
private final String value;
public CPF(String value) {
if (!isValid(value)) {
throw new IllegalArgumentException("无效的CPF:" + value);
}
this.value = format(value);
}
private boolean isValid(String cpf) {
return cpf != null && cpf.matches("\\d{11}") && validateDigits(cpf);
}
private boolean validateDigits(String cpf) {
// 用于验证CPF的校验位
return true; // 仅为示例
}
private String 格式化(String cpf) {
return cpf.replaceAll("(\\d{3})(\\d{3})(\\d{3})(\\d{2})", "$1.$2.$3-$4");
}
public String 值() {
return value;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CPF cpf = (CPF) o;
return Objects.equals(this.value, cpf.value);
}
@Override
public int hashCode() {
return Objects.hash(this.value);
}
@Override
public String toString() {
return value;
}
}
在领域模型中使用 CPF
类
让我们将这个 CPF
类集成到一个领域模型中,比如 Person
类。
public class Person {
private String name;
private CPF cpf;
public Person(String name, String cpf) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("姓名不能为空或全部为空格");
}
this.name = name;
this.cpf = new CPF(cpf);
}
public String getName() {
return name;
}
public CPF getCpf() {
return cpf;
}
@Override
public String toString() {
return "Person{name='" + name + "', cpf=" + cpf + ' }';
}
}
示例使用。
public class Main {
public static void main(String[] args) {
try {
Person person = new Person("John Doe", "12345678909");
// 如果 person 对象实例化成功了,我们就可以确定 CPF 是有效的!
打印 person 对象;
} catch (IllegalArgumentException e) {
System.err.println("错: " + e.getMessage());
}
}
}
这种方法有什么好处?
- 封装与有效性保证:
CPF
对象在创建时保证有效。 - 有意义的领域内的表示:
Person
类更清晰地反映了领域概念。 - 更简洁的业务逻辑:验证逻辑封装在
CPF
类中,使Person
类可以专注于其角色。 - 易于维护:添加新的行为(例如,CPF 掩码)可以在
CPF
类中完成,而不会影响到代码的其他部分。
虽然工具类可能看起来是解决重复出现的问题的快速方案,但它们常常违背了如单一职责原则等基本的软件设计原则,并且与推荐的领域驱动设计实践相冲突。用这些对象来替换它们,可以使代码更干净、更易于测试,并且从而有助于项目长期更好的发展。
下次你想创建一个 XYZUtils
时,自己问一下:这会不会更适合作为一个领域对象呢?
欢迎留下您的评论或建议,也可以分享给其他可能从中受益的开发人员。
关注我看看更多有趣的内容
卢卡斯·费尔南德斯 — Medium在Medium上阅读卢卡斯·费尔南德斯的文章。他热爱软件工程、解决方案设计,并乐于分享……medium.com
➡️ 领英: 点击这里查看我的领英个人资料
共同学习,写下你的评论
评论加载中...
作者其他优质文章