Mikz Mikz - 1 month ago 8
TypeScript Question

Injecting service into domain object once again

I've got specific domain that operates on a geographical data. I'm implementing this project in TypeScript and NodeJS and have following classes:


  • Point - value object containing latitude and longitude

  • Area - value object containing set of points as a shape definition

  • Sector - entity (it's not persisted, but it's mutable) - containing area and set of points that lie inside it



Now I need implement a method named
isPointInside(point: Point)
that calculates whether provided point fits inside of area or not. I don't want to implement it by myself, because out there are libraries that will do it for me. Right now I've decided to use that one: https://github.com/manuelbieh/Geolib .

Also I'm using IoC framework for TypeScript called inversifyjs, nothing fancy - provides DI through annotations.

So I created an interface called "GeoService" that provides methods responible for geographical calculations, and I intend to use it rather heavily in that domain model. Using inversify I provided GeoLib adapter that implements my interface and I'd really like to use it somehow from inside my Area class.

So that's first problem, but it's not the end of my struggles :-).

I've got another class, called SectorGrid, that contains grid of sectors (every sector is the square in this usecase) in two-dimensional data structure. SectorGrid has a method
addPoint(point: Point)
. Responsibility of that method is to find a sector that provided point fits in, and if it doesn't find one, to create it. Now it not only needs GeoService to calculate where initial point of the sector should be (distance from grid center), but also it needs to create Sector - and there I faced the issue while I was writing tests, there was too much logic and I've decided to provide some kind of a sector factory to SectorGrid not only to simplify tests, but also to encapsulate sector creation logic (that's quite complex). So another service to inject now, and no idea how to do it in a way that won't lead me to problems.

For now I just injected those two services as static properties of these classes, but that's not something I'm proud of and I'm looking for other options.

I know that my design may be be overcomplicated and I'm looking for a way to simplify it, but I don't want to end in the Anemic Domain Models land. Having something like Area object and not being able to put geographical calculations inside of it sounds exactly like anemic model for me.

Also I've read quite a few discussions about injecting services into entities, but none of them satisfied me as they either provided to conclusion like "don't do this" or "just do this and don't bother", or provided solutions like domain event that totally doesn't fit in my case.

Answer

A common way of handling cases where domain objects needs to collaborate with services is to inject these services at the method level and apply the ISP principle to ensure the dependencies are not wider than needed.

E.g. addPoint(point: Point, geoService: GeoService)

Another common way of dealing with the problem is to resolve the dependency from the application service and pass the result into the aggregate method, but when that approach is leaking too much logic inside the application layer you should probably use service injection at the method level instead.

but could you elaborate a little more about the latter solution

Well imagine that a Project aggregate must adjust it's completion status and percentage based on it's linked Tasks aggregates in an eventually consistent manner. To do so the Project must find out how many tasks are completed so far.

Rather than passing in a TaskRepository/TaskCompletionSummaryProvider into the Project.adjustCompletionState method you could resolve the dependency at the application layer level.

var project = projectRepository.projectOfId(someProjectId);
var taskCompletionSummary = taskRepository.taskCompletionSummaryOfProject(project.id());
project.adjustCompletionState(taskCompletionSummary);


class Project {
    public void adjustCompletionState(TaskCompletionSummary summary) {
        //The following line could be seen as defensive programming. You could also trust that the application layer is doing it's job correctly. It wouldn't be required at all if a `TaskCompletionSummaryProvider` service would be injected directly instead.
        if (this.id != summary.projectId()) throw new InvalidOperationException('Wrong summary for project');

        if (summary.allCompleted()) this.completionState = ProjectCompletionState.COMPLETED;
        else this.completionState = ProjectCompletionState.inProgress(summary.completionPercentage());
    }
}