从 LeetCode 到工程实现:对简单题的思维差异
在毕业前的一年多时间里,我保持了每天刷 LeetCode 的习惯。 通过每日一题,我熟悉了各种类型的算法题,也积累了不少常见的解题套路。那段时间,算法部分在笔试面试中几乎没有成为我的短板🤗
不过入职之后,我逐渐停止了算法题的日常练习。更多精力放在公司项目的开发、后端技术栈的掌握,以及工程设计思维的积累上。 相比单纯的刷题,这部分的学习让我对系统设计、职责划分与代码抽象有了更深入的理解。
题目背景
今天心血来潮做了一道每日一题:
题目要求设计一个 Bank 类,实现最基本的账户操作:
转账:
transfer(from, to, money)存款:
deposit(account, money)取款:
withdraw(account, money)
只要账户合法、余额充足,就返回 true;否则返回 false。
看似简单,但它恰好是一道能体现算法实现与工程思维差异的好例子😎
题目实现(算法角度)
在算法题语境下,实现非常直接,只需要对账户合法性和余额进行校验即可:
class Bank {
private final long[] balance;
public Bank(long[] balance) {
this.balance = balance;
}
public boolean transfer(int account1, int account2, long money) {
if (account1 <= 0 || account2 <= 0 || account1 > balance.length || account2 > balance.length) {
return false;
}
if (balance[account1 - 1] < money) {
return false;
}
balance[account1 - 1] -= money;
balance[account2 - 1] += money;
return true;
}
public boolean deposit(int account, long money) {
if (account <= 0 || account > balance.length) {
return false;
}
balance[account - 1] += money;
return true;
}
public boolean withdraw(int account, long money) {
if (account <= 0 || account > balance.length) {
return false;
}
if (balance[account - 1] < money) {
return false;
}
balance[account - 1] -= money;
return true;
}
}这段代码清晰、正确,也足以通过所有测试用例。 但从工程角度看,这样的实现还不够「优雅」——它只是对规则的直接翻译,而没有形成结构化的抽象。
工程视角的思考
在实际业务中,方法的语义往往需要更精确的抽象。 例如这里的 account - 1 虽然逻辑正确,但语义上不够清晰,最好通过一个专门的转换函数进行封装:
private int convertToAccountIndex(int account) {
return account - 1;
}接着我们会发现,账户合法性校验与索引转换其实是强相关的逻辑。 因此,更合理的做法是在同一个函数中处理验证与转换:
private int convertToAccountIndex(int account) {
if (account <= 0 || account > balance.length) {
throw new IllegalArgumentException();
}
return account - 1;
}此时问题出现了:题目要求返回 boolean,而我们抛出了异常。 那该如何在不破坏题目结构的前提下,优雅地表达异常状态呢?
抽取状态逻辑:用包装器实现职责分离
题目中每个方法返回的 boolean 值,其实并没有具体的业务语义。 它更像是一个状态标识:表示操作是否执行成功。
既然如此,我们可以将状态逻辑抽离出来,用统一的包装器来执行带异常的业务逻辑:
private static boolean execute(Runnable action) {
try {
action.run();
return true;
} catch (IllegalArgumentException e) {
return false;
}
}这样,每个业务方法就只需关注核心逻辑,不再关心异常捕获或状态转换:
class Bank {
private final long[] balance;
public Bank(long[] balance) {
this.balance = balance;
}
private int convertToAccountIndex(int account) {
if (account <= 0 || account > balance.length) {
throw new IllegalArgumentException();
}
return account - 1;
}
public boolean transfer(int account1, int account2, long money) {
return execute(() -> {
int a1 = convertToAccountIndex(account1);
int a2 = convertToAccountIndex(account2);
if (balance[a1] < money) throw new IllegalArgumentException();
balance[a1] -= money;
balance[a2] += money;
});
}
public boolean deposit(int account, long money) {
return execute(() -> {
int idx = convertToAccountIndex(account);
balance[idx] += money;
});
}
public boolean withdraw(int account, long money) {
return execute(() -> {
int idx = convertToAccountIndex(account);
if (balance[idx] < money) throw new IllegalArgumentException();
balance[idx] -= money;
});
}
private static boolean execute(Runnable action) {
try {
action.run();
return true;
} catch (IllegalArgumentException e) {
return false;
}
}
}这样的写法既满足了题目的要求,又让逻辑职责更加清晰, 工程上也具备了更好的可扩展性与一致性。
工程演化的方向
这种思路在真实项目中可以继续演化成更高层次的抽象:
从简单的 boolean 返回值,到最终在系统层统一控制逻辑, 这其实是一条完整的工程抽象演化路径🥰
小结
LeetCode 关注的是算法正确性,而工程实现关注的是结构与语义的清晰性。 当我们从「写对」转向「写好」,就意味着要更关注职责划分、异常传播、逻辑复用这些工程核心问题。
有趣的是,有时一道人畜无害的简单题, 也能成为重新审视编程方式的契机 —— 从“把问题解出来”,到“把逻辑设计好”, 这是从算法思维到工程思维的自然过渡🤗