微服务设计原则——易维护

news2024/9/30 5:27:37

文章目录

  • 1.充分必要
  • 2.单一职责
  • 3.内聚解耦
  • 4.开闭原则
  • 5.统一原则
  • 6.用户重试
  • 7.最小惊讶
  • 8.避免无效请求
  • 9.入参校验
  • 10.设计模式
  • 11.禁用 flag 标识
  • 12.分页宜小不宜大
  • 参考文献

1.充分必要

不是随便一个功能都需要开发个接口。

虽然一个接口应该只专注一件事,但并不是每个功能都要新建一个接口。要有充分的理由和考虑,即这个接口的存在十分有意义和价值。无意义的接口不仅浪费开发人力,还使服务变得臃肿,增加维护成本。

相关功能我们应该考虑合为一个接口来实现。

2.单一职责

每个 API 应该只专注做一件事情。

就像我们开发人员一样,要么从事后台开发,要么从事前端开发,要么从事服务器运维开发。公司一般不会让一个人包揽所有的开发工作,因为这让员工的职责不够单一,不利于员工在专业领域的深耕,很容易成为万金油。对公司的影响是因员工对专业知识掌握的不够深,导致开发出的软件质量得不到保证。

让接口的功能保持单一,实现起来不仅简单,维护起来也会容易很多,不会因为大而全的冗杂功能导致接口经常出错。

比如读写分离和动静分离的做法都是单一职责原则的具体体现。如果一个接口干了两件事情,就应该把它分开,因为修改一个功能可能会影响到另一个功能。

3.内聚解耦

一个接口要包含完整的业务功能,而不同接口之间的关联要尽可能的小。

这样便降低了对其他接口的依赖程度,如此其他接口的变动对当前接口的影响也会降低。一般都是通过消息中间件 MQ 来完成接口之间的耦合。

4.开闭原则

对扩展开放,对修改关闭。

这句话怎么理解呢,也就是说,我们在设计一个接口的时候,应当使这个接口可以在不被修改的前提下被扩展其功能。换句话说,应当可以在不修改源代码的情况下改变接口的行为。

比如当用户输入个人简介时有个长度限制,我们不应该将长度限制写死在代码,可以通过配置文件的方式来动态扩展,这就做到了对扩展开放(用户简介长度可以变更),对修改关闭(不需要修改代码)。

此外,在设计模式中模板方法模式和观察者模式都是开闭原则的极好体现。

5.统一原则

接口要具备统一的命名规范、统一的出入参风格、统一的异常处理流程、统一的错误码定义、统一的版本规范等。

统一规范的接口有很多优点,自解释、易学习,难误用,易维护等。

6.用户重试

接口失败时,应该尽可能地由用户重试。

失败不可避免,因为接口无法保证100%成功。一个简单可靠的异常处理策略便是由用户重试,而不是由后台服务进行处理。

还是 IM 应用为例,有这样的需求场景。群管理员需要拉黑用户,被拉黑的用户要先剔出群,且后续不允许加入群。那么拉黑由一个独立的接口来完成,需要两个操作。一是将用户剔出群,二是将用户写入群的黑名单存储。此时两个操作无法做到事务,也就是我们无法保证两个操作要么同时成功,要么同时失败。

这种情况下我们该怎么做,既让接口实现起来简单,也能满足需求呢?

我们如果将用户剔出群放到第一步,那么可能会存在踢出群成功,但是写入群的黑名单存储失败。这种情况下提示用户拉黑失败,但却把用户踢出了群,对用户来说,体验上是个功能 bug。

秉着尽可能地由用户重试的原则,我们应该将写入群的黑名单存储放到第一步,踢出群放到第二步。并且踢出群作为非关键逻辑,允许失败。即使踢出群失败,用户有重试的机会,可以后面手动将该用户踢出群。

由用户重试,我们的接口在实现上将变得简单。

如果要引入消息队列存储踢出群的失败日志,让后由后台服务消费重试来保证一定成功,那么实现上将变得复杂且难以维护。不是非常重要的操作,一定不要这么做。

7.最小惊讶

代码尽可能避免让读者蒙圈。

最小惊讶原则(Principle of Least Astonishment)指的是系统或服务的设计应尽量避免让用户或开发者感到意外或困惑,确保系统的行为符合用户的直觉和预期。

最小惊讶原则同样适用于代码层面。

遵循最小惊讶原则有助于提高服务的为可维护性,因为其更容易让人理解。

代码不仅要写给机器看,也要写给人看。很多时候,一段代码需要一群人共同维护,如果你在里面杂七杂八地加了很多不易于别人理解的奇技淫巧,会降低了代码可读性,不利于维护。

只需根据需求来设计并实现,切勿过度设计一个复杂无用、华而不实的服务。能用简单的方法去实现,就别把它搞复杂。目的是为了实现功能、解决问题,而不是炫技。使用一些常见,久经考验的实现方式比一些炫技复杂难理解的实现方式更容易让人接受。

比如 Golang 中将 []byte 转成 string。

// Good
// 直观且安全
x := []byte("Hello World!")
y := string(x)

// Bad
// 虽然性能好,但是不常用且不安全
import "unsafe"

x := []byte("Hello World!")
y := *(*string)(unsafe.Pointer(&x))

8.避免无效请求

不要传递无效请求至下游。

无效请求下游应及早检测发现并拒绝,可能会引发相关入参无效的告警,混淆视听且骚扰。我们应避免传递无效请求至下游,避免浪费带宽和计算资源。

换位思考,谁都不想浪费力气做无用功。

9.入参校验

自己收到的请求要做好入参校验,及早发现无效请求并拒绝,然后告警。发现垃圾请求后推动上游不要传递无效请求至下游。

此时,我们是上游的下游,做好入参校验,避免做无用功。

10.设计模式

适当的使用设计模式,让我们的代码更加简洁、易读、可扩展。

设计模式(Design Pattern)是一套被反复使用、多人知晓、分类编目、代码设计经验的总结。使用设计模式可以带来如下益处。

  • 简洁。比如单例模式,减少多实例创建维护的成本,获取实例只需要一个 Get 函数。
  • 易读。业界经验,多人知晓。如果告知他人自己使用了相应的设计模式实现某个功能,那么他人便大概知晓了你的实现细节,更加容易读懂你的代码。
  • 可扩展。设计模式不仅能简洁我们的代码,还可以增加代码的可扩展性。比如 Go 推崇的 Option 模式,既避免了书写不同参数版本的函数,又达到了无限扩增函数参数的效果,增加了函数扩展性。

11.禁用 flag 标识

为什么接口不要使用 flag 标识,因为这会使接口变得臃肿,违背单一职责,最终难以维护。

这里说下,我们为什么会使用 flag 标识。

有时,我们需要提供一个读接口供上游调用查询相关信息。如主调 A 需要信息 a,主调 B 需要信息 b,主调 C 需要信息 c,主调 D 需要信息 a 和 b。如果为每个主调获取信息都提供单独的接口,那么接口会变得很多。为了减少接口的数量,我们很容易想到给接口增加多个 flag 参数,每个主调在调用接口时携带不同的 flag,表明需要获取哪些信息,然后接口根据入参 flag 获取对应的信息。比如主调 A 调用时将 flag_a 置为 true,主调 B 将 flag_b 置为 true,主调 C 将 flag_c 置为 true,主调 D 将 flag_a 和 flag_c 置为 true。

在项目前期或者 flag 数量较少的情况下,接口功能不是很多时,一般不会暴露出问题。一但开了这个口子,随着需要不同信息主调的增多,接口会不停的增加 flag,最终导致接口变得庞大臃肿,不仅难以阅读维护,还会使接口性能低下。

所以,我们应该禁用 flag 标识,尽可能地保证接口功能单一。

回到上面提到的场景,不适用 flag 标识,我们改如何是好呢?

我们应该坚持单一职责的原则,将信息进行原子分割,每个原子信息作为一个独立的接口对外提供服务。如果需要多个原子信息,我们可以增加一个 proxy 层,以独立接口将需要的相关原子信息汇聚组合。这么做你可能会问,接口变多了,会导致服务难以维护。不用担心,如果服务接口数量过多,我们应该对服务进行拆分。

还是以上面提及的例子为例,接口禁用 flag 前后组织形式对比如下:

12.分页宜小不宜大

对于查询 API 来说,当查询结果集包含成千上万条记录时,返回所有结果是一个挑战,它给服务器、客户端和网络带来了不必要的压力,于是便有了分页接口。

通常我们通过 page 和 size(offset 和 limit)或 after_id 和 limit 实现分页。page 与 size 适合数据总量小的浅分页查询,after_id 和 limit 适合数据总量大的深分页查询。

在设计分页接口时,页大小需要设置上限。这是因为如果没有上限,客户端可以请求任意大的页大小,从而可能导致服务器性能问题,例如一次请求返回过多数据,导致服务器响应变慢,网络传输时间变长,甚至可能引起系统崩溃等问题。

为了防止这种情况的发生,通常会在设计分页接口时设置一个最大页大小限制。当客户端请求的页大小超过最大限制时,应该向客户端返回一个错误提示,告知客户端页大小超过最大限制,建议客户端减小页大小,以保证服务器和客户端的正常运行。

那么页大小设为多少合适呢?

常见的页大小有 10,20,50,100,500 和 1000。如何选择页大小,我们应该在满足特定业务场景需求下,宜小不宜大。

太大的页,主要有以下几个问题:

  • 影响用户体验。页太大,加载会比较慢,用户等待时间会比较长。
  • 影响接口性能。页太大,会增加数据的拉取编解码耗时,降低接口性能。
  • 浪费带宽。很多场景下,用户在浏览的过程中,不会看完一页中的所有数据,返回太大的页是一种浪费。
  • 扩展性差。随着业务的发展,接口在页大小不变的情况下,返回的页数据可能会越来越大,导致接口性能越来越差,最终拖垮接口。

页大小多少合适,没有标准答案,需要根据具体的业务场景来定。但是要坚持一点,页宜小不宜大。如果页大小能用 10 便可满足业务需求,就不要用 20,更不要用 50。


参考文献

SOLID - wikipedia

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

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

相关文章

摩托罗拉刷机包和固件下载地址

发现了一个非常好的摩托罗拉刷机包和固件下载地址:https://firmware.center/ 里面包含了所有的摩托罗拉的刷机包和软件、电路图等等,非常多,我想镜像到本地网盘,但不知道怎么操作,有没有懂得朋友教我全部镜像到国内的…

Kafka生产者(二)

1、生产者消息发送流程 1.1 发送原理 在消息发送的过程中,涉及到了两个线程——main 线程和 Sender 线程。在 main 线程中创建了一个双端队列 RecordAccumulator。main 线程将消息发送给 RecordAccumulator,Sender 线程不断从 RecordAccumulator 中拉取…

Gamma AI:一键生成专业级PPT的智能工具

1. Gamma 简介 Gamma 是一个致力于通过非常简单的ai交互,制作好的视觉体验作品,它始终站在作者的视角新增功能,同时注重观众视角呈现作品。 突破了以往演示文档(ppt、pdf、网站)表现形式,能够借助ai的力量…

informer中的WorkQueue机制的实现分析与源码解读(1)

背景 client-go中的workqueue包里主要有三个队列,分别是普通队列Queue,延时队列DelayingQueue,限速队列RateLimitingQueue,后一个队列以前一个队列的实现为基础,层层添加新功能。 workqueue是整个client-go源码的重点…

每日学术速递8.5—1

1.SV4D: Dynamic 3D Content Generation with Multi-Frame and Multi-View Consistency 标题: SV4D:具有多帧和多视图一致性的动态 3D 内容生成 作者:Yiming Xie, Chun-Han Yao, Vikram Voleti, Huaizu Jiang, Varun Jampani 文章链接&…

LinuxC++(10):调用可执行程序

认识system函数 可以直接用system在代码中实现调用shell命令 /bin/ls -l /tmp表示执行ls -l命令,打开/tmp地址 而前面的/bin/表示这是shell命令,不可少,可以认为,/bin/后面的就是等价于shell里面输入的命令。 然后,cou…

* (头指针分离自 9822ba4) ,提交代码不能到分支——游离分支

背景 通过git checkout commitId(之前的一个版本); 基于这个版本修改提交代码推送代码,但是远端没有更新最新数据。 操作 通过git checkout commitId(之前的一个版本);通过git branch 查看分支情况,发现所处分支在游离分支:切换到master分…

连接池的原理

文章目录 1. 连接池的含义2. 连接池的作用2.1 不使用连接池的情况2.2 使用连接池的情况 3. 连接池和线程池的关系4. 连接池设计要点5. 使用实测 1. 连接池的含义 数据库连接池(Connection pooling)是程序启动时建立足够的数据库连接,并将这些…

《Unity3D网络游戏实战》学习与实践

纸上得来终觉浅,绝知此事要躬行~ Echo 网络上的两个程序通过一个双向的通信连接实现数据交换,这个连接的一端称为一个Socket “端口”是英文port的意译,是设备与外界通信交流的出口。每台计算机可以分配0到65535共65536个端口 每一条Sock…

Java | Leetcode Java题解之第322题零钱兑换

题目&#xff1a; 题解&#xff1a; public class Solution {public int coinChange(int[] coins, int amount) {int max amount 1;int[] dp new int[amount 1];Arrays.fill(dp, max);dp[0] 0;for (int i 1; i < amount; i) {for (int j 0; j < coins.length; j)…

基于springboot+vue+uniapp的智慧物业平台小程序

开发语言&#xff1a;Java框架&#xff1a;springbootuniappJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包&#…

c++ 容器 vector

vector的意思就是向量&#xff0c;就是一个顺序表的意思&#xff0c;这个顺序表可以存任意的类型&#xff0c;因为其线性的内存特点&#xff0c;所以在stl里是经常被使用的存在。 vector vector既然要能储存任意的变量&#xff0c;那么就必须使用模板: 这里的T就是变量类型&a…

【QT】鼠标按键事件 - QMouseEvent QKeyEvent

qt 事件 事件1. 事件概念2. 事件的处理3. 按键事件&#xff08;1&#xff09;单个按键&#xff08;2&#xff09;组合按键 4. 鼠标事件&#xff08;1&#xff09;鼠标单击事件&#xff08;2&#xff09;鼠标释放事件&#xff08;3&#xff09;鼠标双击事件&#xff08;4&#x…

【多线程-从零开始-叁】线程的核心操作

一、创建一个线程-start() start 和 run 的区别&#xff1a;&#xff08;经典面试题&#xff09; run 描述了线程要执行的任务&#xff0c;也可以称为“线程的入口”此处 start 会根据不同的系统&#xff0c;分别调用不同的 API&#xff0c;来执行系统函数&#xff0c;在系统…

原生多模态跟GPT聊天部分测试,大家都用他来做什么;字节推出AI音乐产品-海绵音乐,可以媲美Udio和Suno

✨ 1: 跟GPT聊天 原生多模态跟GPT聊天部分测试&#xff0c;大家都用他来做什么。 说各个国家的语言&#xff0c;例如普通话&#xff0c;或者是广东话等。 ChatGPT担任富有激情的足球比赛解说员 使用新的高级语音模式 视觉&#xff0c;进行实时日语翻译&#xff01; 地址&…

Java编程规范 空格

public static void main(String[] args) { // 缩进4 个空格 String say "hello"; // 运算符的左右必须有一个空格 int flag 0; // 关键词if 与括号之间必须有一个空格&#xff0c;括号内的f与左括号&#xff0c;0与右括号不需要空格 if (flag 0) { System…

秃姐学AI系列之:模型选择 | 欠拟合和过拟合 | 权重衰退

目录 训练误差 泛化误差 验证数据集和测试数据集 验证数据集 Validation Dataset&#xff1a; 测试数据集&#xff1a; K-则交叉验证 总结 过拟合和欠拟合 模型容量 模型容量的影响 估计模型容量 数据复杂度 总结 权重衰退 weight decay 使用均方范数作为硬性…

【八】Zookeeper3.7.1集成Hadoop3.3.4集群安装

文章目录 1.基本原理2.下载并解压ZooKeeper3.配置环境变量4.配置ZooKeeper5.创建数据目录并初始化myid6.启动ZooKeeper7.配置ZooKeeper集成到Hadoop8.重启Hadoop9.ZooKeeper状态检查 1.基本原理 ZooKeeper 是一个分布式协调服务&#xff0c;用于分布式系统中管理配置信息、命名…

51单片机—智能垃圾桶(定时器)

一. 定时器 1. 简介 C51中的定时器和计数器是同一个硬件电路支持的&#xff0c;通过寄存器配置不同&#xff0c;就可以将他当做定时器或者计数器使用。 确切的说&#xff0c;定时器和计数器区别是致使他们背后的计数存储器加1的信号不同。当配置为定时器使用时&#xff0c;每…

vue3 手写日历组件

找了很久vue3的element样式一直没办法修改实现。只能手写日历了。借鉴了一些大佬的代码 调用&#xff1a; 再要使用的地方引入 import calendarelement from ./calendarelement.vue //日历组件 <div > <calendarelement /> //日历</div> 效果&#…