patron - Una alternativa para el patrón Decorator en Java?
patrones de diseño java (2)
En función de tu edición reciente.
Decorator no es adecuado para esto, ya que podría haberse dado cuenta. Esto se debe a que lo que soluciona es el aumento de una funcionalidad única, no el aumento de un árbol de clase completo.
Una posible forma de lograr esto es con la estrategia en su lugar. La estrategia se enfoca algorítmicamente; le permite desacoplar el código de comportamiento (lo siento si un pequeño C # se desliza aquí y allá)
Clase de muestra
public class S{
private List<Integer> Samples = new List<Integer>();
public void addSample(int x){
Samples.Add(new Integer(x));
}
public void Process(IOp[] operations){
for (Op operation : operations){
Process(operation);
}
}
public void Process(ICollection<IOp> operations){
for (Op operation : operations){
Process(operation);
}
}
public void Process(IOp operation){
operation.Compute(this.Samples);
}
}
Operaciones
public interface IOp{
// Interface is optional. Just for flexibility.
public void Compute(List<Integer> data);
}
public class Op<T> implements IOp{
// Generics is also optional. I use this to standardise data type of Result, so that it can be polymorphically accessed.
// You can also put in some checks to make sure Result is initialised before it is accessed.
public T Result;
public void Compute(List<Integer> data);
}
class ComputeMeanOperation extends Op<double>{
public void Compute(List<Integer> data){
/* sum and divide to get mean */
this.Result = /* ... */
}
}
class CheckConvergenceOperation extends Op<boolean>{
public void Compute(List<Integer> data){
/* check convergence */
this.Result = /* ... */
}
}
Uso
public static void main(String args[]){
S s = new S();
s.addSample(1);
/* ... */
ComputeMeanOperation op1 = new ComputeMeanOperation();
CheckConvergenceOperation op2 = new CheckConvergenceOperation ();
// Anonymous Operation
Op<Integer> op3 = new Op<Integer>(){
public void Compute(List<Integer> samples){
this.Result = samples[0]; // Gets first value of samples
}
}
s.Process(op1); // Or use overloaded methods
s.Process(op2);
s.Process(op3);
System.out.println("Op1 result: " + op1.Result);
System.out.println("Op2 result: " + op2.Result);
System.out.println("Op3 result: " + op3.Result);
}
Pros:
- puede agregar y eliminar operaciones arbitrariamente según lo que necesite.
- sin cambios adicionales a la clase de muestra.
- la clase de muestra es una estructura de datos cohesiva.
- modularidad: cada operación es independiente. la interfaz solo expone lo que se requiere. Proceso común para la interacción con cada op.
- Si usted, por cualquier razón, necesita hacer esto repetidamente, puede almacenar todas las operaciones en una matriz, y reutilizar eso en un bucle. Mucho más limpio que llamar a 4-5 métodos y almacenar resultados.
Contras / Limitaciones:
- si sus operaciones requieren gran cantidad de datos, deberá exponer esos datos a sus operaciones, aumentando el acoplamiento (puedo editar la publicación si la necesita). En mi ejemplo, simplemente pasé una lista de muestra única. Si es necesario, puede que tenga que pasar toda la estructura de datos en su lugar.
- si tiene operaciones que dependen del resultado de otra operación, esto no funcionará de la caja. (Esto se puede lograr usando Composite en su lugar - una mega Op que se compone de varias operaciones, el resultado de lo cual se pasa a la siguiente).
Espero que esto se adapte a tus necesidades :)
Supongamos que tiene la siguiente jerarquía de clases relacionadas con estadísticas, estructurada de manera similar al patrón de método de Plantilla :
interface S {
// Method definitions up-to and including the S3 class
}
class S0 implements S {
// Code that counts samples
}
class S1 extends S0 {
// Code that calls the superclass methods and also computes the mean
}
class S2 extends S1 {
// Code that calls the superclass methods and also computes the variance
}
class S3 extends S2 {
// Code that calls the superclass methods and also computes the skewness
}
Supongamos ahora que queremos extender cada una de estas clases para, por ejemplo, verificar la convergencia de una métrica. Para mis propósitos, no necesito hacer esta extensión en tiempo de ejecución. Puedo pensar en las siguientes alternativas:
Cree las subclases
S0C
,S1C
,S2C
yS3C
deS0
,S1
,S2
yS3
respectivamente, cada una con una copia del código que verifica la convergencia:- Pros:
- conceptualmente directo
- los objetos resultantes aún pertenecen a la superclase
- el código fuente de la subclase solo contiene el código adicional de verificación de convergencia
- Contras:
- muchas y muchas duplicaciones de código, con la sobrecarga de sincronización de cambios resultante en el futuro
- Principales contras:
- ¿y si quiero otro conjunto de clases que, por ejemplo, preprocesen las muestras? Estamos hablando de replicación exponencial del mismo código!
- Pros:
Usa el patrón Decorator :
- Pros:
- ¡Sin duplicación de código!
- Contras:
- Los objetos ya no pertenecen a la clase original (se trabajó fácilmente)
- Un rendimiento muy leve (¡existe! ¡Lo he medido!) Alcanzado en Java, debido al uso de llamadas a métodos virtuales, a diferencia de las invocaciones de métodos especiales. No es muy importante, pero aún es notable.
- Principales contras:
- Aproximadamente un trillón de métodos de delegado que deben mantenerse sincronizados con la interfaz de objeto envuelto. El uso de interfaces asegura que no se pierda ningún método, pero aún es difícil de mantener, incluso con IDE que automatizan la generación de métodos de delegado.
- Para tener un patrón de decorador implementado correctamente, todos los decoradores y las clases envolventes deben implementar exactamente la misma interfaz. Esto significa esencialmente que tendría que agregar, por ejemplo, los métodos de comprobación de convergencia a la interfaz
S
, lo que destruye por completo cualquier sensación de modularidad. La única forma de levantar este requisito sería prohibir decoradores anidados en mi código.
- Pros:
Si Java admitiera herencia múltiple, probablemente habría sido capaz de manejar esto heredando tanto de las estadísticas como de una clase base de comprobación de convergencia (o lo que sea). Por desgracia, Java no es compatible con herencia múltiple (¡no, las interfaces no cuentan!).
¿Hay una mejor manera de manejar este problema en Java? Tal vez un patrón de diseño diferente? Una solución más técnica? ¿Algún tipo de baile ritual especial?
PD: Si malinterpreto algo, siéntete libre (suavemente) de señalarlo ...
EDITAR:
Parece que necesito aclarar un poco mis objetivos:
No necesito la composición de objetos en tiempo de ejecución. Lo que quiero es ampliar las capacidades de las clases
S*
con nuevos métodos. Si pudiera crear subclases según sea necesario sin duplicación de código, probablemente lo haría de esa manera. Si pudiera hacerlo en la ubicación de uso (improbable), incluso mejor.Prefiero no escribir el mismo código una y otra vez. Nota: los métodos delegados y los constructores están bien, supongo, los métodos que implementan los algoritmos no.
Me gustaría mantener mis interfaces modulares. Este es mi problema principal con el patrón Decorator: a menos que se coloquen restricciones de anidamiento muy específicas, terminará con una super-interfaz de todas las interfaces ...
EDICION 2:
Para abordar algunos de los comentarios:
Las clases
S*
están estructuradas usando métodos de plantilla:class S0 { int addSample(double x) { ...; } double getMean() { return Double.NaN; } } class S1 extends S0 { int addSample(double x) { super.addSample(x); ...; } double getMean() { return ...; } }
Las clases ampliadas de
S*C
de la primera solución serían las siguientes:interface S { int addSample(double x); double getMean(); } class S0C extends S0 implements S { int addSample(double x) { super.addSample(x); ...; } boolean hasConverged() { return ...; } } class S1C extends S1 { int addSample(double x) { super.addSample(x); ...; } boolean hasConverged() { return ...; } }
Tenga en cuenta la duplicación del método
hasConverged()
.Un decorador de comprobación de convergencia sería así:
class CC<T extends S> implements S { T o = ...; int addSample(double x) { o.addSample(x); ...; } double getMean() { return o.getMean(); } boolean hasConverged() { return ...; } }
El problema: si deseo combinar otro comportamiento separador además de la verificación de convergencia, necesito un decorador por separado, por ejemplo,
NB
, y para tener acceso, por ejemplo, al métodohasConverged()
, el nuevo decorador necesita:- Implementar la misma interfaz que
CC
- Use la misma interfaz que
CC
para su tipo de objeto envuelto ... - ... lo que me obliga a usar esa interfaz para los métodos
S*
si quiero poder usarNB
con objetosS*
sin usarCC
- Implementar la misma interfaz que
Mi selección del patrón de decorador fue solo por falta de una mejor alternativa. Es la solución más limpia que he encontrado hasta ahora.
Al extender las clases
S*
, aún necesito los originales intactos. Poner, por ejemplo, la funcionalidad de convergencia en una superclase común significaría que el comportamiento asociado (y su impacto en el rendimiento) existiría ahora en todas las subclases, que definitivamente no es lo que quiero.
estoy confundido. no está claro por qué necesita el primer árbol de herencia. algo como el siguiente código puede hacer el trabajo para eso:
public class Statistics {
void add(final double x) {
sum+=x;
sum2+=x*x;
sum3+=x*x*x;
n++;
}
double mean() {
return n!=0?sum/n:0;
}
double variance() {
return n!=0?(sum2-sum*sum/n)/(n-1):0;
}
// add method for skewness
double sum,sum2,sum3;
int n;
}