Estoy diseñando un simulador con una jerarquía de objetos simulados. Para iniciar el simulador, los objetos iniciales deben cargarse desde un archivo. Cada objeto tendrá que analizarse a partir de una simple representación textual de clave-valor. Queremos agregar nuevas clases de objetos de vez en cuando.

Desafortunadamente, no hay "herencia estática" en Java. En particular, el siguiente enfoque hubiera sido bueno tener:

public class SimObject {
   /**
    * (unfortunately illegal) - to be implemented by subclasses,
    * returns a new object if successful, or none on failure.
    */
   static abstract SimObject buildFromAttributes(Map<String,String> attrs);

  // ... other constructors, behavior
}

Esto habría permitido el uso de algo como el siguiente código de análisis, que crea un SimObject del tipo apropiado al buscar el primer analizador que lo compila correctamente:

public SimObject parse(Map<String,String> attrs, Class<SimObject>[] classes) {
   for (Class<SimObject> c : classes) {
       SimObject o = c.buildFromAttributes(attrs); // illegal, I know
       if (o != null) return o;
   } 
   throw new IllegalArgumentException("Could not create valid simobject");
}

Como eso no es posible, actualmente estoy usando

public class SimObject {
   /** empty constructor, to call buildFromAttributes from */
   SimObject() {} 
   /**
    * returns a new object if successful, or none on failure.
    */
   abstract SimObject buildFromAttributes(Map<String,String> attrs);

  // ... other constructors, behavior
}

Y

public SimObject parse(Map<String,String> attrs, SimObject[] instances) {
   for (SimObject i : instances) {
       SimObject o = i.buildFromAttributes(attrs);
       if (o != null) return o;
   } 
   throw new IllegalArgumentException("Could not create valid simobject");
}

Lo que funciona, pero parece feo tener un buildFromAttributes que debería haber sido static, en el sentido de que no accede ni modifica el estado de su objeto.

Otra opción sería usar objetos separados para realizar el análisis (llámalos SimObjectBuilder s, y tener uno de esos por SimObject subclase); pero esos objetos estarían muy acoplados a la implementación interna de los objetos que construyen, y me gustaría mantener el código lo más breve y claro posible.

Requisitos adicionales: Me gustaría evitar el uso de un atributo "class" para implementar el análisis basado en la reflexión (lo que eliminaría la necesidad de iterar a través de posibles analizadores al enfocar directamente en el correcto); o bibliotecas externas. Este es un ejercicio de diseño para usar con estudiantes que aprenden OO donde el enfoque es la simplicidad y la claridad, no el código de producción.

1
tucuxi 12 ene. 2018 a las 22:05

3 respuestas

La mejor respuesta

Aquí están sus requisitos como yo lo veo:

1) No desee definir ningún tipo de SimObjectBuilders nuevo.

2) No desee crear objetos SimObject solo para analizarlos.

3) Sin reflexión.

Luego puede usar lambdas y construir en tipo Function envolviéndolo con métodos estáticos definidos en cada subclase de SimObject:

    SimObject o = parse( new HashMap<>(), Arrays.asList(
    SimObject1::buildFromAttributes, SimObject2::buildFromAttributes ) );

    public SimObject parse(Map<String,String> attrs, 
    List< Function<Map<String,String>, SimObject> > factories) {
        for (Function<Map<String,String>, SimObject> f : factories) {
            SimObject o = f.apply(attrs);
            if (o != null) return o;
        } 
        throw new IllegalArgumentException("Could not create valid simobject");
     }

Y aquí están tus subclases:

public class SimObject1 extends SimObject{

    public static SimObject1 buildFromAttributes(Map<String,String> attrs){
        return new SimObject1();
    }
}

public class SimObject2 extends SimObject{

    public static SimObject2 buildFromAttributes(Map<String,String> attrs){
        return new SimObject2();
    }
}
2
tsolakp 13 ene. 2018 a las 15:36

Dado que este es un ejercicio de diseño, separar la creación de objetos de su comportamiento sería un diseño superior.

Puede crear una jerarquía de objetos Factory paralela a la jerarquía de objetos de simulación, que luego le permitiría usar un código similar al que tiene ahora (reemplazando la matriz SimObject con SimObjectFactory).

Además de la claridad que proviene de la separación más estricta del código de análisis y el código de comportamiento, también puede implementar varias fábricas para el mismo tipo de objeto, admitiendo varios formatos de entrada.

1
kewne 12 ene. 2018 a las 19:23

La forma real en que te gustaría usar derrota los principios de OOP:

public SimObject parse(Map<String,String> attrs, Class<SimObject>[] classes) {
   for (Class<SimObject> c : classes) {
       SimObject o = c.buildFromAttributes(attrs); // illegal, I know
       if (o != null) return o;
   } 
   throw new IllegalArgumentException("Could not create valid simobject");
}

Los métodos estáticos no están diseñados para proporcionar un comportamiento de polimorfismo y los métodos static están fuertemente acoplados con la clase que los declara.
Por lo tanto, es más difícil cambiar a otra implementación, usar un mecanismo de inyección de dependencia o burlarse del método de manera directa.

Sobre las otras opciones, resumo:

  • no desea utilizar métodos de instancia porque no desea crear objetos "falsos".

  • No desea separar las responsabilidades: SimObject construyendo y SimObject subclases tampoco.

  • Tampoco quieres usar el reflejo.

Cada idioma tiene sus limitaciones.
Parece que conoce los del lenguaje Java.

Este es un ejercicio de diseño para usar con estudiantes que aprenden OO donde el enfoque es la simplicidad y la claridad, no el código de producción.

La forma en que separa las subclases SimObject de construcción y SimObject está bien y se basa en los principios de OOP:

  • separación de responsabilidades
  • patrón de fábrica
  • objeto de colaboraciones
  • mantenibilidad del código: eliminar o agregar un par de SimObject-SimObjectBuilder es fácil
  • primordial

E incluso si no especificó que la pregunta era para los ejercicios de OOP de los estudiantes, es la forma en que propondría.

pero esos objetos estarían muy acoplados a la implementación interna de los objetos que construyen y me gustaría mantener el código lo más breve y claro posible.

El acoplamiento es un acoplamiento unidireccional: del constructor al objeto a crear.
Está bien y también parece esperado que un generador que construya instancias de una clase específica se acople a esta clase específica.
Entonces, sí, el código limpio tiene un costo de escritura, pero también trae ventajas al tener un código robusto que puede cambiar más fácilmente.

1
davidxxx 13 ene. 2018 a las 09:03