Creating reusable classes for overlapping permutations/combinations of input

58 Views Asked by At

I got in to a design problem where I have different implementation for saving the data and processing other things for the different tools. I used a factory design for saving the data and same in getting the data from the database.

class SaveDataService {
  public void saveData(Data data) {
  }

  public List<Tool> getTools() {
    return List.of();
  }
}

class FishToolSaveDataService extends SaveDataService {
  @Override
  public void saveData(Data data) {
  }

  @Override
  public List<Tool> getTools() {
    return List.of(Tool.FISH);
  }
}

class DogSaveDataService extends SaveDataService {
  @Override
  public void saveData(Data data) {
  }

  @Override
  public List<Tool> getTools() {
    return List.of(Tool.DOG);
  }
}
class Data {
  JsonNode data;
  int version;
}

enum Tool {
  FISH,
  DOG
}

So, My Factory mapper is based on the enum Tool

Now I have a version for each tool to maintain but the saving implementation changes for each version. How do I maintain this without using if else checks in the saveData(Data data) method implementation.

I tried simple if/else check for version the saveData method but it will prolonged to long if I have five different version and two different tools have same logic of saving for the different version. That is duplicating my code.

How can I design this version such that I can reuse my code?

I have 15 tools implementation in to total and I am starting with the version 2 of 3 tools in total but in future more versioning and more tools will come.

1

There are 1 best solutions below

0
Chetan Kinger On

It appears that the problem statement is to be able to create classes that can hold the logic for different Tool types and version combinations. This is a complicated problem statement that can only be answered via an example.

Start by creating an interface to represent the logic for saving a particular version :

public interface VersionDataService {
    public void saveVersionData(Data data);
}

Define some concrete/generic logic for different versions.

public class AlphaVersionDataService {

    public void saveVersionData(Data data) {

         //logic for saving data for alpha versions
    }
}

public class BetaVersionDataService {

    public void saveVersionData(Data data) {

         //logic for saving data for beta versions
    }
}

Now you might have requirements to execute specific logic based on tool type which is not generic. For this, you can create more specialized subclasses such as the one below :

public class GammaFishVersionDataService {

    public void saveVersionData(Data data) {

         //logic for saving data for Gamma Version for Fishes only
    }
}

public class GammaDogVersionDataService {

    public void saveVersionData(Data data) {

         //logic for saving data for Gamma Version for Dogs only
    }
}

We now create a VersionDataServiceFactory that will hold a map. This map will contain the pre-created VersionDataService objects for different permutation/combinations. (See the example at the end which will make more sense later)

class VersionDataServiceFactory {

   private Map<String,VersionDataService> versionDataServicesMap;

   public VersionDataService(Map<String,VersionDataService> versionDataServicesMap) {
       this.versionDataServicesMap= versionDataServicesMap;
   }

   public VersionDataService get(int version,Tool tool) {
      //returns the pre-created object for the given version and tool combination
      return versionDataServicesMap.get(version,tool)
   }
}


class SaveDataService {

  protected VersionDataServiceFactory versionDataServiceFactory;
 
  public SaveDataFactory(SaveDataFactory versionDataServiceFactory) {
     this.versionDataServiceFactory = versionDataServiceFactory;
  }
  
  //other methods unchanged...
  
}

class FishToolSaveDataService extends SaveDataService {

    @Override
    public void saveData(Data data) {
       //get the VersionDataService instance based on the version and tool type.
       VersionDataService versionDataService = versionDataServiceFactory.get(data.getVersion(),Tool.FISH);
       //save the data.
       versionDataService.saveVersionData(data);
    }

    @Override
    public List<Tool> getTools() {
       return List.of(Tool.FISH);
    }
}

Now let's put all this together :

AlphaVersionDataService alpha = new AlphaVersionDataService();
GammaDogVersionDataService gammaDog = new GammaDogVersionDataService();
Map<String,VersionDataService> versionDataServicesMap = new HashMap<>();

String alphaDogKey = Tool.Dog.toString()+"1"; //just a key combination for tool type + version
String alphaFishKey = Tool.Fish.toString()+"1"; //just a key combination for tool type + version
String gammaDogKey = Tool.Dog.toString()+"2"; //just a key combination for tool type + version
map.put(alphaDogKey,alpha);
map.put(alphaFishKey,alpha);//notice how we used the same alpha object for two different Tools? This is how we saved code duplication without using if-else :)
map.put(gammaDogKey,gammaDog);

VersionDataServiceFactory factory = new VersionDataServiceFactory(versionDataServicesMap);

SaveDataService saveDataService = new FishToolDataService(factory);
saveDataService.save(data); 

Notice how the alpha object was reused for both Fish and Dog alpha versions. This solves your objective of reusing the code for different tool types without using if-else. Also notice that when saveDataService.save(data); is called, it automatically fetches the correct VersionDataService implementation for the given Tool type and version combination.


Note : This is a slightly complicated design problem. The code has not been tested but is only indicative of the approach that can be followed. There are a lot more improvements that can be done such as using the Decorator pattern to avoid having to create too many subclasses for each version + tool combination or using the AbstractFactory pattern, or defining a class called VersionDataKey to represent the key for the factory map and much more; but these are aspects intentionally left out for the readers (including you) to discover as covering these aspects would make this answer too long.