如何将单体分解成微服务?

本文您推荐采取三个领域驱动的步骤,能使您的代码库变得更易于管理

毫不讳言,在单体(整体/铁板一块monolith)架构中编写代码是容易的。我们可以随时直接查询数据库,在应用程序的其他部分调用我们想要的任何功能,而不必考虑整体架构组织,因为我们正在向现有架构插入新代码。然而,这种类型风格的发展会导致脆弱的混乱的代码库,其中对应用程序的一部分任何改变都可能会改变甚至破坏其他部分的正常功能,而且没有人知道为什么。

不仅如此,部署也变得困难,而且为新开发人员进入代码库创造了一个糟糕的学习曲线。这些都是不可取的,许多发现自己处于这种情况的人开始阅读并理解基于服务架构(SOA)的优势。问题是 - 单体monolith变得越大,越难分解。

虽然事情是很难的,但却不会变成不可能。话虽如此,我们仍然在感性上感觉到这是不可能,因为从一个混乱的单体直接转到SOA是不太可行的。必须有某种中介状态,这样可以更容易地开始突破。

这个中介状态虽然仍然是一个单体,但是由业务领域进行组织的,而不是原始代码库的纠缠或脆弱的状态。一旦我们达到这一阶段,对于我们的应用程序的未来再做出决定就要容易得多,特别是关于决定分解哪些服务,哪些部件应该保持在一起。

在这篇文章中,我们将介绍领域驱动设计(DDD),然后再通过三个步骤,将一个凌乱的单体转化为刚刚描述的有组织的中间状态。


领域驱动设计
在开始任何重构之前,我们必须弄清楚我们应用程序的业务领域。最简单的方法是将应用程序分解成可以根据业务逻辑来解释的组件。例如,在结算应用程序中,某些领域可能是地址验证,运输,税收和支付处理。在软件中,这种分解或构建您的代码是根据业务逻辑而进行组织的行为称为领域驱动设计(DDD)。

DDD的主要概念之一是在一些有界上下文环境中通过子域来组织您的代码职责功能。在我们的结算应用程序的示例中,结算是一个较大的电子商务平台内的有限上下文,而运输和税收将分别是结算上下文中的子域。


在上图左侧,所有逻辑都在一个进程中处理,造成相互依赖的关系。在右边,所有的职责功能都是由不具有交叉点的域分解出来的。

领域驱动设计可以帮助我们实现SOA的原因在于,通过将代码分解成单个的领域,我们实际上已经创建了类似于服务的部件。

然后,如果需要,这些类似服务的部件可以被分解成主要应用程序所要求的相应的微服务。或者,如果您决定保留单体架构,您现在可以在易于迭代的状态下轻松理解应用程序中发生的情况。

至此,我们可以采取下面一些步骤来组织我们的代码。

步骤1-根据领域“检疫”您的业务逻辑,以消除相互依赖关系

“检疫”一词在这里是有目的选择的一个词语,因为它不仅意味着隔离。通过隔离,我们不只是将功能分离成不同的文件,我们希望进一步,甚至不让它们接触应用程序的其他部分,除非我们特别要通过注入一个依赖关系。

在构建应用程序时,通常情况下,您的逻辑将触及许多不同的领域。让我们再考虑电子商务应用程序中的结算流程。单个结算涉及验证送货地址,计算税率,从运营商处获取运送费用,验证库存是否可用,...有很多步骤。这些完全可以在一个过程中处理所有的结算步骤,但这只会使您的代码难以维护,几乎不可能进行测试。相反,我们要完全分离所有与这些部分相关的逻辑,使它们完全不相互接触。

保持您的应用程序逻辑按领域排序,是建立基于服务架构的第一步。

如果您拥有控制器或实用程序文件,具有不同功能的抓取包,请先分开它们并通过职责功能进行组织,并删除每个功能的相互依赖关系。

步骤2 - 定义您的接口,并隐藏所有其他内容

在一个单体代码库中工作的缺点之一是新的开发人员有一个巨大的学习曲线。当他第一次看到代码库时已经被巨大单体代码的气势压倒,如同面对一座大山或巨石,一切逻辑都混在一个地方,你不知道谁调用谁和在哪里调用。所有的逻辑都在一个地方,你不知道从哪里开始。

作为开发人员,我们不应该了解应用程序的其他部分的内部工作,这样才能方便快速进入代码库工作。

DDD为我们带来的优势之一是它允许我们从业务逻辑的角度考虑我们的应用程序。

每个领域中的所有逻辑都应该由一个单一的接口来表示,这个接口可以用来理解隐藏在其中的一切功能。

以下是计算订单税率的界面示例。


// iTaxCalculator.php
include ValueObject\Address;
include ValueObject\TaxRate;
interface iTaxCalculator
{
/**
* @throws InvalidArgumentError
* @return bool
*/

public function setTaxNexus(array $addresses);
/**
* @return bool
*/

public function setDestination(Address $address);
/**
* @return bool
*/

public function setShippingOrigin(Address $address);
/**
* @return \ValueObject\TaxRate
*/

public function getTaxRate();
}

只要看这个接口,我们就可以推断出它的工作原理,甚至不用看代码。它使用目的地地址,运输来源和商店的税务关系来获得订单的税率。即使在一个单体代码库中,这种在单个接口背后隐藏所有细节的做法也是一个很好的习惯。

步骤3-使用不可变值对象

今天许多流行的编程语言,包括我主要使用的两个:PHP和JavaScript,使得很容易使用关联数组或对象作为容器传递信息。破坏我们的代码库的本质就是大量的数据流过我们各种新的组件并将它们流粘在一起。

其实,只是传递简单的普通对象就可以了,如果有某种合约能说明每个子域中的内容以及将要从中退出的内容,那将是很好的。如果这些对象是不可变的,那么也是很好的,只有在明确定义了setter时才可以显式更改这些对象。

这就是使用值对象的地方。值对象不是模型实例,它们没有ID。它们是具有定义属性的不可变信息容器,它们的状态完全取决于其值。这是一个例子:


//TaxRate.php
class TaxRate
{
/**
* @var float
*/

private $tax_rate;
/**
* @var string add|base|instead
*/

private $rate_type;
public function __construct($tax_rate, $rate_type)
{
$this->tax_rate = $tax_rate;
$this->rate_type = $rate_type;
}
/**
* Get the tax rate
*/

public function getTaxRate()
{
return $this->tax_rate;
}
/**
* Get tax type
*/

public function getTaxType()
{
return $this->tax_type;
}
}

起初看起来似乎是很乏味的,但这使我们有信心在税率上运行的系统中,我们将始终使用这个税率值对象,而不是随机的一堆blob值包装,可能有也可能没有我们需要的领域。

概要
从单体代码到SOA是一项艰巨的任务,不可能一步完成。如果您发现自己的代码库变得太大,无法快速迭代,请立即开始尝试将其分解或破解。使用本文中描述的DDD概念将您的单体组织架构分解到明确的子域中。一旦做到这一点,再开始将代码分解为单独的微服务将变得更加容易。


How to Organize your Monolith Before Breaking it i