2010-03-24 16 views
9

Existen varias maneras diferentes en que puedo inicializar objetos complejos (con dependencias inyectadas y la configuración requerida de miembros inyectados), todas parecen razonables, pero tienen varias ventajas y desventajas. Voy a dar un ejemplo concreto:¿Es una buena o mala práctica llamar a métodos de instancia desde un constructor java?

final class MyClass { 
    private final Dependency dependency; 
    @Inject public MyClass(Dependency dependency) { 
    this.dependency = dependency; 
    dependency.addHandler(new Handler() { 
     @Override void handle(int foo) { MyClass.this.doSomething(foo); } 
    }); 
    doSomething(0); 
    } 
    private void doSomething(int foo) { dependency.doSomethingElse(foo+1); } 
} 

Como se puede ver, el constructor hace 3 cosas, incluyendo llamar a un método de instancia. Me han dicho que llamar a los métodos de instancia de un constructor no es seguro porque elude las comprobaciones del compilador para los miembros no inicializados. Es decir. Pude haber llamado al doSomething(0) antes de configurar this.dependency, que se hubiera compilado pero no funcionado. ¿Cuál es la mejor manera de refactorizar esto?

  1. Hacer doSomething estática y pasar de la dependencia explícita? En mi caso real, tengo tres métodos de instancia y tres campos de miembros que dependen uno del otro, por lo que parece una repetición adicional para que los tres estén estáticos.

  2. Mueva addHandler y doSomething en un método @Inject public void init(). Si bien el uso con Guice será transparente, se requiere una construcción manual para asegurarse de llamar al init() o de lo contrario el objeto no será completamente funcional si alguien se olvida. Además, esto expone más de la API, y ambas parecen malas ideas.

  3. Wrap una clase anidada para mantener la dependencia para asegurarse de que se comporta adecuadamente sin exponer API adicional:

    class DependencyManager { 
        private final Dependency dependency; 
        public DependecyManager(Dependency dependency) { ... } 
        public doSomething(int foo) { ... } 
    } 
    @Inject public MyClass(Dependency dependency) { 
        DependencyManager manager = new DependencyManager(dependency); 
        manager.doSomething(0); 
    }
    Esto empuja los métodos de instancia fuera de todos los constructores, pero genera una capa extra de clases, y cuando ya tuve interior y las clases anónimas (por ejemplo, ese controlador) pueden volverse confusas: cuando probé esto, me dijeron que moviera el archivo DependencyManager a otro archivo, lo que también es desagradable porque ahora hay varios archivos para hacer una sola cosa.

Entonces, ¿cuál es la forma preferida de tratar este tipo de situación?

+1

@Steve: Acabo de eliminar las primeras etiquetas "pre" para que el código muestre el uso de la sintaxis codificada por colores :) – SyntaxT3rr0r

+0

Genial, no sabía que funcionó de esa manera. – Steve

Respuesta

8

Josh Bloch en Java Efectivo recomienda usar un método de fábrica estático, aunque no puedo encontrar ningún argumento para casos como este. Sin embargo, hay un caso similar en Java Concurrency in Practice, específicamente destinado a evitar la filtración de una referencia a this del constructor. Aplicada a este caso, que se vería así:

final class MyClass { 
    private final Dependency dependency; 

    private MyClass(Dependency dependency) { 
    this.dependency = dependency; 
    } 

    public static createInstance(Dependency dependency) { 
    MyClass instance = new MyClass(dependency); 
    dependency.addHandler(new Handler() { 
     @Override void handle(int foo) { instance.doSomething(foo); } 
    }); 
    instance.doSomething(0); 
    return instance; 
    } 
    ... 
} 

Sin embargo, esto puede no funcionar bien con la anotación DI utiliza.

+0

Estoy usando Guice (bueno, en realidad Gin) - no * directamente * admite fábricas estáticas, pero tampoco las previene - Solo necesito agregar una copia extra del método de fábrica en 'GinModule'. La otra alternativa es crear una clase anidada 'Provider ' y anotar 'MyClass' como' @ProvidedBy (MyClass.MyProvider.class) '. Había estado tratando de evitar los métodos de fábrica estáticos ya que la estática puede causar muchos problemas con las pruebas, pero me di cuenta de que el constructor es efectivamente el mismo. – Steve

0

Sí, en realidad es ilegal, lo que realmente debería ni siquiera compilar (pero yo creo que sí)

Considere el Builder vez (y se inclinan hacia inmutable que en términos Builder significa que no se puede llamar cualquier setter dos veces y no puede llamar a ningún setter después de que el objeto haya sido "usado"; llamar a un setter en ese punto probablemente arroje una excepción de tiempo de ejecución).

Puede encontrar las diapositivas de Joshua Bloch en el (nuevo) Builder en una presentación de diapositivas llamada "A partir de Java recargada: esta vez es para el Real", por ejemplo aquí:

http://docs.huihoo.com/javaone/2007/java-se/

+1

El hecho de que compila no lo hace "ilegal"; hay una diferencia entre mala práctica/"no hacer esto" e ilegal –

+0

Tienes razón, probablemente usé la palabra incorrecta, pero cualquier programa de pelusa decente mostraría esto como al menos una advertencia y posiblemente un error - I No estoy seguro de por qué lo permiten, pero aún no estoy seguro de por qué permiten miembros públicos en Java ... Todo es un misterio. –

0

Puede usar un método estático que tome la dependencia y construye y devuelva una nueva instancia, y marque el constructor Friend. No estoy seguro de que Friend exista en java (¿está el paquete protegido?). Sin embargo, esta podría no ser la mejor manera. También podría usar otra clase que sea una fábrica para crear MyClass.

Editar: Wow otro publicado simplemente sugirió esta misma cosa exacta. Parece que puedes hacer que los constructores sean privados en Java. No se puede hacer eso en VB.NET (no estoy seguro de C#) ... muy bueno ...

9

También tiene problemas de herencia. Si su constructor está siendo llamado en la cadena para instanciar una subclase de su clase, puede llamar a un método que se invalida en la subclase y se basa en un invariante que no se establece hasta que se haya ejecutado el constructor de la subclase.

+0

Buen punto - ¡No había pensado en eso! En este caso, no es un problema ya que 'MyClass' es' final', pero es algo a tener en cuenta. – Steve

4

Querrá tener cuidado con el uso de métodos de instancia desde el constructor, ya que la clase aún no se ha construido por completo. Si un método llamado utiliza un miembro que aún no se ha inicializado, bueno, sucederán cosas malas.

Cuestiones relacionadas