接到新需求时,从何开始设计?

Posted JavaEdge.

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了接到新需求时,从何开始设计?相关的知识,希望对你有一定的参考价值。

即便我们能够极尽所能把代码写整洁,规避各种坏味道,但我们小心翼翼维护的代码,还是可能因为新的需求被破坏。

新的需求总会在路上,所以,写代码时需要时时刻刻保持嗅觉。

实现驳回

有个功能,内容作品提交之后要由相应的编辑审核。
审核有审核通过和不通过,这是系统中早就开发完成的。

有一天,新的需求来了:驳回审核通过的章节,让作者重新修改。造成作品需要驳回的原因有很多,比如,审核标准的调整,这就会导致原先通过审核的作品又变得不合格。

先看代码库已有怎样的基础。

首先,系统已有审核通过、审核不通过的接口。

PUT /chapter/chapterId/review
DELETE /chapter/chapterId/review

该设计将章节(chapter)的审核(review)当作了一个资源。
创建章节时,章节的审核状态就创建好了:

  • 审核通过
    相当于对这次审核进行了修改
  • 审核不通过
    相当于删除了该资源

对应这俩接口的服务接口:

章节上有个状态字段,标识现在章节处于什么样的状态:

  • 待审核
  • 审核通过
  • 审核不通过

已知这些基础,那驳回的需求如何设计?

新增一个驳回功能,自然要:

  • 新增一个驳回接口
  • 然后,在服务中增加一个驳回服务
  • 最后,在状态中增加一个驳回状态

看起来很合理,要准备写代码了呢。

这里有个坏味道来自要新增一个接口。

来一个新需求,就增加一个新接口,对大部分同学,这是一种多么正常的编程思维呀。
但必须对新增接口保持谨慎

接口,是系统暴露出的能力,一旦一个接口提供出去,你就不知道什么人会以何方式使用该接口。

很多系统有大量接口,仔细梳理会发现,有很多接口提供相似功能,这会让新人懵逼。即便你打算对系统进行重构,当清理掉一个你以为没人用的接口,就会有人跑出来告诉你,该接口调整影响了他们业务。

所以,必须对接口调整尤其慎重。最好从源头就开始限制,当我们想对外提供一个接口时,扪心自问:真的必须要提供新接口吗?

我面对该需求的第一反应和大多数人一样,也是新增接口。但是否真的要新增一个接口?
如果新增接口,就要复用已有接口,但复用前提是:新增的业务动作可通过已有业务完成,或是对已有业务进行微调就可以。

到底是新增or复用,还是要回到业务。

原业务中:

  • 审核通过会进入下一阶段
  • 审核不通过,就退回到作者那,进行修改

那驳回后呢?也会要求作者去修改。

发现了吧?驳回的动作和审核不通过的后续操作一致,只是起始状态:

  • 若原来状态是待审核
    经过一个审核不通过的动作,状态就变成审核不通过
  • 若原来状态是审核通过
    经过一个驳回动作,状态就变成驳回

所以,完全可复用原来的审核不通过接口。

既然是复用接口,所有的变化就都是内部变化了,可根据章节的当前状态判断,设置相应状态。
代码上,既不需要新增驳回接口,也无需新增驳回服务,只需修改 Chapter 类的内部,改动量比预期的小了很多。
代码结构如下:

这样,只需增加一个驳回状态,在当前状态是审核通过时,赋值这个新状态。
看来,已经把这次要改动的代码限制在一个最小范围。

但真的需要这么一个状态吗?

是否增加一个驳回状态,回答这个问题还是要回到业务上、:

驳回后续的处理与审核不通过的状态到底有何不同?

按PM本来的需求,他是希望做出一些不同。比如:

  • 审核不通过状态,编辑端则无法查看
  • 处于驳回状态,编辑则可以查看

但在当前产品状态下,是否可统一二者呢?即都按审核不通过处理?

PM想了想,觉得其实也可以。于是,两种不同状态在这里得到统一,最后根本没必要新增这个驳回新状态。

最终,这次的业务调整,后端服务代码没做任何修改,只是前端在需要驳回时,增加了一个对审核不通过的调用,而所有这一切的起点,只是我们对于新增接口的嗅觉!

定时提交

一般作者写完一章后,就直接提交,这是系统已有功能。
现在有个新需求:有时,作者会囤稿,为保证自己每天都能有作品提交,作者希望作品能在自己设定的时间提交,即一个章节在它创建时,并不会直接提交到编辑那里去审核,而等到特定时间再完成作品的提交。

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

那么,你会如何实现该需求?

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

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 关联(组合):

class ChapterSchedule 

  private ChapterId chapterId;
  private ZonedDateTime scheduleTime;
  ...

这样,后续再有关于调度的信息,即可放至该模型里。而且核心模型 Chapter 保持不变。

把定时提交的信息与章节本身分开,是因为这二者的改变原因不同。将二者混在一起,就违反了单一职责原则。

看来已经得到很合理的方案了,有了基础数据结构,修改对应接口和服务都易如反掌了。

但这就结束了吗?

新增的需求是定时发布,有这么个需求,和作者激励有关。
要想确定作者的激励,就要确定章节的提交时间,但如何确定章节提交时间?

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

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

至此,突然发现,模型里居然没有存放提交时间的地方。
是的,得修改实体了,给它增加一个提交时间:

class Chapter 
  ...
  private ZonedDateTime submittedAt;

肯定有读者好奇了:之前讨论那么多,不就为了不在 Chapter 新增信息,你现在就这么轻易新增字段了?

一个字段该不该加在一个类里,取决于其改变原因:

  • 定时时间确实不该加
  • 这里的提交时间却应该加
    提交时间本就是章节的一个属性,只不过之前,这个信息与创建时间共用。如今,因为定时提交的需求,二者应该分开了

难道不能直接用 submittedAt 去存储调度时间?

严格地说,不行。因为调度时间可能与具体提交时间有差异。
比如,因为某种原因,系统宕机了,启动后,调度任务执行,这时可能已经过了调度时间很久了,但这时提交章节,它的时间就不会是调度时间。

还记得为什么要做这个分析吗?
因为要改动核心实体,而这就是一个坏味道高发区。

总结

新需求到来时需要关注:

  • 增加新接口
  • 改动实体

接口和实体,也是一个系统对外界产生影响的重要部分,一个是对客户端提供能力,一个是产生持久化信息。所以,我们必须谨慎地思考它们的变动,它们也是坏味道产生的高发地带。

对于接口,我们对外提供得越少越好,而对于实体,必须仔细分析它们的定位。

谨慎地对待接口和实体的变化。

以上是关于接到新需求时,从何开始设计?的主要内容,如果未能解决你的问题,请参考以下文章

数据可视化大屏案例分析

分页功能设计(解决数据重复问题)

产品经理与程序员的矛盾从何而来?(给产品经理们的建议,给程序员的建议。程序员处在信息传导到最末端,因此比较弱势信息不准确)

常见linux命令释义(第八天)—— Bash Shell 的操作环境

面试官:系统需求多变时如何设计?

设计模式之观察者模式--PHP