Search code examples
javagenericsrefactoringreusability

Clean code - Avoiding explicit type casts with collections built on generic data types


I am using a Map to read the rows from a spreadsheet and store its contents in the following way:

public class DocumentRow {

    private Map<Column, DocumentCell<?>> rowContents = new HashMap<>();

    private int rowNum;

    public DocumentCell<?> getValue(Column column) {
        return rowContents.get(column);
    }

    public void setValue(Column column, DocumentCell<?> value) {
        rowContents.put(column, value);
    }

    public DigitalDocument toDomainObject() {
        DomainObject domainObject = new DomainObject();
        domainObject.setTextValue((String) rowContents.get(TEXT_VALUE).getValue());
        domainObject.setNumericValue((int) rowContents.get(NUMERIC_VALUE).getValue());
        domainObject.setDateValue((LocalDate) rowContents.get(DATE_VALUE).getValue());
        return domainObject;
    }
}

public class DocumentCell<T> {
    private T value;
}

public enum Column {
    TEXT_VALUE("Text_Column_Name", DataType.STRING),
    NUMERIC_VALUE("Numeric_Column_Name", DataType.NUMBER),
    DATE_VALUE("Date_Column_Name", DataType.DATE);
}

(I have ommited some obvious classes for the sake of brevity)

The row values are provided as:

row.setValue(column, new DocumentCell<>(getDateCellValue(spreadSheetCell)));

Is there way this could be made cleaner so that I don't require these unchecked casts while constructing the domain object? Or a better way to design this?


Solution

  • The problem with enum is that you can't use generic types. You can see more about that in How to implement enum with generics?.

    First, we need to create an "enum-like" class with constants using generic types.

    class Column<T> {
        public static final Column<String> TEXT_VALUE = new Column<>("Text_Column_Name", String.class);
        public static final Column<Number> NUMERIC_VALUE = new Column<>("Numeric_Column_Name", Number.class);
        public static final Column<Date> DATE_VALUE = new Column<>("Date_Column_Name", Date.class);
    
        String name;
        Class<T> clazz;
    
        private Column(String name, Class<T> clazz){
            this.name = name;
            this.clazz = clazz;
        }
    }
    

    That way, we can secure the insertion of value in the map to match the type of Column with the method :

    public <U> void setValue(Column<U> column, DocumentCell<U> value) {
        rowContents.put(column, value);
    }
    

    Example :

    DocumentRow row = new DocumentRow();
    row.setValue(Column.TEXT_VALUE, new DocumentCell<String>("asdf"));
    row.setValue(Column.TEXT_VALUE, new DocumentCell<Integer>(4)); //Don't compile, can't set an `Integer` document cell into a `Column.TEXT_VALUE`
    

    Now, we are sure that a value inserted for Column.TEXT_VALUE will hold a String, and the same for every Column constants.

    Since we have insured the type during the insertion, we can be dirty and cast DocumentCell<?> from the map to the same type of Column:

    public <U> U getValue(Column<U> column) {
        @SuppressWarnings("unchecked")
        DocumentCell<U> doc = (DocumentCell<U>) rowContents.get(column);
        return doc.getValue(column);
    }
    

    And a small example of the result :

    String s = row.getValue(Column.TEXT_VALUE);
    Integer i = row.getValue(Column.TEXT_VALUE); //DON'T COMPILE : `row.getValue` will return a value of the type define by `Column`, here a `String`
    

    Complete usage example:

    DocumentRow row = new DocumentRow();
    row.setValue(Column.TEXT_VALUE, new DocumentCell<>("asdf"));
    row.setValue(Column.NUMERIC_VALUE, new DocumentCell<>(4));
    row.setValue(Column.DATE_VALUE, new DocumentCell<>(new Date()));
    
    String s = row.getValue(Column.TEXT_VALUE);
    Number i = row.getValue(Column.NUMERIC_VALUE);
    Date d = row.getValue(Column.DATE_VALUE);
    

    Note that in the last example, I didn't provides the type for DocumentCell, the compiler knows it will use the same a the Column parameter before.

    And you can find the complete code on this ideone project.


    Of course, we can drop the "enum-like" part and initialized the Column instance on need. All we need to do is set the constructor visible (not private at least) and we can create new "column type mappings"

    Column<LocalDateTime> colDate = new Column<>("A new Date", LocalDateTime.class);
    row.setValue(colDate , new DocumentCell<>(LocalDateTime.now()));