我在职业生涯中参与了许多Symfony项目,其中客户经常向我们的公司寻求帮助的一个常见问题是他们的软件被锁定在旧的框架版本中,或者由于寻找和修复错误的成本太高而难以维护。
通常,我试图深入了解为什么这些遗留项目处于那种状态。我经常发现一个常见的模式:在项目开始时,团队需要在严格的期限内快速创建一个应用程序。通常,他们会这样开始:
- 使用composer安装Symfony骨架项目
- 删除演示代码
- 自动生成实体
- 自动生成控制器
- 准备开发应用程序
对我来说,这些步骤不是最佳实践,因为他们立即开始编写代码,而不是理解域和行为。我认为在先前解释的情况下,他们是被框架引导的。在我看来,更好的方法是集中精力于领域,并将Symfony(或框架一般)视为工具而不是软件的主要核心,因为你的软件的真正价值在于领域,你实现解决问题的解决方案。
从框架引导具有许多副作用,其中最危险的是将领域和框架耦合在一起,这可能会引起许多问题,例如:
- 不可能升级框架和供应商
- 维护成本高,因为每个错误或新功能都需要花费大量时间才能完成
- 开发人员没有动力,因为堆栈对于第一个原因来说非常古老
- 不可维护的应用程序
- 许多技术债务
但是不要害怕,有一种架构可以帮助你避免这些问题:六边形架构。
六边形架构的历史
六边形架构是由Alistair Cockburn发明的,旨在避免面向对象软件设计中已知的结构陷阱,例如层之间的不必要依赖关系和用户界面代码与业务逻辑的污染,并于2005年发表。
六边形架构将系统分成几个松散耦合的可互换组件,例如应用程序核心、数据库、用户界面、测试脚本和与其他系统的接口。这种方法是传统分层架构的替代方案。 (维基百科)
当我阅读和解释这个定义时,许多开发人员会问我:这是一种过度设计的策略吗?
好吧,你有更多的类,更多的概念,以及更多的时刻需要思考类的正确位置、为类命名或更好的变量名;再次取决于你,我只能建议尝试应用此策略并提高自己的技能。
实际问题
写于10年前的项目被锁定在旧的PHP版本中,你希望迁移到新版本。升级PHP意味着你需要升级框架和供应商,并触及业务逻辑,因为一切都是耦合的。你无法以安全的方式升级,因为代码没有完全覆盖测试。在这种情况下,你有一个不可维护的应用程序。如果将领域和框架耦合在一起,所有这些问题都很常见。
使用六边形架构,你可以将框架和领域分开,因此可以通过触及代码的一小部分而不是业务逻辑来升级供应商和框架。将框架和领域分开实际上是指将它们拆分为不同的目录。我稍后会详细介绍它。
耦合代码的另一个很好的例子是当你拥有远程服务并且它们发生更改时。
假设你有一个支付网关提供程序,它发布了一个新版本,而你当前在应用程序中使用的版本已不再受支持。你可以切换到新版本或将其替换为另一个网关提供程序,但是你知道必须在整个项目中重构许多部分,因为你的领域与库或服务严密耦合。因此,你需要花费大量精力重写许多部分,并可能会引入错误。
使用六边形架构,你可以仅替换和更改适配器,而无需触及框架之外的业务逻辑,因为它与框架分离。
耦合领域和框架具有创建不可维护的应用程序的黑暗副作用。
可维护的应用程序
可维护性是指(减少)技术债务的缺失。技术债务是我们(错误)决策的债务,它需要在时间和挫败感中偿还。
可维护的应用程序是指以我们可以合理实现的最慢速率增加技术债务。
高度可维护的应用程序的措施是什么?
- 应用程序的一部分发生变化应尽可能少地影响其他地方
- 添加功能不应要求触及代码库中的任何部分
- 添加与应用程序交互的新方法应尽可能少地进行更改
- 调试应要求尽可能少的解决方法
- 测试应该相对容易
为了尽可能少地触及新功能或遗留功能的代码,重要的是委托具有单一责任的特定类。
单一责任
遵循的一个好的概念是代码的单一责任,但是架构也存在单一责任:出于相同原因的更改应该被分组,例如:
- 所有与框架有关的事情
- 所有与领域逻辑有关的事情
- 所有与API调用有关的事情
因此,我们可以在项目中创建最重要的区分:领域、应用程序和基础设施。
对于领域,我指的是:
- 实体:模型、值对象和聚合…
- 边界对象的接口
对于应用程序,我指的是:
- 用例(应用程序服务)
对于基础设施,我指的是
- 框架
- 边界对象的实现
- 控制器、CLI命令
为什么是六边形?
边数是任意的。重点是它有许多面。每个面代表我们应用程序的一个“端口”。每个端口都可以被适配器使用,使我们的系统正常工作。让我们深入解释一下端口和适配器的含义。
端口
端口类似于契约,因此它们在代码库中没有任何表示。
每个应用程序用例可以被调用的方式(通过UI、API等)都有一个端口,以及所有数据离开应用程序的方式(持久性、通知其他系统等)。Cockburn将这些称为主要和次要端口,通常开发人员称它们为输入和输出端口。
主要和次要是沟通意图和支持实现之间的区别。
端口示例:
interface ProductRepositoryInterface
{
public function find(ProductId $id): ?Product;
}
端口只是我们想要做的定义。它们并不表明如何实现它们。
适配器
适配器是端口的实现,因为对于这些抽象端口,我们需要一些代码来使连接工作。
它们非常具体,包含低级代码,并且根据定义与其端口解耦。
适配器示例:
public class MysqlProductRepository implements ProductRepositoryInterface
{ private $repository; public function __construct(ProductRepository $repository)
{
$this->repository = $repository;
} public function find(ProductId $id): ?Product
{
return $this->repository->find(id);
}
}
让我们尝试在实际系统中表示我们的端口和适配器
正如你所看到的,我们有CLI命令或HTTP请求调用我们基础架构层内的输入适配器。适配器在领域层内实现了我们的输入端口。另一方面,我们在基础架构层内有我们的输出适配器,它们在领域内实现了我们的输出端口,并且可以与像数据库这样的外部系统进行交互。
因此,在我们的PHP应用程序中,我们可以拥有如下结构:
在此示例中,你拥有两个不同的上下文:付款和购物车。在每个上下文中,在此示例中,存在领域、应用程序和基础架构之间的区别。并不是必须拥有所有这些目录,有时可能不存在应用程序层或基础架构层。
在你的领域中,你拥有不参考任何供应商(不总是正确的,例如通常在我的领域中我使用Ramsey/UUID)的领域逻辑。在此文件夹中,你还可以使用对象指定要使用数据的所有端口。
在你的应用程序文件夹中,你可以拥有服务和用例。
在你的基础架构文件夹中,你可以拥有框架代码和适配器,因此使用你喜欢的供应商和技术实现领域端口。
依赖反转原则
现在,如果你将六边形架构与依赖反转原则相结合,你可以再次改善你的项目。依赖反转原则意味着高层模块不应依赖于低层模块。两者都应依赖于抽象。
因此,基础架构类可以依赖于应用程序类和领域类。应用程序类可以依赖于领域类,但不能依赖于基础架构类。
领域类不能依赖于基础架构或应用程序类。
使用六边形架构的优势
对我来说,使用六边形架构有很多优势,例如:
- 将领域与基础架构分开,增加了可测试性,因为代码的许多部分不需要数据库连接、Internet连接或文件系统。你可以创建许多单元测试。
- 你可以替换适配器,而不会影响端口,你可以更改数据库,领域不需要更改。
- 你可以推迟选择供应商、数据库、服务器等,因为更重要的是对你的领域进行建模,因此你可以在需要做出选择时拥有更多的知识。
- 你可以更新供应商和框架,而不触及你的领域代码。
何时使用它
目前,我尝试始终使用此架构,因为当你开始以这种思维方式思考时,很难回头。
没有六边形架构的旧代码怎么办?
通常,对于不遵循此架构的旧应用程序,我建议团队开始尝试新事物,以使领域和代码更好、更清晰。
它始于创建新目录,如基础架构和领域。
现在,这些目录可以开发新的概念和功能。
对于旧功能,如果可能且较小,我尝试创建拉取请求以使用新架构迁移小概念。当我迁移旧的遗留代码时,我尝试遵循我喜欢的黄金法则:
留下比你发现更好的代码。
让我们再次改善我们的项目
为了改进你的领域和代码,我建议使用
- DDD(领域驱动设计)
- CQRS模式(命令查询责任分离)
- 事件溯源
- TDD
- BDD
所有这些概念、方法和方法都可以再次改善项目。
评论(0)