一个代码坏味道的高发地带:改动核心的实体

2022-10-05  本文已影响0人  GuangHui

一次定时提交的实现

在我们的系统中,一般情况下,作者写完一章之后就直接提交了,这是系统中已经实现好的一个功能。现在来了新的需求,有时候,作者会囤一些稿子,为了保证自己每天都有作品提交,作者希望作品能够按自己设定的时间去提交,也就是说,一个章节在它创建的时候,并不会直接提交到编辑那里去审核,而是要到特定的时间之后,再来完成作品的提交。

实际上,“每天都有作品提交”就是一种连续的签到,通常来说,系统都会给连续签到以奖励,这也是对于作者的一种激励手段。

如果你面对这样一个需求,你会怎么实现呢?

与这个需求最直接相关的代码就是章节信息了:

class Chapter {
// 章节 ID
private ChapterId chapterId;
// 章节标题
private String title;
// 章节内容
private String content;
// 章节状态
private Status status;
// 章节创建时间
private ZonedDateTime createdAt;
// 章节创建者
private String createdBy;
// 章节修改者
private String modifiedBy;
// 章节修改时间
private ZonedDateTime modifiedAt;
...
}

显然,要实现这个需求,需要有一个定时任务,定期去扫描那些需要提交的作品。这个是没有问题的,但是,这些定时的信息要放在哪里呢?

我似乎已经看到你跃跃欲试的样子了。你可能会想:这个实现还不简单,在章节上加上一个调度时间就行了:

class Chapter {
...
private ZonedDateTime scheduleTime;
}

确实,这么实现并不复杂。但我想请你稍微停顿一下,别急着写这段代码。这种做法我又嗅到了一丝坏味道,因为我们要改动实体了。

有需求就改动实体,这几乎是很多人不假思索的编码习惯,然而,对于一个业务系统而言,实体是其中最核心的部分,对它的改动必须有谨慎的思考。

随意修改实体,必然伴随着其它部分的调整,而经常变动的实体,就会让整个系统难以稳定下来。一般来说,一个系统的业务并不会经常改变,所以,核心的业务实体应该是一个系统中最稳定的部分。

不过,你可能会说:“我有什么办法,需求总在变,就总会改动到这个实体。”
需求总在变,这是没有错的,但它是否真的要改动到业务实体呢?很多时候,这只是应有的职责没有分析清楚而已。

具体到我们这个例子里面,我们需要的是定时提交一个章节,而这个定时信息并不是核心业务实体的一部分,只是在一种特定场景下所需要的信息而已。所以,它根本不应该添加到 Chapter 这个类里面。

不放在 Chapter 这个类里面,那要放到哪呢?很显然,这里少了一个模型,一个关于调度的模型。我们只要增加一个新的模型,让它和 Chapter 关联在一起就好了:

class ChapterSchedule {
private ChapterId chapterId;
private ZonedDateTime scheduleTime;
...
}

有了这个模型,后续再有关于调度的信息就可以放到这个模型里面了,而更重要的是,我们的核心模型 Chapter 在这个过程中是保持不变的。

我们之所以要把定时提交的信息与章节本身分开,因为这二者改变的原因是不同的。你或许已经发现了,是的,如果将二者混在一起,就是违反了单一职责原则。

到这里,定时提交的问题看上去已经得到了一个很合理的解决,有了基础的数据结构,修改对应的接口和服务,对大多数程序员来说,都是一件驾轻就熟的事情。那么,这个讨论就结束了吗?我们可能暂时还不能停下来。

我们新增的需求是定时发布,之所以要有这么个需求,因为这和作者的激励是相关的。要想确定作者的激励,就要确定章节的提交时间,问题是,我们怎么确定章节的提交时间呢?

在原来实现中,创建时间就是提交时间,因为章节是立即提交的,而现在创建时间和提交时间有可能不同了。

你可能会想到,创建时间不行,那就用修改时间。我告诉你,这也不行,修改时间是章节信息最后一次修改的时间,它有可能因为各种原因变更,最简单的就是编辑审核通过,这个时间就会变。

分析到这里,我们突然发现,模型里居然没有一个地方可以存放提交时间,是的,我们需要修改实体了,我们要给它增加一个提交时间:

class Chapter {
...
private ZonedDateTime submittedAt;
}

到这里,估计有些人已经懵了。前面我们辛辛苦苦地讨论,为的就是不在 Chapter 里增加信息,而这里,我们竟然就增加了一个字段。

前面我们说了,一个字段该不该加在一个类上,取决于其改变的原因。前面的定时时间确实不该加,而这里的提交时间却是应该加的。提交时间本来就是章节的一个属性,只不过如前面所说,之前,这个信息与创建时间是共用的,而如今,因为定时提交的出现,二者应该分开了。

或许你还有一个疑问,我们难道不能直接用 submittedAt 去存储调度时间吗?严格地说,不行。因为调度时间可能与具体提交的时间有差异。我举个例子,因为某种原因,系统宕机了,启动之后,调度任务执行,这时可能已经过了调度时间很多了,但这个时候提交章节,它的时间就不会是调度时间。

至此,我们完整地分析完了定时提交的实现,你还记得我们为什么要做这个分析吗?没错,因为它要改动核心的实体,而这又是一个坏味道的高发地带。

上一篇 下一篇

猜你喜欢

热点阅读