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?
-
@DocBrown Absolutely! Great catch. Fixing now.Guven– Guven2019年12月04日 18:50:57 +00:00Commented Dec 4, 2019 at 18:50
3 Answers 3
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")
-
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– Guven2019年12月05日 07:45:02 +00:00Commented 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 objectsuser352406– user3524062019年12月05日 09:00:53 +00:00Commented Dec 5, 2019 at 9:00
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.
-
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 thatForecastGenerating
is doing too much work.Guven– Guven2019年12月04日 15:12:37 +00:00Commented 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 caseForecastGenerator
could not possibly know what the hell ALL means, for it has no ability to discoverLocation
s. If it is the case thatForecastGenerator
or some port it wraps knows, thenForecastGenerator
knows what theLocation
s 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– Kain0_02019年12月04日 22:57:05 +00:00Commented Dec 4, 2019 at 22:57
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.
Explore related questions
See similar questions with these tags.