Reactive Clean Architecture With Vert.x
Clean Architecture gives a clear No! to mixing frameworks and business rules. But that doesn't mean we have to resign from all cool technologies. We just have to adapt.
Join the DZone community and get the full member experience.
Join For FreeI have recently written about Clean Architecture and provided an implementation example by Uncle Bob himself. This time, we’ll look at the implementation from a bit different angle — i.e. is it possible to implement Clean Architecture and still take advantage of modern, reactive frameworks like Vert.x? Here it comes!
What Was That Clean Architecture Again?
Clean Architecture divides our code into four conceptual circles – Enterprise Business Rules, Application Business Rules, Interface Adapters, and Frameworks & Drivers. Between these circles, there is a strong dependency rule – dependencies can only flow inwards (see the picture below). One of the rule’s implications is that the code in the inner part should have no knowledge of the frameworks being used. In our case, these will be Vert.x and Guice. In general, you should be able to switch UI technologies, databases, and frameworks without impacting the two inner circles.
If you want to read more about the theory behind Clean Architecture, make sure to check out my previous post and Uncle Bob’s upcoming book.
Reference Implementation
When working towards my reactive implementation, I used Uncle Bob’s repository as a reference:
One could go crazy with all the types of objects in the codecastSummaries use case package above. To make sure we’ll all on the same page, let me explain how I understand the role of each of them:
- Controller – handles input e.g. web requests and orchestrates the work between the use case, view, and presenter
- Input Boundary – an interface to the use case, a clear indication where the Interface Adapters circle ends and Use Cases circle begins
- Output Boundary – an interface to the present the use case output, a clear indication where the use case ends processing and presentation begins
- Use Case – orchestrates entities and other classes (e.g. external services) to satisfy a user’s goal
- Response Model – a “stupid” representation of the use case results – we don’t want any business logic to leak through the output boundary
- Presenter – converts data to a presentable form and presents them using a view
- View Model – a presentable form of data. It’s usually similar to response model, with the only difference being that the response model does not care about being presentation-friendly
- View – an object that handles actual displaying on the screen e.g. rendering an HTML page
It’s good to think about those objects as useful roles instead of obligatory parts of the architecture. Sometimes common sense suggests skipping some of them or making one object play multiple roles and we shouldn’t constrain ourselves too much.
Problem Domain
Despite using it as a reference, I didn’t want to code up the same thing as in Uncle Bob’s repository. I set out to implement something extremely simple – displaying a list of activities in which, for now, an activity consists only of a name. I wanted to put as close to 100% focus on the architecture and as close to 0% focus on the domain itself. Hence, my Activity entity ended up looking like this:
public class Activity extends Entity {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
The Entity base class looks almost the same, with the difference that it contains an id field instead of name.
The Use Case
As I already mentioned, the use case is really simple. All it needs to do is retrieve the data from a database and pass it through the output boundary:
public class ListActivitiesUseCase implements ListActivitiesInputBoundary {
private final ActivityGateway activityGateway;
@Inject
public ListActivitiesUseCase(ActivityGateway activityGateway) {
this.activityGateway = activityGateway;
}
@Override
public void listActivities(ListActivitiesOutputBoundary presenter) {
activityGateway.findAll(Callback.of(
activities -> presenter.success(toResponseModel(activities)),
presenter::failure));
}
private List<ActivityDetails> toResponseModel(List<Activity> activities) {
return activities
.stream()
.map(Activity::getName)
.map(ActivityDetails::new)
.collect(Collectors.toList());
}
}
As the response model would consist of a single field, we’re passing a collection of ActivityDetails directly through the boundary. This could be considered short-sighted, as any new field in the response model would force us to wrap it in a class and change the output boundary, but it saves us some brain cycles analyzing the code for some time.
Don’t Call Us, We’ll Call You
Since in the reactive approach the thread responsible for processing our request should not wait for the results of a use case and catch exceptions, we need a way to process these asynchronously. The easiest way to deal with this is by using callbacks. But since we don’t want to couple our code to the framework, we need to use our own piece of code, instead of Vert.x’s Handler and AsyncResult. That’s why I created the Callback interface:
public interface Callback<T> {
static <T> Callback<T> of(Consumer<T> success, Consumer<Throwable> failure) {
return new DelegatingCallback<>(success, failure);
}
void success(T t);
void failure(Throwable throwable);
class DelegatingCallback<T> implements Callback<T> {
private final Consumer<T> success;
private final Consumer<Throwable> failure;
// delegate
}
}
As you can see, it contains two methods: success for asynchronously returning a result and failure for asynchronous indication of an exception. You can see the class in action in the use case implementation above. The use case invokes a database query, which can take a long time and thus was implemented in a non-blocking manner using callbacks:
public class ActivityGatewayImpl implements ActivityGateway {
// fields & c-tor
@Override
public void findAll(Callback<List<Activity>> callback) {
getConnection(connection -> connection.query("SELECT * FROM Activities;", asyncRs -> {
if (asyncRs.succeeded()) {
List<Activity> activities = asyncRs.result()
.getRows()
.stream()
.map(this::toActivity)
.collect(Collectors.toList());
callback.success(activities);
} else {
callback.failure(asyncRs.cause());
}
}), callback::failure);
}
// getConnection() & toActivity()
}
The Output Boundary
The output boundary of our use case is responsible only for presenting activities and communicating a failure if necessary. Hence, I made it extend the Callback interface directly:
public interface ListActivitiesOutputBoundary extends Callback<List<ActivityDetails>> {
}
Since there’s no need for a special view model, and a presenter of the use case would only pass the result straight to the view, I decided to skip it and let the view play the role of an output boundary instead. Hence, the view was implemented like this:
public class ListActivitiesView implements ListActivitiesOutputBoundary {
private final RoutingContext ctx;
public ListActivitiesView(RoutingContext ctx) {
this.ctx = ctx;
}
@Override
public void success(List<ActivityDetails> activityDetailsList) {
FreeMarkerTemplateEngine engine = FreeMarkerTemplateEngine.create();
ctx.put("activities", activityDetailsList);
engine.render(ctx, "templates/index.ftl", res -> {
if (res.succeeded()) {
ctx.response().end(res.result());
} else {
ctx.fail(res.cause());
}
});
}
@Override
public void failure(Throwable throwable) {
ctx.fail(throwable);
}
}
Since the view implementation is part of the UI, it can contain direct references to the Vert.x framework i.e. the RoutingContext.
Binding Things Together
Now that we have all the pieces of our use case, we can bind them together and expose them via the web. Binding is done using a simple Guice module:
public class ActivityModule extends AbstractModule {
private final Vertx vertx;
private final JsonObject config;
public ActivityModule(Vertx vertx, JsonObject config) {
this.vertx = vertx;
this.config = config;
}
@Override
protected void configure() {
bind(JDBCClient.class)
.toInstance(JDBCClient.createShared(vertx, config));
bind(ActivityGateway.class)
.to(ActivityGatewayImpl.class);
bind(ListActivitiesInputBoundary.class)
.to(ListActivitiesUseCase.class);
}
}
And exposing via the web is done using the Vert.x Web Router mechanism:
public class MainVerticle extends AbstractVerticle {
@Override
public void start() {
Router router = Router.router(vertx);
Injector injector = Guice.createInjector(new ActivityModule(vertx, config()));
ListActivitiesInputBoundary listActivitiesUseCase = injector.getInstance(ListActivitiesUseCase.class);
router.get().handler(ctx -> {
ListActivitiesView view = new ListActivitiesView(ctx);
listActivitiesUseCase.listActivities(view);
});
vertx.createHttpServer()
.requestHandler(router::accept)
.listen(8080);
}
}
Wrap Up
That’s it. The final code structure looks like this:
As you can see, newer frameworks like Vert.x and Clean Architecture are not mutually exclusive. What’s more, there’s not much extra coding that you need to do to prevent the framework from spreading all over your codebase.
You can find all the code from this article here.
Published at DZone with permission of Grzegorz Ziemoński, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments