1. 为什么想写这个系列
1.1 高级程序员的真实困境
工作到第五到第八年的程序员,普遍会遇到一个尴尬的局面:功能照样能写,性能问题也能排查,框架用得很溜,但每次代码评审的时候,总被问住几个问题——"为什么这个模块要这么拆?""这个依赖方向是不是反了?""如果下周要加一个新业务类型,你这里要改几个文件?"
这些问题,不是靠背八股文能回答的。leetcode刷得再熟,也解决不了系统腐化的问题。我见过太多业务代码写得飞快、一到扩展就摔跤的团队,也见过不少个人能力很强、但合在一起互相踩脚的工程组织。
这个系列想聊的,不是某个具体框架的用法,也不是某种语言特性,而是比它们更底层、更难掌握的东西:软件设计中的取舍原则。为什么叫原则而不叫规范?因为规范是死的,原则是活的。死的东西学得再熟练,换个场景就失效;活的东西能让你在没见过的情况下,也敢做判断。
高级程序员和初中级程序员的分水岭,表面上看是经验多少、技术广度,实际上是对"为什么"这件事的追问深度。初级程序员关心怎么调通接口,中级程序员开始琢磨怎么设计接口,高级程序员则要回答"这个系统为什么长成这个样子,以及它下一步应该长成什么样子"。这个系列想解决的,就是这个核心问题。
1.2 从"能实现"到"会设计"的转折点
我自己的转折点发生在一次极度痛苦的线上故障之后。当年接手一个交易系统,状态机逻辑分散在十几个类里,每个类几百上千行,看起来每个类单独读都能理解,但改一个状态流转要动四个文件,测试要跑半天,还要提心吊胆。那次故障的根因,就是两个人分别改了各自负责的类,都以为自己对状态的理解是全局正确的,结果产生了不一致。
事后做复盘的时候,技术负责人没有说"以后多写测试""代码评审仔细点"这种话,而是拿出了一套分层设计文档,把状态流转抽成独立模块,用接口明确边界,几周之后,同样的需求改动成本下降了不止一个量级。
那件事给我的冲击特别大。过去我以为写代码拼的是大脑算力——谁能在脑子里装下更多细节,谁就能写出更复杂的系统。但那次之后我明白,复杂系统的设计恰恰相反:不是装下更多细节,而是把细节隔离在外面,让每个局部都足够简单。这才是高级程序员的思维方式。
从"能实现"到"会设计",中间隔着的是对软件本质的理解。软件的作用是什么?是满足业务需求。但业务需求一定是会变的,软件真正要应对的不是当下的需求,而是需求的变化。能实现当下需求的代码,不一定能优雅地应对变化;而会设计的人,从一开始就知道变化是不可避免的,所以他会刻意地留出空间、确定边界、控制依赖。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 设计原则到底是什么
2.1 不是规则手册,而是风险意识
很多人一听到"设计原则",第一反应是背出来:单一职责、开闭原则、里氏替换、接口隔离、依赖倒置……背得出名字,考试能拿分,但回到真实代码里依然不会用。问题出在哪里?出在把原则当成了教条。
原则不是法律条款,不会因为你违反了一次就报警。原则更像是在无数错误中沉淀出来的风险指数——沿着这个方向走,你会大概率遇到哪些坑;在某个路口换条路,又会有什么代价。你要做的不是"永不违反",而是"知道自己在违反什么,并且清楚代价是什么"。
举个例子:单一职责原则说一个类应该只有一个引起它变化的原因。但实际业务里,完全做到单一职责很容易走向极端,就是类爆炸——一个两三百行的类拆成二十个三五行的小类,每个都做一点点事,调用关系弯弯绕绕,可读性反而崩了。所以反倒是写出又臭又长的类,再把它们内部逻辑理清楚,比强行拆出十几个类更实用。
那为什么还要学原则?因为如果你根本没有原则意识,你会靠直觉去写,而直觉在大多数场景下都是走最短路径——把方法写长、把类写大、把逻辑塞进去,结果就是第一个能跑就行。而懂原则的人,至少会停下来想一想:这个设计将来要面对什么变化?我是现在拆还是以后拆?拆了之后依赖关系怎么理?这就是"风险意识"和"无意识"之间的区别。
2.2 好的设计到底长什么样
好的设计是可以被量化的。不靠感觉,看四个维度:可读性、可维护性、可扩展性、可测试性。这四者相互关联,但又有各自独立的衡量方式。
可读性衡量的是"新来的人需要多久能上手",可维护性是"改一个需求要动多少文件",可扩展性是"加一类新业务,原有代码要改多少",可测试性是"业务逻辑在多大程度上能和外部依赖解耦"。这四个指标达标的设计,即使代码风格粗糙一点,也很难坏到哪里去;这四个指标崩掉的设计,即使代码写得花团锦簇,也只是漂亮的废墟。
我见过很多团队做代码评审,全程在讨论命名和格式问题,这是舍本逐末。命名当然重要,但它不是设计问题的核心。你应该把评审焦点放在依赖关系上,放在边界上,放在变化应对上。一个命名糟糕但边界清晰的设计,至少还能改;反过来,一个命名精美但耦合成一团的设计,是动都不敢动的。
2.3 识别你正在踩的坑
有几种信号出现时,你就该意识到当前设计出了问题。
第一种信号:改动扩散。加一个字段,改了三个层级的七八个文件;改一个状态,要用 grep 搜索结果再逐个确认。这就是耦合过重的表现。
第二种信号:回归恐惧。每次发版前,全组人都有一个心理负担:这次改动会不会弄挂什么?回归测试列表越来越长,但没有人敢划掉任何一项。
第三种信号:平级复制。新需求跟旧需求只是稍微有些不同,于是你把旧代码复制一份,改几个参数放进去。复制粘贴本身并不是罪大恶极,但如果复制之后,你不敢把公共部分抽出来,那说明公共逻辑的边界设计是有问题的。
第四种信号:依赖倒挂。一个领域服务类,包含一个 DAO 字段、一个 MQ 字段、一个外部 HttpClient 字段,里面的领域方法根本没有自己的纯逻辑,全在调别人。这个类实质上就是个薄壳协调器,领域逻辑全都散落在外部依赖里。
这些信号在图纸阶段是看不到的,它们藏在代码的演进过程中。设计原则的学习目标不是让你一次做对,而是让你在这些信号出现时,有能力判断严重程度,知道优先处理哪个,以及用什么方式处理。
3. 这个系列准备聊什么
3.1 模块边界的本质拆解
第一篇想聊的是模块边界。模块这个词很泛,在公司层面叫服务,在系统层面叫组件,在代码层面叫包或者类。但不管粒度如何,定义模块边界要回答的都是同一个问题:什么东西应该放进同一块,什么东西应该隔离开?
我的基本观点:边界跟随变化频率。变化频率相同的代码放在一起,变化频率不同的代码拆开。这个原则在公司架构上体现在微服务划分上,在代码层面体现在包结构和类设计上。说到底,不是因为"业务领域"天然就分成订单模块和库存模块所以你这么拆,而是因为订单规则的变化和库存规则的变化往往是独立的,所以拆开以后,彼此之间的影响才能被隔离。
这一篇会聊清楚,怎么判断"变化的频率",以及怎么利用这个判断来画模块边界。也会聊到那些经常被忽略的坏味道,比如不同业务场景共用一张大表导致的变化互相踩踏,或者为了所谓"复用"强行把所有逻辑塞进一个基类导致的灾难。
3.2 依赖方向的控制策略
第二篇是依赖关系。代码写多了你会发现,系统的复杂度不取决于代码行数,而取决于依赖关系——一个模块依赖了几个外部模块,外部模块又依赖了几个别的模块,这种传递性的依赖是复杂度的大头。
依赖问题的核心不是"减少依赖的数量",而是"控制依赖的方向"。有些依赖是合理的,比如基础设施模块被业务模块依赖,这没问题。要命的是核心业务模块反过来依赖基础设施,或者两个业务模块之间存在循环依赖,或者顶层模块依赖底层模块的实现细节。
控制依赖方向最有效的手段是依赖倒置:抽象不应该依赖细节,细节应该依赖抽象。但这句话落地的时候很容易走偏——不是每个地方都值得引入抽象层,抽象也分好坏。这一篇会聊如何判断"什么值得抽象""什么不值得",以及如何用接口正向管理依赖,而不是让依赖在代码里横冲直撞。
3.3 接口设计的深层逻辑
第三篇讲接口设计。这里说的接口不是 API 文档里的 HTTP 端点,而是代码层面上对外开放的"契约"——方法的签名、类的公开方法、模块提供的功能边界。高级程序员应该具备一种能力:光看一个接口的签名,就能大致推断出背后实现的复杂度、健壮性和可测试性。
好的接口应该做到三件事:意图清晰(看到名字就知道是干什么的)、使用安全(很难用错)、变化隔离(接口内部的改动不扩散到调用方)。这三件事做起来都不容易。
关于意图清晰,最直接的手段是命名,但命名只是最低要求,真正好的接口设计是用类型系统来约束使用方式,让错误的使用方式在编译期就暴露出来,而不是等到运行时。使用安全则涉及到参数约束、返回值设计、异常抛出策略,都是细节活儿。变化隔离是接口设计的重点——接口暴露的单位应该是"稳定概念",而不是"内部实现"。
3.4 变化应对与扩展预留
第四篇聊的是"扩展"这个永恒的话题。所有设计原则,最终目的都是为了应对变化。但这里有个常见的误区:为了应对所有可能的变化,把系统设计得过于复杂,什么策略模式、工厂模式、观察者模式全用上,结果变化还没来,复杂度先把你压垮了。
这就是过度设计。识别它跟适度预留之间的分界线,是一门大学问。我的经验是两个判断标准:变化是不是真的会来(去找业务方或产品确认),以及变化来的代价有多大(如果来了之后重构成本很高,现在就值得做防护;如果重构很容易,那就先不过度设计)。
另外,应对变化的手段不只是"抽象",还有"契约"和"配置"。有些变化适合用策略模式抽象出来,有些变化应该用版本化接口来应对,有些变化甚至只需要一份配置文件就能解决。手段选错了,也会带来新的复杂度。
4. 写在序言的最后
4.1 我对高级程序员的定义
参与这个系列讨论的人,我不在乎职级,不需要名片上的 Title 有多响,我只看三件事:第一,你写过不少代码,你被线上故障坑过;第二,你不满足于"功能能跑",你开始追问"怎么设计才能长期稳定";第三,你在团队里有一定话语权,你的设计选择会影响别人。
如果你符合这三条,那你就是我说的高级程序员——哪怕你的职级卡在某个位置很久了。反过来,如果一个人 P7/P8 的头衔挂得亮闪闪,但写代码全靠复制粘贴和面向搜索引擎编程,他其实不属于我讨论的读者。
这个系列不追求大而全,也不打算把设计模式、设计原则、架构风格全部讲一遍——那是教科书做的事。我只挑那些我在实践中最常遇到、最容易踩坑、判断起来最模糊的问题来聊,把我们踩过的坑、总结的规律、修正后的理解,用最直接的方式写出来。
4.2 设计原则是"判断力"而非"知识"
最后我想强调一个观点:设计原则本质上是判断力,而不是知识。知识可以靠读书记下来,判断力必须靠实践练出来。但高质量实践的前提,是有好的参照系——你需要知道"好的设计"长什么样子,需要知道"别人在类似场景下怎么取舍",才能在轮到自己做判断的时候,不靠掷骰子决定。
这个系列就是一个参照系。我会尽量把原则讲得具体,落得了地,不只是抽象口号。比如讲高内聚时,我会给出一套具体的方法来帮你识别内聚是否合理;讲开闭原则时,我会分析几种常见的扩展方式各自的适用场景和代价。你不需要照搬我说的每一句话,但你可以拿我的判断标准当一面镜子,比对一下自己手头的设计,看看哪些地方其实早有预兆。
另外,写这个系列我还有个私心:每写一篇,我自己也要重新梳理一遍实践中的经验。写东西是检验理解的最好方式——如果你不能把一个原则讲得让刚入行的人也能大概听懂,那你自己很可能还没真正理解它。所以这个系列既是写给你们,也是写给我自己。
4.3 我在实战中最大的感受
说句掏心窝的话,做了这么多年设计与架构,我最大的感受就八个字:设计是取舍,不是炫技。
一个设计好不好,不取决于它用了多少模式、多新潮的技术栈、多复杂的抽象。最顶级的架构师和高级程序员之间的差距,往往就是取舍的清晰度——知道什么必须坚持,什么可以妥协,什么要果断放弃。这种判断力没有捷径,只能在一次次失败、重写、复盘里磨出来。
最后分享一个我每次做设计决策前都要问自己的问题:如果半年后有一个新需求进来,这个设计会让我开香槟,还是让我拍大腿?想清楚这个问题,很多纠结的设计难题其实就不那么难了。
