2

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?

1
  • @DocBrown Absolutely! Great catch. Fixing now.
    – Guven
    Dec 4, 2019 at 18:50

3 Answers 3

2
+50

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")
2
  • Are you suggesting that the primary UI adapter feeds off from 2 different primary ports? One that provides locations and the other one that provides forecasts.
    – Guven
    Dec 5, 2019 at 7:45
  • No. I mean that Location and Forecast are objects of your application domain. And you decide if you expose them to the outside as they are, of for example you create dtos and expose the dtos. In case you use dtos, then the driver port methods signature refer to dtos, and the business logic, i.e. the driver port implementation would have to convert dtos to/from domain objects
    – user352406
    Dec 5, 2019 at 9:00
1

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.

2
  • I am not too stuck on what all means to be honest. The domain would know what all means and fetch the locations. With your Fix 1, don't we still have the same problem of driver adapter (UIBased) accessing driven port (LocationFetching)? That doesn't really fix anything. Fix 2 is what I had proposed in my question as well; it is just that ForecastGenerating is doing too much work.
    – Guven
    Dec 4, 2019 at 15:12
  • @Guven How does the domain know? And if it knows, why is it it not communicating that? Your decision is about who has the responsibility. This could be the layer above the ForecastGenerator, in which case ForecastGenerator could not possibly know what the hell ALL means, for it has no ability to discover Locations. If it is the case that ForecastGenerator or some port it wraps knows, then ForecastGenerator knows what the Locations are, it is not uncohesive for it to communicate that information to its clients, given that it is demanding a selection from that information.
    – Kain0_0
    Dec 4, 2019 at 22:57
1

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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.