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

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.向旧兼容

向旧兼容也称为向后兼容(Backward Compatibility),是指在软件系统或接口更新时,确保新版本的系统或接口能够正常运行旧版本的功能或与旧版本的客户端进行交互。向旧兼容性设计对于保持系统的稳定性和用户体验至关重要,尤其是在大规模分布式系统中,向旧兼容性可以防止新版本导致的业务中断或用户操作失败。

  • 【强制】新增功能:在接口更新时,尽量以增加新功能的方式而不是修改或移除旧功能。
  • 【强制】前后兼容的协议。设计协议时,允许接收端忽略无法识别的字段,而不是直接拒绝处理。比如新增协议字段,而不修改旧协议字段。
  • 【强制】默认值支持:如果必须增加新的参数或字段,确保它们具有合理的默认值,使旧版本的客户端无需更改即可继续使用。
  • 【建议】版本控制:引入版本控制机制,如在API路径中包含版本号(如 /api/v1/resource),确保旧版本的客户端可以继续访问原有的API版本,而不会受到新版本更改的影响。

13.向新兼容

向新兼容也称为向前兼容(Forward Compatibility),是指指系统、接口或协议在设计时考虑到未来的版本更新,使得当前版本能够与未来的版本兼容。这意味着即使未来的版本发生变化,当前的系统或客户端仍然能够正确地理解或处理这些变化,而不需要进行重大修改。

  • 【强制】兼容上游扩展。账户类型、品类、字段取值等,自身服务需要做到兼容,并在非法输入时,编写防御性代码来处理这些情况,做好容错。
  • 【建议】兼容性字段:在数据库设计中保留一些扩展字段或列,用于未来可能的功能扩展。

参考文献

SOLID - wikipedia
怎么理解软件设计中的开闭原则?- 知乎

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值