1

In my project I have a Model abstract class, which defined a getId() method and a different classes that extends Model. In order to be enable each class to define the type of the ID field as it needs, I made the Model class generic:

public abstract class Model<T> {
    public abstract T getId();
}

So I can extend it now:

public class Person extends Model<String> {
    @Override
    public String getId() {
        return getName();
    }
}

That is ok.

Now I would like to create a class that act as a change set, where I can manage updated and deleted models. In this class, I would like to manage the updated Models using a map, being the model id the key and the model itself the value. (A changeset will have only one type, for instance ChangeSet will only contain person, ChangeSet will only contain cars and so on).

I would like to instantiate it like this:

ChangeSet<Person> changeSet = new ChangeSet<Person>();

It makes sense to me that it is possible to infer that the ID will be a String because Person extends Model.

But it doesn't seem to be possible. The nearest I could get were these two alternatives:

public class ChangeSet<M extends Model, T>{
    private final Map<T, M> updated = new HashMap<>();
...
}

so I can store in a set the models that were updated. But Eclipse is compaining that I should specify the generic type of the Model, creating a signature like this:

public class ChangeSet<M extends Model<T>, T>{
    private final Map<T, M> updated = new HashMap<>();
...
}

Eclipse is fine with it, but now I need this code to instantiate an object.

ChangeSet<String, Person> changeSet = new ChangeSet<Person<String>, String>();

It is not so elegant and having duplicated "String" is awkward.

Is there any way I can get (near) to my desired declaration?

JSBach
  • 4,679
  • 8
  • 51
  • 98
  • what version of JDK are you using? – ring bearer Jul 08 '15 at 15:57
  • With 1.7, I think that is the nearest we can get. In Java 8, you will be able to write this as `ChangeSet changeSet = new ChangeSet();` without duplication. Which can be further written as `ChangeSet changeSet = new ChangeSet<>();` with diamond operator. I think this is possible via type inference - https://docs.oracle.com/javase/tutorial/java/generics/genTypeInference.html – ring bearer Jul 08 '15 at 16:08

2 Answers2

2

A good combination of interface usage and generics would definitely result in a simple cleaner implementation that is also extensible.

Here is what I propose - I actually implemented this and it works fine :

Update based on comments from the OP

The Model interface can be "generified" to constrain the return types:

package org.example;

/**
 * Created by prahaladd on 08/07/15.
 */
public interface Model<T extends  Identifier>
{
    T getIdentifier();
}

Implement the model class that uses a particular type of identifier :

package org.example;



/**
 * Created by prahaladd on 08/07/15.
 */
public class Person implements Model<StringIdentifier>
{

    private final String name;
    private final String id;

    public Person(String id, String name)
    {
        this.id = id;
        this.name = name;
    }
    @Override
    public StringIdentifier getIdentifier()
    {
        return new StringIdentifier(id);
    }

    public String getName()
    {
        return  name;
    }
}

ChangeSet implementation now changes a bit to mimic the Map interface as below. It in-fact now takes in the type of the identifiers that would be stored as keys:

package org.example;

import java.util.Map;

/**
 * Created by prahaladd on 08/07/15.
 */
public class ChangeSet<T extends Identifier, M extends  Model<T>>
{
    //Refer to PECS - http://stackoverflow.com/questions/2723397/java-generics-what-is-pecs

    private Map<? super Identifier, M> changeMap;

    public void addChange(M element)
    {
        changeMap.put(element.getIdentifier(),element);
    }

    public M getChangedElementForId(T id)
    {
        return changeMap.get(id);
    }
}

All these changes are not so bad - you can instantiate the ChangeSet pretty much easily as below :

package org.example;

public class Main {

    public static void main(String[] args)
    {
        Person p1 = new Person("1", "Tom");
        Person p2 = new Person("2", "Jerry");

         //change set is instantiated without any redundant generic parameters
        ChangeSet<StringIdentifier, Person> changes = new ChangeSet<StringIdentifier,Person>();

        //assume that there were some changes and you want to add them to the changeset.
        changes.addChange(p1);
        changes.addChange(p2);

        //retrieve element from the changeset for an id

        p1= changes.getChangedElementForId(new StringIdentifier("1"));
        p2 = changes.getChangedElementForId(new StringIdentifier("2"));


    }
}

Alternate Solution

Firstly - define an Interface that would encapsulate your ID. This is not an overkill; given that you have different types of IDs using an Interface to define the contract for an identifier would go a long way to make your code clean and extensible:

package org.example;

/**
 * Created by prahaladd on 08/07/15.
 */
public interface Identifier<T>
{
    T getIdentifier();
}

Now that you have defined an Identifier interface, you can define different implementations for it corresponding to your various ID types. For e.g. below I have provided an implementation for the StringIdentifier which generates IDs of type string:

package org.example;

/**
 * Created by prahaladd on 08/07/15.
 */
public class StringIdentifier implements Identifier<String>
{

    private final String identifier;

    public StringIdentifier(String id)
    {
        identifier = id;
    }

    @Override
    public String getIdentifier()
    {
        return "someId";
    }
}

Now define the Model interface. Ideally, the Model interface should not deal with any of the ID types, it should just know that it has to return an Identifier (as is your use case).

package org.example;

/**
 * Created by prahaladd on 08/07/15.
 */
public interface Model
{
    Identifier getIdentifier();
}

Now provide an implementation of the Model interface. For e.g. below is the Person class that has been mentioned in your query:

package org.example;

/**
 * Created by prahaladd on 08/07/15.
 */
public class Person implements Model
{

    private final String name;
    private final String id;

    public Person(String id, String name)
    {
        this.id = id;
        this.name = name;
    }
    @Override
    public Identifier getIdentifier()
    {
        return new StringIdentifier(id);
    }

    public String getName()
    {
        return  name;
    }
}

Now define the ChangeSet. The ChangeSet should only know that it stores mapping between ID objects and the corresponding Model. It does not really know about the type of the ID objects. This makes the ChangeSet class extremely flexible to even support heterogenous collection in addition to the homogenous ones that you want.

package org.example;

import java.util.Map;

/**
 * Created by prahaladd on 08/07/15.
 */
public class ChangeSet<M extends  Model>
{
    //Refer to PECS - http://stackoverflow.com/questions/2723397/java-generics-what-is-pecs

    private Map<? super Identifier, M> changeMap;

    private Class identifierType;

    public void addChange(M element)
    {
        //prahaladd - update :  save the identifier type for a later check.
        if(identifierType != null)
        {
             identifierType = element.getIdentifier.getClass();
        } 
        changeMap.put(element.getIdentifier(),element);
    }

    public M getChangedElementForId(Identifier id)
    {
        //prahaladd updated - verify that the type of the passed in id
        //is the same as that of the changeset identifier type.
        if(!id.getClass().equals(identifierType))
        {
                 throw new IllegalArgumentException();
        }
        return changeMap.get(id);
    }
}

Now the hard work pays off. Take a look at the below client implementation:

package org.example;

public class Main {

    public static void main(String[] args)
    {
        Person p1 = new Person("1", "Tom");
        Person p2 = new Person("2", "Jerry");

        ChangeSet<Person> changes = new ChangeSet<Person>();

        //assume that there were some changes and you want to add them to the changeset.
        changes.addChange(p1);
        changes.addChange(p2);

        //retrieve element from the changeset for an id

        p1= changes.getChangedElementForId(new StringIdentifier("1"));
        p2 = changes.getChangedElementForId(new StringIdentifier("2"));
    }
}

Exactly as you envisioned!! As you can see, there is nothing fancy that has been done here. Plain Object oriented concepts and a thoughtful combination of interface and generics.

Hope this helps!!

Prahalad Deshpande
  • 4,709
  • 1
  • 20
  • 22
  • Hi Prahalad, very thoughtful response and great explanation. But your class model comes at a price. It introduces a wrapper class Identifier for the id and an additional implementation class (StringIdentifier). To call ChangeSet#getChangedElementForId() you need a temporary Identifier object. And it is not typesafe: Given a variable named changes of type ChangeSet you could call changes.getChangedElementForId(new IntIdentifier(1)) which of course does not make sense (always returns null), but is also not spotted by the compiler (as wanted by the question author). – wero Jul 08 '15 at 19:03
  • @Prahalad that is a very nice solution, would you have any suggestions for the issue that wero raised about the "find by id"? – JSBach Jul 09 '15 at 07:53
  • @JSBach I am providing an update to my solution and will preserve the original answer for the sake of an alternate reference. In order to address wero's comment - a not so nice way of doing it would be to introduce some type of validation within the ChangeSet class for getChangedElementForId which checks that the passed in identifier is of the same type as the identifier that the map expects(Updated my previous solution with a comment). The changeset can do this without really knowing the underlying type. However, the updated solution handles this naturally and I am more inclined towards it – Prahalad Deshpande Jul 09 '15 at 18:37
0

If the updated map is an implementation detail of ChangeSet, you could simply write:

public class ChangeSet<M extends Model<?>> {
    private final Map<Object,M> updated = new HashMap<>();

    public void add(M model) {
        updates.put(model.getId(), model);
    }
}

but this is not a good solution if ChangeSet should have methods which rely on the id type T of the Model.

wero
  • 32,544
  • 3
  • 59
  • 84
  • This is exactly the problem: I have an addModel method that checks if the model already exists and "merges" both if it does. This is what triggered the problem in the first place :( – JSBach Jul 08 '15 at 15:15
  • what are the parameters of your addModel-method? – wero Jul 08 '15 at 16:09
  • Sorry for the delay... public void addModel(final M model) { if (!map.contains(model.getId())) { ..... } – JSBach Jul 09 '15 at 07:53