【问题标题】:Cross module communication in modular monolith模块化单体中的跨模块通信
【发布时间】:2023-01-04 02:12:06
【问题描述】:

我一直在这篇文章中学习模块化单体项目结构:https://codewithmukesh.com/blog/modular-architecture-in-aspnet-core

其中大部分对我来说很有意义,但我不太明白的是:

跨模块通信只能通过接口/事件/内存总线进行。跨模块数据库写入应保持最少或完全避免。

跨模块通信究竟是什么样子的?

假设我有 3 个模块:

  • 产品
  • 用户
  • 安全

我的安全模块为 DisableUser 注册了一个端点。此端点的工作是更新 User 以及与具有禁用状态的用户关联的每个 Product

Security模块如何在一个工作单元中调用User & Product更新状态方法?

我的理解是,这种模式旨在让以后更容易将模块提取到微服务,所以我想将它作为某种任务可以更容易地更改为消息代理,但我只是不确定如何这应该看起来。

我的例子显然是做作的,我的主要观点是当涉及读/写时模块如何相互通信?

【问题讨论】:

  • 意思(我的解释)是通信不会通过像 Kafka 这样的消息代理进行。相反,您可以在共享项目中定义要订阅的消息,以便各个模块注册。这可以通过传统事件或多播委托来完成,或者如果您按照项目建议使用 MediatR,您将在共享项目中定义一些接口 IMyEventNofitication : INotificationHandler<MyEvent>,并在每个想要订阅的模块中使用您的逻辑实现它事件。然后您通过 MediatR 发布所述事件。
  • 补充一下:虽然 MediatR 鼓励命令查询分离,但它并不直接强制执行。在这种情况下,INotificationHandler 可能应该被认为是一个命令,因此应该只由假设意图是改变状态的将派发给它的命令注册。如果使用此基础架构,您在某个时候决定过渡到微服务,您将重新定义通知处理程序,而不是推送到您的消息代理,以供其他服务接收,并让其他相关服务使用这些消息。

标签: c# asp.net-core design-patterns architecture modular-monolith


【解决方案1】:

理论

此类问题中对术语有很多误解,所以让我们标记 2 个完全不同的架构 -单体架构微服务架构.因此,介于两者之间的一种架构是模块化单体架构.

单体架构大多有一个大问题 - high coupling and low cohesion 因为你没有强大的方法来避免它。因此,程序员决定考虑构建不同架构的新方法,以真正解决高耦合低内聚问题。

微服务架构是一个解决方案(尽管它也解决了其他问题)。微服务架构的要点就是分离服务彼此避免高耦合(因为在服务之间设置通信并不像在单体架构中那么容易)。

但是程序员无法通过“单击”从一种架构转移到完全不同的架构,因此从整体架构构建微服务架构的一种(但不是唯一)方法是模块化整体首先(只是解决高耦合低内聚问题但在整体中)然后轻松地将模块提取到微服务。

沟通

为了降低耦合度,我们应该关注服务之间的通信。 让我们处理您在问题中提出的示例。

想象一下我们有这样的单体架构:

我们肯定会在这里看到高耦合问题。假设我们想要构建它更加模块化。为此,我们需要在模块之间添加一些东西以将它们彼此分开,我们还希望模块能够进行通信,因此我们唯一需要添加的就是总线。

像这样的东西:

附言可以完全分离而不是 im-memory 总线(如 kafka 或 rabbitmq)

所以你的主要问题是关于如何在模块之间进行通信,有几种方法可以做到这一点。

通过接口通信(同步方式)

模块可以通过接口直接(同步)相互调用。接口是一种抽象,所以我们不知道接口背后是什么。它可以是模拟的或真正的工作模块。这意味着一个模块对其他模块一无所知,它只知道与之通信的一些接口。

public interface ISecurityModule { }
public interface IUserModule { }
public interface IProfileModule { }

public class SecurityModule : ISecurityModule
{
    public SecurityModule(IUserModule userModule) { } // Does not know about UserModule class directly
}

public class UserModule : IUserModule
{
    public UserModule(IProfileModule profileModule) { } // Does not know about ProfileModule class directly
}

public class ProfileModule : IProfileModule
{
    public ProfileModule(ISecurityModule securityModule) { } // Does not know about SecurityModule class directly
}

毫无疑问,您可以通过方法调用在接口之间进行通信,但是这种解决方案并不能很好地解决高耦合问题。

总线通讯(异步方式)

总线是在模块之间建立通信的更好方式,因为它强制您使用事件/消息/命令进行通信。您不能再直接使用方法调用。

为此,您应该使用一些总线(分离的或内存中的库)。我建议检查其他问题(如this)以找到为您的体系结构构建此类通信的正确方法。

但请注意 - 使用总线使模块之间的通信异步,因此它迫使您重写内部模块行为以支持这种通信方式。

关于您使用 DisableUser 端点的示例。 SecurityModule 可以在安全模块中禁用用户的总线中发送命令/事件/消息 - 因此其他服务可以处理此命令/事件/消息并使用当前模块逻辑“禁用”它。

下一步是什么

接下来是一个微服务架构,具有完全分离的服务,通过分离的总线与分离的数据库进行通信:

例子

不久前,我完成了课程后完全在微服务架构中的项目。
如果您需要好的微服务架构示例,请查看here

图像是使用 Excalidraw 创建的

【讨论】:

  • 感谢您的答复。在构建模块化单体方法时,内存总线是否合适?在我看来是这样。它不会因序列化而减慢速度,并且如果您将模块转换为微服务,则可以更轻松地交换到持久消息总线。我问这个是因为到处都在讨论内存中的消息总线,他们指出它不是用于生产,但我假设他们指的是微服务结构而不是单体。
  • @Guerrilla 如果您有单体应用,我认为在生产中使用内存总线没有任何缺点,但是,同样,这肯定取决于您的单体应用的架构。你应该考虑可扩展性、错误处理、模块之间的同步/异步通信,但作为未来构建微服务的一个步骤——这是一个很好的步骤。
【解决方案2】:

乍一看,我认为一种方法是使用 Mediator 事件,因为项目已经在使用它。它会很好地工作并将所有内容分开。

To define Mediator event check this.
您在共享核心中定义事件,例如:

public class UserDisabled : INotification
{
    public string UserId { get; set; }
}

当用户被禁用时,您将在用户模块中发布事件

await mediator.Publish(new UserDisabled{UserId = "Your userId"});

最后在每个需要对事件做出反应的模块中声明事件处理程序

public class UserDisabledHandler : INotificationHandler<UserDisabled>
{
    public UserDisabledHandler()
    {
        //You can use depency injection here
    }
    public Task Handle(UserDisabled notification, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }
}

但是值得注意的是,如果你想切换到实际的微服务,这将不起作用。我对微服务不太熟悉,但我认为您需要某种形式的事件总线,而这正是微服务变得复杂的地方。
this Microsoft book 中有相关信息。

【讨论】:

  • mediator 绝对是最好的模式,而 MediatR 是实现此模式的最佳 .net 库。切换到微服务时,您需要定义您希望的通信方式。您有 2 个选择: - 同步通信:您的用户微服务需要有一个您的产品微服务将调用的“DisableUser”Rest(或类似)端点 - 异步通信:您需要一个消息处理程序/消息队列,如 Apache Kafka、Rabbit MQ或类似的将消息从一个微服务传播到另一个微服务
【解决方案3】:

如果您使用 Modular Monolith,请查看cap。即使在 Modular Monolith 中,发件箱在异步通信中也很重要。 Cap 将为您提供处理重试、确保数据一致性甚至添加进程外处理程序的方法。我想还有其他的库,但我用的是这个。

【讨论】:

    猜你喜欢
    • 2012-01-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-12-31
    • 1970-01-01
    • 2017-01-23
    • 2016-05-24
    相关资源
    最近更新 更多