在本章中,我们将研究组织代码的不同方式,并介绍一个直接反映六边形体系结构的表达式包结构。
在新建的软件项目中,我们首先要做的是做好软件包的结构。我们建立了一个漂亮的结构,并打算在项目的其余部分使用。然后,在项目进行过程中,事情变得紧张起来,我们意识到在许多地方,包的结构只是一个漂亮的门面,而非结构化的混乱代码。一个包中的类从其他包中导入了不应该被导入的类。
我们通过在域包中引入AccountRepository接口并在持久化包中实现它
这个包层次不是最优的
1、首先,我们的应用程序的功能片或特征之间没有包的边界。如果我们 添加一个管理用户的功能,我们将在web包中添加一个UserController,在域包中添加一个UserService, UserRepository和User到域包,而UserRepositoryImpl到持久化包。如果没有进一步的结构,这可能很快就会成为一个混乱的类,导致应用程序的所谓不相关的功能之间产生不必要的副作用。
2、第二,我们无法看到我们的应用程序提供哪些用例。你能告诉我们 AccountService或AccountController类实现了哪些用例?如果我们要寻找某个功能,我们必须猜测哪个服务实现了它,然后在该服务中寻找负责的方法。
3、同样,我们也无法在包结构中看到我们的目标架构。我们可以猜测 我们遵循六边形的架构风格,然后浏览Web和 持久性包中的类,以找到网络和持久性适配器。但是,我们一眼就看不到web适配器的哪些功能是由web适配器调用的,以及持久性适配器为域层提供了哪些功能。传入和传出的端口都隐藏在代码中。
实质上,我们已经把所有与账户有关的代码放到了高级包账户中。我们 也删除了层包。
每一组新的特性都将得到一个新的高级包,我们可以通过对应该使用的类使用包私有可见性来强制特性之间的包边界 不能从外部访问。
包边界与包私有可见性相结合,使我们能够避免特性之间不必要的依赖关系。检查。
我们还将AccountService重命名为SendMoneyService,以缩小其责任范围(我们实际上也可以在逐层打包的方法中这样做)。现在我们可以看到,该代码实现了 "寄钱 "这一用例,只需看一下类的名称。使应用程序的功能在代码中可见。
然而,按特性打包的方法使我们的架构比按层打包的方法更不明显。我们没有包名来识别我们的适配器,而且我们仍然看不到 入和出的端口。更重要的是,尽管我们已经颠倒了域代码和持久化代码之间的依赖关系,使SendMoneyService只知道AccountRepository接口而不知道它的实现,但我们不能使用包-私有可见性来保护域代码不意外地依赖持久化代码。
那么,我们怎样才能使我们的目标架构一目了然呢?
在六边形架构中,我们有实体、用例、传入和传出的端口以及传入和传出的 和出站(entities, use cases, incoming and outgoing ports and incoming and outgoing )(或 "驱动 "和 “被驱动”)适配器作为我们的主要架构元素。让我们把它们 到一个表达这个架构的包结构中。
架构的每个元素都可以直接映射到其中一个包。在最高层。我们又有一个名为 "账户 "的包,表明这是实现 "账户 "用例的模块。
在下一个层次,我们有包含我们的领域模型的领域包。应用程序包包含了围绕这个领域模型的服务层。SendMoneyService实现了传入端口接口SendMoneyUseCase,并使用传出端口接口LoadAccountPort和UpdateAccountStatePort,这些都是由持久化适配器实现的。
适配器包包含调用应用层的入站端口的入站适配器以及为应用层的出站端口提供实现的出站适配器。在我们的例子中,我们正在建立一个简单的Web应用程序,其中包括适配器web和persistence,每个适配器都有自己的子包。
这种表达性的包结构促进了对体系结构的主动思考。我们有很多包,并且必须考虑要把我们目前正在处理的代码放到哪个包中。
但是,那么多的包难道不意味着一切都必须是公开的,以便允许跨包访问吗?
至少对于适配器包来说,这并不是真的。它们所包含的所有类都可能是包的私有类。因为除了通过端口接口,它们不会被外部世界调用,而端口接口是在
应用包内。所以,从应用层到适配器类之间没有意外的依赖关系类。检查。
然而,在应用程序和域包中,有些类确实必须是公开的。这些端口必须是公共的,因为它们必须在设计上可以被适配器访问。
将适配器代码移到他们自己的包里有一个额外的好处,即我们可以非常容易地用另一个实现替换一个适配器,如果需要的话。想象一下,我们已经开始
实现了一个简单的键值数据库,因为我们不确定哪个数据库是最好的,而现在我们需要切换到一个SQL数据库。我们只需在一个新的适配器中实现所有
相关的出站端口,然后删除旧的包。
这种包结构的另一个非常吸引人的优点是,它直接映射到DDD概念。在我们的例子中,高层包,即account,是一个有边界的上下文,它有专门的入口和
出口点(端口)来与其他受限上下文进行通信。在domain 包中,我们我们可以使用DDD提供的所有工具,建立我们想要的任何领域模型。
与每个结构一样,在软件项目的整个生命周期中维护这个包结构需要纪律。此外,在某些情况下,当包结构只是不适合,我们 除了扩大架构/代码差距并创建一个不反映架构的包之外,没有其他方法。
没有完美。但是通过一个表达性的包结构,我们至少可以减少代码和体系结构之间的差距。
上面描述的包结构对于一个清洁的体系结构有很长的帮助,但是这种体系结构的一个基本要求是应用程序层不依赖于输入和输出适配器,
对于传入的适配器,就像我们的web适配器一样,这很容易,因为控制流指向与适配器和域代码之间的依赖关系相同的方向。
适配器只是在应用层中调用服务。为了明确划分我们应用程序的入口,我们可能想在端口接口之间隐藏实际的服务。
对于传出的适配器,就像我们的持久性适配器一样,我们必须使用依赖关系倒置原则来将依赖关系与控制流的方向相反。
我们已经看到了这是如何运作的。我们在应用程序层中创建一个接口,它由适配器中的一个类实现。
在我们的六边形架构中,这个接口是一个端口。然后,应用程序层调用这个端口接口来调用适配器的功能,如图10所示。
web控制器调用一个传入端口,它由一个服务实现。该服务调用输出端口,由适配器实现
但谁为应用程序提供实现端口接口的实际对象呢?我们我们不希望在应用层中手动实例化端口,因为我们不希望引入对一个适配器的依赖。这就是依赖注入发挥作用的地方。我们引入了一个依赖于所有层的中立组件。这个组件负责实例化大多数类 我们的架构。
在上图的例子中,中立的依赖注入组件将创建类的实例。由于AccountController需要SendMoneyUseCase,依赖注入将在构建时给它一个SendMoneyService类的实例。控制器不知道它实际上得到了一个SendMoneyService实例,因为它只需要知道这个接口。
同样地,在构建SendMoneyService实例时,依赖注入机制将注入一个AccountPersistenceAdapter类的实例,以LoadAccountPort的名义接口的名义注入一个 AccountPersistenceAdapter 类的实例。该服务永远不知道接口背后的实际类。
我们已经看了一个六边形架构的包结构,它使实际的代码结构尽可能地接近目标架构。在代码中寻找架构的某个元素,现在只需要沿着架构图中某些方框的名称在包结构中进行导航即可。架构图中的某些方块的名称,沿着包结构向下导航,有助于沟通、开发和维护。
在接下来的章节中,我们将看到这种包结构和依赖性注入的作用,因为 我们将在应用层实现一个用例,一个Web适配器和一个持久化适配器。