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

news2024/10/5 19:11:42

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

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

实现驳回

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

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

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

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

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 去存储调度时间?

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

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

总结

新需求到来时需要关注:

  • 增加新接口
  • 改动实体

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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/157440.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

lxmlxpath一站式教学

文章目录XPath定义XPath 概览安装lxml初步使用xpath 常用表达式获取所有节点获取子节点获取父亲节点属性匹配获取文本获取属性属性多值匹配多属性匹配按序选择节点轴选择总结XPath定义 XPath 是一门在 XML 文档中查找信息的语言。XPath 可用来在 XML 文档中对元素和属性进行遍历…

OMNet++安装pydev

菜单栏->Help->Install New SoftWare->Add 输入 Pydev http://pydev.sourceforge.net/pydev_update_site/5.2.0链接为下载5.2.0版本&#xff0c;若jdk<1.8安装5.2.0之前的版本。\color{blue}{链接为下载5.2.0版本&#xff0c;若jdk<1.8安装5.2.0之前的版本。}链…

无代理Bean对象Spring循环依赖处理

Bean A和B互相依赖 A对象或者B对象,创建对象时会形成死循环,解决: Spring中创建对象:实例化、初始化 闭环:此时,图中是一个闭环,如果想解决这个问题,那么就必须要保证不会出现第二次创建A(B)对象这个步骤,也就是说从容器中获取A的时候必须要能够获取到 思考,在spring…

RabbitMQ(三)工作原理及核心概念

目录工作原理图1.Connection 连接2.Channel 信道3.Virtual host 虚拟主机4.Queue 队列5.Exchange 交换机5.1 direct5.2 fanout5.3 topic5.4 headers官网地址&#xff1a;https://www.rabbitmq.com/ 下载地址&#xff1a;https://www.rabbitmq.com/download.html RabbitMQ 的5大…

chk文件恢复和文件恢复的区别有哪些?

文件恢复也就是我们常说的数据恢复&#xff0c;一般用于恢复误删除、损坏的文件数据&#xff0c;那么chk文件恢复又是什么意思呢&#xff1f;两者有什么区别呢&#xff1f;文件恢复市面上常见的文件恢复软件&#xff0c;一般都是用于恢复那些被删除、损坏的文件数据恢复&#x…

Jenkin权限控制——项目矩阵授权策略

开启授权策略 Jenkins的项目权限控制通过【授权策略】实现&#xff0c;【授权策略】需要plugins提供&#xff0c;首先需要安装Role-based Authorization Strategy 安装Role-based Authorization Strategy 管理Jenkins——》插件管理——》搜素——》Role-based Authorizatio…

距离教资考试还有两天,你的照片准备好了没有?

距离教资考试还有两天不知道你的教资报名照准备好了没有?照片不合格可是不能报名成功的&#xff01; ​ ​ 1、本人近6个月以内免冠正面彩色证件照 2、一寸白底&#xff0c;比例3:4 3、不允许戴帽子、头巾、发带、墨镜 4、照片中显示考生头部和肩膀上部 5、照片为jpg或jpe…

云游戏渐入佳境,众玩家同行不同路

配图来自Canva可画 有人曾在2019年问道&#xff1a;“云游戏能成为5G第一个2C的杀手级应用吗&#xff1f;”在当时&#xff0c;几乎所有人对这个问题的回答&#xff0c;都有一种“只缘身在此山中”的迷雾感&#xff0c;还看不清脚下的路会延伸到何处。彼时各大厂商都在备战&am…

MQTT 发布/订阅模式介绍

MQTT 发布/订阅模式 发布订阅模式&#xff08;Publish-Subscribe Pattern&#xff09;是一种消息传递模式&#xff0c;它将发送消息的客户端&#xff08;发布者&#xff09;与接收消息的客户端&#xff08;订阅者&#xff09;解耦&#xff0c;使得两者不需要建立直接的联系也不…

向量检索服务迭代演进

背景在搜索推荐系统里&#xff0c;有一个重要的服务是向量检索服务&#xff0c;也就是求向量在空间中最相近的topk个向量&#xff0c;在搜索系统里&#xff0c;涉及大量的召回引擎可能需要用到向量检索服务&#xff0c;需要在线实时计算&#xff0c;并且可快速响应&#xff1b;…

Jetpack Compose中的startActivityForResult的正确姿势

之前在 Jetpack Compose中的导航路由 里简单的提到了从 Compose 导航到其他 Activity 页面的方式&#xff0c;对于不带返回结果的则就是跟以前一样简单的启动Activity的代码&#xff0c;而如果是startActivityForResult方式的&#xff0c;需要使用带回调的方式去启动&#xff0…

为什么 TCP 协议有性能问题

TCP 协议可以说是今天互联网的基石&#xff0c;作为可靠的传输协议&#xff0c;在今天几乎所有的数据都会通过 TCP 协议传输&#xff0c;然而 TCP 在设计之初没有考虑到现今复杂的网络环境&#xff0c;当你在地铁上或者火车上被断断续续的网络折磨时&#xff0c;你可能都不知道…

Python 机器学习 数据归一化

众所周知机器学习使计算机从研究数据和统计数据中学习机器学习是向人工智能&#xff08;AI&#xff09;方向迈进的一步。机器学习是一个分析数据并学习预测结果的程序。此篇文章主要介绍Python机器学习的数据归一。1、数据归一化当您的数据具有不同的值&#xff0c;甚至具有不同…

一些神经网络基础知识归纳

神经网络&#xff08;NN&#xff09;复杂度 NN复杂度&#xff0c;多用NN层数和NN参数个数表示 如上图示 空间复杂度 层数隐藏层的层数1个输出层 上图为2层NN 总参数 3*44 4*2226 时间复杂度 乘加运算次数 3*44*220 指数衰减学习率 可以先用较大学习率&#xff0c;快速得…

KingbaseES V8R3数据库运维案例之---不完整的启动包(incomplete startup packet)复现

案例说明&#xff1a; 在KingbaseES V8R3数据库的sys_log日志中&#xff0c;出现以下故障信息“不完整的启动包(incomplete startup packet)”日志信息。本案例复现此日志信息发生的原因。 如下图所示&#xff0c;日志信息&#xff1a; 适用版本&#xff1a; KingbaseES V8R3 1…

计算最大公约数和最小公倍数被Java程序员用代码写出来啦

沉淀、分享、成长&#xff0c;让自己和他人都能有所收获&#xff01;&#x1f604; 一、前言 嘿&#xff0c;怎么突然讲到最大公约数了&#xff1f; 这么想你肯定是没有好好阅读前面章节中讲到的RSA算法&#xff0c;对于与欧拉结果计算的互为质数的公钥e&#xff0c;其实就需…

树莓派系统安装,网络配置,系统配置

如何安装树莓派的系统 以及 树莓派新系统SSH连接被拒绝的解决方法 1. 烧录方式1&#xff08;官方&#xff0c;简单&#xff09; 在下面网站下载 https://www.raspberrypi.com/software/ 打开以后选64位系统 选择安装的u盘 设置ssh&#xff0c;WiFi&#xff0c;登录密码等…

MySQL高级【表级锁】

1&#xff1a;表级锁1.1&#xff1a;介绍表级锁&#xff0c;每次操作锁住整张表。锁定粒度大&#xff0c;发生锁冲突的概率最高&#xff0c;并发度最低。应用在MyISAM、 InnoDB、BDB等存储引擎中。 对于表级锁&#xff0c;主要分为以下三类&#xff1a; 表锁元数据锁&#xff0…

PLC常见的输入设备及其接线方式列举

PLC常见的输入设备有按钮、行程开关、接近开关、转换开关、拨码器、各种传感器等&#xff0c;输出设备有继电器、接触器、电磁阀等。下面&#xff0c;我们来详细看看PLC如何与这些设备正确地连接输入和输出线路。1.PLC与主令电器类设备的连接下图是PLC与按钮、行程开关、转换开…

现代C++并行与并发笔记 附C++17线程池实现项目实战

文章目录让程序在特定时间休眠启动和停止线程互斥量&#xff08;mutex&#xff09;进行延迟初始化——std::call_once将执行的程序推到后台——std::async信号量&#xff08;condition_variable&#xff09;C11 线程池前置知识返回值类型推导 result_of 和 invoke_resultpackag…