编程原则的工程实践:从 KISS 到正交性
原则不是教条
几年前带一个校招生,他技术功底不错,学习能力也强,入职没多久就把《重构》和《代码整洁之道》翻了个遍。然后事情开始变得有趣起来。
他接手了一段业务代码,发现订单创建和退款创建里有一段相似的参数校验逻辑。他本能地觉得这违反了 DRY 原则,于是花了两天时间把这段逻辑抽成了一个通用的 ValidationEngine,支持规则配置、支持链式校验、支持自定义错误码映射。代码从 20 行变成了 200 行,引入了三个新类和一个配置文件。
上线后第二周,产品说退款的金额上限要从 10 万调到 50 万,但订单的不变。改这个需求本来只需要改一个数字,结果因为共用了 ValidationEngine,他不得不在通用逻辑里加了一个 if-else 分支来区分场景。再过两周,订单校验需要新增一个风控维度,退款不需要。通用引擎再加一个条件分支。三个月后,这个"消除重复"的引擎变成了一个没人敢碰的怪物。
这不是 DRY 原则的问题,而是对 DRY 原则的机械理解。 他看到了代码的重复,却没有看到两段代码背后代表的是两种不同的业务知识——它们今天碰巧相同,但明天一定会分道扬镳。
从那之后我经常跟团队说一句话:编程原则是路标,不是法律。路标告诉你大致方向,但前面是山路还是平路、要不要绕行、能不能抄近道,你得自己判断。更重要的是,这些原则之间经常互相矛盾——DRY 和 KISS 会打架,YAGNI 和开闭原则会冲突,单一职责拆到极致反而会让系统变得更难理解。真正的功力不在于背诵原则,而在于知道什么时候该用哪一条、什么时候该故意违反哪一条。
下面我按照主题把常见的编程原则分成几组,聊聊它们在真实工程中的样子。
做减法的原则:KISS、YAGNI 与做最简单能工作的事
这三条原则的精神内核是一致的:克制。在一个鼓励"多做"的工程文化里,"少做"反而是最难的事。
KISS:简单不是简陋
KISS(Keep It Simple, Stupid)大概是最容易被误解的原则之一。很多人把"简单"等同于"简陋"或"偷懒",觉得不用设计模式、不做分层就是 KISS。但恰恰相反,真正的简单是深思熟虑后的结果,不是偷工减料。 简单意味着更少的活动部件、更少的状态、更少的分支路径。做到这一点通常比做一个复杂方案更难。
一个真实的例子。我在某个项目里见过一个"特性开关"系统,它的设计是这样的:
// 过度设计的特性开关
public class FeatureToggleEngine {
private PluginRegistry pluginRegistry;
private RuleEvaluator ruleEvaluator;
private ConfigurationProvider configProvider;
private FeatureToggleCache cache;
public boolean isEnabled(String feature, UserContext ctx) {
Rule rule = configProvider.loadRule(feature);
List<Plugin> plugins = pluginRegistry.getPlugins(feature);
EvaluationContext evalCtx = buildContext(ctx, plugins);
return ruleEvaluator.evaluate(rule, evalCtx);
}
}
这套东西支持插件式规则引擎、支持动态加载、支持用户维度的灰度。听起来很专业,但实际上整个系统一共只有 6 个特性开关,而且全都是简单的开/关控制,没有任何灰度需求。真正需要的是什么?
// 够用的特性开关
public class FeatureFlags {
private static final Map<String, Boolean> FLAGS = Map.of(
"new_checkout_flow", true,
"dark_mode", false,
"v2_search", true
);
public static boolean isEnabled(String feature) {
return FLAGS.getOrDefault(feature, false);
}
}
六行代码解决问题。如果未来真的需要灰度能力,到那时候再加也不迟。
Saint-Exupery 说过一句话,我觉得是对 KISS 最好的注解:"Perfection is achieved not when there is nothing more to add, but when there is nothing left to take away." 完美不是无可增加,而是无可删减。
YAGNI:你不会需要它的
YAGNI(You Ain't Gonna Need It)是 KISS 的延伸,专门针对"未来需求"的过度设计。
我经历过一个教科书级别的反面案例。某个团队在项目初期就搭建了一套完整的数据库抽象层,理由是"将来可能要从 MySQL 迁移到 PostgreSQL"。这套抽象层包括自定义的 Query Builder、方言转换器、连接池代理——完全屏蔽了底层数据库的差异。
结果怎样?三年过去了,数据库迁移从未发生。但这套抽象层带来的问题倒是实实在在:没法用 MySQL 的特定优化(比如 INSERT ... ON DUPLICATE KEY UPDATE)、调试 SQL 性能问题时要穿透三层封装才能看到真正执行的语句、ORM 的延迟加载在抽象层下面出现了诡异的行为。团队花了大量时间维护一个解决"想象中的问题"的系统,同时不断给"真实存在的问题"打补丁。
YAGNI 的核心洞察是:写出来的每一行代码都是负债,不是资产。 代码要维护、要测试、要被后来的人理解。如果这些代码解决的是一个不存在的问题,那它就是纯粹的负债。
Make It Work, Make It Right, Make It Fast
这条原则规定了正确的工作顺序,而大多数人搞错了顺序——尤其是第三步。
先让它跑起来,用最直接的方式实现功能,验证逻辑是对的。然后让它正确,重构代码结构,处理边界情况,写测试。最后让它快——但只有在性能确实是问题的时候。
我见过太多提前优化的案例。有一次一个同事花了整整一周优化一个数据处理循环,用上了位运算、对象池、手写内存管理,把循环体的执行时间从 200 微秒降到了 15 微秒。代码从清晰易读变成了只有他自己能看懂的"性能艺术品"。
后来做压测发现,瓶颈根本不在这个循环上,而在数据库的一个全表扫描查询。那个查询耗时 800 毫秒,加个索引就降到了 5 毫秒。他花一周优化的那个循环,在整个请求链路里占比不到 0.002%。
过早优化是万恶之源,这话 Knuth 说过。但比这更重要的是:在优化之前,先量化。 不要凭直觉猜瓶颈在哪里,用 profiler 去测。人类的直觉在性能问题上出奇地不靠谱。
消除重复的原则:DRY 与它的陷阱
DRY(Don't Repeat Yourself)大概是被引用最多、同时也被误用最多的编程原则。
DRY 的真正含义
DRY 的原始定义来自 Andrew Hunt 和 David Thomas 的《程序员修炼之道》:"Every piece of knowledge must have a single, unambiguous, authoritative representation within a system." 注意,这里说的是 knowledge(知识),不是 code(代码)。
两段代码看起来一模一样,但它们可能代表的是完全不同的知识。 回到文章开头的例子:
// 订单金额校验
if (amount > 0 && amount <= 100000) {
processOrder(amount);
}
// 退款金额校验
if (amount > 0 && amount <= 100000) {
processRefund(amount);
}
这两段代码形式上完全相同,但它们背后的业务知识是不同的。订单金额的上限是 10 万,退款金额的上限也恰好是 10 万——但这是两条独立的业务规则。订单的上限可能因为合规要求调整到 50 万,退款的上限可能因为风控策略降低到 5 万。如果你把它们抽成一个 validateAmount() 函数,当业务需要差异化调整时,你就会陷入尴尬。
错误的 DRY 是消除代码的重复;正确的 DRY 是消除知识的重复。
什么时候该用 DRY
那什么情况下应该消除重复呢?当两段代码不仅看起来一样,而且改变的原因也一样的时候。
比如,系统中有三个地方都在做用户手机号的格式校验:注册、修改个人信息、绑定手机号。这三个场景的校验规则来自同一条业务规则——"合法的中国大陆手机号格式"。如果手机号规则变了(比如新增了某个号段),这三个地方必须同步修改。这才是真正的知识重复,应该抽成一个共享函数。
// 这是正确的 DRY:一条业务规则,一个权威来源
public class PhoneValidator {
private static final Pattern CN_MOBILE =
Pattern.compile("^1[3-9]\\d{9}$");
public static boolean isValid(String phone) {
return phone != null && CN_MOBILE.matcher(phone).matches();
}
}
判断的标准不是"代码像不像",而是"改变的原因是不是同一个"。 如果两段代码因为不同的业务需求而可能各自演化,即使今天一模一样,也不要合并。如果两段代码永远因为同一个原因而同步变化,即使今天看起来有细微差异,也应该统一。
划边界的原则:关注分离、单一职责与正交性
这三条原则本质上在讨论同一个问题:怎么画线。在代码中画出清晰的边界,让每一部分各管各的,互不干扰。
关注分离:一个方法不该知道太多事情
看一段在业务代码里极其常见的"全能方法":
public Response createOrder(HttpRequest request) {
// 1. 鉴权
String token = request.getHeader("Authorization");
User user = tokenService.verify(token);
if (user == null) return Response.unauthorized();
// 2. 参数解析与校验
OrderDTO dto = parseBody(request);
if (dto.getAmount() <= 0) return Response.badRequest("金额无效");
// 3. 业务逻辑
Order order = new Order(user.getId(), dto.getAmount());
order.applyDiscount(discountService.calculate(user));
orderRepository.save(order);
// 4. 发消息通知
kafkaTemplate.send("order-created", order.toEvent());
// 5. 构造响应
return Response.ok(order.toVO());
}
这个方法做了五件事:鉴权、参数校验、核心业务逻辑、消息发送、响应构造。任何一件事的改变都需要修改这个方法。鉴权方式从 JWT 换成 OAuth?改这个方法。消息中间件从 Kafka 换成 RocketMQ?改这个方法。响应格式要加个字段?还是改这个方法。
关注分离之后:
// Controller:只负责 HTTP 层的事务
@PostMapping("/orders")
public Response createOrder(@Authenticated User user,
@Valid OrderDTO dto) {
Order order = orderService.create(user, dto);
return Response.ok(order.toVO());
}
// Service:只负责核心业务逻辑
public Order create(User user, OrderDTO dto) {
Order order = new Order(user.getId(), dto.getAmount());
order.applyDiscount(discountService.calculate(user));
orderRepository.save(order);
eventPublisher.publish(new OrderCreatedEvent(order));
return order;
}
鉴权交给框架的拦截器,参数校验交给注解,消息发送抽象成事件发布。每一层只关心自己那一件事。改变鉴权方式不需要碰业务逻辑,改变消息中间件不需要碰 Controller。
单一职责:什么是"变化的原因"
单一职责原则(SRP)经常被简化为"一个类只做一件事",但 Robert Martin 的原始表述是:一个类应该只有一个变化的原因(reason to change)。
"变化的原因"是什么?不是技术上的分类,而是谁会要求你改这段代码。一个 UserService 如果同时处理用户的序列化格式和用户的业务校验规则,那它就有两个变化的原因:前端团队可能要求改序列化格式(比如从 XML 换成 JSON),业务团队可能要求改校验规则(比如新增实名认证)。这两个变化来自不同的利益相关方,进度不同、频率不同、测试方式也不同,它们不应该被塞在同一个类里互相影响。
正交性:被低估的核心原则
在我看来,正交性是所有设计原则中最值得反复强调的一个,但它很少被单独拿出来讨论。
正交性的意思是:系统中的一个维度发生变化时,不应该影响其他维度。 借用线性代数的概念——正交的向量互不干扰,改变一个方向上的分量不会影响另一个方向。
举一个具体的例子。假设你要把日志框架从 Log4j 换成 Logback,你需要改多少个文件?如果答案是"几百个业务类都要改",那说明你的日志使用和业务逻辑不是正交的——它们耦合在一起了。
非正交的设计:
// 业务代码直接依赖具体的日志实现
import org.apache.log4j.Logger;
public class OrderService {
private static final Logger log =
Logger.getLogger(OrderService.class);
public void create(Order order) {
log.info("创建订单: " + order.getId());
// ...业务逻辑
}
}
正交的设计:
// 业务代码依赖抽象
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class OrderService {
private static final Logger log =
LoggerFactory.getLogger(OrderService.class);
public void create(Order order) {
log.info("创建订单: {}", order.getId());
// ...业务逻辑
}
}
用 SLF4J 这样的门面之后,底层从 Log4j 切换到 Logback 只需要改 POM 依赖和一个配置文件,业务代码一行都不用动。这就是正交——日志实现这个维度的变化,不会波及到业务逻辑这个维度。
检验正交性的方法很简单:问自己"如果我要替换 X,需要改多少个与 X 无关的文件?" 如果答案不是"零"或"接近零",你的设计就有正交性问题。
把这个思路推广到 API 设计上。假设一个配置 API 是这样的:
// 非正交的 API:存储格式和业务语义耦合
config.setJsonProperty("order.maxRetry", "3");
调用方既要知道业务配置项的含义,又要知道底层是 JSON 存储。如果将来存储格式换成 YAML 或数据库,所有调用方都要改。正交的设计应该隐藏存储细节:
// 正交的 API:调用方不需要知道存储格式
config.set("order.maxRetry", 3);
存储格式是一个维度,业务配置是另一个维度,它们应该可以独立变化。
控制依赖的原则:最小耦合、迪米特法则与组合优于继承
前面说的是怎么划边界,这一组原则说的是划完边界之后,边界两侧怎么打交道。
最小耦合:依赖越少越好
在做架构评审时,我有一个简单的判断标准:打开一个 Service 类,数一下它的构造函数参数或注入的依赖有多少个。 如果超过 7 个,这个类几乎一定有问题。
我见过一个真实的 OrderService,它依赖了 15 个其他服务:UserService、ProductService、InventoryService、PricingService、DiscountService、PaymentService、LogisticsService、NotificationService、AuditService、RiskService、ConfigService、CacheService、MetricsService、ABTestService、FeatureFlagService。这意味着这 15 个服务中任何一个的接口变更,都可能导致 OrderService 需要修改。任何一个服务出故障,都可能导致订单创建失败。测试这个类需要 mock 15 个依赖。
耦合的代价不是线性增长的,而是组合爆炸。 15 个依赖意味着 15 个潜在的变更源、15 个潜在的故障点,以及它们之间可能产生的交互问题。
解决办法不是把 15 个依赖减少到 14 个,而是重新审视这个类的职责划分。一个需要 15 个依赖的类,几乎一定是承担了太多职责。把它拆成 3-4 个更小的服务,每个只依赖 3-4 个接口,整个系统的可维护性会有质的飞跃。
迪米特法则:不要和陌生人说话
迪米特法则(Law of Demeter)说的是:一个对象应该只和它的直接朋友交流,不应该和朋友的朋友交流。
看一个经典的"火车残骸"式代码:
// 坏:链式调用穿透了整个对象图
String zipCode = user.getAddress().getCity().getZipCode();
这行代码看起来简洁,但它把你的代码和 User、Address、City 三个类的内部结构绑死了。如果 Address 的结构变了(比如 City 不再是一个独立对象而是一个字符串),所有写了这种链式调用的地方都要改。
// 好:告诉对象做什么,而不是向对象要数据再自己做
String zipCode = user.getShippingZipCode();
这样 User 内部怎么组织 Address 和 City 的关系,是它自己的事。外部调用方只知道"我可以向 User 要一个邮编",不需要知道内部是 address.city.zipCode 还是 shippingInfo.postalCode。
迪米特法则的本质是信息隐藏:你不需要知道的结构细节,就不应该知道。 你知道得越多,你被耦合得就越深。
组合优于继承:继承是最强的耦合
在所有的代码关系中,继承是耦合最强的一种。子类和父类之间是白盒依赖——子类不仅依赖父类的接口,还依赖它的实现细节。父类改一个私有方法的行为,子类可能就炸了。
一个在业务系统里反复出现的陷阱:
// 第一版:看起来很合理
class User {
String name;
String email;
void login() { ... }
}
class VIPUser extends User {
int level;
double discount;
void login() {
super.login();
recordVIPLogin(); // VIP 登录有额外的积分逻辑
}
}
问题出在哪里?有一天 User 类的 login() 方法增加了一个返回值,或者加了一个参数,VIPUser 的覆写方法需要同步修改。更糟的是,如果产品说"用户可以同时是 VIP 用户和企业用户",你就陷入了 Java 的单继承困境——VIPEnterpriseUser 该继承谁?
用组合来解决:
class User {
String name;
String email;
private MembershipStrategy membership;
void login() {
// ...基础登录逻辑
membership.onLogin(this);
}
}
interface MembershipStrategy {
void onLogin(User user);
double getDiscount();
}
class VIPMembership implements MembershipStrategy {
public void onLogin(User user) { /* VIP 积分逻辑 */ }
public double getDiscount() { return 0.8; }
}
用户的会员类型变成了一个可替换的策略。VIP 和企业会员可以自由组合,新增一种会员类型不需要修改 User 类。这就是组合的力量:用"有一个"代替"是一个",用接口契约代替实现继承。
这里顺便提一句里氏替换原则(LSP):如果你的子类不能在所有场景下替代父类使用而不出问题,那你就不应该用继承。很多继承关系在设计时看起来合理(VIPUser "是一个" User),但在实际使用中会违反 LSP——比如 VIPUser 的某些方法有额外的前置条件,或者返回值的语义发生了变化。当你发现继承关系让你不舒服的时候,通常意味着应该用组合。
面向未来的原则:开闭原则、为维护者编码与童子军规则
前面的原则关注的是代码的结构,这一组关注的是时间——代码要活很多年,而写它的人可能早就不在了。
开闭原则:对扩展开放,对修改关闭
开闭原则(OCP)不要停留在抽象层面来理解它,看一个具体场景。
一个支付系统,第一版支持支付宝和微信支付:
// 不符合开闭原则:每加一个支付方式都要改这个方法
public void pay(String channel, BigDecimal amount) {
switch (channel) {
case "alipay":
// 支付宝逻辑
break;
case "wechat":
// 微信支付逻辑
break;
// 加 Apple Pay?在这里加 case...
}
}
每次新增一个支付渠道,你都需要修改这个方法。修改意味着引入 bug 的可能,意味着需要重新测试所有已有的支付逻辑,意味着合并冲突(如果有两个人同时在加不同的支付方式)。
符合开闭原则的做法:
public interface PaymentGateway {
boolean supports(String channel);
PayResult pay(BigDecimal amount, PayContext ctx);
}
// 新增 Apple Pay:写一个新类,不碰任何已有代码
public class ApplePayGateway implements PaymentGateway {
public boolean supports(String channel) {
return "apple_pay".equals(channel);
}
public PayResult pay(BigDecimal amount, PayContext ctx) {
// Apple Pay 的具体逻辑
}
}
新增支付渠道变成了新增一个类,完全不需要修改已有的代码。已有的支付宝和微信的逻辑不会因为你加了 Apple Pay 而受到任何影响。
开闭原则的实现手段是抽象。 通过定义稳定的接口,让新的变化以"扩展"的形式加入系统,而不是以"修改"的形式侵入已有代码。
为维护者编码
有一句在程序员社区流传很广的话:"Always code as if the person who ends up maintaining your code is a violent psychopath who knows where you live." 翻译过来就是"写代码时要假设维护者是个知道你家住址的暴脾气"。虽然夸张,但道理是真的。
我接手过一个内部系统的维护工作,原作者已经离职。打开代码的那一刻,我体会到了什么叫"技术暴力"。
变量名全是单字母加数字:a1、b2、tmp3。一个核心方法有 300 行,中间穿插着三层嵌套的 try-catch。最致命的是一段位运算逻辑——用 6 个 bit 分别存储了 6 种业务状态,通过位与和位或来判断组合状态。没有一行注释解释为什么要用位运算(估计是为了"性能"),也没有注释解释每个 bit 代表什么状态。我花了三天才搞懂这 20 行代码在做什么,又花了两天写测试确认我的理解是对的。
这段代码在性能上确实更快——大概快了 0.01 毫秒。但它让每一个后来的维护者多花几天时间来理解。这种"聪明"的代码是真正的技术债。
为维护者编码的核心原则:
- 变量名和函数名要表达意图,不要表达实现
- 非显而易见的逻辑必须写注释解释"为什么",而不是"做什么"
- 不要为了微不足道的性能提升牺牲可读性
- 如果你觉得一段代码需要注释才能看懂,先考虑能不能重写得不需要注释
童子军规则
Robert Martin 提出的童子军规则很简单:离开时让营地比来时更干净。 映射到代码上就是:每次你碰一个文件,离开时让它比你打开时更好一点——改一个命名、删一段死代码、补一句注释。
但这条规则有一个重要的约束:范围要合理。 我见过有人在一个修复线上 bug 的 PR 里顺手重构了整个模块。review 的人分不清哪些改动是修 bug、哪些是重构,测试团队也不知道回归测试的范围应该多大。结果修 bug 的 PR 反复被打回,原本一天能上线的修复拖了一周。
童子军规则的正确姿势:小步改进,和功能改动明确分开。 如果重构范围比较大,单独开一个 PR。如果是顺手改的小优化,确保 reviewer 能一眼分辨出来。
原则之间的冲突与权衡
如果前面每一条原则都读进去了,你应该已经隐约感觉到一个问题:这些原则之间是会打架的。 这不是理论上的可能性,而是每天都在发生的事情。
DRY vs KISS
两个 API 接口的处理逻辑有 70% 相似。DRY 说:把共同部分抽出来。KISS 说:抽象会增加复杂度。
如果你抽一个共享的 handler,就需要用参数和条件分支来处理那 30% 的差异。结果这个"统一"的 handler 里充满了 if (isTypeA) 的判断,比两个独立的 handler 更难理解,也更容易在修改一个场景时不小心影响另一个。
// DRY 的做法:抽一个共享 handler
public Response handleRequest(Request req, boolean isTypeA) {
// 公共逻辑...
if (isTypeA) {
// A 的特殊逻辑
} else {
// B 的特殊逻辑
}
// 更多公共逻辑...
if (isTypeA) {
// A 的另一段特殊逻辑
}
// ...
}
// KISS 的做法:各写各的,接受重复
public Response handleTypeA(Request req) {
// A 的完整逻辑,简单直接
}
public Response handleTypeB(Request req) {
// B 的完整逻辑,简单直接
}
在很多情况下,后者是更好的选择。 两个独立的方法各自 50 行,比一个 80 行但充满条件分支的"统一方法"更容易理解和维护。这里 KISS 赢了 DRY。
但如果那 70% 的相似逻辑来自同一条业务规则(比如都是同一套风控校验流程),那就应该抽出来——因为这时候 DRY 保护的是知识的一致性,一旦风控规则变了,你不想记住"有两个地方要改"。
判断标准:重复的是"知识"还是"代码"。如果是知识,DRY 优先;如果只是代码碰巧像,KISS 优先。
YAGNI vs 开闭原则
YAGNI 说"不要为未来设计",开闭原则说"要方便未来扩展"。这两者怎么调和?
答案是:不要构建功能,但要留下接缝。
以前面支付系统的例子来说,YAGNI 告诉你不要在第一版就建一个"通用支付网关框架",支持二十种支付方式的动态注册和热加载。但开闭原则告诉你,至少把支付逻辑藏在一个接口后面,这样将来加新的支付方式时不需要改已有的代码。
定义一个接口的成本很低,但它留下的扩展空间很大。 这就是 YAGNI 和 OCP 的平衡点:不构建不需要的实现,但留下简单的扩展接口。接口是轻量的——它不包含实现,不需要维护逻辑,不会引入 bug——但它给未来的变化留了一扇门。
SRP vs KISS
单一职责拆到极致会怎样?一个简单的用户注册流程被拆成 UserInputValidator、UserFactory、UserPersistenceService、WelcomeEmailSender、RegistrationEventPublisher、RegistrationOrchestrator 六个类。每个类确实只有一个职责,非常"干净"。
但当一个新来的开发者要理解注册流程时,他需要在六个文件之间跳转,理解它们的协作关系,才能拼凑出完整的图景。如果把核心逻辑放在一个 RegistrationService 里,可能只有 80 行代码,但读一个文件就能理解整个流程。
SRP 的目标是让变化可控,但如果拆得太细导致理解成本剧增,就需要退一步。 实践中的经验法则是:如果两个职责几乎总是同时变化、几乎总是被同一个人修改、几乎总是在同一个上下文中被讨论,那就没必要强行拆开。"一个变化的原因"不是一个精确的定义,它需要你对业务有判断力才能合理运用。
结语
写了这么多原则和案例,最后想说的反而是最简单的一句话:好的代码不是最聪明的代码,而是下一个人能看懂、能改动、能扩展而不心惊胆战的代码。
编程原则是前人踩过无数坑之后留下的路标。KISS 告诉你克制,DRY 告诉你统一知识,关注分离告诉你画好边界,迪米特法则告诉你管好依赖,开闭原则告诉你面向未来。但这些路标指的是方向,不是精确坐标。你不能闭着眼睛沿着路标走,因为路标之间有时候指向不同的方向——DRY 和 KISS 打架、YAGNI 和 OCP 拉锯、SRP 和可理解性博弈。
真正的工程判断力,不是记住所有原则然后逐条执行,而是在具体场景下感知到原则之间的张力,然后做出一个"足够好"的决定。 这种判断力没有捷径,只能通过写代码、犯错误、读别人的代码、维护别人的系统,一点一点积累。
如果非要给出一条元原则的话,我会说:用最简单的方式解决当下的问题,同时不给下一个人制造麻烦。 大多数时候,遵循这一条就够了。