Search code examples
javaspring-bootdesign-patternsfactory-pattern

What design pattern to use for a common method with multiple implementations having different input parameter and different return type?


I am new to design patterns, and thus have a limited knowledge of what all is available. I am trying to implement a solution for a problem and request the user community to give some guidance on what design pattern to use and how it should be implemented.

Task : Implement central mapping factory that takes some input x and converts it to some output y. New implementation can be added in future.

  1. Input parameter is different for each implementation
  2. Ouput parameter type is different for each implementation

So far I have implemented factory pattern but I am doubtful on following points:

  • Warning in MapperFactory Raw use of parameterized class MappingUtil
  • In the service class when I use factory mapperFactory.getMappingUtils("A").mapData(a) I have to cast the output since each implementation has it's own specific outputformat(Is this a bad practice)

Is this the correct approach to the problem or am I following any anti-pattern in the implementation

public interface MappingUtil<X, Y> {
    Y mapData (X value);
}
 
@Component
public class A implements MappingUtil<Input A, OutputA> {

    @Override
    public OutputA mapData(Input A){
    // Perform some logic and convert InputA to OutputA and return Output A object
    }
   }

@Component
public class B implements MappingUtil<Input B, OutputB> {

    @Override
    public OutputB mapData(Input B){
    // Perform some logic and convert InputB to OutputB and return OutputB object
    }
   }

@Component
public class C implements MappingUtil<Input C, OutputC> {

    @Override
    public OutputC mapData(Input C){
    // Perform some logic and convert InputC to OutputC and return OutputC object
    }
   }
}

Factory Class

@Component
public class MapperFactory {

private final static Logger LOGGER = LoggerFactory.getLogger(MapperFactory.class);
private static Map<String, MappingUtil> mapperStrategyMapping;

private final A a;
private final B b;
private final C c;

@PostConstruct
void init() {
    mapperStrategyMapping = new HashMap<>();
    mapperStrategyMapping.put("A", a);
    mapperStrategyMapping.put("B", b);
    mapperStrategyMapping.put("C", c);
}

@Autowired
MapperFactory(A a,
              B b,
              C c) {
    this.a = a;
    this.b = b;
    this.c = c;
}

public MappingUtil getMappingUtil(String mapperCode) throws Exception {
    if (mapperCode != null) {
        return mapperStrategyMapping.get(mapperCode);
    } else {
        LOGGER.error("Empty mapper code received");
        throw new Exception("Empty mapper code");
    }
}

Usage of Factory :

@Service
public class XyzImpl implements Xyz{

    private final MapperFactory mapperFactory;

    @Autowired
    XyzImpl(MapperFactory mapperFactory){
        this.mapperFactory = mapperFactory;
    } 

    public void doFunction1(Input a , Input b){
        OutputA outputA = (Output A) mapperFactory.getMappingUtils("A").mapData(a);
        OutputB outputB = (Output B) mapperFactory.getMappingUtils("B").mapData(b);
        // Perform some logic on Output A Output B
    }

    public void doFunction2(Input c){
        OutputC outputC = (Output C) mapperFactory.getMappingUtils("C").mapData(c);
        // Perform some logic on Output C
    }
}

Solution

  • I cannot see anything clearly wrong, but I'll keep looking. My initial suggestion would be: why instead of reinventing the wheel with MappingUtil you just don't use java.util.Function (https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/function/Function.html). It's a functional interface that's doing exactly what you need, getting a value of type T and returning a type R. You code for one implementation would look like:

    @Component
    public class A implements Function<InputA, OutputA> {
    
        @Override
        public OutputA apply(InputA input){
        // Perform some logic and convert InputA to OutputA and return Output A object
        }
    }
    

    In your MapperFactory the field would be:

    private static Map<String, Function> mapperStrategyMapping;
    

    You could also simplify your getMappingUtil as follow:

    public Function getMappingUtil(String mapperCode) {
        return Optional.ofNullable(mapperCode)
            .map(mCode -> mapperStrategyMapping.get(mCode))
            .orElseThrow(() -> new IllegalArgumentException("Empty mapper code received"));
    }
    

    To avoid the cast, which I don't like either I cannot see many options, one option could be setting a boundary in terms of superclass for the return type of your Function, but maybe this is outside the scope of your exercise.