* 原文地址：[The perils of shared code](https://www.innoq.com/en/blog/the-perils-of-shared-code)
* 原文作者：[Daniel Westheide](https://www.innoq.com/en/staff/daniel-westheide)
* 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)
* 译者：[Gocy](https://github.com/Gocy015/)
* 校对者：[marcmoore](https://github.com/marcmoore), [phxnirvana](https://github.com/phxnirvana)

# 代码复用的风险性

人们常说好心办坏事。在软件项目中，通过代码库来实现微服务之间的代码复用就是这样一种情况。几乎在所有采用了微服务架构的项目组织中，各个独立的团队和开发者都应该基于某些核心代码库来构建他们的微服务。很显然，尽管人们早已知道这其中潜在的问题，但依然有许多的人没有给予足够的重视。在这篇文章中，我将会论证为什么采用代码库可能起初看起来很有吸引力，又为何会成为一个麻烦，以及你应该如何缓解这些问题。

## 代码复用的目的

利用库来实现代码复用往往是为了这两个目的：共享域逻辑和基础组件层的抽象。

1. **可共享的域模型（ Shared domain model ）：** 域模型中的某一部分可能在多个 **有界上下文（ Bounded Contexts ）** 中是一样的，因此，相比起反复地实现这一段域逻辑，你会砍掉重复实现的需求并消除在多次实现中出现逻辑不一致的可能性。这部分人们想要共享的域逻辑一般是核心域或是数个泛型子域。在域驱动设计的术语中，这也被称作 **共享内核 （ Shared Kernel ）** 。通常，你会在这里面找到像 **会话控制（ Session ）** 、认证逻辑这样的概念，当然，其中的内容不仅仅局限于这些。与之相似的方法还有 **标准数据模型（ Canonical Data Model ）**。

2. **基础组件层的抽象（ Infrastructure layer abstractions ）：** 你希望避免反复地实现有用的基础组件层抽象逻辑，所以你把它们放到库中。通常，这些库提供了统一的数据库访问、消息发送、序列化接口以及其他各种服务。

上述两个目标的出发点是一致的 - 避免代码重复，即遵循 **DRY** 原则（ “ Don't repeat yourself ! ”）。这些逻辑只实现一次有以下几个好处：

- 你不需要花费宝贵的时间去解决那些已经解决过的问题。
- 有一套统一的消息发送、数据库访问及其他操作的接口，意味着当开发者在阅读或修改由他人编写的微服务模块代码时更加轻松。
- 你不需要担心业务逻辑或是基础组件的具体实现在不同模块之间出现细微的差异。取而代之的是，它们都只有一个统一而且正确的实现方式。

## 代码复用的问题

听起来非常棒的理论都有着自己的问题，而这些问题很可能比那些你用自己的代码库来解决的问题更加让人头疼。 Stefan Tilkov 已经详细解释了 [为什么你应该避免使用标准数据模型](https://www.innoq.com/en/blog/thoughts-on-a-canonical-data-model/) 。在他的基础之上，我再补充一些其它的问题。

### 分布式整体

通常，人们总下意识地认为把代码放入库中意味着永远都不需要担心其中的服务出现错误或是使用了过时的实现方式，因为他们只需要把依赖的库升级到最新版本就可以了。

当你依赖于通过升级自己的库，来对所有微服务的某些功能作出一致的改变时，你实际上在服务之间建立了强耦合关系。你因此而失去了微服务架构的一大优势，那就是各个服务更新迭代的相互独立性。

我见过许多这样的案例：所有的服务必须同时发布，以保证功能的正常使用。如果你的项目已经走到了这一步，那么毋庸置疑，你其实建立了一个分布式整体。

一个常见的案例就是用代码生成技术来为你的服务提供一个用户代码库，譬如使用 Swagger 描述你的服务 API 。开发者会比想象中更倾向于滥用这个功能来进行大的修改，因为依赖其服务的用户“仅仅”需要更新用户代码库的版本就行了。这可不是你 [迭代一个分布式系统](http://olivergierke.de/2016/10/evolving-distributed-systems/) 所应该做的。

### 依赖关系地狱


代码库，尤其是那些为了解决基础组件的问题而提供一个通用实现的库，往往有一个通病：它们引入了一大堆自身所依赖的库。你的库的传递依赖树越大，就越可能步入被称之为依赖关系地狱的噩梦。由于你的微服务很可能还要依赖其它具有传递依赖关系的库，迟早会有一些库传递性地引入一些版本冲突的库，而简单地在库的版本之间选择是不可行的，因为它们在二进制上无法兼容。（译者注：此处原文想表达的意思应该是，在庞大的依赖树中，可能有两个节点依赖了同一个库的不同版本，而这个共同依赖的库的两个版本之间无法兼容。）

当然，你可以通过让你的核心库依赖所有微服务所需要用到的库来解决这个问题。但这依旧意味着微服务不能够独立进行迭代更新，比方说你更新了微服务所依赖的某一个特定的库 - 这些微服务就都要与你的核心库的发布步调一致了。除此之外，在每个单独的服务可能都只需要依赖少数几个的库的情况下，你何必强制使它们依赖一大堆其它的库呢？

### 自顶而下的库设计方式

更多时候，我所见到的库常常是数名架构师强迫开发者实现的，这是一种自顶而下的库设计方式。

通常，这类库所暴露的 API 要么局限性强、缺乏灵活性，要么是使用了错误的抽象，因为它们的设计者对实际应用中存在的广泛的差异性了解不足。这样的库通常会让那些不得不使用它，和那些试图绕过其局限性的开发者遭受挫折。

### 使用统一语言进行约束

强制使用库所导致的一个最明显缺陷就是，迁移到不同的语言（或是平台，譬如 JVM 或 .NET ）变得更加困难，这同样失去了微服务架构的优势，即根据特定问题选择最合适的技术方案的能力。如果一段时间后你意识到这些库终究还是需要在不同的语言或环境中运行，你就必须要提供许多奇怪的支持。举个例子， Netflix 提供了一个 [Prana](https://github.com/Netflix/Prana) 的插件，这个插件运行了一系列非 JVM 服务，来为 Netflix 技术栈提供 HTTP API 。

## 我们能不能做得更好？

面对众多因采用库来实现代码复用而出现的问题，最极端的解决方案是直接不引入任何的库。这样做的话，你就得做一些复制-粘贴工作，或是为新的微服务模块提供一个模板工程以便将你的服务从上述的窘境中解放出来。基础组件相关的代码和域模型中的共享内核逻辑都可以这么做。实际上，在 Eric Evans 的经典小蓝书《 Domain-Driven Design 》中，他提到，“不同团队在各自的 KERNEL 副本中进行改动，并定期与其它团队进行整合”[[1]](#fn:1)。可见，共享内核并不一定要依赖库的形式。

如果你觉得复制粘贴不是个好的主意，也完全没问题。毕竟正如前文所说，利用库实现代码复用是有一定的好处的。这样做的话，有以下几个重要的事情需要考虑：

### 最少依赖的轻量库

试着把大型的共享代码库拆分成一系列小的、功能性强的库，每一个库都只解决一个特定的问题。试着让这些库仅仅依赖其实现语言的标准库。没错，只使用语言的标准库编程可能有时会不那么舒服，但和为公司中的所有团队（如果你的库是开源的，则不局限于你的公司）所带来的极大益处相比，这点麻烦微不足道。

当然，零依赖并不总是可行，尤其是考虑到那些基础组件相关的问题。对于这种情况，尽量减少每一个独立库的依赖。同时，有时候把集成了其它库的部分代码独立成一个单一的、与你的库的核心逻辑相互独立的模块也是可取的。

### 留下选择余地

永远不要认为在某个时间点那些服务会把你的共享库升级到最新版本。换句话说，不要强迫其它团队升级代码库，而应该给他们按照各自的进度进行升级的自由。尽管这意味着你需要把自己的库改造成能够前后兼容，但如此一来你便实现了服务间的解耦，不仅降低了微服务架构的操作成本，还为你带来了一些好处。

可能的话，不光要避免代码库的强制升级，还要让别人自行选择是否要使用你的库。

### 自底而上的库设计方式

最后，如果你真的要用共享库的方式，我所见到的成功的项目都是采用自底而上的方式设计代码库的。与实际用例中可用性低的象牙塔式库设计原则不同，让你的团队先实现各个微服务，仅当某些在产品中证明过自己的固定的模式在不同的服务中出现时，才将它们提取出来放入库中。

1.
Evans, Eric: Domain-Driven Design: Tackling Complexity in the Heart of Software, p. 355 [ ↩](#fnref:1)
