Vi è una corrispondenza biunivoca tra le due rappresentazioni, e la conversione da una rappresentazione all'altra è possibile sfruttando le seguenti relazioni:
Ho creato un progettino (in java) che investiga su questo argomento.
Una factory restituisce una rappresentazione concreta piuttosto che un'altra e lo fa reperendo i mapper che associano una rappresentazione ad un'altra.
Ogni costruttore di ogni implementazione, in modo dichiarativo, ovvero tramite attributi, mostra di essere mappabile in altri costruttori. Sa come trasformare la n-pla di parametri di inizializzazione nella equivalente n-pla usata da un altro costruttore e/o da un'altra implementazione.
Un complesso è definito come segue.
Il come costruirlo lo gestiremo nella factory.
public interface ComplexNumber {
public double getReal();
public double getImg();
}
Abbiamo le due seguenti implementazioni, Cartesiana e Polare:
public class ComplexNumberCartesian implements ComplexNumber {
protected double real;
protected double img;
@instanceConverter(instanceConverterMap = CartesianToPolarMapper.class)
public ComplexNumberCartesian(double real, double img) {
this.real=real;
this.img = img;
}
public double getReal() {
return real;
}
public double getImg() {
return img;
}
...
}
Polare:
Nel costruttore abbiamo l'attributo @instanceConverter, che indica a sua volta la classe mapper, che associa a (real, img), l'equivalente coppia (ampiezza,angolo).public class ComplexNumberPolar implements ComplexNumber {
protected double magnitude;
protected Angle angle;
protected double real;
protected double img;
@instanceConverter (instanceConverterMap = PolarToCartesianMapper.class)
{
this.magnitude = magnitude;
this.angle = angle;
this.real = magnitude*Math.cos(angle.getValue());
this.img = magnitude*Math.sin(angle.getValue());
}
public double getReal() {
return real;
}
public double getImg() {
return magnitude*Math.sin(angle.getValue());
}
public boolean equals(Object object)
{
...
}
public int hashCode()
{
}
public String toString()
{
}
}
Ecco la definizione di questa annotation:
Identity è il caso banale appunto dell'identità.@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.CONSTRUCTOR)
public @interface instanceConverter {
Class instanceConverterMap() default IdentityToObject.class;
}
Noi sfruttiamo il mapper dalla forma cartesiana (o rettangolare) a quella polare:
In teoria la conversione specifica dipende anche dal tipo di costruttore della classe target invocato, e quindi si specifica anche il targetParamsType che è un array di classi (che consente di reperire il costruttore che accetta quelle classi come parametri).public class CartesianToPolarMapper implements WrapperToObject{
public Wrapper getWrapperForClassConstructorWithParameters(
Class targetClass,
Class targetParamsTypes[],
Class[] originParamsTypes)
{
if (targetClass.equals(ComplexNumberPolar.class)) {
return new CartesianToPolar();
}
throw new RuntimeException("wrapper not defined for the target class");
}
}
Questo non viene considerato nel nostro caso perché abbiamo un solo costruttore per Polar.
Viene restituito un "wrapper", che è il seguente:
public class CartesianToPolar implements Wrapper {
public Object[] wrap(Object[] object) {
return new Object[] {Math.sqrt((Math.pow((Double) object[0],2.0))+Math.pow((Double) object[1],2.0)),new Angle(Math.atan2((Double) object[1], (Double) object[0]))};
}
}
Il wrapper inverso, cioè da Polare a Cartesiano, che può essere reperito tramite l'annotazione associata al costruttore della implementazione polare, è il seguente:
public class PolarToCartesian implements Wrapper{
public Object[] wrap(Object[] object) {
return new Object[]{(Double)object[0]*
Math.cos(((Angle)object[1]).getValue()),
(Double)object[0]*Math.sin(((Angle)object[1]).getValue())};
}
}
La factory sfrutta queste informazioni:
public class ComplexNumbersFactory {
....
public static ComplexNumber getComplexFromCartesianPar(double first ,double second)
{
if (CARTESIAN.equals(implementation))
{
return new ComplexNumberCartesian(first,second);
}
if (POLAR.equals(implementation))
{
ComplexNumber converted = (ComplexNumber) Utilities.getInstanceOfThisActualGivenConstructorOfOther(
ComplexNumberPolar.class,ComplexNumberCartesian.class,
new Object[]{first,second},
new Class[]{double.class,double.class},
new Class[]{double.class,Angle.class});
return converted;
}
throw new RuntimeException("unadmitted implementation mode "+implementation);
}
}
Questo è il codice che reperisce il tutto reperisce il codice di conversione, esegue la conversione, e restituisce l'equivalente istanza nell'oggetto target:
public static Object getInstanceOfThisActualGivenConstructorOfOther(
Class targetClass,
Class originClass,
Object[] instanceOriginCompatible,
Class[] instanceOriginClasses,
Class[] instTargetClass)
{
try {
Constructor constructor = originClass.getConstructor(instanceOriginClasses);
WrapperToObject wrapper = (WrapperToObject) constructor.getAnnotation(instanceConverter.class).instanceConverterMap().newInstance();
Object[] convertedPars = ((wrapper.getWrapperForClassConstructorWithParameters(targetClass, instTargetClass,instanceOriginClasses).wrap(instanceOriginCompatible)));
Constructor targetConstructor = targetClass.getConstructor(instTargetClass);
Object convertedObject = targetConstructor.newInstance(convertedPars);
return convertedObject;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
Se la factory è settata in modo cartesiano, restituisce l'implementazione cartesiana senza nessuna conversione.
Se la factory è settata in modo polare, allora essa utilizza una funzione di conversine interrogando la classe ComplexNumberCartesian. Cioè chiede al suo costruttore, di fornire un wrapper in grado di eseguire il mapping tra parametri di istanza per il tipo concreto Cartesian, a parametri di istanza per il tipo concreto Polar.
Eventuali nuove estensioni non implicano cambiamenti al codice che ne faccia uso (salvo che eventualmente dover settare una proprietà), ma solo nella factory, purché queste nuove implementazioni rispettino il vincolo di dichiarare come mapparsi nelle implementazioni preesistenti (e viceversa).
La conversione, dovrebbe anche rispettare il principio che la conversione B->A applicata alla conversione A->B dovrebbe essere l'identità.
Verifichiamo con junit la creazione di due diverse implementazioni dello stesso numero, e ne testiamo l'uguaglianza:
ComplexNumber first = new ComplexNumberCartesian(1.0,1.0);Il tipo concreto restituito dopo che abbiamo settato la factory in modo polar è appunto polare, e quindi il cast non da eccezione, ed inoltre sfruttiamo dei metodi aggiuntivi che solo il polar mette a disposizione, che sono getAngle e getMagnitude che restituiscono i valori che ci aspettiamo coerenti per il numero 1+i.
ComplexNumbersFactory.setImplementation(ComplexNumbersFactory.POLAR);
ComplexNumber second= ComplexNumbersFactory.getComplexFromCartesianPar(1.0,1.0);
assertEquals(((ComplexNumberPolar)second).getAngle(),new Angle(Math.PI/4));
assertEquals(((ComplexNumberPolar)second).getMagnitude(),Math.sqrt(2.0));
assertEquals(first,second);
Riassumendo
Rispetto a diversi scopi un tipo concreto piuttosto che un altro può avere vantaggi di efficienza in casi particolari, ma bisogna nascondere la rappresentazione concreta per evitare che il programma chiamante dipenda da queste nuove implementazioni.
Usiamo attributi per rafforzare il legame che c'è tra classi imparentate tra loro.
"Extends" o "implements" garantisco che sintatticamente possono essere applicati a metodi deifiniti in termini della loro classe astratta (o interfaccia) ma niente di più.
Il dover mettere anche questa metainformazione dichiarativa può significare: ehi... se stai creando una nuova implementazione dovresti anche occuparti di dichiarare come fare a rendere possibile sostituire la tua implementazione al posto di quelle preesistenti, garantendo che tutto funzioni allo stesso modo di prima.
(nota: il codice versionato è stato rifattorizzato rispetto a quanto scritto in questo post, quindi potrebbe non corrispondere in nomi di classi packages).
Saluti e Buon Anno!
T.
1 commento:
Interesting to know.
Posta un commento