Question

I am applying the Hexagonal Architecture (Ports and Adapters) to my system and I have noticed a dependency from my primary (driver) side adapter to the secondary (driven) side port. This doesn't seem right; there should be way to handle this.

Let's say I have two very basic ports in my domain; one is at the driver side and one is at the driven side.

// Primary Port 
interface ForecastGenerating {
    Forecast[] generateForecastsForAllLocations();
    Forecast[] generateForecastsForLocation(Location location);
}

// Secondary Port
interface LocationFetching {
    Location[] fetchAllLocations();
    Location fetchLocationbyId(String locationId);
}

I then have my domain logic as below. It expects a concrete implementation of the LocationFetching port.

// Domain Implementation
class ApplicationForecastGenerator implements ForecastGenerating {

    private LocationFetching locationFetching;
    public ApplicationForecastGenerator(LocationFetching locationFetching) {
        this.locationFetching = locationFetching
    }

    Forecast[] generateForecastsForAllLocations() {
        Location[] locations = this.locationFetching.fetchAllLocations();
        // Do my domain thing and generate forecasts
    }
    Forecast[] generateForecastsForLocation(Location location) {
        // Do my domain thing and generate forecasts
    }
}

And finally, we have the primary adapter that is tying this all together:

// Primary Adapter Implementation
class UIBasedForecastGenerator {

    private ForecastGenerating forecastGenerating;
    public UIBasedForecastGenerator(ForecastGenerating forecastGenerating) {
        this.forecastGenerating = forecastGenerating;
    }

    public void userTappedOnGenerateButton() {        
        Location location; // How does the primary adapter get its hands on the Location object?
        Forecast[] forecasts = this.forecastGenerating.generateForecastsForLocation(location);
        System.out.println(forecasts);
    }
}

The question in the primary adapter implementation is that how do I get a reference to the Location object? I can definitely use the LocationFetching port and have a dependency on it but that sounds a bit odd to me; a driver side adapter having a dependency on the driven side port. I feel like domain should be responsible for providing this object but the ForecastGenerating port shouldn't expose such a functionality; it seems to be out of scope of forecast generation.

How do we handle such dependencies in this architecture?

Was it helpful?

Solution

Ports belong to the application (the hexagon), or domain as you call it.

So Location is a domain object.

It's up to you to expose it to the UI (primary adapater) or not (for example primary port would expose a DTO to the primary adapter).

Besides that, I would name the ports according to their purpose, matching the "ForDoingSomething" format. Ask to yourself "what is this port for?"... the response will be the name of the port.

  • Primary Port: "ForGeneratingForecast" (instead of "ForecastGenerating")
  • Secondary Port: "ForFetchingLocations" (instead of "LocationFetching")

OTHER TIPS

ForecastGenerating is in an awkward position...

interface ForecastGenerating {
    Forecast[] generateForecastsForAllLocations(); //<How does it know what All means?
    Forecast[] generateForecastsForLocation(Location location); //<Location is an index, is this index bounded by something?
}

On the one hand it is acting as if it knows what the locations are. On the other-hand its pretending that it has no role in managing locations.

Fix 1: drop the all, and maybe replace it with a multi-location function.

interface ForecastGenerating {
    Forecast[] generateForecastsForLocations(Location[] locations); //<optional can be dropped.
    Forecast[] generateForecastsForLocation(Location location);
}

Now its not presuming what the locations are. It genuinely does not know. Whomever (like UIBasedForecastGenerator) asks for a Forecast needs to gain access to the locations and LocationFetching provides that.

Fix 2: Allow it to express the specific set of supported locations...

interface ForecastGenerating {
    Location[] locations(); //<this is the definition of All.
    Forecast[] generateForecastsForAllLocations();
    Forecast[] generateForecastsForLocations(Location[] locations); //<optional can be dropped.
    Forecast[] generateForecastsForLocation(Location location);
}

Now its not being secretive about what All means. It is quite clear that it means all locations(). UIBasedForecastGenerator can now simply ask.


If Location objects are generic descriptions (probably have some combining/intersecting abilities), and work with any given ForecastGenerator implementation - then I would lean toward Fix 1.

If Location objects are specialised descriptions for working with just that ForecastGenerator - then I would lean toward Fix 2.

While the generating of forecasts is one isolated concern and the fetching of locations is another, it seems to me that the presentation of these two are not isolated, that is the combination of Location and Forecast is a concern in its own right.

It therefore seems appropriate to me to put a controller in the middle, that can handle coordinating the two systems and provide whatever data the UI might need. This means a controller that can return both Forecast and Location objects.

Your primary driver is currently responsible for both coordinating the domain (run the secondary adapter) and producing results. Instead, free the ForecastGenerating service from the LocationFetching dependency (it shouldn't care where locations come from, just that they are locations), and create a separate service to act on the LocationFetching port.

So, step 1: separate your ports from your services

// Primary port
interface LocationForecastController {
    Forecast[] generateForecastForLocation(Location location);
    Forecast[] generateForecastForAll();
    Location[] getAllLocations();
    Location getLocationById(String locationId);
}
// Secondary Port
interface LocationFetching {
    Location[] fetchAllLocations();
    Location fetchLocationById(String locationId);
}

// Services
interface ForecastGeneratingService {
    Forecast[] generateForecastsForLocation(Location location);
    Forecast[] generateForecastsForMultipleLocations(Location[] locations);
}

interface LocationFetchingService {
    Location[] fetchAllLocations();
    Location fetchLocationById(String locationId);
}

Step 2: Coordinate the services in a concrete controller

// Application side port implementation
class ApplicationLocationForecastController extends LocationForecastController {
    private LocationService locationService;
    private ForecastGeneratingService forecastService;

    public ApplicationLocationForecastController(LocationService locationService, 
      ForecastGeneratingService forecastService) {
      this.locationService = locationService;
      this.forecastService = forecastService;
    }

    public Forecast[] generateForecastsForLocation(Location location) {
      return this.forecastService.generateForecastForLocation(location);
    }

    public Forecast[] generateForecastsForAll() {
      Location[] locations = this.locationService.fetchAllLocations();
      return this.forecastService.generateForecastsForMultipleLocations(locations);
    }

    public Location[] getAllLocations() {
      return this.locationService.getAllLocations();
    }

    public Location getLocationById(String locationId) {
      return this.locationService.fetchLocationById(locationId);
    }
}

Step 3: Implement services

// Location Service
class ConcreteLocationService extends LocationService {
  private LocationFetching locationFetching;

  public ConcreteLocationService(LocationFetching locationFetching) {
      this.locationFetching = locationFetching;
  }

  // ... Wrapper around locationFetching functions
}

class ConcreteForecastGeneratingService extends ForecastGeneratingService {
  // Presumably stateless?
  public Forecast[] generateForecastsForLocation(Location location) {
    // Domain logic, possibly delegated to Forecast object
  }

  public Forecast[] generateForecastsForMultipleLocations(Location[] locations) {
    // More domain logic.
  }
}

Finally: implement adapter using LocationForecastController

 // UI Adapter
class UIBasedForecastGenerator {
    private LocationForecastController locationForecastController;
    public UIBasedForecastGenerator(LocationForecastController locationForecastController) {
        this.locationForecastController = locationForecastController;
    }

    public void userTappedOnGenerateButton() {
        Location location = this.locationForecastController.getLocationById(locationId) // Assumes the relevant location ID is somewhere in the UI.
        Forecast[] forecasts = this.locationForecastController.generateForecastsForLocation(location);
        System.out.println(forecasts);
    }
}

It may seem a bit contrived to create another wrapper around LocationFetching since in this example they would probably be pretty much 1:1, but this way the primary port is isolated from any changes on the secondary port's implementation by a layer of abstraction and the primary port is not directly dependent either on the implementations of the secondary port nor on the domain logic itself. This also frees up reuse of the services by other primary ports that might have their own needs of coordinating these two concerns.

Licensed under: CC-BY-SA with attribution
scroll top