整洁架构下通知系统邮件内容渲染与持久化方案问询
问题描述
我负责的系统中有一个通知子域,基于系统其他部分的集成事件处理并发送所有邮件与SMS通知。每个用例在应用层创建通知实体,通过基础设施层的通知仓库保存(保存的通知进入队列,最终由定时任务发送),通知创建代码示例如下:
$notification = Notification::create( Channel::EMAIL, $recipients, $subject, $content, $context );
邮件内容多为非纯文本,此部分属于UI/表示层范畴,我困惑于应用层如何获取该内容而不破坏控制流?
更新补充
查阅相关文献后我有了一些进展,先补充通知的使用场景:通知初始会发送给收件人(如订单确认邮件发给客户、员工手动发送消息、订单取消邮件等,支持邮件与SMS渠道);管理界面中员工/管理员需查看特定实体(如订单)的通知历史,包括收件人、发送时间、状态(排队、已发送、错误),通知需永久持久化。
针对初始的表示层问题,我为每种通知引入了Presenter接口,接口置于应用层(或应放在领域层?),具体实现则在UI层。例如订单确认通知有OrderConfirmationPresenterInterface接口,包含present方法。目前通过依赖注入(DI)注入接口实现,用于创建通知实体:
$notification = Notification::create( Channel::EMAIL, $recipients, $subject, $this->emailPresenter->present($orderId, $something, $somethingElse), $context );
即由Presenter返回实际生成的表示内容(通常为HTML格式的邮件内容),作为字符串加入通知实体并持久化到数据库中。我想确认此实现路径是否正确?
此外还有一个疑问:创建通知实体时,应持久化已生成的表示内容,还是仅持久化输入数据、在发送通知时再生成表示内容?前者能确保留存通知的实际呈现样式,但会导致每条记录重复存储HTML;后者仅存储数据,但后续若修改表示逻辑,查看历史通知时的渲染结果可能与最初发送给客户的内容不一致。
解答
一、Presenter接口的实现路径合理性
你的方案是合理的,接口的放置位置建议放在应用层而非领域层:
- 领域层应聚焦核心业务规则,通知的内容渲染属于"如何呈现"的表示逻辑,不属于领域核心规则,放在应用层更合适,能保持领域层的纯净性。
- 应用层作为领域层与UI层的中间层,定义Presenter接口可以实现依赖反转:应用层依赖抽象接口,UI层实现具体的渲染逻辑,既不破坏控制流(依赖方向是UI层→应用层,而非应用层直接依赖UI层),也符合端口与适配器的设计思想——应用层定义端口(Presenter接口),UI层作为适配器实现端口。
二、持久化内容 vs 持久化输入数据的选择
两种方案各有优劣,需结合业务需求权衡:
持久化已生成的表示内容(HTML)
- 优势:完全保证历史通知与发送给用户的内容一致,管理员查看历史时能看到用户实际收到的样式,符合审计和回溯需求;发送时无需再次渲染,减少计算开销。
- 劣势:存储体积更大,尤其是HTML内容可能包含重复的样式代码;如果后续需要批量修改历史通知的样式(业务上基本很少有这种需求),无法通过修改模板实现,只能批量更新数据库记录。
持久化输入数据,发送时再渲染
- 优势:存储体积小,仅保存核心数据;后续修改模板样式时,新发送的通知会自动应用新样式。
- 劣势:历史通知的渲染结果可能与用户实际收到的不一致,尤其是当模板逻辑或依赖的外部数据(如品牌标识、政策文本)发生变化时,会破坏通知的历史真实性;发送时需要重复执行渲染逻辑,增加一定的计算成本。
推荐方案:如果系统对通知历史的真实性要求较高(比如电商订单通知、合规类通知),优先选择持久化已生成的表示内容。可以通过对HTML内容进行压缩(比如去除多余空格、合并CSS)来降低存储开销;如果业务对存储成本敏感,且通知历史的样式一致性要求不高,再考虑仅存储输入数据。
内容的提问来源于stack exchange,提问作者Tor

