总览
六角体系结构是一种软件体系结构,它使应用程序可以由用户,程序,自动测试或批处理脚本平等驱动,并且可以独立于其运行时目标系统进行开发。目的是创建一个无需用户界面或数据库即可运行的应用程序,以便我们可以对该应用程序运行自动回归测试,在运行时系统(例如数据库)不可用时使用该应用程序,或无需用户界面即可集成应用程序。
动机
许多应用程序有两个目的:用户端和服务器端,通常以两层,三层或n层体系结构设计。n层体系结构的主要问题是没有认真对待层线,从而导致应用程序逻辑越过边界泄漏。业务逻辑和交互之间的这种纠缠使不可能或很难扩展或维护应用程序。
例如,当应用程序业务逻辑未完全隔离在其自身边界内时,添加新的有吸引力的UI以支持新设备可能是一项艰巨的任务。此外,应用程序可以有两个以上的方面,这使得很难更好地适应一维图层体系结构。
六角形或端口和适配器或洋葱结构解决了这些问题。在这种体系结构中,内部应用程序通过一定数量的端口与外部系统进行通信。在这里,术语“六角形”本身并不重要,而是表明了在应用程序中以均匀和对称的方式插入端口和适配器的效果。主要思想是通过使用端口和适配器隔离应用程序域。
在端口和适配器周围组织代码
让我们构建一个小型的anagram应用程序,以展示如何在端口和适配器周围组织代码以表示应用程序内部和外部之间的交互。在左侧,我们有一个应用程序,例如控制台或REST,而内部则是核心业务逻辑或域。anagram服务采用两个字符串,并返回一个布尔值,该布尔值对应于两个String参数是否彼此为字母。在右侧,我们有服务器端或基础结构,例如,一个用于记录有关服务使用情况的度量标准的数据库。
下面的Anagram应用程序源代码显示了如何在内部隔离核心域以及如何提供端口和适配器以与其进行交互。
域层
域层代表应用程序的内部,并提供与应用程序用例进行交互的端口。
· IAnagramServicePort 接口定义了一个方法,该方法接受两个String字并返回一个布尔值。
· AnagramService 实现该IAnagramServicePort接口并提供业务逻辑以确定两个String参数是否为anagram。它还使用IAnagramMetricPort来将服务使用度量输出到服务器端运行时外部实体(例如数据库)。
应用层
应用程序层为外部实体与域交互提供了不同的适配器。交互依赖项进入内部。
· ConsoleAnagramAdaptor 使用IAnagramServicePort来与应用程序内的域进行交互。
· AnagramsController 还使用IAnagramServicePort与域进行交互。同样,我们可以编写更多的适配器,以允许各种外部实体与应用程序域进行交互。
基础设施层
提供适配器和服务器端逻辑,以从右侧与应用程序进行交互。服务器端实体(例如数据库或其他运行时设备)使用这些适配器与域进行交互。请注意,交互依赖项位于内部。
外部实体与应用程序交互
以下两个外部实体使用适配器与应用程序域进行交互。如我们所见,应用程序域是完全隔离的,并且由它们平等地驱动,而不管外部技术如何。
这是一个使用适配器与应用程序域交互的简单控制台应用程序:
@Configuration
public class AnagramConsoleApplication {
4
@Autowired
5
private ConsoleAnagramAdapter anagramAdapter;
6
7
public static void main(String[] args) {
8
Scanner scanner = new Scanner([http://System.in](https://link.zhihu.com/?target=http%3A//System.in));
9
String word1 = scanner.next();
10
String word2 = scanner.next();
11
boolean isAnagram = anagramAdapter.isAnagram(word1, word2);
12
if (isAnagram) {
13
System.out.println("Words are anagram.");
} else {
System.out.println("Words are not anagram.");
}
}
}
这是一个简单的测试脚本示例,该脚本使用REST适配器模拟用户与应用程序域的交互。
@SpringBootTest
@AutoConfigureMockMvc
public class AnagramsControllerTest {
private static final String URL_PREFIX = "/anagrams/";
@Autowired
private MockMvc mockMvc;
@Test
public void whenWordsAreAnagrams_thenIsOK() throws Exception {
String url = URL_PREFIX + "/Hello/hello";
this.mockMvc.perform(get(url)).andDo(print()).andExpect(status().isOk())
.andExpect(content().string(containsString("{\"areAnagrams\":true}")));
}
@Test
public void whenWordsAreNotAnagrams_thenIsOK() throws Exception {
19
String url = URL_PREFIX + "/HelloDAD/HelloMOM";
this.mockMvc.perform(get(url)).andDo(print()).andExpect(status().isOk())
.andExpect(content().string(containsString("{\"areAnagrams\":false}")));
}
@Test
public void whenFirstPathVariableConstraintViolation_thenBadRequest() throws Exception {
String url = URL_PREFIX + "/11/string";
this.mockMvc.perform(get(url)).andDo(print()).andExpect(status().isBadRequest()).andExpect(
content().string(containsString("string1")));
}
@Test
public void whenSecondPathVariableConstraintViolation_thenBadRequest() throws Exception {
String url = URL_PREFIX + "/string/11";
this.mockMvc.perform(get(url)).andDo(print()).andExpect(status().isBadRequest()).andExpect(
content().string(containsString("string2")));
}
}
结论
使用端口和适配器,应用程序域在内部六边形处被隔离,并且无论外部系统或技术如何,它都可以由用户或自动测试脚本同样驱动。
最后,开发这么多年我也总结了一