Forzar a otros desarrolladores a llamar al método después de completar su trabajo

34

En una biblioteca en Java 7, tengo una clase que proporciona servicios a otras clases. Después de crear una instancia de esta clase de servicio, se puede llamar a un método varias veces (llamémoslo doWork()método). Entonces no sé cuándo se completa el trabajo de la clase de servicio.

El problema es que la clase de servicio usa objetos pesados ​​y debería liberarlos. Configuré esta parte en un método (llamémoslo release()), pero no está garantizado que otros desarrolladores utilicen este método.

¿Hay alguna manera de obligar a otros desarrolladores a llamar a este método después de completar la tarea de clase de servicio? Por supuesto que puedo documentar eso, pero quiero forzarlos.

Nota: No puedo llamar al release()método en el doWork()método, porque doWork() necesita esos objetos cuando se llama a continuación.

Hasanghaforian
fuente
46
Lo que está haciendo allí es una forma de acoplamiento temporal y generalmente se considera un olor a código. Es posible que desee forzarse a sí mismo para crear un mejor diseño.
Frank
1
@Frank Estoy de acuerdo, si cambiar el diseño de la capa subyacente es una opción, entonces esa sería la mejor opción. Pero sospecho que esto es un envoltorio para el servicio de otra persona.
Dar Brett
¿Qué tipo de objetos pesados ​​necesita su servicio? Si la clase de Servicio no puede administrar esos recursos automáticamente por sí misma (sin requerir un acoplamiento temporal), entonces esos recursos deberían administrarse en otro lugar e inyectarse en el Servicio. ¿Ha escrito pruebas unitarias para la clase de servicio?
VENIDO del
18
Es muy "un-Java" requerir eso. Java es un lenguaje con recolección de basura y ahora se te ocurre algún tipo de artilugio en el que requieres que los desarrolladores "limpien".
Pieter B
22
@PieterB ¿por qué? AutoCloseablefue hecho para este propósito exacto.
BgrWorker

Respuestas:

48

La solución pragmática es hacer la clase AutoCloseabley proporcionar un finalize()método como respaldo (si es apropiado ... ¡vea a continuación!). Luego confía en los usuarios de su clase para usar try-with-resource o llamar close()explícitamente.

Por supuesto que puedo documentar eso, pero quiero forzarlos.

Desafortunadamente, no hay forma 1 en Java para obligar al programador a hacer lo correcto. Lo mejor que puede hacer es recoger el uso incorrecto en un analizador de código estático.


Sobre el tema de finalizadores. Esta característica de Java tiene muy pocos casos de buen uso. Si confía en los finalizadores para ordenar, se encuentra con el problema de que puede tomar mucho tiempo para que suceda. El finalizador solo se ejecutará después de que el GC decida que ya no se puede alcanzar el objeto. Es posible que eso no suceda hasta que JVM realice una recopilación completa .

Por lo tanto, si el problema que está tratando de resolver es recuperar los recursos que deben liberarse antes de tiempo , entonces ha perdido el bote al usar la finalización.

Y en caso de que no haya entendido lo que digo anteriormente ... ¡ casi nunca es apropiado usar finalizadores en el código de producción, y nunca debe confiar en ellos!


1 - Hay formas. Si está preparado para "ocultar" los objetos de servicio del código de usuario o controlar estrictamente su ciclo de vida (por ejemplo, https://softwareengineering.stackexchange.com/a/345100/172 ), entonces el código de usuario no necesita llamar release(). Sin embargo, la API se vuelve más complicada, restringida ... e IMO "fea". Además, esto no obliga al programador a hacer lo correcto . ¡Está eliminando la capacidad del programador de hacer lo incorrecto!

Stephen C
fuente
1
Los finalizadores enseñan una muy mala lección: si la atornillas, te la arreglaré. Prefiero el enfoque de netty -> un parámetro de configuración que instruye a la fábrica (ByteBuffer) a crear aleatoriamente con una probabilidad de X% bytebuffers con un finalizador que imprime la ubicación donde se adquirió este búfer (si aún no se ha lanzado). Por lo tanto, en producción, este valor se puede establecer en 0%, es decir, sin gastos generales, y durante las pruebas y el desarrollo a un valor más alto como 50% o incluso 100% para detectar las fugas antes de lanzar el producto
Svetlin Zarev
11
y recuerde que no se garantiza que los finalizadores se ejecuten nunca, incluso si la instancia del objeto se está recolectando basura.
Juent
3
Vale la pena señalar que Eclipse emite una advertencia del compilador si un AutoCloseable no está cerrado. Esperaría que otros IDE de Java también lo hagan.
meriton - en huelga
1
@jwenting ¿En qué caso no se ejecutan cuando el objeto es GC'd? No pude encontrar ningún recurso que indique o explique eso.
Cedric Reichenbach
3
@Voo Usted entendió mal mi comentario. Tiene dos implementaciones -> DefaultResourcey FinalizableResourceque extiende la primera y agrega un finalizador. En producción se usa el DefaultResourceque no tiene finalizador-> sin gastos generales. Durante el desarrollo y las pruebas, configura la aplicación para usar FinalizableResourceque tiene un finalizador y, por lo tanto, agrega sobrecarga. Durante el desarrollo y las pruebas, eso no es un problema, pero puede ayudarlo a identificar fugas y corregirlas antes de llegar a la producción. Sin embargo, si una fuga alcanza PRD, puede configurar la fábrica para crear X% FinalizableResourcepara identificar la fuga
Svetlin Zarev
55

Este parece ser el caso de uso de la AutoCloseableinterfaz y la try-with-resourcesdeclaración introducida en Java 7. Si tiene algo como

public class MyService implements AutoCloseable {
    public void doWork() {
        // ...
    }

    @Override
    public void close() {
        // release resources
    }
}

entonces sus consumidores pueden usarlo como

public class MyConsumer {
    public void foo() {
        try (MyService myService = new MyService()) {
            //...
            myService.doWork()
            //...
            myService.doWork()
            //...
        }
    }
}

y no tener que preocuparse por llamar a un explícito releaseindependientemente de si su código arroja una excepción.


Respuesta adicional

Si lo que realmente está buscando es asegurarse de que nadie se olvide de usar su función, debe hacer un par de cosas

  1. Asegúrese de que todos los constructores MyServicesean privados.
  2. Defina un único punto de entrada para usar MyServiceque garantice que se limpie después.

Harías algo como

public interface MyServiceConsumer {
    void accept(MyService value);
}

public class MyService implements AutoCloseable {
    private MyService() {
    }


    public static void useMyService(MyServiceConsumer consumer) {
        try (MyService myService = new MyService()) {
            consumer.accept(myService)
        }
    }
}

Entonces lo usarías como

public class MyConsumer {
    public void foo() {
        MyService.useMyService(new MyServiceConsumer() {
            public void accept(MyService myService) {
                myService.doWork()
            }
        });
    }
}

Originalmente no recomendé esta ruta porque es horrible sin las interfaces funcionales y lambdas de Java-8 (donde se MyServiceConsumerconvierte Consumer<MyService>) y es bastante imponente para sus consumidores. Pero si lo que solo quieres es que release()te llamen, funcionará.

Walpen
fuente
1
Esta respuesta es probablemente mejor que la mía. Mi camino tiene un retraso antes de que el Recolector de Basura entre en juego.
Dar Brett
1
¿Cómo se asegura de que los clientes usarán try-with-resourcesy no solo omitirán try, perdiendo así la closellamada?
Frank
@walpen De esta manera tiene el mismo problema. ¿Cómo puedo obligar al usuario a usar try-with-resources?
hasanghaforian
1
@Frank / @hasanghaforian, Sí, de esta manera no se fuerza a que se llame cerca. Tiene el beneficio de la familiaridad: los desarrolladores de Java saben que realmente debe asegurarse de que .close()se llame cuando se implemente la clase AutoCloseable.
walpen
1
¿No podría emparejar un finalizador con un usingbloque para la finalización determinista? Al menos en C #, esto se convierte en un patrón bastante claro para que los desarrolladores tengan el hábito de usarlo cuando aprovechan los recursos que necesitan una finalización determinista. Algo fácil y adictivo es la siguiente mejor opción cuando no puedes obligar a alguien a hacer algo. stackoverflow.com/a/2016311/84206
AaronLS
52

sí tu puedes

Puede obligarlos absolutamente a que se liberen, y hacer que sea 100% imposible de olvidar, haciendo imposible que creen nuevas Serviceinstancias.

Si hicieron algo como esto antes:

Service s = s.new();
try {
  s.doWork("something");
  if (...) s.doWork("something else");
  ...
}
finally {
  s.release(); // oops: easy to forget
}

Luego cámbielo para que tengan acceso de esta manera.

// Their code

Service.runWithRelease(new ServiceConsumer() {
  void run(Service s) {
    s.doWork("something");
    if (...) s.doWork("something else");
    ...
  }  // yay! no s.release() required.
}

// Your code

interface ServiceConsumer {
  void run(Service s);
}

class Service {

   private Service() { ... }      // now: private
   private void release() { ... } // now: private
   public void doWork() { ... }   // unchanged

   public static void runWithRelease(ServiceConsumer consumer) {
      Service s = new Service();
      try {
        consumer.run(s);
      }
      finally {
        s.release();
      } 
    } 
  }

Advertencias:

  • Considere este pseudocódigo, han pasado años desde que escribí Java.
  • Puede haber variantes más elegantes en estos días, tal vez incluyendo esa AutoCloseableinterfaz que alguien mencionó; pero el ejemplo dado debería funcionar fuera de la caja, y a excepción de la elegancia (que es un buen objetivo en sí mismo) no debería haber una razón importante para cambiarlo. Tenga en cuenta que esto tiene la intención de decir que puede usar AutoCloseabledentro de su implementación privada; El beneficio de mi solución sobre el uso de sus usuarios AutoCloseablees que, nuevamente, no se puede olvidar.
  • Tendrá que desarrollarlo según sea necesario, por ejemplo, para inyectar argumentos en la new Servicellamada.
  • Como se menciona en los comentarios, la cuestión de si una construcción como esta (es decir, tomar el proceso de crear y destruir a Service) pertenece o no a la persona que llama está fuera del alcance de esta respuesta. Pero si decides que lo necesitas absolutamente, así es como puedes hacerlo.
  • Un comentarista proporcionó esta información sobre el manejo de excepciones: Google Books
AnoE
fuente
2
Estoy contento con los comentarios para el voto negativo, por lo que puedo mejorar la respuesta.
AnoE
2
No voté en contra, pero es probable que este diseño sea más problemático de lo que vale. Agrega una gran complejidad e impone una gran carga a las personas que llaman. Los desarrolladores deben estar lo suficientemente familiarizados con las clases de estilo de prueba con recursos que usarlas es una segunda naturaleza cada vez que una clase implementa la interfaz adecuada, por lo que generalmente es lo suficientemente bueno.
jpmc26
30
@ jpmc26, curiosamente creo que ese enfoque reduce la complejidad y la carga para las personas que llaman al evitar que piensen en el ciclo de vida de los recursos. Aparte de eso; el OP no preguntó "debería forzar a los usuarios ..." sino "cómo forzar a los usuarios". Y si es lo suficientemente importante, esta es una solución que funciona muy bien, es estructuralmente simple (simplemente envolviéndola en un cierre / ejecutable), incluso transferible a otros idiomas que también tienen lambdas / procs / closures. Bueno, dejaré que la democracia del SE juzgue; el OP ya ha decidido de todos modos. ;)
AnoE
14
Nuevamente, @ jpmc26, no estoy tan interesado en discutir si este enfoque es correcto para cualquier caso de uso único. El OP pregunta cómo hacer cumplir tal cosa y esta respuesta le dice cómo hacer cumplir tal cosa . No nos corresponde a nosotros decidir si es apropiado hacerlo en primer lugar. La técnica que se muestra es una herramienta, nada más, nada menos, y útil para tener en una bolsa de herramientas, creo.
AnoE
11
Esta es la respuesta correcta en mi humilde opinión, es esencialmente el patrón de agujero en el medio
jk.
11

Puede intentar utilizar el patrón de Comando .

class MyServiceManager {

    public void execute(MyServiceTask tasks...) {
        // set up everyting
        MyService service = this.acquireService();
        // execute the submitted tasks
        foreach (Task task : tasks)
            task.executeWith(service);
        // cleanup yourself
        service.releaseResources();
    }

}

Esto le brinda control total sobre la adquisición y liberación de recursos. La persona que llama solo envía tareas a su Servicio, y usted mismo es responsable de adquirir y limpiar recursos.

Sin embargo, hay una condición. La persona que llama aún puede hacer esto:

MyServiceTask t1 = // some task
manager.execute(t1);
MyServiceTask t2 = // some task
manager.execute(t2);

Pero puede abordar este problema cuando surja. Cuando haya problemas de rendimiento y descubra que algunas personas que llaman hacen esto, simplemente muéstreles la forma correcta y resuelva el problema:

MyServiceTask t1 = // some task
MyServiceTask t2 = // some task
manager.execute(t1, t2);

Puede hacer esto arbitrariamente complejo implementando promesas para tareas que dependen de otras tareas, pero luego liberar cosas también se vuelve más complicado. Este es solo un punto de partida.

Asíncrono

Como se ha señalado en los comentarios, lo anterior realmente no funciona con solicitudes asincrónicas. Eso es correcto. Pero esto se puede resolver fácilmente en Java 8 con el uso de CompleteableFuture , especialmente CompleteableFuture#supplyAsyncpara crear los futuros individuales y CompleteableFuture#allOfrealizar la liberación de los recursos una vez que todas las tareas hayan finalizado. Alternativamente, uno siempre puede usar Threads o ExecutorServices para lanzar su propia implementación de Futures / Promises.

Poligoma
fuente
1
Agradecería que los votantes negativos dejaran un comentario por qué piensan que esto es malo, de modo que pueda abordar esas preocupaciones directamente o al menos obtener otro punto de vista para el futuro. ¡Gracias!
Polygnome
+1 voto a favor. Este puede ser un enfoque, pero el servicio es asíncrono y el consumidor necesita obtener el primer resultado antes de solicitar el próximo servicio.
hasanghaforian
1
Esta es solo una plantilla básica. JS resuelve esto con promesas, podría hacer cosas similares en Java con futuros y un recopilador que se llama cuando finalizan todas las tareas y luego se limpia. Definitivamente es posible obtener asíncronas e incluso dependencias allí, pero también complica mucho las cosas.
Polygnome
3

Si puede, no permita que el usuario cree el recurso. Deje que el usuario pase un objeto visitante a su método, lo que creará el recurso, lo pasará al método del objeto visitante y lo liberará después.

9000
fuente
Gracias por su respuesta, pero discúlpeme, ¿quiere decir que es mejor pasar recursos al consumidor y luego obtenerlos nuevamente cuando solicite servicio? Entonces el problema persistirá: el consumidor puede olvidarse de liberar esos recursos.
hasanghaforian
Si desea controlar un recurso, no lo suelte, no lo pase completamente al flujo de ejecución de otra persona. Una forma de hacerlo es ejecutar un método predefinido del objeto visitante de un usuario. AutoCloseablehace algo muy similar detrás de escena. Bajo otro ángulo, es similar a la inyección de dependencia .
9000
1

Mi idea era similar a la de Polygnome.

Puede tener los métodos "do_something ()" en su clase simplemente agregando comandos a una lista privada de comandos. Luego, tenga un método "commit ()" que realmente haga el trabajo y llame a "release ()".

Entonces, si el usuario nunca llama a commit (), el trabajo nunca se realiza. Si lo hacen, los recursos se liberan.

public class Foo
{
  private ArrayList<ICommand> _commands;

  public Foo doSomething(int arg) { _commands.Add(new DoSomethingCommand(arg)); return this; }
  public Foo doSomethingElse() { _commands.Add(new DoSomethingElseCommand()); return this; }

  public void commit() { 
     for(ICommand c : _commands) c.doWork();
     release();
     _commands.clear();
  }

}

Luego puede usarlo como foo.doSomething.doSomethineElse (). Commit ();

Sava B.
fuente
Esta es la misma solución, pero en lugar de pedirle a la persona que llama que mantenga la lista de tareas, la mantiene internamente. El concepto central es literalmente el mismo. Pero esto no sigue ninguna convención de codificación para Java que haya visto y es bastante difícil de leer (cuerpo del bucle en la misma línea que el encabezado del bucle, _ al principio).
Polygnome
Sí, es el patrón de comando. Solo puse un código para dar una idea de lo que estaba pensando de la manera más concisa posible. Sobre todo escribo C # en estos días, donde las variables privadas a menudo tienen el prefijo _.
Sava B.
-1

Otra alternativa a las mencionadas sería el uso de "referencias inteligentes" para administrar sus recursos encapsulados de una manera que permita al recolector de basura limpiar automáticamente sin liberar recursos manualmente. Su utilidad depende, por supuesto, de su implementación. Lea más sobre este tema en esta maravillosa publicación de blog: https://community.oracle.com/blogs/enicholas/2006/05/04/understanding-weak-references

Dracam
fuente
Esta es una idea interesante, pero no puedo ver cómo el uso de referencias débiles resuelve el problema del OP. La clase de servicio no sabe si va a obtener otra solicitud doWork (). Si libera su referencia a sus objetos pesados, y luego recibe otra llamada doWork (), fallará.
Jay Elston el
-4

Para cualquier otro lenguaje orientado a clases, diría que lo coloque en el destructor, pero como esa no es una opción, creo que puede anular el método de finalización. De esa manera, cuando la instancia se salga del alcance y se recolecte basura, se liberará.

@Override
public void finalize() {
    this.release();
}

No estoy muy familiarizado con Java, pero creo que este es el propósito para el cual finalize () está destinado.

http://docs.oracle.com/javase/7/docs/api/java/lang/Object.html#finalize ()

Dar Brett
fuente
77
Esta es una mala solución al problema por la razón que Stephen dice en su respuestaIf you rely on finalizers to tidy up, you run into the problem that it can take a very long time for the tidy-up to happen. The finalizer will only be run after the GC decides that the object is no longer reachable. That may not happen until the JVM does a full collection.
Daenyth
44
E incluso entonces, el finalizador puede no correr de hecho ...
Jwent
3
Los finalizadores son notoriamente poco confiables: pueden correr, nunca pueden correr, si corren no hay garantía de cuándo correrán. Incluso los arquitectos de Java, como Josh Bloch, dicen que los finalizadores son una idea terrible (referencia: Java efectivo (2ª edición) ).
Este tipo de problemas me hacen extrañar C ++ con su destrucción determinista y RAII ...
Mark K Cowan