函数设计(上):短小、只做一件事、每个函数一个抽象层级
好,咱们今天聊函数设计。
说实话,我见过太多代码,一个函数能写两三百行。你想想看,这种函数谁敢动?改一行代码,心里都得哆嗦半天。我自己刚入行那会儿也干过这种事——一个函数里塞了登录验证、数据查询、格式转换、日志记录,甚至还顺带发了封邮件。结果呢?后来需求一变,我盯着那个函数看了整整一下午,愣是不敢下手。
所以今天咱们先聊三个最核心的原则:短小、只做一件事、每个函数一个抽象层级。这三个原则,说白了就是函数设计的基石。
1. 短小:函数该有多短?
我个人习惯,一个函数尽量控制在 20 行以内。超过 30 行,我就开始警觉了。
你可能会问:20 行够用吗?我告诉你,够。而且往往更清晰。
核心观点:函数越短,越容易理解,越容易测试,越容易复用。
来看个例子。这是我在一个老项目中看到的代码:
public void processOrder(Order order) {
// 验证订单
if (order == null) {
throw new IllegalArgumentException("订单不能为空");
}
if (order.getItems() == null || order.getItems().isEmpty()) {
throw new IllegalArgumentException("订单项不能为空");
}
for (Item item : order.getItems()) {
if (item.getPrice() <= 0) {
throw new IllegalArgumentException("价格不合法");
}
}
// 计算总价
double total = 0;
for (Item item : order.getItems()) {
total += item.getPrice() * item.getQuantity();
}
// 应用折扣
if (total > 1000) {
total = total * 0.9;
}
// 保存订单
saveToDatabase(order, total);
// 发送通知
sendEmail(order.getCustomerEmail(), "订单已处理");
}
这个函数干了多少事?验证、计算、折扣、保存、发邮件。五个职责挤在一起。我建议拆成这样:
public void processOrder(Order order) {
validateOrder(order);
double total = calculateTotal(order);
double finalTotal = applyDiscount(total);
saveOrder(order, finalTotal);
notifyCustomer(order);
}
private void validateOrder(Order order) {
if (order == null) throw new IllegalArgumentException("订单不能为空");
if (order.getItems().isEmpty()) throw new IllegalArgumentException("订单项不能为空");
for (Item item : order.getItems()) {
if (item.getPrice() <= 0) throw new IllegalArgumentException("价格不合法");
}
}
private double calculateTotal(Order order) {
double total = 0;
for (Item item : order.getItems()) {
total += item.getPrice() * item.getQuantity();
}
return total;
}
private double applyDiscount(double total) {
return total > 1000 ? total * 0.9 : total;
}
private void saveOrder(Order order, double total) {
// 保存逻辑
}
private void notifyCustomer(Order order) {
// 通知逻辑
}
每个函数都短小精悍。你一眼就能看出它在干什么。测试也好写——每个函数单独测,不用再担心牵一发动全身。
小技巧:写函数的时候,试着用一句话描述它的功能。如果这句话里出现了「并且」、「然后」、「同时」这些词,说明它该拆了。
2. 只做一件事:怎么判断「一件事」?
这个原则听起来简单,做起来其实挺难的。什么叫「一件事」?
我有个判断方法:如果这个函数还能再拆出一个有意义的函数,那它就不止做了一件事。
举个例子。下面这个函数,你觉得它做了几件事?
public void renderPage(PageData pageData) {
String html = pageData.getHtmlContent();
html = html.replace("{{title}}", pageData.getTitle());
html = html.replace("{{body}}", pageData.getBody());
if (pageData.isLoggedIn()) {
html = html.replace("{{login}}", "欢迎回来," + pageData.getUserName());
} else {
html = html.replace("{{login}}", "<a href='/login'>登录</a>");
}
response.write(html);
}
表面上看,它只做了一件事:渲染页面。但仔细看,它其实包含了模板替换、登录状态判断、HTML 生成。这些完全可以拆开。
我建议这样:
public void renderPage(PageData pageData) {
String html = pageData.getHtmlContent();
html = replaceTemplateVariables(html, pageData);
html = renderLoginSection(html, pageData);
response.write(html);
}
private String replaceTemplateVariables(String html, PageData pageData) {
html = html.replace("{{title}}", pageData.getTitle());
html = html.replace("{{body}}", pageData.getBody());
return html;
}
private String renderLoginSection(String html, PageData pageData) {
String loginHtml = pageData.isLoggedIn()
? "欢迎回来," + pageData.getUserName()
: "<a href='/login'>登录</a>";
return html.replace("{{login}}", loginHtml);
}
你看,每个函数只关心自己的事。renderLoginSection 只负责生成登录区域的 HTML,它不需要知道模板里还有 title 和 body。这就是「只做一件事」。
注意:「一件事」不是指一行代码。一个函数里可以有多个步骤,只要这些步骤都在同一个抽象层级上,并且共同完成一个目标,那就算「一件事」。
3. 每个函数一个抽象层级
这个原则,我是在重构一个支付系统时才真正理解的。
当时有个函数,里面既有高层逻辑(比如「验证支付信息」),又有底层细节(比如「把字符串转成 BigDecimal」)。读起来特别累,因为你得在高层和底层之间来回切换思维。
抽象层级,说白了就是:一个函数里的代码,应该处于同一个「距离细节的远近」上。
来看个例子:
// 不好的写法:抽象层级混乱
public void processPayment(Payment payment) {
// 高层:验证支付
if (payment.getAmount() <= 0) {
throw new IllegalArgumentException("金额必须大于0");
}
// 底层:字符串处理
String amountStr = String.format("%.2f", payment.getAmount());
String maskedCard = payment.getCardNumber().replaceAll("\\d(?=\\d{4})", "*");
// 高层:调用支付网关
PaymentGateway gateway = new PaymentGateway();
gateway.charge(payment);
// 底层:日志格式
System.out.println("支付完成:" + maskedCard + " 金额:" + amountStr);
}
这个函数里,验证支付是高层,字符串格式化是底层,调用支付网关又是高层,打印日志又是底层。读起来像在坐过山车。
正确的做法是:
public void processPayment(Payment payment) {
validatePayment(payment);
chargePayment(payment);
logPayment(payment);
}
private void validatePayment(Payment payment) {
if (payment.getAmount() <= 0) {
throw new IllegalArgumentException("金额必须大于0");
}
}
private void chargePayment(Payment payment) {
PaymentGateway gateway = new PaymentGateway();
gateway.charge(payment);
}
private void logPayment(Payment payment) {
String amountStr = String.format("%.2f", payment.getAmount());
String maskedCard = payment.getCardNumber().replaceAll("\\d(?=\\d{4})", "*");
System.out.println("支付完成:" + maskedCard + " 金额:" + amountStr);
}
现在,processPayment 只包含高层逻辑。底层细节被隐藏到了各自的函数里。你读 processPayment 的时候,只需要关心「验证、支付、记录」这三个步骤,不用去想字符串怎么格式化、卡号怎么脱敏。
判断方法:读一个函数时,如果某行代码让你想「这行代码在干什么?」,那它很可能处于不同的抽象层级。
知识体系总览
下面这张图,把今天讲的三个原则串起来了。你可以看到它们之间的关系:
避坑指南
聊了这么多原则,我再分享几个实际中容易踩的坑:
- 过度拆分:我曾经见过有人把一行代码也拆成一个函数。没必要。拆分的粒度应该是「有意义的逻辑单元」,不是「每行一个函数」。
- 命名掩盖复杂:函数名叫 processAndValidateAndSave,一看就知道它做了三件事。别用这种名字来掩盖函数过长的本质。
- 抽象层级跳跃:我见过一个函数,前半部分在拼 SQL,后半部分在渲染 HTML。这种代码读起来特别累,因为你的思维要在数据库和前端之间来回切换。
我的习惯:写函数的时候,先写注释,再写代码。注释写的是「这个函数要做什么」,代码写的是「怎么做」。如果注释里出现了「并且」,那就拆。
嗯,今天咱们聊了三个原则。短小让你一眼看懂,只做一件事让你不用猜,一个抽象层级让你不用来回切换思维。这三个原则,说白了就是让读代码的人舒服。你想想看,代码写出来是给人读的,顺便让机器执行一下。所以,让读代码的人舒服,才是真正的目标。
下次写函数的时候,不妨试试:写完之后,数一数行数,看看能不能再拆,检查一下抽象层级是否一致。养成习惯之后,你会发现代码质量提升得特别快。
公众号:蓝海资料掘金营,微信deep3321