Friday, December 28, 2018

The Web is a detail?

Should programs be mostly independent of how they communicate with each other?

Yes, if you can do it right.

If you do it wrong, you'll end up with a big hunk of useless complexity. You have been warned. The rest of this post describes one way of "doing it right" in Java. TL;DR use Autovalue and Mapstruct in a separate module.

One of the nicest things about Java is that it allows checking your interfaces at compile time, before running any of your code. So you can have one module that depends on the interfaces of your transport technology, and another module that doesn't. You can even have Maven prevent people from accidentally spreading that dependency.
This enables building and testing the domain code entirely independently of transport technology choice. Ideally, you could add support for another transport later by adding another module and not making any changes to your domain code. If your transport technology involves code generation, you might keep the generated Data Transfer Objects (DTOs) in the same module as your adapter code.

If you need multiple transports from day one, then this architecture is obviously the right way to go. It's more controversial if you're starting with a single transport, and it's also easier to get wrong. There's a strong argument to be made that you aren't going to need it. Besides being disciplined about dependencies, the most important thing is to keep the adapter layer small. We can do that with some open source code generation tools...

Let's say that you have a bunch of DTOs to work with messages that are sent and received by your message encoding technology, e.g. Google Protocol Buffers (Protobuf). Instead of working with those technology specific objects directly, your domain code can work with corresponding lightweight value objects.

Initially, you'll want to have one abstract class corresponding to each Protobuf message. With a little bit of annotation, AutoValue will nicely generate value objects for you, as well as fluent builders to compensate for Java's lack of named parameters. Here's an example, assuming you have a FooRequest message with fields bar and baz:

@AutoValue
abstract class FooRequest {
 abstract int getBar();
 abstract String getBaz();

 Builder builder() { return new AutoValue_FooRequest.Builder(); }

 @AutoValue.Builder

 static abstract class Builder {
  abstract Builder setBar(int i);
  abstract Builder setBaz(String s);
  abstract FooRequest build();
 }
}

That goes in your domain module, and your domain code can proudly rely on it. Imagine a similar FooResponse. For example, you might have a FooUseCase with a method that accepts a FooRequest. E.g.
@Singleton
class FooUseCase {
 FooResponse handle(FooRequest r) {
  someFunction(r.getFoo(), r.getBar());
  return FooResponse.builder()
   .setBar(...)
   .setBaz(...)
   .build();
 }
 ...
}

To map between the DTO and the domain object, use the following Mapstruct annotated abstract class in the adapter module:

@Singleton
abstract class FooUseCaseAdapter {
 FooRequestMapper requestMapper = new FooUseCaseAdapter$FooRequestMapperImpl(); //generated by mapstruct
 FooResponseMapper responseMapper = new FooUseCaseAdapter$FooResponseMapperImpl(); //generated by mapstruct
 @Inject FooUseCase foo;
 @Inject Sender sender;

 void handle(FooIncomingMessage m) {
  sender.send(responseMapper.map(foo.handle(requestMapper.map(m)));
 }

 @Mapper(unmappedTargetPolicy=ERROR, unmappedSourcePolicy=IGNORE)

 static abstract class FooRequestMapper {
  FooRequest map(FooIncomingMessage m);
 }

 @Mapper(unmappedTargetPolicy=IGNORE, unmappedSourcePolicy=ERROR)

 static abstract class FooResponseMapper {
  FooOutgoingMessage map(FooResponse r);
 }
}

Using Mapstruct means that you don't have to write adapter code for each field. If you have a field of the same name in the DTO class and in the Autovalue class, the generated code will map that field automatically. The "unmapped" policy of ERROR means that you will be stopped at compile time from accidentally renaming a field in the DTO but not in the domain, or vice versa. It also prevents you from forgetting to add a field to the DTO when you add a field to the domain. You'll want to use the "target" and "source" as above, lest you get lots of false positives as people evolve the DTOs independently of your application.

So our YAGNI overhead is two trivial classes for each DTO. One of them needs two lines for each DTO field used by the application. Considering that the application needs changes anyway to use a new field, those two extra changes do not seem onerous. Besides not having to write and maintain the generated code, there's the advantage that its design doesn't drift. The Autovalue classes may accumulate logic, via interfaces and default values, for example. That's perfectly fine.

This whole exercise assumes that most of your automated tests will be on the Autovalue classes. The unmapped error policy reduces the risk of bugs, even if you don't test the DTO mapping at all, but the paranoid among us might want to have a test or two to exercise each field. Mapstruct is smart enough to map between different types, so that you can use an long integer on your DTO and an Instant in your domain, for example. If you use that feature, it makes sense to test it.

What about generated enum types? Perhaps compromise is best here: generate or pull the enum classes into a separate module, and as long as these don't do strange things in static initialization, just rely on that module in the domain. The opportunity for coupling related to enums should be small. The alternative is to duplicate all the enums manually. Though Mapstruct will automatically map and warn regarding those too, that overhead may not pay for itself, especially for enums that have values that the application just needs to pass on without special handling.

Notice in the example above that a single adapter handles publishing as well as incoming message processing. We could have the domain code return a tuple-like object which contains references to Autovalue objects for all of the messages which could be published in reaction to the incoming message. This is very easy to test, and doesn't require any mocking at all. Alternatively, we could instead have the domain call some kind of publisher interface, with two implementations: a mock implementation for testing, as well as an adapter of the real transport. The real implementation would be bound to the interfaces using a dependency injection configuration in the adapter. This approach enables the adapters to be quite uniform, and to hardly ever change. If the number of possible messages to publish for each case is small, the first approach is simpler.

(The name of this post is a quote from Bob Martin. How the database might also be a detail is a topic for another post. Event sourcing makes it less relevant.)