本文的翻译是专门为Java Developer课程的学生准备的。
作为开发人员,我们经常不得不处理难以维护的旧代码。您知道在复杂的大型意大利面条式代码中理解简单逻辑有多么困难。改进代码或开发新功能成为开发人员的噩梦。软件设计的主要目标之一是易于维护。维护不善的代码变得难以管理。扩展不仅困难,而且吸引新开发人员也成为一个问题。在IT世界中,事情发展很快。如果要求您紧急实施新功能,或者要从关系数据库切换到NoSQL,您的第一反应是什么?
良好的测试覆盖范围可增强开发人员的信心,即新版本不会有任何问题。但是,如果您的业务逻辑与基础结构逻辑交织在一起,则其测试可能存在问题。
为什么是我?但是有足够的空话,让我们看看六角形的体系结构。使用此模板将帮助您提高可维护性,可测试性和其他好处。六角建筑概论
术语六角形结构(六角形,六边形结构)是由阿利斯泰尔科伯恩在2006年创造的。这种体系结构样式也称为“ 端口和适配器体系结构”。简而言之,应用程序的组件通过许多端点(端口)进行交互。要处理请求,必须具有与端口匹配的适配器。在这里,您可以与计算机上的USB端口进行类比。如果您有兼容的适配器(充电器或闪存驱动器),则可以使用它们。
该体系结构可以示意性地表示为一个六边形,在中心(核心)具有业务逻辑,被与其交互的对象以及控制它的组件所围绕,从而提供输入数据。在现实生活中,用户,API调用,自动化脚本和单元测试会相互影响,并为您的应用程序提供输入。如果您的业务逻辑与用户界面逻辑混合在一起,那么您将遇到许多问题。例如,很难将数据输入从用户界面切换到单元测试。该应用程序还与外部对象进行交互,例如数据库,消息队列,Web服务器(通过HTTP API调用)等。如有必要,迁移数据库或将数据上传到文件中,您应该能够做到这一点而不会影响业务。逻辑。顾名思义,“ 端口和适配器 ”是通过交互进行交互的“端口”,“适配器”是处理用户输入并将其转换为域的“语言”的组件。适配器封装了与外部系统(例如数据库,消息队列等)进行交互的逻辑,并促进了业务逻辑与外部对象之间的通信。下图显示了将应用程序划分为的层。
六角形体系结构区分了应用程序中的三层:域,应用程序和基础结构:- 域。该层包含核心业务逻辑。他不需要知道外部层的实现细节。
- 应用程序。层充当域和基础结构层之间的桥梁。
- 基础设施。实现域与外界的交互。对他来说,内层看起来像一个黑匣子。
根据此体系结构,两种类型的参与者与应用程序交互:主要(驱动程序)和次要(驱动程序)。关键角色发送请求并管理应用程序(例如,用户或自动测试)。次要的提供了与外界通信的基础结构(这些是数据库适配器,TCP或HTTP客户端)。这可以表示如下:
六边形的左侧由为域提供输入的组件组成(它们“控制”应用程序),右侧由由我们的应用程序控制的组件组成。例
让我们设计一个将存储电影评论的应用程序。用户应该能够发送带有电影名称的请求,并获得五个随机评论。为简单起见,我们将创建一个控制台应用程序,将数据存储在RAM中。对用户的响应将显示在控制台上。我们有一个向应用程序发送请求的用户(User)。因此,用户成为“经理”(驾驶员)。该应用程序应该能够从任何类型的存储中接收数据,并将结果输出到控制台或文件。受管理(驱动)的对象将是“数据仓库”(IFetchMovieReviews
)和“响应打印机”(IPrintMovieReviews
)。下图显示了我们应用程序的主要组件。
左侧是提供数据输入应用程序的组件。右侧是允许您与数据库和控制台进行交互的组件。让我们看一下应用程序代码。控制口public interface IUserInput {
public void handleUserInput(Object userCommand);
}
托管端口public interface IFetchMovieReviews {
public List<MovieReview> fetchMovieReviews(MovieSearchRequest movieSearchRequest);
}
public interface IPrintMovieReviews {
public void writeMovieReviews(List<MovieReview> movieReviewList);
}
受管端口适配器将从影片存储库(MovieReviewsRepo)中获得影片。在控制台上显示电影评论将是一堂课ConsolePrinter
。让我们实现上述两个接口。public class ConsolePrinter implements IPrintMovieReviews {
@Override
public void writeMovieReviews(List<MovieReview> movieReviewList) {
movieReviewList.forEach(movieReview -> {
System.out.println(movieReview.toString());
});
}
}
public class MovieReviewsRepo implements IFetchMovieReviews {
private Map<String, List<MovieReview>> movieReviewMap;
public MovieReviewsRepo() {
initialize();
}
public List<MovieReview> fetchMovieReviews(MovieSearchRequest movieSearchRequest) {
return Optional.ofNullable(movieReviewMap.get(movieSearchRequest.getMovieName()))
.orElse(new ArrayList<>());
}
private void initialize() {
this.movieReviewMap = new HashMap<>();
movieReviewMap.put("StarWars", Collections.singletonList(new MovieReview("1", 7.5, "Good")));
movieReviewMap.put("StarTreck", Arrays.asList(new MovieReview("1", 9.5, "Excellent"), new MovieReview("1", 8.5, "Good")));
}
}
域
我们应用程序的主要任务是处理用户请求。您需要获取胶片,对其进行处理,然后将结果传输到“打印机”。目前,我们只有一项功能-电影搜索。为了处理用户请求,我们将使用标准接口Consumer
。让我们看一下主类MovieApp
。public class MovieApp implements Consumer<MovieSearchRequest> {
private IFetchMovieReviews fetchMovieReviews;
private IPrintMovieReviews printMovieReviews;
private static Random rand = new Random();
public MovieApp(IFetchMovieReviews fetchMovieReviews, IPrintMovieReviews printMovieReviews) {
this.fetchMovieReviews = fetchMovieReviews;
this.printMovieReviews = printMovieReviews;
}
private List<MovieReview> filterRandomReviews(List<MovieReview> movieReviewList) {
List<MovieReview> result = new ArrayList<MovieReview>();
for (int index = 0; index < 5; ++index) {
if (movieReviewList.size() < 1)
break;
int randomIndex = getRandomElement(movieReviewList.size());
MovieReview movieReview = movieReviewList.get(randomIndex);
movieReviewList.remove(movieReview);
result.add(movieReview);
}
return result;
}
private int getRandomElement(int size) {
return rand.nextInt(size);
}
public void accept(MovieSearchRequest movieSearchRequest) {
List<MovieReview> movieReviewList = fetchMovieReviews.fetchMovieReviews(movieSearchRequest);
List<MovieReview> randomReviews = filterRandomReviews(new ArrayList<>(movieReviewList));
printMovieReviews.writeMovieReviews(randomReviews);
}
}
现在,我们定义一个CommandMapperModel
将命令映射到处理程序的类。public class CommandMapperModel {
private static final Class<MovieSearchRequest> searchMovies = MovieSearchRequest.class;
public static Model build(Consumer<MovieSearchRequest> displayMovies) {
Model model = Model.builder()
.user(searchMovies)
.system(displayMovies)
.build();
return model;
}
}
控制端口适配器
用户将通过界面与我们的系统进行交互IUserInput
。该实现将使用ModelRunner
并委派执行。public class UserCommandBoundary implements IUserInput {
private Model model;
public UserCommandBoundary(IFetchMovieReviews fetchMovieReviews, IPrintMovieReviews printMovieReviews) {
MovieApp movieApp = new MovieApp(fetchMovieReviews, printMovieReviews);
model = CommandMapperModel.build(movieApp);
}
public void handleUserInput(Object userCommand) {
new ModelRunner().run(model)
.reactTo(userCommand);
}
}
现在,让我们看一下使用上述界面的用户。public class MovieUser {
private IUserInput userInputDriverPort;
public MovieUser(IUserInput userInputDriverPort) {
this.userInputDriverPort = userInputDriverPort;
}
public void processInput(MovieSearchRequest movieSearchRequest) {
userInputDriverPort.handleUserInput(movieSearchRequest);
}
}
应用
接下来,创建一个控制台应用程序。托管适配器被添加为依赖项。用户将创建一个请求并将其发送到应用程序。该应用程序将接收数据,处理并显示对控制台的响应。public class Main {
public static void main(String[] args) {
IFetchMovieReviews fetchMovieReviews = new MovieReviewsRepo();
IPrintMovieReviews printMovieReviews = new ConsolePrinter();
IUserInput userCommandBoundary = new UserCommandBoundary(fetchMovieReviews, printMovieReviews);
MovieUser movieUser = new MovieUser(userCommandBoundary);
MovieSearchRequest starWarsRequest = new MovieSearchRequest("StarWars");
MovieSearchRequest starTreckRequest = new MovieSearchRequest("StarTreck");
System.out.println("Displaying reviews for movie " + starTreckRequest.getMovieName());
movieUser.processInput(starTreckRequest);
System.out.println("Displaying reviews for movie " + starWarsRequest.getMovieName());
movieUser.processInput(starWarsRequest);
}
}
有什么可以改进,改变的
- 在我们的实施中,您可以轻松地从一个数据存储切换到另一个数据存储。可以将存储实现注入到代码中,而无需更改业务逻辑。例如,您可以通过编写数据库适配器将数据从内存传输到数据库。
- 您可以实现“打印机”,而不是输出到控制台,该“打印机”会将数据写入文件。在这样的多层应用程序中,添加功能和修复错误变得更加容易。
- 要测试业务逻辑,您可以编写复杂的测试。适配器可以单独进行测试。因此,可以增加整体测试范围。
结论
可以注意到六角形体系结构的以下优点:- 伴奏 -松散耦合且独立的图层。在不影响其他层的情况下将新功能添加到一层变得很容易。
- 可测试性 -单元测试编写简单,执行迅速。您可以使用模拟依赖关系的存根对象为每一层编写测试。例如,我们可以通过在内存中建立数据仓库来消除对数据库的依赖。
- 适应性 -主要业务逻辑变得独立于外部对象的变化。例如,如果您需要迁移到另一个数据库,则我们不需要对域进行更改。我们可以为数据库创建适当的适配器。
参考文献
就这样。在课程中见!