Tuesday, December 8, 2009

A modular adapter

This post is related to some code I wrote about creating a way to map different but similar objects, and is related to the adapter pattern.

This is not a "standard" way to use the adapter pattern, so if you are looking for the official adapter pattern, this post is not for you.

Suppose that two classes A and B do similar stuffs, in different way, and we are not able to decide which one we are going to use.
We want to find how they are similar, and how can we deal about deciding witch one we are going to really use at run time, in terms of how initialize them (in the A way or in the B way), and witch one will be the actual class used (real A o real B, provided that they will be viewed as a common interface).

We want to manage wrappers that maps one to another and create an abstraction layer that hides the real class, and manage in run time the concrete implementation.

Here is an example:
StringString manages and print strings, and is created passing the plain String objects:

public class StringString {
private String string;

public StringaString(String string) {
this.string = string;
}
public void print() {
System.out.println(string);
}
}




The char version is called StringChar:

public class StringChar implements StringaGetter {
private char[] chars;
public StringaChar(char[] chars) {
this.chars = chars;
}
public void print() {
System.out.println(chars);
}
}




They do really the same thing, but they are not created in the same way.

The wrapping code that allows to create the StringChar from String object is the conversion from String to char array string.toCharArra() and the wrapping code to initialize the StringChar having the charArray is new String(aCharArray).

What about creating of an abstraction layer between the how to create, and what it will be?
We will need to manage the wrappers between them, in order to be able, at run time to decide witch contract we are going to obey for the object creation/method, and what the concrete implementation will be.

We could choose any combination.

We are also going to change both the objects in order to implement a common interface so we will inspect them in a unique way established by an implemented interface, whatever is their actual class.

Here are our steps:

1) the common interface that both should implement is StringGetter:

public interface StringaGetter {
public String getString();
}


2) both classes are slightly modified to to implement that interface from now:



public class StringaString implements StringGetter {
private String string;

// wee will see this later
@convertible(contractClass = StringGetter.class)
public StringaString(String string) {
this.string = string;
}
public void print() {
System.out.println(string);
}

public String getString() {
return string;
}
}



and:

public class StringaChar implements StringGetter {
private char[] chars;

@convertible(contractClass = StringGetter.class)
public StringaChar(char[] chars) {
this.chars = chars;
}
public void print() {
System.out.println(chars);
}


public String getString() {
return new String(chars);
}

}


Both the classes are different in the way they are created and have some new behavior that obey to the contract defined by the StringGetter interface.
Moreover, we added a @convertible(contractClass = StringGetter.class) tag to the constructor.
Any other class that implements StringGetter interface and that add the same annotation to the constructor could be used for the same purposes.

The next step is to create an abstraction layer that will let us select any creational contracts, and any concrete implementation that means, the "apparent" implementation and the "actual" implementation.

This abstraction layer manages a map of wrappers.
A wrapper is an implementation of the following interface:


public interface Wrapper {
Object[] wrap(Object[] object);
}


For example the StringToChar wrapper is the following:

public class StringToChar implements Wrapper{
public Object[] wrap(Object[] object) {
return new Object[]{((String)object[0]).toCharArray()};
}
}


which converts a 1 size Object[] array containing a String to a one size Object[] array containing the corresponding charArray().

The opposite is the CharToString wrapper:


public class CharToString implements Wrapper {
public Object[] wrap(Object[] object) {
return new Object[]{new String((char[])object[0])};
}
}


We need to inform our abstraction layer about using the two wrappers:

ConversionMap.getInstance().setMapper(
StringString.class.getConstructor(new Class[]{String.class}),
StringChar.class.getConstructor(new Class[]{char[].class}),
StringGetter.class,
new StringToChar());


The conversion from the parameters suitable for the former constructor to the latter that will be used in order to manage objects that implements the StringGetter.class interface is a StringToChar.

This is about the reverse direction:

ConversionMap.getInstance().setMapper(
StringChar.class.getConstructor( new Class[]{char[].class}),
StringString.class.getConstructor(new Class[]{String.class}),
StringGetter.class,
new CharToString());


At the end: we are going to put the "wrappable" constructors in a map, managed by the "modular factory", and giving them a name:

MapOfConverterModularFactory mapConv = new MapOfConverterModularFactory(StringaGetter.class);
mapConv.putImplementation("charString",
StringaChar.class.getConstructor(new Class[]{char[].class}));
mapConv.putImplementation(
"StrinString",StringaString.class.getConstructor(new Class[]{String.class}));


And now we can choose to select witch implementation to use for the creational purposes and witch one the real implementation:


mapConv.setCurrentImplementation("charString");
mapConv.setRereferenceImplementation("StrinString");


Current is the "actual", and reference is the "apparent".

We were used to call StringString myString = new StringString("hello");
and now we will instead call:


StringGetter ob = (StringGetter)mapConv.getObject(
new Object[]{"hello"})


And ob is built according to the StringString style, and of course, his current concrete class is StringChar:


assertEquals(ob.getClass(),StringaChar.class);


This is the opposite situation:


mapConv.setReferenceImplementation("charString");
mapConv.setCurrentImplementation("StrinString");


We were used to call StringString myString = new StringString("hello");
and now we will instead call:


StringGetter ob = (StringGetter)mapConv.getObject(
new Object[]{new char[]{'h','e','l','l','o'}));


And ob is built according to the StringString style, and of course, his current concrete class is StringString:


assertEquals(ob.getClass(),StringString.class);


All the code will be on line asap.


Tony (8/12/2009)

No comments: