搜 索

从入门到放弃:如何优雅地写出让同事看不懂的代码(Java 篇)

  • 6阅读
  • 2025年05月24日
  • 0评论
首页 / 编程 / 正文

前言:什么叫「优雅地让人看不懂」?

让人看不懂的代码分两种境界:

quadrantChart x-axis "低技术含量" --> "高技术含量" y-axis "低迷惑程度" --> "高迷惑程度" quadrant-1 "天花板(本文目标)" quadrant-2 "炫技型选手" quadrant-3 "新手村居民" quadrant-4 "纯纯的折磨" "拼音变量名": [0.1, 0.3] "超长函数": [0.15, 0.4] "随机魔法数字": [0.2, 0.5] "过度设计模式": [0.7, 0.75] "函数式地狱": [0.75, 0.8] "元编程黑魔法": [0.9, 0.85] "自创DSL": [0.85, 0.9] "反射套反射": [0.95, 0.95]

第四象限(低技术、高迷惑)是纯折磨,没品位。比如全用拼音缩写,yhje(应还金额)、dqzt(当前状态),这种只是懒,称不上艺术。

第一象限(高技术、高迷惑)才是我们的目标。看起来高深莫测,同事 Review 时频频点头,实际上根本没看懂,最后无奈点了 Approve。这才叫优雅

下面,我们分七个层次,手把手教你登上这座山峰。


第一层:命名的艺术——让人猜到死

1.1 单字母变量,但要用得有气质

菜鸟:

int i = 0;  // 太普通了,没有灵魂

高手:

// 用单字母,但要选那种有「多种解读可能性」的字母
// l(小写L)和 1(数字一)长得一模一样,在某些字体下完全无法区分
int l = list.size();
int ll = l - 1;
int lll = ll / 2;
// 读者:这三个变量是什么关系?l 是啥?ll 是啥?lll 又是啥?
// 你(微笑):二分查找的左右指针和中间值

1.2 名字要有「多义性」

public class DataManager {
    
    // data 是什么 data?Manager 管的是什么?没有人知道。
    // 这是一种哲学上的开放性。
    private Object data;
    
    // process 了什么?返回什么?参数是什么意思?
    // 完全的自由,完全的混沌。
    public Object process(Object input, boolean flag, int mode) {
        // flag 是 true 还是 false 的时候做什么,只有上帝知道
        if (flag) {
            return mode == 1 ? handle(input) : transform(input);
        }
        return validate(input) ? enrich(input) : input;
    }
}

1.3 终极命名技巧:用正确的词表达相反的含义

// 经典操作:isNotInvalid、hasNoAbsence、disableNotification
// 三重否定,人类大脑短路专用

public boolean isNotInvalidUser(User user) {
    // 这个方法返回 true 代表用户「有效」
    // 但你得先在脑子里转三圈才能反应过来
    return !user.isInvalid();
}

// 更进阶:命名和逻辑反着来
public boolean isActive(Account account) {
    // 看方法名:是不是 active 的?
    // 看实现:判断的是 FROZEN 状态
    // 读者:所以返回 true 表示 active 还是 frozen?
    return account.getStatus() == Status.FROZEN;
}

命名混淆难度进阶路线:

graph LR A[单字母变量 i j k] --> B[无意义缩写 tmp val obj] B --> C[误导性命名 isActive 实判冻结] C --> D[多重否定 isNotInvalidUser] D --> E[哲学命名 DataManager ProcessHelper] E --> F[🏆 终极形态 AbstractGenericBaseProcessor DefaultImpl] style F fill:#gold

第二层:注释的逆向运用

普通人觉得注释要解释代码。大师知道注释的真正用途是干扰视线。

2.1 注释和代码说不同的话

/**
 * 计算用户积分
 * @param userId 用户ID
 * @return 积分值
 */
public BigDecimal calculateFee(String accountId, String currency, LocalDate date) {
    // 方法名是 calculateFee,注释说 calculatePoints
    // 参数名和 JavaDoc 完全对不上
    // 读者:这到底是算费率还是算积分?
    // 答案:手续费。但你需要先通读完整个方法才能知道。
}

2.2 用注释遮住真正的问题

public void processTransaction(Transaction tx) {
    // TODO: 这里逻辑有点复杂,以后优化
    // (注:这个 TODO 是 2019 年写的)
    if (tx.getAmount().compareTo(BigDecimal.ZERO) > 0) {
        if (tx.getType() != null) {
            if (tx.getType().equals("CREDIT")) {
                if (tx.getStatus() != null && !tx.getStatus().equals("CANCELLED")) {
                    // 核心业务逻辑藏在第五层 if 里
                    // 任何人想改这里都要先读懂这座金字塔
                    doActualWork(tx);
                }
            }
        }
    }
}

2.3 注释比代码还长,但没说任何有用的事

/**
 * 此方法用于处理相关业务逻辑的核心处理流程。
 * 本方法是整个系统中非常重要的一个方法,承担着
 * 关键的业务处理职责。调用此方法时,请确保传入
 * 正确的参数。关于参数的具体要求,请参考相关文档。
 * 关于相关文档的位置,请联系相关负责人。
 * 
 * 注意:此方法有副作用。(具体是什么副作用,不告诉你)
 * 注意:线程不安全。(在哪里不安全,不告诉你)
 * 注意:在某些情况下可能抛出异常。(什么情况,不告诉你)
 *
 * @author 张三(已离职)
 * @since 不知道
 * @version 可能是 3,也可能是 4
 */
public void handle(Object obj) {
    // ...
}

第三层:设计模式——用锤子看什么都是钉子

设计模式是让代码「看起来」有高度的最佳工具。关键技巧:所有问题,先套模式。

3.1 工厂的工厂的工厂

// 需求:根据支付类型,创建不同的处理器
// 正常人:一个 switch 就完事了
// 大师:

// 先来一个抽象工厂
public interface ProcessorAbstractFactory {
    ProcessorFactory createProcessorFactory();
}

// 再来工厂的工厂
public interface ProcessorFactory {
    ProcessorCreator getProcessorCreator(String type);
}

// 再来创建者
public interface ProcessorCreator {
    Processor createProcessor(ProcessorConfig config);
}

// 再来构建者
public class ProcessorConfig {
    public static Builder builder() { return new Builder(); }
    public static class Builder {
        // 15 个字段,全都有 setter
        // 全部都是 optional,但不传某几个会 NPE
        // 文档里没有说明哪几个必传
    }
}

// 最终调用方式(就是为了创建一个对象):
Processor processor = processorAbstractFactoryLocator
    .getFactory(FactoryType.DEFAULT)
    .createProcessorFactory()
    .getProcessorCreator(paymentType)
    .createProcessor(
        ProcessorConfig.builder()
            .withMode(Mode.STANDARD)
            .withTimeout(5000)
            .build()
    );

3.2 责任链套策略套观察者

graph TD REQ[一个简单的转账请求] --> C1 subgraph 责任链 C1[Handler1 校验金额] --> C2[Handler2 校验账户] --> C3[Handler3 校验限额] --> C4[Handler4 校验时间窗口] --> C5[Handler5 校验黑名单] end C5 --> SM[策略管理器 决定用哪个策略] subgraph 策略模式 SM --> S1[Strategy: 普通转账] SM --> S2[Strategy: 快捷转账] SM --> S3[Strategy: 跨行转账] end S1 & S2 & S3 --> EM[事件发布器] subgraph 观察者模式 EM --> O1[Observer: 审计日志] EM --> O2[Observer: 通知服务] EM --> O3[Observer: 风控系统] EM --> O4[Observer: 积分系统] EM --> O5[Observer: 报表系统] end O1 & O2 & O3 & O4 & O5 --> DONE[转账完成 恭喜你读到了这里] style REQ fill:#ff6b6b style DONE fill:#51cf66

实现代码当然对应有 47 个类文件。新同事上手周期:两周。


第四层:函数式编程——Stream 地狱欢迎你

Java 8 的 Stream API 是上天赐给我们的礼物——不是因为它好用,而是因为它可以把三行代码写成永远不换行的一行

4.1 基础款:能链就链,绝不换行

// 需求:找出活跃用户中,本月消费超过 1000 的前 10 名,按消费降序

// 普通人写法(太好读了,不行):
List<User> activeUsers = users.stream()
    .filter(u -> u.isActive())
    .collect(Collectors.toList());

// ... 中间几步 ...

// 大师写法:
return users.stream().filter(u->u.isActive()&&u.getLastLoginDate().isAfter(LocalDate.now().minusMonths(1))).map(u->new UserConsumptionDto(u.getId(),u.getName(),transactionRepository.findByUserId(u.getId()).stream().filter(t->t.getDate().getMonth()==LocalDate.now().getMonth()&&t.getStatus()==TransactionStatus.SUCCESS).map(Transaction::getAmount).reduce(BigDecimal.ZERO,BigDecimal::add))).filter(dto->dto.getTotalConsumption().compareTo(new BigDecimal("1000"))>0).sorted(Comparator.comparing(UserConsumptionDto::getTotalConsumption).reversed()).limit(10).collect(Collectors.toList());
// 一行。永远一行。格式化是懦夫的选择。

4.2 进阶款:嵌套 Stream,套娃到底

// 计算所有账户的分组统计(如果你能一遍读懂,请联系我,我要拜你为师)
Map<String, Map<String, DoubleSummaryStatistics>> result = accounts.stream()
    .collect(Collectors.groupingBy(
        Account::getCurrency,
        Collectors.groupingBy(
            a -> a.getTransactions().stream()
                .filter(t -> t.getAmount().compareTo(BigDecimal.TEN) > 0)
                .findFirst()
                .map(t -> t.getType().name())
                .orElse("UNKNOWN"),
            Collectors.summarizingDouble(
                a -> a.getTransactions().stream()
                    .filter(t -> t.getStatus() == TransactionStatus.SUCCESS)
                    .mapToDouble(t -> t.getAmount().doubleValue())
                    .sum()
            )
        )
    ));

4.3 终极款:Optional 俄罗斯套娃

// 获取用户的主账户的最近一笔交易的商户名称
// 正常逻辑:三行加 null 检查
// 大师逻辑:

String merchantName = Optional.ofNullable(userId)
    .map(userRepository::findById)
    .flatMap(Function.identity())
    .map(User::getAccounts)
    .filter(accounts -> !accounts.isEmpty())
    .map(accounts -> accounts.stream()
        .filter(Account::isPrimary)
        .findFirst()
        .orElseGet(() -> accounts.get(0)))
    .map(Account::getTransactions)
    .filter(txs -> !txs.isEmpty())
    .map(txs -> txs.stream()
        .max(Comparator.comparing(Transaction::getCreatedAt))
        .orElse(null))
    .map(Transaction::getMerchant)
    .map(Merchant::getName)
    .orElse("Unknown");

// 效果:代码「函数式」,「优雅」,「现代」
// 代价:没有人能在 debug 时设断点,因为没有变量

第五层:泛型与反射——黑魔法真正开始

5.1 泛型上界下界全套上

// 一个「灵活」的工具方法
// 功能:复制集合中的元素
// 正常人:Collections.copy()
// 大师:

public static <T, R extends T, E extends Collection<? super R>, 
               S extends Collection<? extends T>> 
    E flexibleCopy(S source, Supplier<E> collectionFactory, 
                   Function<? super T, ? extends R> mapper,
                   Predicate<? super T> filter) {
    return source.stream()
        .filter(filter)
        .map(mapper)
        .collect(Collectors.toCollection(collectionFactory));
}

// 调用方式(祝你类型推断成功):
List<SpecialAccount> result = flexibleCopy(
    accounts,
    ArrayList::new,
    a -> (SpecialAccount) enrichAccount(a),
    a -> a.getBalance().compareTo(BigDecimal.ZERO) > 0
);
// IDE 可能飘红,可能不飘红,取决于月相。

5.2 反射套反射,动态代理加持

@SuppressWarnings("unchecked")  // 消音警告,让问题消失在看不见的地方
public class MagicInvoker {

    // 动态调用任意对象的任意方法,参数类型自动推断
    // 功能强大,出了问题 Stack Trace 深达 40 层
    public static <T> T invoke(Object target, String methodName, Object... args) {
        try {
            Class<?>[] paramTypes = Arrays.stream(args)
                .map(arg -> arg == null ? Object.class : arg.getClass())
                .toArray(Class[]::new);
            
            // 这里用了递归反射:反射去找方法,方法里可能又调用了反射
            Method method = findMethod(target.getClass(), methodName, paramTypes);
            method.setAccessible(true);  // 访问控制?不存在的。
            
            return (T) method.invoke(target, args);
        } catch (Exception e) {
            // 把所有异常都包一层,让 Stack Trace 更加迷幻
            throw new RuntimeException("Magic invocation failed on " + 
                target.getClass().getSimpleName() + "#" + methodName, e);
        }
    }

    // 出了空指针异常,你会看到这样的堆栈:
    // at MagicInvoker.invoke(MagicInvoker.java:23)
    // at MagicInvoker$$EnhancerByCGLIB$$a1b2c3.invoke(<generated>)
    // at ProxyChain$DynamicProxyHandler.handle(ProxyChain.java:67)
    // at ProxyChain$$FastClassByCGLIB$$d4e5f6.invoke(<generated>)
    // at ... (还有 35 行)
    // Caused by: NullPointerException at YourActualCode.java:10
}

异常追踪难度对比:

graph TD subgraph 普通代码的异常 E1[NullPointerException] E1 --> |Caused by| L1[YourCode.java line 42] L1 --> |一眼定位| FIX1[✅ 3分钟修完] end subgraph 反射套反射的异常 E2[RuntimeException: Magic invocation failed] E2 --> |Caused by| R1[InvocationTargetException] R1 --> |Caused by| R2[UndeclaredThrowableException] R2 --> |Caused by| R3[ReflectiveOperationException] R3 --> |Caused by| R4[IllegalAccessException] R4 --> |Caused by| R5[NullPointerException] R5 --> |at| L2[YourCode.java line 42] L2 --> |经过40层StackTrace| FIX2[😭 3小时后放弃] end style FIX1 fill:#51cf66 style FIX2 fill:#ff6b6b

第六层:过度抽象——把简单问题复杂化

6.1 为一个 if 语句建立完整的抽象体系

// 需求:判断金额是否大于零

// 普通人(1行):
if (amount.compareTo(BigDecimal.ZERO) > 0) { ... }

// 大师(1个接口 + 3个类 + 1个枚举 + 工厂 + Spring Bean):

// Step 1: 定义规则接口
public interface BusinessRule<T> {
    RuleResult evaluate(T context);
}

// Step 2: 抽象基类(当然要有)
public abstract class AbstractBusinessRule<T> implements BusinessRule<T> {
    protected abstract boolean doEvaluate(T context);
    
    @Override
    public RuleResult evaluate(T context) {
        boolean result = doEvaluate(context);
        return new RuleResult(result, result ? null : getFailureMessage());
    }
    
    protected abstract String getFailureMessage();
}

// Step 3: 具体规则实现
@Component("positiveAmountRule")
public class PositiveAmountRule extends AbstractBusinessRule<AmountContext> {
    @Override
    protected boolean doEvaluate(AmountContext ctx) {
        return ctx.getAmount().compareTo(BigDecimal.ZERO) > 0;
    }
    
    @Override
    protected String getFailureMessage() {
        return "Amount must be positive";
    }
}

// Step 4: 规则引擎(当然要有引擎)
@Service
public class RuleEngine {
    private final Map<String, BusinessRule> rules;
    
    public <T> RuleResult execute(String ruleName, T context) {
        return Optional.ofNullable(rules.get(ruleName))
            .map(rule -> rule.evaluate(context))
            .orElseThrow(() -> new RuleNotFoundException(ruleName));
    }
}

// Step 5: 调用方
RuleResult result = ruleEngine.execute(
    "positiveAmountRule", 
    new AmountContext(amount)
);
if (!result.isPassed()) throw new BusinessException(result.getMessage());

// 以上代码完美替代了:
// if (amount.compareTo(BigDecimal.ZERO) <= 0) throw new BusinessException("...");
// 类文件数量:1 → 7
// 代码行数:1 → 80
// 可维护性:你觉得呢?

6.2 把配置写成 DSL

// 与其把业务规则写死,不如自己发明一门语言
// 这样不仅别人看不懂,连你自己三个月后也看不懂

// 自制规则 DSL
TransferRule rule = RuleBuilder
    .when(ctx -> ctx.getAmount().compareTo(new BigDecimal("10000")) > 0)
        .and(ctx -> ctx.getSender().getRiskLevel() != RiskLevel.LOW)
        .or(ctx -> ctx.getReceiver().getCountry().equals("XX"))
    .then(Action.REQUIRE_APPROVAL)
        .withPriority(Priority.HIGH)
        .withTimeout(Duration.ofHours(24))
        .withEscalation(EscalationPolicy.builder()
            .after(Duration.ofHours(4))
            .notifyRole(Role.COMPLIANCE_OFFICER)
            .withMessage(MessageTemplate.PENDING_REVIEW)
            .build())
    .otherwise(Action.AUTO_APPROVE)
    .build();

第七层:线程与并发——通往不稳定的终点

7.1 自制线程池,参数全靠感觉

// 为什么用 JDK 提供的线程池?自己写多有成就感!
public class MySpecialExecutor {
    
    // 核心线程数:服务器 CPU 核数 × 一个神秘系数
    // 这个系数是某个前同事在 2021 年的某个深夜调出来的
    // 他已经离职了,没人知道为什么是 2.7
    private static final int CORE_POOL_SIZE = Runtime.getRuntime()
        .availableProcessors() * 2 + (int)(Math.random() * 3);  // 加点随机性,保持神秘
    
    // 队列大小:Integer.MAX_VALUE,内存不够了再说
    private final ThreadPoolExecutor executor = new ThreadPoolExecutor(
        CORE_POOL_SIZE,
        CORE_POOL_SIZE * 10,
        60L, TimeUnit.SECONDS,
        new LinkedBlockingQueue<>(Integer.MAX_VALUE),  // OOM 预约入口
        r -> {
            Thread t = new Thread(r);
            t.setName("my-special-thread-" + System.nanoTime()); // 每个线程名都是唯一的!没法过滤!
            return t;
        },
        (r, e) -> {
            // 拒绝策略:默默地把任务丢掉,不抛异常,不记日志
            // 功能悄无声息地消失,运维人员永远不会知道发生了什么
        }
    );
}

7.2 双重检查锁——但少检查一步

public class SingletonService {
    
    // 注意:没有 volatile
    // 在某些 JVM 实现和 CPU 架构下,这里会出现指令重排序
    // 导致另一个线程拿到未初始化完成的对象
    // 这个 bug 只在生产环境高并发下偶发
    // 而且复现概率极低,每次排查都找不到原因
    private static SingletonService instance;  // ← 少了 volatile
    
    public static SingletonService getInstance() {
        if (instance == null) {
            synchronized (SingletonService.class) {
                if (instance == null) {
                    instance = new SingletonService();
                    // new 操作分三步:分配内存 → 初始化对象 → 赋值引用
                    // JVM 可能把第二步和第三步重排
                    // 另一个线程可能在对象初始化完成前就拿到了引用
                    // 然后调用未初始化的字段
                    // 然后 NPE,时机全靠缘分
                }
            }
        }
        return instance;
    }
}

并发 Bug 的生命周期:

graph LR W[写代码 看起来没问题] --> D[本地测试 完全正常] D --> R[Code Review 同事没看出来] R --> P[上线 运行正常] P --> M[某个深夜 偶发 NPE] M --> I[排查三小时 无法复现] I --> C[关闭工单 标注偶发问题] C --> P P --> M M --> I I --> C C --> Q[季度故障复盘 这个问题又出现了] Q --> FIX[终于找到 volatile 加上去] FIX --> DONE[✅ 一个字符解决 三个月的困扰] style M fill:#ff6b6b style Q fill:#ff6b6b style DONE fill:#51cf66

彩蛋:代码审查时的防御话术

写完迷幻代码,还需要在 Code Review 时守住阵地:

同事的质疑你的回应
「这个变量名是什么意思?」「这是领域驱动设计里的通用语言,你读一下 DDD 那本书就懂了。」
「为什么不直接用 if-else?」「这样扩展性更好,遵循开闭原则,符合 SOLID 设计思想。」
「这个 Stream 太难读了。」「你对函数式编程可能还不太熟悉,这是声明式的写法,比命令式更优雅。」
「为什么要用反射?」「低耦合,高内聚。我们需要在运行时保持灵活性。」
「这个泛型边界我看不懂。」「类型安全,编译期错误好过运行期错误,对吧?」
「注释和代码不一致。」「注释描述的是意图,代码描述的是实现,它们本来就是不同层面的东西。」
「你这里有个潜在的并发问题。」「我们的业务场景下,这个代码路径不会并发执行。」(迷之自信)

总结:这是一篇反讽文

如果你读到这里还没意识到这篇文章在讽刺什么,那你可能需要再看一遍。

真正好的代码应该是:

graph LR GOOD[好代码的特征] GOOD --> G1[任何人读三遍能懂] GOOD --> G2[命名即文档] GOOD --> G3[函数只做一件事] GOOD --> G4[错误处理清晰] GOOD --> G5[测试覆盖关键路径] GOOD --> G6[简单解决简单问题] BAD[坏代码的特征] BAD --> B1[只有作者能懂,三个月后作者也不懂] BAD --> B2[命名反映实现细节而不是业务含义] BAD --> B3[一个函数做一百件事] BAD --> B4[异常被吞掉或包成谜] BAD --> B5[没有测试,我自己测过的] BAD --> B6[用复杂解法解决简单问题] style GOOD fill:#51cf66 style BAD fill:#ff6b6b

「Any fool can write code that a computer can understand. Good programmers write code that humans can understand.」
— Martin Fowler

任何傻瓜都能写出机器能理解的代码。优秀的程序员写出人类能理解的代码。


本文所有「反面教材」,都曾以不同形式出现在真实的生产代码中。这不是编的。这是见过的。

如果你在本文中看到了自己写的代码的影子——恭喜,你有进步空间。

如果你完全没看到自己的影子——要么你真的写得很好,要么你还没意识到问题在哪里。

能写出让同事看不懂的代码,是本事。能克制住不这样写,才是修行。


评论区
暂无评论
avatar