Java中的六角形架构示例

本文的翻译是专门为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>();
        // logic to return random reviews
        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);
    }

}

有什么可以改进,改变的


  • 在我们的实施中,您可以轻松地从一个数据存储切换到另一个数据存储。可以将存储实现注入到代码中,而无需更改业务逻辑。例如,您可以通过编写数据库适配器将数据从内存传输到数据库。
  • 您可以实现“打印机”,而不是输出到控制台,该“打印机”会将数据写入文件。在这样的多层应用程序中,添加功能和修复错误变得更加容易。
  • 要测试业务逻辑,您可以编写复杂的测试。适配器可以单独进行测试。因此,可以增加整体测试范围。

结论


可以注意到六角形体系结构的以下优点:

  • 伴奏 -松散耦合且独立的图层。在不影响其他层的情况下将新功能添加到一层变得很容易。
  • 可测试性 -单元测试编写简单,执行迅速。您可以使用模拟依赖关系的存根对象为每一层编写测试。例如,我们可以通过在内存中建立数据仓库来消除对数据库的依赖。
  • 适应性 -主要业务逻辑变得独立于外部对象的变化。例如,如果您需要迁移到另一个数据库,则我们不需要对域进行更改。我们可以为数据库创建适当的适配器。

参考文献



就这样。在课程中

All Articles