Create a mapping function depending on type hierarchy

I have an application that follows Domain Driven Design rules. One of the rules of DDD is that the domain layer should not depend on the infrastructure layer. I have a tool that checks if there are any dependencies (imports) from domain to infrastructure. In my infrastructure layer, I have a class hierarchy like this:

infrastructure

In my domain layer, like this:

domain

Now, I need to map my domain classes to the infrastructure entities in order to save them in my database.

I have an out port in my domain which is an interface:

// infra/ports/out/RuleRepository.java
public interface RuleRepository {
  Rule save(Rule rule);
}

This interface in implemented in the infrastructure layer:

// domain/RuleRepositoryJpaAdapter.java
public class RuleRepositoryJpaAdapter implements RuleRepository {

  // extends CrudRepository<RuleEntity, Long>
  RuleCrudRepository jpaRepository;
  RuleMapper mapper;

  @Override
  public Rule save(Rule rule) {
    return jpaRepository.save(mapper.mapToEntity(rule))
      .mapToDomain();
  }
}

I’m looking for a way to implement mapToEntity without having to check the type of the rule. The best way would be to add a mapToEntity method to Rule, IpRule, BlackListRule but this would break the unit tests that check if there is any imports between the domain and infrastructure layer. If there any other way?

What I have right now:

public class RuleMapper {

  public RuleEntity mapToEntity(Rule rule) {
    if (rule instanceof IpRule) {
      return new IpRuleEntity().mapFromDomain(rule);
    } else if (rule instanceof BlackListRule) {
      return new BlackListRuleEntity().mapFromDomain(rule);
    } else {
      throw new IllegalArgumentException("Unexpected argument of type " + (rule == null ? "null" : rule.getClass().getName()));
    }
  }

}

Answer

You could use something like the Visitor Pattern to implement double dispatch.

In this example, it could look something like this:

public abstract class Rule {
    // Maps a rule to a result type T
    public interface Mapper<T> {
        T mapIpRule(IpRule rule);
        T mapBlackListRule(BlackListRule rule);
    }

    public abstract <T> T map(Mapper<T> mapper);
    
    // ...
}

public class IpRule extends Rule {
    @Override
    public <T> T map(Mapper<T> mapper) {
        return mapper.mapIpRule(this);
    }

    // ...
}

public class BlackListRule extends Rule {
    @Override
    public <T> T map(Mapper<T> mapper) {
        return mapper.mapBlackListRule(this);
    }

    // ...
}

public class RuleMapper implements Rule.Mapper<RuleEntity> {

  public RuleEntity mapToEntity(Rule rule) {
    return rule.map(this);
  }

  @Override
  public RuleEntity mapIpRule(IpRule rule) {
    return new IpRuleEntity();
  }

  @Override
  public RuleEntity mapBlackListRule(BlackListRule rule) {
    return new BlackListRuleEntity();
  }
}

It has a nice property that there is compile-time check that all subtypes are handled correctly. If a new subtype of Rule is added later, it will need to implement the map method, which will require adding a method to the Rule.Mapper interface, which in turn will require the RuleMapper implementation to implement that method. In the example given in the question that uses runtime type checks with instanceof, it’s possible to miss a case, resulting in an IllegalArgumentException at runtime.

However, you will need to judge whether the extra complexity is worthwhile for your specific situation. It might be that your existing RuleMapper is just fine.