如何让Java中的事件源更简单(附代码)

103 阅读5分钟

事件源是指持久化事件,而不仅仅是当前状态。事件源对于审计目的和分析或重建以前的系统状态以进行业务分析很有帮助。

让我们看一下它的细节:每当你对应用程序的状态做了改变,你就把这个改变记录为一个事件。

你可以回放从记录开始到某一时间的事件。然后你就重新创建了那个时候的应用程序的状态。而通过将事件合并到一个不同的数据结构中,你可以提供一个用户特定的状态视图(一个 "查询模型")。

想想一个购物车:一个典型的电子商务应用只会在用户进行结账时存储购物车的状态。如果你想知道哪些购物车中的物品被用户删除了,以优化购买流程呢?这时,存储每个事件就会变得很有帮助,例如ShoppingCartItemRemoved

事件源Hello World例子

在这个例子中,一个用户向后端发送了一个POST HTTP请求,其中包含一个CreateGreeting 命令的数据。这个命令包含要问候的人的名字。

后台将该命令转换为一个GreetingCreated 事件。这个事件包含命令中的人名,以及默认的敬语(Hello, ):

Image description

该事件还包含你在中间看到的实体的ID:Greeting 实体,它消费命令,并产生事件。这样,这个实体的状态就可以在以后被重建了。

通过产生事件,Greeting 实体已经接受了有效的命令,事件将此作为一个事实记录下来。该事件现在被存储在一个日志中,例如内存、关系型或NoSQL数据库。

到目前为止,Greeting 实体的状态还没有改变。要改变状态,Greeting 将事件和当前状态作为输入,并产生一个新的状态类的实例:

Image description

GreetingState 的对象是不可改变的。在应用事件后,Greeting 将旧的状态替换为新的状态。

如果你以后想改变吉尔的问候语呢?这可以通过ChangeSalutation 命令来完成。如果你在请求的URL中对Jill的Greeting 实体的id进行编码,那么命令的处理就会像这样:

Image description

注意,该事件只捕获与即将发生的变化有关的信息。它不需要捕获GreetingState 中的所有信息。

应用SalutationChanged 事件看起来是这样的:

Image description

有趣的是:Greeting 从事件中获取敬语,并将其与当前状态下的personName 结合起来,以产生新的状态。

实现事件源很难

我所看到的问题是这样的。当构建一个以事件为源的应用程序时,有一个陡峭的学习曲线。你不仅需要适应这种关于状态的新思维方式--你还需要学习事件源库/框架的细节。

我想改变这种状况。我创建了Being库。它的目的是尽可能地减少技术上的复杂性。你可以在GitHub上找到它。它正处于早期开发阶段,所以我非常感谢你的任何反馈。

命令和事件处理代码

当你使用Being时,你需要定义命令处理程序:实体消耗哪些类型的命令,以及它对每个命令的反应产生哪些事件。

你还需要定义事件处理程序:对于每一种事件类型,创建哪种新的实体状态作为对它的反应。

下面显示的Greeting 实体的行为有以下代码

public class Greeting implements AggregateBehavior<GreetingCommand, GreetingState> {
	@Override
	public GreetingState initialState(final String id) {
		return GreetingState.identifiedBy(id);
	}

	@Override
	public CommandHandlers<GreetingCommand, GreetingState> commandHandlers() {
		return CommandHandlers.handle(
			commandsOf(CreateGreeting.class).with((cmd, state) -> new GreetingCreated(state.id, "Hello,", cmd.personName)),
			commandsOf(ChangeSalutation.class).with((cmd, state) -> new SalutationChanged(state.id, cmd.salutation)));
	}

	@Override
	public EventHandlers<GreetingState> eventHandlers() {
		return EventHandlers.handle(
			eventsOf(GreetingCreated.class)
				.with((event, state) -> new GreetingState(event.id, event.salutation, event.personName)),
			eventsOf(SalutationChanged.class)
				.with((event, state) -> new GreetingState(event.id, event.salutation, state.personName)));
	}
}

除了定义Greeting 的起始状态的initialState() 方法外,这看起来应该很熟悉。

第一个命令处理程序消耗一个包含要问候的人的名字的CreateGreeting 命令,并产生一个GreetingCreated 事件。

但用户也可以通过ChangeSalutation 命令来改变问候语。这个命令只包含新的敬语文本,而不是人的名字。这个人是由实体的ID(state.id )来识别的。

命令处理程序和事件处理程序都可以使用该实体的当前状态。因此,当应用SalutationChanged 事件时,人的名字不是从事件中获取的,而是从实体的当前状态获取的:(event,state) -> new GreetingState(event.id, event.salutation, state.personName)

问候实体状态的代码

下面是代表实体状态的GreetingState 类的代码:

public final class GreetingState {
    public final String id;
    public final String salutation;
    public final String personName;

    public static GreetingState identifiedBy(final String id) {
        return new GreetingState(id, "", "");
    }

    public GreetingState(final String id, final String salutation, final String personName) {
        this.id = id;
        this.salutation = salutation;
        this.personName = personName;
    }

    @Override
    public String toString() {
        return "GreetingState [id=" + id + ", salutation=" + salutation + ", personName=" + personName + "]";
    }
    
    // hashCode() and equals() omitted for brevity
}

正如你所看到的,状态类的对象是不可改变的。

命令和事件的代码

命令是简单的POJO,正如你在下面的例子中看到的那样:

public class CreateGreeting implements GreetingCommand{
    public final String personName;

    public CreateGreeting(String personName) {
        this.personName = personName;
    }

    @Override
    public String toString() {
        return "CreateGreeting [personName=" + personName + "]";
    }
}

一个实体的命令实现了一个公共接口,就像例子中的GreetingCommand ,它可能是空的:

public interface GreetingCommand {
}

为命令设置一个公共接口的原因是类型安全。使用这个命令接口作为实体类的第一个类型参数,如上所示。

每个事件类必须是IdentifiedDomainEvent 的子类:

public final class GreetingCreated extends IdentifiedDomainEvent {
    public final String id;
    public final String salutation;
    public final String personName;

    public GreetingCreated(final String id, final String salutation, String personName) {
        super(SemanticVersion.from("1.0.0").toValue());
        this.id = id;
        this.salutation = salutation;
        this.personName = personName;
    }

    @Override
    public String identity() {
        return id;
    }

    @Override
    public String toString() {
        return "GreetingCreated [id=" + id + ", salutation=" + salutation + ", personName=" + personName + "]";
    }
}

作为是基于强大的VLINGO XOOM平台,定义了IdentifiedDomainEvent 的超类。

结论

除了我上面展示的内容,你还需要定义HTTP请求处理程序。Being网站解释了如何做到这一点。

如果你对这个话题感兴趣,我想邀请你看一看。我也非常感谢你的反馈。