news 2026/10/15 22:02:17

编程概念如何塑造清晰代码结构:封装、抽象与依赖治理实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
编程概念如何塑造清晰代码结构:封装、抽象与依赖治理实战

1. 从概念到结构:先说清楚这次要聊什么

我最近在整理一个模拟项目X的时候,翻出了三年前写的一段核心逻辑,差点没认出来那是自己写的——模块边界糊成了一团,一个类里塞了五种职责,接口设计得像是随手拍的。那个模块当时确实能跑,但后面每加一个功能,我都得在调用关系里翻半天。最要命的是,每次改动都得重新读一遍整个流程,因为代码结构完全没有传达出它背后的编程概念。

这就是我想在这篇笔记里认真聊的事:Programming Concepts 和 CodeStructure 从来不是两门课,而是同一件事的两面。概念是你脑子里的模型,结构是落到代码里的形状。很多人学了各种编程概念,问了就是"了解",但写出来的代码还是一团乱麻,因为概念没有转化成具体的结构决策。我理解的"Programming Concepts & CodeStructure 3"这个系列主题,核心就是探讨这种转化过程——当概念落到实处,代码结构会怎么变,反过来结构又会怎么塑造你对概念的理解。

这篇内容适合三类人:一是刚接触设计原则、想把代码整理清楚的新手,二是写过半年以上代码、觉得"代码能跑就行"开始不满足的进阶者,三是想系统梳理自己项目结构方法论、准备带人或者做重构的老人。我不打算堆一堆术语,就讲我在真实项目里的拆分过程、踩过的坑、以及现在回看时的复盘。

2. 隐藏在代码结构背后的三个编程概念

2.1 封装:不是"私有字段"就算封装了

很多人一提封装,第一反应就是访问修饰符,私有、保护、公开一套流程安排得明明白白。但我在实际项目里发现,封装的核心从来不是你能不能访问某个字段,而是你需不需要知道这个东西存在。

拿我参与过的某个图像处理Demo来举例。最开始我们把像素数据的处理逻辑直接平铺在调用方里,大概长这样:

# 糟糕的版本:调用方被迫了解内部实现 def resize_image(image, target_width, target_height): pixels = image.decompress() # 调用方要知道有个解压步骤 new_pixels = [] for row in range(target_height): new_row = [] for col in range(target_width): # 这里有几百行插值算法 new_row.append(interpolate(pixels, col * image.width / target_width, row * image.height / target_height)) new_pixels.append(new_row) return compress_to_format(new_pixels, image.format)

这段问题最大的不是算法写得差,而是调用方被迫知道了"图片对象内部有一个压缩格式、有一个像素数据需要解压"这些细节。后面我们换存储格式的时候,所有调用方代码全部要跟着改。这就是封装失败的典型症状:知识泄漏。

后来我们把相关概念收拢成了一个专门的模块,对外只暴露"读像素""写像素"这样的操作,把格式、压缩、插值全部关在内部:

# 重构后:调用方只依赖稳定的接口 def resize_image(image, target_width, target_height): source = image.read_pixels() result = compute_resize(source, target_width, target_height) return image.create_from_pixels(result)

这个改动的好处,程序员同行一看就明白:换格式的时候,只需要改内部实现,所有调用方一行不用动。我在重构完第二天加一个"去除EXIF信息"的需求时,几乎没碰到原来的代码,这就是封装释放出来的自由度。

2.2 抽象层级:你的代码是不是在同一个平面上说话

第二个概念更隐蔽:抽象层级的一致性。我见过太多模块,一个方法里既有高层业务判断,又混着底层数据处理,读起来像一部电影一会儿拍政客谈判、一会儿怼着螺丝钉拍特写。

正确的做法是让每一层的代码只和同一抽象级别的对象交流。举一个订单处理的小例子:

# 抽象层级混乱:业务、数据、IO全混在一起 def process_order(order_id): conn = database.get_connection() row = conn.query("SELECT * FROM orders WHERE id = ?", order_id) user_row = conn.query("SELECT email FROM users WHERE id = ?", row.user_id) if row.status == "pending": message = f"您的订单{row.id}已确认" smtp.send(user_row.email, message) conn.execute("UPDATE orders SET status = 'confirmed' WHERE id = ?", order_id)

这个函数读起来非常累,因为它同时在处理数据库连接、SQL查询、业务状态判断、邮件发送、状态更新五件事。拆开之后,每一层各管一段:

def process_order(order_id): order = order_repository.find(order_id) if order.is_pending(): order.confirm() notification_service.notify_user(order.user_email(), "您的订单已确认") order_repository.save(order)

现在每一行都可以单独读、单独测试。order.is_pending()是业务概念,order_repository.find()是数据访问概念,notification_service是外部服务概念——它们各自站在自己的抽象平面上。

我在这上面吃过亏。有一阵子赶进度,图省事把三层逻辑写在一个方法里,确实两天就上线了。但后面每次需求变动,我都要把那个方法从头到尾读一遍,才能确定改动波及的范围。这个成本远比当时"省下的拆分时间"高得多。

2.3 组合:比继承更稳的结构连接方式

第三个概念是老生常谈的"组合优先于继承"。从我看到的实际项目来说,真正容易出问题的不是用了继承,而是为了让代码复用而强行制造继承关系。

举个例子,模拟项目X里有个日志模块需要把日志写到文件、写到数据库、还要通过网络发到远程服务。第一种方案是定义一个BaseLogger,然后让FileLogger、DatabaseLogger、NetworkLogger都继承它,共用里面通用的格式化逻辑。听起来很顺,但问题马上来了:如果文件版需要加一个"按天滚动"的逻辑,数据库版需要加"批量写入"的逻辑,你会发现基类开始变得臃肿,所有子类都被迫看到了自己不需要的东西。

改成组合之后,我们把"日志格式化"和"日志输出"拆成两个独立的概念,用组合的方式装起来:

class FileLogger: def __init__(self, formatter, file_path): self._formatter = formatter self._writer = FileWriter(file_path) def log(self, message): formatted = self._formatter.format(message) self._writer.write(formatted, rotation="daily")

每个部分只管一件事,需要新的组合方式时新建一个类,不需要动其他部分。这个概念看似简单,但它对代码结构的影响非常深远:整个系统的扩展点从"继承树的叶子"变成了"组合的接口",灵活度高了一个量级。

3. 模块拆分与依赖治理:一次真实重构的完整复盘

3.1 我遇到的循环依赖:症状与定位

前面讲概念,现在说一个具体的实战场景。某次模拟项目X要做功能扩展,我在改动之前先梳理了整个代码模块的依赖关系,结果吓了一跳:模块A调模块B,模块B调模块C,模块C又调模块A——形成了循环依赖。

这种结构平时看不出问题,因为Python或者Java这种语言在运行时并不强制你遵守依赖方向。但它的隐患非常大:任何一次改动,理论上都可能波及整个循环链路上的所有模块。而且因为是循环的,你很难单独测试其中任何一个模块,因为它们互相需要对方。

为了定位依赖关系,我写了一个简单的依赖扫描脚本,只统计import语句,然后输出模块之间的依赖方向。花了一个下午的时间,把整个项目的依赖图理清楚了。这不是什么高级工具,就是遍历代码文件、提取import行、建立关系列表,然后标注循环节点。工具不在多,在于你想清楚要问什么问题。

3.2 打破循环的第一步:找到稳定点

我处理循环依赖的办法,通常分三步。

第一步,找出循环链路上变化最慢、最稳定的模块。比如上层的领域模型接口,它本身几乎不依赖基础设施,改成什么样它都很少动。第二步,把双方都依赖但又不属于彼此的东西下沉到这个稳定模块里。第三步,通过依赖反转让上层模块依赖稳定接口,而不是依赖具体实现。

具体到当时那个项目,A和C之间互相需要的是一个"任务状态"的概念。A想要知道任务是不是暂停了,C需要根据任务类型决定处理策略。我把这个概念抽出来放进了一个新的基础模块,A和C都只依赖这个基础模块,循环链条直接断开。其实改动很简单,核心是一个保证稳定的接口。

这里有个我的心得:打破循环依赖,不是靠设计技巧,而是靠识别"概念归属"。你要问的不是"哪个模块能安排到哪层",而是"这个概念本质上属于谁"。任务状态本质上是领域概念,不属于A的实现细节,也不属于C的实现细节,它应该属于领域层。想清楚了这一点,代码结构自然就有了正确的形状。

3.3 分层结构与单向依赖的落地细节

重构之后,整个项目的依赖方向变成了一条单向链路:

  • 最底层是基础工具模块:没有业务含义,谁都能依赖
  • 再往上是领域模型模块:包含任务、订单这类核心概念,只依赖基础工具
  • 再往上是应用服务模块:负责协调领域模型完成具体用例,依赖领域模型
  • 最外层是接口和基础设施模块:包含数据库、消息队列、外部API等,依赖上面的所有层

这个结构看起来简单,但落地时有几个细节值得说一说。

一个细节是,基础设施层对领域模型的"反向依赖"必须用接口切断。比如领域模型需要存储订单,它定义了一个OrderRepository接口,但具体是谁用MySQL还是PostgreSQL实现,领域层根本不需要知道。这样测试的时候我可以直接在内存里实现这个接口,跑完就丢弃,速度飞快。

另一个细节是,依赖方向要在团队约定里明确写出来,而不是靠自觉。我们当时在工程文档里画了这样一张依赖图,依赖方向一旦画反了,代码评审的时候就会被提出来。别小看这种"笨功夫",依赖治理最难的不是改代码,是让所有人持续遵守。

还有一点:没有绝对的分层,只有相对的分层,过度追求"所有依赖都在物理文件层面严格的层级目录"会导致结构上的过度折腾。我见过一些项目搞了五层分层架构的文件目录,结果每个模块都定义了一堆"中转"对象,开发效率直线下降。分层是用来管理变化的方式,不是用来表演的行为艺术。

3.4 重构收益:一个两周后的验证

重构完成之后的两周,我接到一个需求:给"任务"增加一个"重新指派"的操作,要求通知相关的人。放在旧结构里,这个需求要同时改动调用链上至少三个模块,而且因为循环依赖,我还得额外小心别踩到其他逻辑。

但在新结构下,我只做了两件事:在领域模型里给Task增加一个reassign方法,然后在应用服务里调用它并发送通知。改动量大概只有三四个文件,而且每个文件的职责都很清楚。这就是依赖治理的红利——当结构正确的时候,每个新需求都像是往抽屉里放东西,而不是把整个屋子重新装修一遍。

4. 可测试性在悄悄塑造代码结构

4.1 测试难写是结构问题的信号

我判断一段代码结构好不好,有一个特别朴素的指标:看它好不好测。如果每次你想为某个模块写测试,都要先初始化数据库、再模拟外部接口、还得控制时间日期,那大概率是这个模块的抽象层级有问题,或者依赖没有切断。

之前接手某个图像处理Demo的时候,有个模块内部大量使用了全局配置和当前系统时间。我要测试一个"过期文件自动清理"的逻辑,为了构造"现在刚好过期"的情况,我不得不改系统时间,改完再改回来,测试跑得战战兢兢。后来我把"时间获取"抽成了一个接口,测试时注入一个可以手动推进的假时间,测试代码一下子就从"特技表演"变成了"按部就班"。

说到底,可测试性不是一个附加属性,它逼着你把代码结构改成依赖清晰的形状。如果一个类很难测,几乎可以肯定它承担了过多职责,或者它和外部世界的边界模糊了。反过来,当你开始为了测试而改造代码的时候,你就是在做正确的结构优化。

4.2 依赖注入:把变化变成插拔式

依赖注入这个概念很多人一学就会,一用就乱。乱在什么地方?乱在不知道什么时候该注入。

我自己的经验是:不要为了注入而注入。假如这个依赖从来没有可能被替代,注入它就是纯粹增加复杂度。真正值得注入的是那些会变的东西——数据库、时间、消息队列、外部服务。至于HashMap这种东西,你注入它干嘛呢?

我在模拟项目X的日志模块里就用了一个简单的依赖注入方案。日志写入的目标可能是文件、控制台,也可能是内存缓冲区用于测试。我把LogWriter设计成一个接口,然后在启动配置里决定实际传入哪个实现。生产上写文件,测试里写内存。代码结构因此没有改变一分一毫,只是把接口换了一个实现。

class LogService: def __init__(self, writer: LogWriter): self._writer = writer def log(self, message: str): self._writer.write(f"{datetime.now()}: {message}") # 生产环境 service = LogService(FileWriter("app.log")) # 测试环境 service = LogService(MemoryWriter())

这种模式最明显的收益是,测试一个复杂模块时根本不需要启动整个系统,构造两三个假对象就解决了。我后来写复杂功能时,会在写实现的同步就写好测试。测试代码跑起来有多顺,反过来就在告诉我接口设计得有多好。

4.3 测试驱动的结构约束

我听过一种说法:"测试是文档",不准确。更准确一点说,测试是架构的合同——它写好了每个模块对外部世界的期望。如果某个模块的测试文件里出现了大量对基础设施的mock,说明这个模块对基础设施的依赖太重了。

我在新项目中经常采用一个轻量的测试策略:核心逻辑全部写成纯函数或者极简类,不依赖任何IO,测试直接调用;涉及IO或者外部服务的模块,把它们包到薄薄的适配层里,适配层自己单独测试,甚至可以做到集成测试才真正接触外部系统。

这种策略要落地,要求模块边界的划分必须跟业务概念对齐。这个约束反过来逼着我重新审视每个类的职责边界。比如"订单价格计算"这个概念,纯粹输入输出,根本不需要知道数据库;而"订单持久化"这个概念,天然就是跟数据库绑一起的。两者天然要分开,测试驱动只是把这个道理推到了显眼的位置。

5. 让代码结构保持健康的日常操作

5.1 我常用的"结构体检"方法

代码结构跟身体健康一样,不能等病倒了再后悔。我每隔一段时间会对着自己的代码做一次"体检",方法很简单:

第一个指标是改动量。刚加完一个新功能,如果发现需要改动超过十个文件,而且其中有七八个只是顺带调整,那大概率是结构没有预留对扩展点。

第二个指标是文件长度。我一般会给文件和函数设置一个长度底线,超过阈值就要反省。别误会,长的文件不是绝对不能有,但大部分情况下长文件意味着混杂了多个概念。比如一个文件超过500行,我读它的时候通常要快速定位"这个文件到底干什么",找半天找不到重点,这种文件一般离拆分不远了。

第三个指标是依赖方向。我会定期用脚本扫一遍模块间的依赖,看有没有出现循环依赖、有没有出现底层模块依赖上层模块的情况。别等到重构时才看,日常就把这个当作视觉观察的一部分,往往在问题还小的时候就发现了。

第四个指标是注释率。如果一段代码需要一大段注释才能讲清楚它是干什么的,往往是因为这段代码自己不清楚自己干什么。注释应该解释"为什么",而不应该解释"是什么"。如果"是什么"需要注释来解释,通常就是命名和结构出了问题。

5.2 命名:结构对外的第一张脸

代码结构真正的评价标准里,命名其实是排在最前面的。结构可以是抽象的,但命名是实打实地刻在代码里的。你有没有见过变量叫a1、data2、temp的函数?这种命名方式对理解结构的伤害是巨大的。一个合理命名的过程,本质上是一次"概念再确认"的过程:你之所以能给它起个好名字,是因为你真正清楚它在这个系统里是什么角色。

我的经验是,当你突然发现一个类的名字起不出来或者起了好几个都不太满意的时候,通常说明这个类的职责本身就不清晰。比如你想叫OrderManager但又觉得什么都管,那你就该考虑它是不是该拆成OrderRepository、OrderValidator、OrderNotifier了。

我自己写代码的时候,会故意在写完第一个版本之后,把所有名字重新读一遍,把"听起来模糊"的改掉。这个习惯很简单,但对代码结构的影响特别大——因为好名字往往意味着边界更清楚,边界清楚了,结构自然不会散。

5.3 代码评审中如何考查结构

说到结构维护,最日常的场景其实是代码评审。我在评审别人代码的时候,通常不是只看实现对不对,而是会在心里问几个结构性问题。

第一个问题:这个改动所在的模块,它有没有变化的理由,跟其他模块的变化频率是不是趋同?如果发现一个"核心模块"频繁跟着外部接口改动,说明依赖方向可能反了,或者核心模块的吸收能力不够强。

第二个问题:改动是否需要外部方配合?如果增加一个功能,需要同时改数据库表、改接口协议、改前端结构,那么这个功能本身可能横跨了太多层。这时候要么这个功能应该作为独立的模块独立发展,要么现有的分层边界没有对齐业务边界。

第三个问题:测试代码的改动量是否合理。一个结构好的系统,加一个新功能通常意味着新增测试,而不是大规模修改测试。如果某个改动引得一个测试文件里大面积修改,那通常要么是测试写得太脆弱(耦合了具体实现细节),要么是系统本身结构不对,一个改动牵一发动全身。

这些问题不是一个形式,而是一种思考习惯,坚持下来之后,你会慢慢发现自己写代码的时候也在提前预判这些问题的走向。

5.4 给不同阶段程序员的实操建议

如果在代码结构上刚入门,我的建议是先学会拆函数。把大函数拆成小函数,每个小函数只有一个清晰的任务,这是所有结构优化里最基础也最见效的一步。拆完之后,你会发现测试变容易了,分析改动影响也变容易了。这一步不需要什么高深理论,就是"让每个函数说清楚一句话"。

如果已经掌握了拆函数,开始写模块级别的代码,我的建议是找一个自己正在做的项目,把模块依赖画出来,找循环依赖。这一步会让你真正理解"依赖方向"是种什么体验。你画完依赖图之后,大概率会发现很多东西其实没有设计,只是"顺手"放着。把随手放着的东西理顺了,比学任何新框架都好使。

如果你已经在带项目或者负责架构,我的建议是把抽象当成基础设施,把接口当成合同。不要等着全局架构图来规定你怎么写代码,任何一个模块的对外接口,都是你和这个模块的未来使用者之间的一份合同。每次改接口,都要想想这份合同被破坏的代价。

说到底,编程概念的学习永远不会完成,代码结构的优化也永远不会停止。两者永远是相互交织的:你理解的概念越深,你能看到的坏结构就越多;你修好的结构越多,你对概念的体会就越真实。这个系列的内容走到这里,我最大的感受是:纸上得来的概念终究是虚的,只有自己动手重构几回,才能真正明白结构是怎么一回事。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 22:01:54

ClawHub恶意技能登顶事件:排名操纵如何引发供应链攻击

最近ClawHub上爆出的这起安全事件,圈里讨论热度一直没降。事情核心并不复杂:攻击者利用ClawHub排名机制中可以被操纵的漏洞,把一个恶意skill刷到热门榜第一,再利用用户对榜单的信任惯性,让供应链攻击在大量安装用户中快…

作者头像 李华
网站建设 2026/10/15 22:00:30

PHP反序列化实战:AreUSerialz题目中的绕过技巧全解析

很多人第一次碰反序列化题,都会觉得它像“字符串拼接 魔法方法调用”的玄学。这次我复盘的是某竞赛平台上的一道经典题AreUSerialz,题名本身就在玩谐音梗:“Are you serial?”——你会序列化吗?这道题最大的价值不是最后拿到 fl…

作者头像 李华
网站建设 2026/10/15 21:57:56

用数字炸弹小游戏练透Python基础语法

教新人学Python最难的不是语法背得够不够熟,而是缺一个“自己想动手写出来的东西”。循环学了,条件学了,刷了一堆练习题,真到要把语法串成一个能跑的程序,很多人就卡住了。我这些年带过不少从零起步的开发者&#xff0…

作者头像 李华
网站建设 2026/10/15 21:57:46

E4X深度解析:XML与脚本语言融合的兴衰与替代方案

配合这个标题,我不打算写成泛泛的技术介绍,而是从一个实际开发者的角度,把E4X的来龙去脉、语法细节、坑和替代方案都摊开讲清楚。文章会兼顾“这技术怎么用”和“为什么现在没人提它了”两个层面,给还在维护老旧AS3项目的人一份参…

作者头像 李华
网站建设 2026/10/15 21:57:45

echarts-all-map.rar实战指南:ECharts 5地图数据转换、注册与省市下钻

简介:ECharts作为百度开源的JavaScript数据可视化库,其地图组件是展示地理数据分布的重要工具,而官网已不再提供完整的离线地图资源下载。这份echarts-all-map.rar正是面向需要离线使用中国省市地图的Web前端开发者与数据分析师整理的珍贵合集…

作者头像 李华