面向对象开发实践之消息中心设计(二)

模型设计的目标,是承载变化,而不是预判所有变化。

本文是《面向对象开发实践之消息中心设计》系列的第二篇。

第一篇中,我重点讨论了如何用面向对象的方法启动一个消息中心设计 。这一篇将更进一步,聚焦在一个更"脏"、也更容易失控的部分:消息模型设计

在多数项目中,消息中心之所以难维护,并不是因为技术有多复杂,而是模型一开始就承载了过多变化。本文会从"最小可行模型"开始,一步步说明:

  • 消息模型最初只需要解决什么问题
  • 为了适配哪些真实业务场景,我们引入了哪些字段和结构
  • 哪些设计是"必须的",哪些是"刻意延后"的

一条消息最本质是什么?

如果抛开 UI、样式、跳转、未读数等需求,一条消息最本质只回答三个问题:

  1. 给谁看的?

  2. 什么时候产生的?

  3. 发生了什么?

对应到模型中,最小消息单元可以极度克制:

java 复制代码
class Message {
	Long id; // 消息ID
	Long userId; // 接收用户
    String title;	// 消息标题
	String content; // 文本内容
	LocalDateTime sendTime;
}

这个模型几乎什么都不能做 ,但它有一个重要意义:它定义了"消息"与"业务"的最小边界。所有后续设计,都是在这个边界之外有意识地扩展,而不是一开始就把所有业务塞进来。

第一轮拓展:为什么需要「消息分类」?

很快我们会遇到第一个真实需求:

  • 用户想单独查看「系统消息」
  • 活动消息不应该和评论提醒混在一起
  • 不同分类的未读数要分开统计

这时,引入 消息分类(Category) 是非常自然的:

java 复制代码
enum MessageCategory {
    SYSTEM,
    ACTIVITY,
    INTERACTION,
    ORDER
}

模型随之演进:

java 复制代码
class Message {
    Long id;
    Long userId;
    MessageCategory category;
    String title;
    String content;
    LocalDateTime sendTime;
}

设计取舍

  • ✅ 分类是用户视角的概念
  • ❌ 它不等同于业务类型(不要直接用业务枚举)

这一层的目的很单纯:支撑列表查询与未读数统计

第二轮扩展:业务关联,但不侵入业务

接下来几乎一定会出现的问题是:

"这条消息是关于哪条评论 / 哪个活动的?"

一个常见但危险的做法是:

java 复制代码
class CommentMessage extends Message {
    Comment comment;
}

这种设计的问题在于:

  • 消息与业务生命周期强绑定
  • 业务删除 / 变更会直接影响历史消息

正确做法:只存业务标识,不存业务对象

java 复制代码
enum MessageBizType {
    COMMENT,
    LIKE,
    MENTION,
    VIOLATION,
    LOTTERY
}


class Message {
    Long id;
    Long userId;
    MessageCategory category;
    MessageBizType bizType;
    Long bizId; // 业务主键
    String title;
    String content;
    LocalDateTime sendTime;
}

这个设计解决了什么?

  • 消息可以长期存在

  • 业务可以自由演进、删除、归档

  • 是否实时查询业务,由"渲染阶段"决定

这是一个非常关键的边界划分。

内容结构化:为"部分跳转文字"做准备

在第一篇中提到过这样的需求:

"社区规范""修改>>" 需要支持点击跳转

如果 content 只是纯字符串,这种需求几乎无法优雅支持。

首先定义跳转类型

java 复制代码
public enum MessageJumpType {
    NONE,           // 无跳转
    URL,            // 外链
    APP_ROUTE,      // 应用内部路由
    MINI_PROGRAM,   // 小程序路径
    ACTIVITY_PAGE,  // 自动跳消息归属活动页面
}

定义跳转配置模型:

java 复制代码
public class MessageJumpConfig {
    private String url;             // URL 跳转
    private String route;           // App 内部路由
    private String miniProgramPath; // 小程序路径
    private Map<String,Object> params; // 路由参数

    // 工具构造方法
    public static MessageJumpConfig url(String url) { ... }
    public static MessageJumpConfig route(String route) { ... }
    public static MessageJumpConfig mini(String path) { ... }
}

针对消息文本中部分文本可点击跳转(类似超链接),这类消息通常称为富文本消息(Rich Text Message)

但不能直接给整个富文本,否则跳转逻辑难以做成标准化,所以我们要做 结构化富文本(Structured Rich Text)

核心设计方案

将文本拆成多个"片段(Segment)",每个片段可以是;

  • TEXT:普通文本
  • LINK:可点击的跳转链接

结构如下:

java 复制代码
public class RichTextSegment {
    private RichTextType type;         // TEXT / LINK
    private String text;               // 段落文本
    private MessageJumpConfig jumpConfig;  // LINK 时使用
}

卡片内容的 text 字段改为支持富文本列表:

java 复制代码
public class RichTextContent {
    private List<RichTextSegment> segments;
}

消息模型中不再直接存展示文本,而是存 可渲染结构

java 复制代码
class Message {
    ...
    RichTextContent content;
}

设计价值:

  • 支持部分文字跳转

  • 支持前端自由渲染

  • 后续可扩展为富文本 / 高亮 / icon / 图片

消息支持图片

常见的消息图片分为两种:

  • 第一种是在消息最上方,宽度撑开展示,或者展示在左侧或者右侧,这种暂且可以认为是同一种类型,后面可以用布局来切换展示位置;
  • 第二种是在消息中间,作为内容的一部分

基于前面的内容,我们来支持这两种样式。首先把图片作为一个对象,定义图片的最小单元:

java 复制代码
class MessageImage {
    private String image;
}

同样,这里再支持图片点击跳转:

java 复制代码
class MessageImage {
    private String url;
    private MessageJumpConfig jumpConfig;
}

可以在消息内容中,增加该类型,来支持图片展示:

java 复制代码
public class RichTextSegment {
    private RichTextType type;         // TEXT / LINK / IMAGE
    private String text;               // 段落文本
    private MessageJumpConfig jumpConfig;  // LINK 时使用
    private MessageImage image;			// 支持图片
}

同样在消息体中,增加该字段,来实现第一种展示形式:

java 复制代码
class Message {
    ... 
    MessageImage image;
}

布局与样式:模型不关心"长什么样"

面对多种卡片样式,一个常见误区是:在 Message 里加一堆 UI 字段

更优雅的做法,可以提前约定多种卡片结构布局,当需要前端按照什么样式来渲染,返回该样式即可。这里定义一个消息布局:

例如这里约定两种渲染样式:

  • 样式一:图片(宽度撑开,可省略)+ 标题(左对齐,可省略)+ 内容(左对齐)+ 时间(右对齐)

  • 样式二:标题(左对齐) + 时间(右对齐)+ 内容(左对齐)+ 图片(宽高固定,右对齐)

这里定义消息布局,消息体中增加布局配置支持:

java 复制代码
enum MessageLayout {
    IMAGE_TITLE_TEXT,
    TITLE_TEXT_RIMAGE
}

class Message {
    ...
    MessageLayout layout;
}

关键点

  • 模型只描述布局类型

  • 具体怎么渲染,由 Renderer 决定

DO / DTO / VO 的分层思考

DO:持久化模型

  • 面向存储
  • 字段稳定、可索引

DTO:传输与创建

  • 用于创建消息
  • 不暴露内部字段
  • 可封装行为

VO:展示模型

  • 已完成渲染
  • 直接面向前端
java 复制代码
class MessageVO {
    Long id;
    boolean read;
    MessageCategory category;
    MessageLayout layout;
    MessageImage image;
    RichTextContent content;
    LocalDateTime sendTime;
}

这是隔离变化的关键一环。

哪些设计被我刻意延后了?

在这一阶段,刻意没有:引入 MQ、引入复杂状态机、做多级缓存、过度拆表

原因很简单:模型设计的目标,是承载变化,而不是预判所有变化。

结语:模型不是"想清楚一次",而是"允许演进"

一个好的消息模型,并不是字段多、覆盖全,而是:

  • 有清晰边界
  • 能容纳不确定性
  • 不被某个业务绑死

在下一篇文章中,我会进一步展开:

  • 不同消息类型(评论 / 点赞 / @ / 违规 / 活动 / 订单)的建模方式
  • Renderer 如何与模型协作
  • 消息创建、去重与渲染的完整链路

这是一个从模型走向行为的过程。

如果你正在做类似系统,希望这篇文章能帮你在"加字段之前",多想一步。

相关推荐
云水木石2 小时前
Rust 语言开发的 Linux 桌面来了
linux·运维·开发语言·后端·rust
狗哥哥2 小时前
Vite 插件实战 v2:让 keep-alive 的“组件名”自动长出来
前端·vue.js·架构
法欧特斯卡雷特2 小时前
Kotlin 2.3.0 现已发布!又有什么好东西?
后端·架构·开源
要开心吖ZSH3 小时前
应用集成平台-系统之间的桥梁-思路分享
java·kafka·交互
TsengOnce3 小时前
阿里云ECS多版本JDK切换
java·python·阿里云
wearegogog1233 小时前
基于C#的FTP客户端实现方案
java·网络·c#
听风吟丶3 小时前
Java NIO 深度解析:从核心组件到高并发实战
java·开发语言·jvm
野生技术架构师3 小时前
Java面试题及答案总结(互联网大厂新版)
java·面试·状态模式
a努力。3 小时前
小红书Java面试被问:ThreadLocal 内存泄漏问题及解决方案
java·jvm·后端·算法·面试·架构