Composition or Inheritance for classes with almost similar implementations but different input and outputs for methods?

Tags: , , , ,



I have the following classes, which have quite similar method implementations. Only the classes’ method inputs and outputs seem to be of different types. When I put it like this, it sounds like a case for inheritance, however, the fact that the inputs and outputs are different and are related to two lambdas, make me wonder if they should remain without any relationship, as one lambda cannot be thought of in place of another (To be a case for inheritance).

My first class looks like the following.

public class JobPersistenceManager {
    private String jobIndexName;
    private JobLambda JobLambda;
    private MyDataPersistence myDataPersistence;
    private DataProcessorUtils dataProcessorUtils;
    private static final String JOB_ID = "jobId";
    private static final String JOB_NAME = "jobName";

    @Inject
    public JobPersistenceManager(@Named("jobIndexName") String jobIndexName,
                                 JobLambda JobLambda,
                                 MyDataPersistence myDataPersistence) {
        this.jobIndexName = jobIndexName;
        this.JobLambda = JobLambda;
        this.myDataPersistence = myDataPersistence;
        createIndexIfNotExists(this.jobIndexName);
    }

    public SearchDocumentResult searchJob(MyJobInput myJobInput) throws IOException {
        return myDataPersistence
                .searchDocument(this.jobIndexName,
                        dataProcessorUtils.transformObjectDataPayloadToMap(myJobInput));
    }

    public MyJobOutput invokeCreateJobLambdaAndIndexData(final MyJobInput myJobInput)
            throws IOException {
        String personRequestPayload = dataProcessorUtils.transformObjectDataInputJson(myJobInput);
        Map<String, String> createdJobOutput = this.JobLambda.invokeLambda(personRequestPayload);
        this.indexCreatedJob(myJobInput, createdPersonOutput);
        return MyJobOutput.builder().withJobID(createdJobOutput.get(JOB_ID))
                .withJobName(createdJobOutput.get(JOB_NAME)).build();
    }

    public int indexCreatedJob(final MyJobInput myJobInput,
                               final Map<String, String> createdJobOutput) throws IOException {
        myJobInput = modifyJobInput(myJobInput);
        String documentToIndex = dataProcessorUtils.transformObjectDataInputJson(myJobInput);
        return myDataPersistence.indexDocument(this.jobIndexName, documentToIndex);
    }

    private void createIndexIfNotExists(final String indexName) {
        if (!myDataPersistence.doesIndexExist(indexName)) {
            myDataPersistence.createIndex(CreateIndexInput.builder().indexName(indexName).build());
        }
    }
}

My second class looks like the following.

public class EmployeePersistenceManager {
    private EmployeeLambda employeeLambda;
    private MyTestDataPersistence myTestDataPersistence;
    private DataProcessorUtils dataProcessorUtils;
    private String employeeIndexName;
    private static final String PERSON_ID_KEY = "personId";
    private static final String PERSON_NAME_KEY = "personName";

    @Inject
    public EmployeePersistenceManager(@Named("employeeIndexName") String employeeIndexName,
                                    EmployeeLambda employeeLambda,
                                    MyTestDataPersistence myTestDataPersistence,
                                    DataProcessorUtils dataProcessorUtils) {
        this.employeeIndexName = employeeIndexName;
        this.employeeLambda = employeeLambda;
        this.myTestDataPersistence = myTestDataPersistence;
        this.dataProcessorUtils = dataProcessorUtils;
        createIndexIfNotExists(employeeIndexName);
    }

    public SearchDocumentResult searchPerson(EmployeeInput employeeInput) throws IOException {
        return myTestDataPersistence
                .searchDocument(employeeIndexName,
                        dataProcessorUtils.transformObjectDataPayloadToMap(employeeInput));
    }

    public EmployeeOutput invokeCreatePersonLambdaAndIndexData(final EmployeeInput employeeInput)
            throws IOException {
        String personRequestPayload = dataProcessorUtils.transformObjectDataInputJson(employeeInput);
        Map<String, String> createdPersonOutput = this.employeeLambda.invokeLambda(personRequestPayload);
        this.indexCreatedEmployee(employeeInput, createdPersonOutput);
        return EmployeeOutput.builder().withPersonId(createdPersonOutput.get(PERSON_ID_KEY))
                .withPersonName(createdPersonOutput.get(PERSON_NAME_KEY)).build();
    }

    public int indexCreatedEmployee(final EmployeeInput employeeInput,
                                  final Map<String, String> createdPersonOutput) throws IOException {
        employeeInput = modifyEmployeeInput(employeeInput);
        String documentToIndex = dataProcessorUtils.transformObjectDataInputJson(employeeInput);
        return myTestDataPersistence.indexDocument(this.employeeIndexName, documentToIndex);
    }

    public Map.Entry<String, Map<String, String>> invokeLambda(final String payload) {
        return new AbstractMap.SimpleEntry<>(payload, this.employeeLambda.invokeLambda(payload));
    }

    private void createIndexIfNotExists(final String indexName) {
        if (!myTestDataPersistence.doesIndexExist(indexName)) {
            myTestDataPersistence.createIndex(CreateIndexInput.builder().indexName(indexName).build());
        }
    }
}

As you can see, the methods perform almost the same actions. Only the indexCreatedEmployee and indexCreatedJob methods from the classes have an extra step of processing the input.

Should I keep these classes as they are now without any relationships between them, or should I create an abstract persistence manager class and perform the following.

Move createIndexIfNotExists to the abstract class Create abstract methods search(), invokeLambda() and indexCreatedData() methods and implement them in each child class. The data types MyJobInput and MyEmployeeInput are POJO classes that don’t have any relationship. So I guess these methods I mentioned would then take “Object” parameters? EmployeeLambda and JobLambda are again classes with no relationship between them. Another concern I had towards creating some sort of inheritance was that, Employee Lambda and JobLambda cannot be used inter-changeably. So was wondering if they should inherit the same parent class just because they’re both lambda classes.

OR is there another way to go about this? Any advice would be much appreciated. Thank you very much in advance.

Answer

As promised yesterday, here is what I would do.

Create a Lambda interface and make JobLambda and EmployeeLambda implement it

public interface Lambda {

    Map<String, String> invokeLambda(String payload);

}

public class JobLambda implements Lambda {
    //... your implementation
}

public class EmployeeLambda implements Lambda {
    //... your implementation   
}

Do the same for DataPersistence

public interface DataPersistence {

    boolean doesIndexExist(String indexName);

    void createIndex(CreateIndexInput createIndexInput);

    int indexDocument(String indexName, String documentToIndex);

    SearchDocumentResult searchDocument(String indexName, Map<String, String> payloadMap);

}

public class MyDataPersistence implements DataPersistence {
    //... your implementation
}

public class MyTestDataPersistence implements DataPersistence {
    //... your implementation
}

Then create a parent class PersistenceManager which contains all the duplicated methods, parametrized for the type of input/output:

(Note: I didn’t complete everything, but I did something just to make you understand the concept)

public class PersistenceManager<I, O> {
    protected static final String ID = "Id";
    protected static final String NAME = "Name";
    private String indexName;
    private Lambda lambda;
    private DataPersistence dataPersistence;
    private DataProcessorUtils dataProcessorUtils;

    public PersistenceManager(String indexName, Lambda lambda, DataPersistence dataPersistence, DataProcessorUtils dataProcessorUtils) {
        this.indexName = indexName;
        this.lambda = lambda;
        this.dataPersistence = dataPersistence;
        this.dataProcessorUtils = dataProcessorUtils;
        createIndexIfNotExists(indexName);
    }

    public SearchDocumentResult search(I input) {
        return dataPersistence.searchDocument(indexName, dataProcessorUtils.transformObjectDataPayloadToMap(input));
    }

    public O invokeCreateLambdaAndIndexData(final I input) {
        String requestPayload = dataProcessorUtils.transformObjectDataInputJson(input);
        Map<String, String> createdOutput = this.lambda.invokeLambda(requestPayload);
        //continue generalizing following the same logic
    }

    public int indexCreated(I input, Map<String, String> createdOutput) {
        //continue generalizing following the same logic
    }

    private void createIndexIfNotExists(final String indexName) {
        if (!dataPersistence.doesIndexExist(indexName)) {
            dataPersistence.createIndex(CreateIndexInput.builder().indexName(indexName).build());
        }
    }
}

At this point, you can specialize your classes by simply choosing the parameters

… all the rest of the code will be shared in the parent class.

public class JobPersistenceManager extends PersistenceManager<MyJobInput, MyJobOutput> {

    private static final String JOB_ID = "Job" + ID;
    private static final String JOB_NAME = "Job" + NAME;

    public JobPersistenceManager(String indexName, Lambda lambda, DataPersistence dataPersistence, DataProcessorUtils dataProcessorUtils) {
        super(indexName, lambda, dataPersistence, dataProcessorUtils);
    }
}

public class EmployeePersistenceManager extends PersistenceManager<MyEmployeeInput, MyEmployeeOutput> {

    private static final String EMPLOYEE_ID = "Employee" + ID;
    private static final String EMPLOYEE_NAME = "Employee" + NAME;

    public EmployeePersistenceManager(String indexName, Lambda lambda, DataPersistence dataPersistence, DataProcessorUtils dataProcessorUtils) {
        super(indexName, lambda, dataPersistence, dataProcessorUtils);
    }
}

… and use them like this:

PersistenceManager employeePersistenceManager = new EmployeePersistenceManager(...);
employeePersistenceManager.search(employeeInput); //<-- the code is in the base class


Source: stackoverflow