不可变业务对象和 MessagePack 消息之间的自动转换

发布于 2024-12-13 03:31:34 字数 1159 浏览 3 评论 0原文

在 Java 中,我想使用不可变 POJO 的层次结构来表达我的域模型。

例如,

final ServiceId id = new ServiceId(ServiceType.Foo, "my-foo-service")
final ServiceConfig cfg = new ServiceConfig("localhost", 8080, "abc", JvmConfig.DEFAULT)
final ServiceInfo info = new ServiceInfo(id, cfg)

所有这些 POJO 都有公共最终字段,没有 getter 或 setter。 (如果您是 getters 的粉丝,请假装这些字段对于 getters 来说是私有的。)

我还想使用 序列化这些对象MessagePack 库,以便通过网络传递它们,将它们存储到 ZooKeeper 节点等。

问题是 MessagePack 仅支持公共非最终字段的序列化,因此我无法按原样序列化业务对象。 此外,MessagePack 不支持 enum,因此我必须将 enum 值转换为 intString 进行序列化。 (是的,如果您向 enum 添加注释,请参阅下面的评论。)

为了处理这个问题,我有一个手写的相应的“消息”对象层次结构,其中包含每个业务之间的转换 。对象及其对应的消息对象。显然这并不理想,因为它会导致大量重复代码,并且人为错误可能会导致字段丢失等。

对于这个问题有没有更好的解决方案?

  • 编译时生成代码?
  • 有什么方法可以在运行时生成适当的可序列化类吗?
  • 放弃 MessagePack?
  • 放弃业务对象中的不变性和枚举?
  • 是否有某种通用包装器库可以将可变对象(消息对象)包装为不可变对象(业务对象)?

MessagePack 还支持 Java Bean 的序列化(使用 @MessagePackBeans 注释),因此如果我可以自动将不可变对象与 Java Bean 相互转换,这可能会让我更接近解决方案。

In Java, I would like to use hierarchies of immutable POJOs to express my domain model.

e.g.

final ServiceId id = new ServiceId(ServiceType.Foo, "my-foo-service")
final ServiceConfig cfg = new ServiceConfig("localhost", 8080, "abc", JvmConfig.DEFAULT)
final ServiceInfo info = new ServiceInfo(id, cfg)

All of these POJOs have public final fields with no getters or setters. (If you are a fan of getters, please pretend that the fields are private with getters.)

I would also like to serialize these objects using the MessagePack library in order to pass them around over the network, store them to ZooKeeper nodes, etc.

The problem is that MessagePack only supports serialization of public, non-final fields, so I cannot serialize the business objects as-is. Also MessagePack does not support enum, so I have to convert enum values to int or String for serialization. (Yes it does, if you add an annotation to your enums. See my comment below.)

To deal with this I have a hand-written corresponding hierarchy of "message" objects, with conversions between each business object and its corresponding message object. Obviously this is not ideal because it causes a large amount of duplicated code, and human error could result in missing fields, etc.

Are there any better solutions to this problem?

  • Code generation at compile time?
  • Some way to generate the appropriate serializable classes at runtime?
  • Give up on MessagePack?
  • Give up on immutability and enums in my business objects?
  • Is there some kind of generic wrapper library that can wrap a mutable object (the message object) into an immutable one (the business object)?

MessagePack also supports serialization of Java Beans (using the @MessagePackBeans annotation), so if I can automatically convert an immutable object to/from a Java Bean, that may get me closer to a solution.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

帅哥哥的热头脑 2024-12-20 03:31:34

巧合的是,我最近创建了一个项目,它的功能与您所描述的几乎完全一样。使用
不可变数据模型提供了巨大的好处,但许多序列化技术似乎接近
不变性是事后的想法。我想要一些可以解决这个问题的东西。

我的项目 Grains 使用代码生成来创建不可变的实现
域模型的。该实现足够通用,可以适应不同的序列化框架。
目前支持 MessagePack、Jackson、Kryo 和标准 Java 序列化。

只需编写一组描述您的域模型的接口即可。例如:

public interface ServiceId {
    enum ServiceType {Foo, Bar}

    String getName();
    ServiceType getType();
}

public interface ServiceConfig {
    enum JvmConfig {DEFAULT, SPECIAL}

    String getHost();
    int getPort();
    String getUser();
    JvmConfig getType();
}

public interface ServiceInfo {
    ServiceId getId();
    ServiceConfig getConfig();
}

Grains Maven 插件会在编译时生成这些接口的不可变实现。
(它生成的源代码旨在供人类阅读。)然后您可以创建对象的实例。这个例子
显示了两种构造模式:

ServiceIdGrain id = ServiceIdFactory.defaultValue()
    .withType(ServiceType.Foo)
    .withName("my-foo-service");

ServiceConfigBuilder cfg = ServiceConfigFactory.newBuilder()
    .setHost("localhost")
    .setPort(8080)
    .setUser("abc")
    .setType(JvmConfig.DEFAULT);

ServiceInfoGrain info = ServiceInfoFactory.defaultValue()
    .withId(id)
    .withConfig(cfg.build());

我知道,不像您的 public final 字段那么简​​单,但是如果没有 getter,继承和组合是不可能的
和二传手。而且,这些对象可以使用 MessagePack 轻松读取和写入:

MessagePack msgpack = MessagePackTools.newGrainsMessagePack();

byte[] data = msgpack.write(info);
ServiceInfoGrain unpacked = msgpack.read(data, ServiceInfoGrain.class);

如果 Grains 框架不适合您,请随时检查其 MessagePack 模板
您可以编写一个通用 TemplateBuilder,它使用反射来设置手写域模型的最终字段。窍门
是创建一个自定义 TemplateRegistry 来允许注册您的自定义构建器。

Coincidentally, I recently created a project that does pretty much exactly what you are describing. The use of
immutable data models provides huge benefits, but many serialization technologies seem to approach
immutability as an afterthought. I wanted something that would fix this.

My project, Grains, uses code generation to create an immutable implementation
of a domain model. The implementation is generic enough that it can be adapted to different serialization frameworks.
MessagePack, Jackson, Kryo, and standard Java serialization are supported so far.

Just write a set of interfaces that describe your domain model. For example:

public interface ServiceId {
    enum ServiceType {Foo, Bar}

    String getName();
    ServiceType getType();
}

public interface ServiceConfig {
    enum JvmConfig {DEFAULT, SPECIAL}

    String getHost();
    int getPort();
    String getUser();
    JvmConfig getType();
}

public interface ServiceInfo {
    ServiceId getId();
    ServiceConfig getConfig();
}

The Grains Maven plugin then generates immutable implementations of these interfaces at compile time.
(The source it generates is designed to be read by humans.) You then create instances of your objects. This example
shows two construction patterns:

ServiceIdGrain id = ServiceIdFactory.defaultValue()
    .withType(ServiceType.Foo)
    .withName("my-foo-service");

ServiceConfigBuilder cfg = ServiceConfigFactory.newBuilder()
    .setHost("localhost")
    .setPort(8080)
    .setUser("abc")
    .setType(JvmConfig.DEFAULT);

ServiceInfoGrain info = ServiceInfoFactory.defaultValue()
    .withId(id)
    .withConfig(cfg.build());

Not as simple as your public final fields, I know, but inheritance and composition are not possible without getters
and setters. And, these objects are easily read and written with MessagePack:

MessagePack msgpack = MessagePackTools.newGrainsMessagePack();

byte[] data = msgpack.write(info);
ServiceInfoGrain unpacked = msgpack.read(data, ServiceInfoGrain.class);

If the Grains framework doesn't work for you, feel free to inspect its MessagePack templates.
You can write a generic TemplateBuilder that uses reflection to set the final fields of your hand-written domain model. The trick
is to create a custom TemplateRegistry that allows registration of your custom builder.

眼波传意 2024-12-20 03:31:34

听起来您已经合并而不是分离了应用程序的读写问题。此时您可能应该考虑 CQRS。

根据我的经验,不可变的域对象几乎总是附加到审计故事(要求)或其查找数据(枚举)。

您的域可能大部分应该是可变的,但您仍然不需要 getter 和 setter。相反,您应该在对象上使用动词,这些动词会导致域模型发生修改,并在域中发生有趣的事情时引发事件(对业务感兴趣 - 业务==有人为您的时间付费)。您可能感兴趣的是通过线路传递的事件,而不是域对象。也许甚至是命令(这些与事件类似,但源是域所在的有界上下文外部的代理——事件是模型有界上下文的内部)。

您可以使用一项服务来保存事件(以及另一个服务来保存命令),这也是您的审核日志(完成您的审核故事)。

您可以有一个事件处理程序将事件推送到总线上。这些事件应包含简单信息或实体 ID。响应这些事件的服务应使用提供的信息履行其职责,或者应使用给定的 ID 查询所需的信息。

您确实不应该公开域模型的内部状态。这样做会破坏封装,这并不是真正值得做的事情。如果我是你,我会看一下 Axon Framework。它可能比单独使用 MessagePack 让您走得更远。

It sounds like you have merged, rather than separated, the read and write concerns of your application. You should probably consider CQRS at this point.

In my experience, immutable domain objects are almost always attached to an audit story (requirement), or it's lookup data (enums).

Your domain should probably be, mostly, mutable, but you still don't need getters and setters. Instead you should have verbs on your objects which result in a modified domain model, and which raise events when something interesting happens in the domain (interesting to the business -- business == someone paying for your time). It's probably the events that you're interested in passing over the wire, not the domain objects. Maybe it's even the commands (these are similar to events, but the source is an agent external to the bounded context in which your domain lives -- events are internal to the model's bounded context).

You can have a service to persist the events (and another one to persist commands), which is also your audit-log (fulfilling your audit stories).

You can have an event handler that pushes your events onto your bus. These events should contain either simple information or entity ID's. The services that respond to these events should perform their duties using the information provided, or they should query for the information they need using the given ID's.

You really shouldn't be exposing the internal state of your domain model. You're breaking encapsulation by doing that, and that's not really a desirable thing to do. If I were you I'd take a look at the Axon Framework. It's likely to get you further than MessagePack alone.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文