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!!