面向对象不是为了消除复杂性,而是为了使复杂性受控。
通过前三篇的实战,我们从消息建模、创建链路、渲染体系,一步步构建了一个可扩展的消息中心。
在原本的专栏大纲里,第四篇应该是关于 MQ 事务、分布式锁、Redis 缓存分片以及 Outbox 模式的硬核架构实现。但在复盘前三篇的内容时,我突然意识到:如果我花了大量篇幅去讨论如何调优中间件,那么这个系列就背离了"面向对象开发实践"的初衷。
所以这一篇,我想暂时抛开那些"怎么实现"的细节,回过头来看看我们这一路是怎么思考 的------为什么要把消息拆成 Context、Message、Renderer?为什么宁愿多查一次数据库也不愿意把业务对象塞进消息里?这些选择背后,其实是面向对象中隔离变化、明确职责、保持克制这几个朴素原则在支撑。
架构设计往往是为了解决"规模"带来的复杂度,它关注的是吞吐量、延迟和一致性;而面向对象则是为了解决"逻辑"带来的复杂度,它关注的是职责、边界和抗熵增能力。
一个好的领域模型,应该对底层架构保持无感。不管你是用 MySQL 还是 Redis、同步还是异步,核心的业务逻辑不应该被这些技术细节绑架。 过度沉溺于基础设施的实现细节,反而会掩盖领域逻辑的纯粹性。
因此,我决定跳过那些"通用的架构套路",去聊聊在整个设计过程中,那些隐藏在类图与架构图背后的面向对象哲学。
领域驱动:从"怎么存"到"是什么"
真正的面向对象要求我们识别出对象的"不变式"和"变化点"。
在设计初期,最诱人的捷径是直接建立 message 表,然后往里堆字段。但我们选择了先建模再建表。
面向对象要求我们将现实世界抽象为抽象模型。在消息中心里,我们将消息拆解为"发生的事实(Context)"、"存储的实体(Message)"和"展示的策略(Renderer)"。在设计模型时,我们经历如下三个步骤:
- 用户需要看什么?(确定展示职责)
- 业务会发生什么?(确定创建职责)
- 最后才定义我们需要存什么?(确定模型字段)。
面向对象是由外向内的设计,通过定义对象间的协作契约,倒逼出最精简的数据模型,从而避免了表结构的臃肿和语义混乱。
隔离变化:多态不仅仅是if-else的替代品
好的设计应该像乐高积木,新增功能(如新增一种业务消息)应该只需要增加代码,而不是修改代码。
我们在设计 RichText 和 Renderer 时大量使用了策略模式。
新增加一种业务消息(如"勋章点亮"),我们不需要在主流程中加一行if-else。只需要新增一个类,并注册到工厂中,整个系统就"无缝"支持了新业务。 面向对象追求的不是类的数量,而是扩展的轻松程度 。比如我们之前设计的 Renderer:新增一种消息类型,你只需要加一个类实现 MessageRenderer,注册进去就行,主流程一行代码不用改。这才是面向对象带来的实在价值。
并且我们坚持 Message 不持有业务实体,只支有 bizId。这种设计在初期看起来增加了查询成本,但它带来的收益是巨大的。业务系统的变更、数据的逻辑删除,都不会让消息中心陷入数据不一致的泥潭。
防腐层(ACL)的哲学:Context 的克制
如果 Context 开始携带完整业务实体,消息中心就会变成一个无法演进的巨型聚合。
在第三篇中,我们引入了 MessageCreateContext。
这是典型的"防腐层"思想。Context 对象不直接依赖任何具体的业务 Entity,它只携带最核心的"事实"。
这种克制防止了消息中心变成一个"全知全能"的怪物。消息中心不需要知道什么是"优惠券规则"或"违规算法",它只需要知道"谁、在什么时候、看到了什么",消息中心不应拥有判断业务对错的逻辑,它只负责记录事实的切片。
警惕过度设计:何时该停下来?
这是作为高级工程师最需要总结的:设计是有成本的。
- 准则 :
- 如果系统只有两种消息且未来一年不会变,硬编码
if-else或许才是最经济的。 - 我们引入这么多抽象层(Handler, Renderer, Context),是因为预判了"消息类型爆炸"和" UI 频繁变更"这两个核心痛点。
- 如果系统只有两种消息且未来一年不会变,硬编码
- 原则:不要为不存在的未来过度设计,但要为确定的变化留出插槽。
总结
这一系列文章不仅是关于"消息中心"的,它其实是关于如何管理软件熵增的。
软件系统的崩塌往往是从一个"顺便写"的功能开始的。通过面向对象的严谨推导,我们给系统建立了一套防御机制。当你发现新增一个消息模板只需要 10 分钟,且不需要改动一行核心代码时,你就能体会到设计模式带来的那种"掌控感"。
真正的面向对象,是让代码在不断迭代中依然保持年轻。
它不是一种约束,而是一种自由------一种在业务洪流中,依然能清晰定义边界、掌控变化的自由。