不要试图把一切都变成服务——首先组织你的整体
不要试图把一切都变成服务——首先组织你的整体
你可以采取 3 个领域驱动的步骤来使你的代码库进入一个更易管理的状态
让我们面对现实吧,编写代码成为一个整体的一部分是很容易的。我们可以随时直接查询数据库,在应用程序的其他部分调用我们想要的任何函数,并且不必过多考虑组织,因为我们正在插入一个现有的架构。然而,这种类型的开发导致的问题是一个脆弱的、纠缠不清的代码库,其中对应用程序一部分的任何更改都可能改变甚至破坏其他部分的某些东西,而没有人知道为什么。不仅如此,部署变得困难,这也为新开发人员进入代码库创造了一个可怕的学习曲线。这是完全不可取的,许多发现自己处于这种情况的人开始阅读和理解基于服务的架构(SOA)的优点。问题是——整块石头越大,就越难破碎。
但是仅仅因为事情很难,并不意味着它不可能。也就是说,会觉得不可能,因为从一个凌乱的整体直接转向 SOA 是不可行的。我们必须找到某种中间状态,让我们更容易开始打破僵局。这个中间状态仍然是一个整体,但它是由领域组织的,没有我们原始代码库的纠缠或脆弱性。一旦我们达到这一点,就很容易对我们的应用程序的未来做出决定,特别是关于决定将什么分解成服务以及哪些部分应该保持在一起。
【打破一个整体】可能感觉不可能,因为从一个混乱的整体直接转向 SOA 是不太可行的。必须有某种我们可以达到的中间状态,让我们更容易开始分解事物。
在这篇文章中,我们将介绍领域驱动设计(DDD ),然后通过三个步骤,你可以将一个杂乱的整体转化为刚才描述的有组织的中间状态。
领域驱动设计
在开始任何重构之前,我们必须弄清楚我们的应用程序的域是什么。最简单的方法是将应用程序分解成可以用业务逻辑来解释的组件。例如,在结帐应用程序中,一些域可能是地址验证、运输、税收和支付处理。在软件中,这种分解或构建你的代码以按照业务逻辑进行组织的行为被称为领域驱动设计(DDD) 。
DDD 的一个主要概念是在某个整体的有界上下文中,通过子域来组织你的代码职责。在我们的 checkout 应用程序的例子中,checkout 是一个大型电子商务平台中的有界上下文,而 shipping 和 tax 都是 check out 上下文中的子域。

On the left side, all of the logic is handled in a single process, creating interdependencies. On the right, all of the responsibilities are broken out by domain without intersection
领域驱动的设计能够帮助我们实现 SOA 的原因是,通过将我们的代码分解成单独的领域,我们本质上创建了类似服务的部分,这些部分组合在一起形成了我们的应用程序。然后,如果需要的话,这些类似服务的部分可以分解成它们自己的微服务,由主应用程序调用。或者,如果您决定保留 monolith,那么您现在就拥有了一个易于迭代且易于理解应用程序中正在发生的事情的状态。
领域驱动的设计可以帮助我们实现 SOA,因为通过将我们的代码分解成单独的领域,我们在本质上创建了类似服务的部分,这些部分组合在一起形成了我们的应用程序
也就是说,让我们进入一些可以用来组织代码的步骤。
步骤 1—基于域隔离您的业务逻辑,以消除相互依赖性
这里特意选择了“隔离”这个词,因为它不仅仅意味着隔离。通过隔离,我们不仅仅意味着将功能分离到不同的文件中,我们还想更进一步,甚至不让它们接触应用程序的其他部分,除非我们特别希望通过注入依赖来实现。
通过隔离,我们不仅仅意味着将功能分离到不同的文件中,我们还想更进一步,甚至不让它们接触应用程序的其他部分
在构建应用程序时,通常情况下,您的逻辑会涉及许多不同的领域。再想想电子商务应用程序中的结账过程。一次结账包括确认送货地址、计算税率、从承运商处获取运费、确认库存可用等等。完全有可能在一个过程中处理所有的签出,但是这只会使您的代码难以维护并且几乎无法测试。相反,我们希望将与这些部分相关的所有逻辑完全分离,这样它们就不会相互接触。
让您的应用程序逻辑按域排序,是拥有基于服务的体系结构的第一步。如果你有不同功能的控制器或实用程序文件,首先把它们分离出来,按职责组织起来,去掉每个功能的相互依赖性。
第二步——定义你的界面并隐藏其他所有东西
在单一代码库中工作的一个缺点是对于一个新的开发者来说有一个巨大的学习曲线。第一次看到一个代码库是一种压倒性的感觉,所有的东西都在一个地方,你不知道什么调用什么,在哪里发生。所有的逻辑都在一个地方,你不知道从哪里开始。
作为开发人员,我们不应该为了在代码库中工作而必须理解应用程序其他部分的内部工作方式。DDD 带给我们的一个优势是,它允许我们从业务逻辑的角度来思考我们的应用程序。每个领域中的所有逻辑都应该用一个接口来表示,这个接口可以用来理解隐藏在它后面的所有东西的功能。
作为开发人员,我们不应该为了在代码库中工作而必须了解应用程序每个其他部分的内部工作方式…每个领域中的所有逻辑都应该由一个单一的接口来表示,该接口可以用来了解隐藏在它背后的所有功能。
下面是一个计算订单税率的界面示例。
// iTaxCalculator.phpinclude 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.phpclass 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;
}
}
乍一看,这样做似乎是乏味和不必要的,但它给了我们信心,在一个根据税率操作的系统中,我们将始终使用税率,而不是随机的值块,这些值块可能有也可能没有我们需要的字段。
摘要
从单一代码到 SOA 是一项艰巨的任务,不可能一步到位。如果你发现自己的代码库变得太大而无法快速迭代,不要马上开始尝试修改它。相反,把你的目标定为用这篇文章中描述的 DDD 概念把你的独石组织成定义明确的子域。一旦这样做了,开始将代码分解为单独的服务就容易多了。