¿Cuál es el idioma "Ejecutar alrededor"?

151

¿Qué es este idioma "Ejecutar alrededor" (o similar) del que he estado escuchando? ¿Por qué podría usarlo y por qué no querría usarlo?

Tom Hawtin - tackline
fuente
9
No me había dado cuenta de que eras tú, tachuela. De lo contrario, podría haber sido más sarcástico en mi respuesta;)
Jon Skeet
1
Entonces, este es básicamente un aspecto, ¿verdad? Si no, ¿en qué se diferencia?
Lucas

Respuestas:

147

Básicamente es el patrón en el que escribe un método para hacer cosas que siempre se requieren, por ejemplo, asignación de recursos y limpieza, y hacer que la persona que llama pase "lo que queremos hacer con el recurso". Por ejemplo:

public interface InputStreamAction
{
    void useStream(InputStream stream) throws IOException;
}

// Somewhere else    

public void executeWithFile(String filename, InputStreamAction action)
    throws IOException
{
    InputStream stream = new FileInputStream(filename);
    try {
        action.useStream(stream);
    } finally {
        stream.close();
    }
}

// Calling it
executeWithFile("filename.txt", new InputStreamAction()
{
    public void useStream(InputStream stream) throws IOException
    {
        // Code to use the stream goes here
    }
});

// Calling it with Java 8 Lambda Expression:
executeWithFile("filename.txt", s -> System.out.println(s.read()));

// Or with Java 8 Method reference:
executeWithFile("filename.txt", ClassName::methodName);

El código de llamada no tiene que preocuparse por el lado de apertura / limpieza: será atendido por él executeWithFile.

Esto fue francamente doloroso en Java porque los cierres eran muy profundos, comenzando con Java 8, las expresiones lambda se pueden implementar como en muchos otros lenguajes (por ejemplo, expresiones lambda C # o Groovy), y este caso especial se maneja desde Java 7 con try-with-resourcesy AutoClosablestreams.

Aunque "asignar y limpiar" es el ejemplo típico dado, hay muchos otros ejemplos posibles: manejo de transacciones, registro, ejecución de algún código con más privilegios, etc. Es básicamente un poco como el patrón de método de plantilla pero sin herencia.

Jon Skeet
fuente
44
Es determinista. Los finalizadores en Java no se llaman determinísticamente. Además, como digo en el último párrafo, no solo se usa para la asignación de recursos y la limpieza. Es posible que no necesite crear un nuevo objeto en absoluto. Generalmente es "inicialización y desmontaje", pero eso puede no ser la asignación de recursos.
Jon Skeet
3
Entonces, ¿es como en C donde tienes una función que pasas en un puntero de función para hacer algún trabajo?
Paul Tomblin
3
Además, Jon, te refieres a los cierres en Java, que todavía no tiene (a menos que me lo haya perdido). Lo que usted describe son clases internas anónimas, que no son exactamente lo mismo. El soporte de cierres verdaderos (como se ha propuesto, vea mi blog) simplificaría considerablemente esa sintaxis.
philsquared
8
@ Phil: Creo que es una cuestión de grado. Las clases internas anónimas de Java tienen acceso a su entorno circundante en un sentido limitado , por lo que, aunque no son cierres "completos", son cierres "limitados", diría yo. Ciertamente me gustaría ver cierres adecuados en Java, aunque marcado (continuación)
Jon Skeet
44
Java 7 agregó try-with-resource y Java 8 agregó lambdas. Sé que esta es una vieja pregunta / respuesta, pero quería señalar esto a cualquiera que esté viendo esta pregunta cinco años y medio después. Ambas herramientas de lenguaje ayudarán a resolver el problema que este patrón fue inventado para solucionar.
45

La expresión Ejecutar alrededor se usa cuando tienes que hacer algo como esto:

//... chunk of init/preparation code ...
task A
//... chunk of cleanup/finishing code ...

//... chunk of identical init/preparation code ...
task B
//... chunk of identical cleanup/finishing code ...

//... chunk of identical init/preparation code ...
task C
//... chunk of identical cleanup/finishing code ...

//... and so on.

Para evitar repetir todo este código redundante que siempre se ejecuta "alrededor" de sus tareas reales, debe crear una clase que se encargue automáticamente:

//pseudo-code:
class DoTask()
{
    do(task T)
    {
        // .. chunk of prep code
        // execute task T
        // .. chunk of cleanup code
    }
};

DoTask.do(task A)
DoTask.do(task B)
DoTask.do(task C)

Este modismo mueve todo el código redundante complicado en un solo lugar y deja su programa principal mucho más legible (¡y mantenible!)

Eche un vistazo a esta publicación para ver un ejemplo de C #, y este artículo para un ejemplo de C ++.

e.James
fuente
7

Un Método Ejecutar Alrededor es donde pasa código arbitrario a un método, que puede realizar el código de configuración y / o desmontaje y ejecutar su código en el medio.

Java no es el lenguaje en el que elegiría hacer esto. Es más elegante pasar un cierre (o expresión lambda) como argumento. Aunque los objetos son posiblemente equivalentes a los cierres .

Me parece que el Método Ejecutar Alrededor es algo así como la Inversión de Control (Inyección de Dependencia) que puede variar ad hoc, cada vez que llama al método.

Pero también podría interpretarse como un ejemplo de Acoplamiento de control (decirle a un método qué hacer por su argumento, literalmente en este caso).

Bill Karwin
fuente
7

Veo que tiene una etiqueta Java aquí, así que usaré Java como ejemplo, aunque el patrón no sea específico de la plataforma.

La idea es que a veces tiene un código que siempre involucra la misma plantilla antes de ejecutar el código y después de ejecutarlo. Un buen ejemplo es JDBC. Siempre toma una conexión y crea una declaración (o declaración preparada) antes de ejecutar la consulta real y procesar el conjunto de resultados, y luego siempre realiza la misma limpieza repetitiva al final, cerrando la declaración y la conexión.

La idea con execute-around es que es mejor si puede factorizar el código repetitivo. Eso te ahorra algo de tipeo, pero la razón es más profunda. Aquí es el principio de no repetirse (DRY): aísla el código en una ubicación, por lo que si hay un error o necesita cambiarlo, o simplemente quiere entenderlo, todo está en un solo lugar.

Sin embargo, lo que es un poco complicado con este tipo de factorización es que tienes referencias que las partes "antes" y "después" deben ver. En el ejemplo de JDBC, esto incluiría la conexión y la declaración (preparada). Entonces, para manejar eso, esencialmente "envuelve" su código de destino con el código repetitivo.

Puede estar familiarizado con algunos casos comunes en Java. Uno es los filtros de servlet. Otro es AOP en torno a los consejos. Un tercero son las diversas clases xxxTemplate en Spring. En cada caso, tiene algún objeto contenedor en el que se inyecta su código "interesante" (por ejemplo, la consulta JDBC y el procesamiento del conjunto de resultados). El objeto contenedor hace la parte "antes", invoca el código interesante y luego hace la parte "después".


fuente
7

Consulte también Code Sandwiches , que analiza esta construcción en muchos lenguajes de programación y ofrece algunas ideas de investigación interesantes. Con respecto a la pregunta específica de por qué uno podría usarlo, el documento anterior ofrece algunos ejemplos concretos:

Tales situaciones surgen cada vez que un programa manipula recursos compartidos. Las API para bloqueos, sockets, archivos o conexiones de bases de datos pueden requerir que un programa cierre explícitamente o libere un recurso que adquirió anteriormente. En un lenguaje sin recolección de basura, el programador es responsable de asignar memoria antes de su uso y liberarla después de su uso. En general, una variedad de tareas de programación requieren que un programa realice un cambio, opere en el contexto de ese cambio y luego lo deshaga. Llamamos a tales situaciones sándwiches de código.

Y después:

Los sándwiches de código aparecen en muchas situaciones de programación. Varios ejemplos comunes se relacionan con la adquisición y liberación de recursos escasos, como bloqueos, descriptores de archivos o conexiones de socket. En casos más generales, cualquier cambio temporal del estado del programa puede requerir un sándwich de código. Por ejemplo, un programa basado en GUI puede ignorar temporalmente las entradas del usuario, o un núcleo del sistema operativo puede desactivar temporalmente las interrupciones de hardware. Si no se restaura el estado anterior en estos casos, se producirán errores graves.

El documento no explora por qué no usar este idioma, pero describe por qué es fácil equivocarse sin la ayuda a nivel de idioma:

Los sándwiches de código defectuoso surgen con mayor frecuencia en presencia de excepciones y su flujo de control invisible asociado. De hecho, las características especiales del lenguaje para administrar sándwiches de código surgen principalmente en idiomas que admiten excepciones.

Sin embargo, las excepciones no son la única causa de los sándwiches de código defectuosos. Cada vez que se realizan cambios en el código del cuerpo , pueden surgir nuevas rutas de control que omiten el código posterior . En el caso más simple, un mantenedor solo necesita agregar una returndeclaración al cuerpo de un emparedado para introducir un nuevo defecto, lo que puede conducir a errores silenciosos. Cuando el código del cuerpo es grande y el antes y el después están ampliamente separados, tales errores pueden ser difíciles de detectar visualmente.

Ben Liblit
fuente
Buen punto, azurefrag. He revisado y ampliado mi respuesta para que realmente sea más una respuesta independiente por derecho propio. Gracias por sugerir esto.
Ben Liblit
4

Trataré de explicar, como lo haría con un niño de cuatro años:

Ejemplo 1

Papá Noel viene a la ciudad. Sus elfos codifican lo que quieran a sus espaldas y, a menos que cambien, las cosas se vuelven un poco repetitivas:

  1. Consigue papel de regalo
  2. Consigue Super Nintendo .
  3. Envolverlo.

O esto:

  1. Consigue papel de regalo
  2. Consigue la muñeca Barbie .
  3. Envolverlo.

.... ad nauseam un millón de veces con un millón de regalos diferentes: observe que lo único diferente es el paso 2. Si el paso dos es lo único que es diferente, entonces ¿por qué Santa está duplicando el código, es decir, por qué está duplicando los pasos? 1 y 3 un millón de veces? Un millón de regalos significa que está repitiendo innecesariamente los pasos 1 y 3 un millón de veces.

Ejecutar alrededor ayuda a resolver ese problema. y ayuda a eliminar el código. Los pasos 1 y 3 son básicamente constantes, permitiendo que el paso 2 sea la única parte que cambia.

Ejemplo # 2

Si aún no lo obtiene, aquí hay otro ejemplo: piense en un bocadillo: el pan en el exterior siempre es el mismo, pero lo que está en el interior cambia según el tipo de arena que elija (por ejemplo, jamón, queso, mermelada, mantequilla de maní, etc.). El pan siempre está en el exterior y no necesita repetirlo mil millones de veces por cada tipo de arena que está creando.

Ahora, si lees las explicaciones anteriores, quizás te resulte más fácil de entender. Espero que esta explicación te haya ayudado.

BKSpurgeon
fuente
+ para la imaginación: D
Señor. Erizo
3

Esto me recuerda el patrón de diseño de la estrategia . Tenga en cuenta que el enlace al que apunté incluye código Java para el patrón.

Obviamente, uno podría realizar "Ejecutar alrededor" haciendo un código de inicialización y limpieza y simplemente pasando una estrategia, que siempre estará envuelta en un código de inicialización y limpieza.

Al igual que con cualquier técnica utilizada para reducir la repetición del código, no debe usarlo hasta que tenga al menos 2 casos donde lo necesite, tal vez incluso 3 (al principio de YAGNI). Tenga en cuenta que la eliminación de la repetición del código reduce el mantenimiento (menos copias de código significa menos tiempo dedicado a copiar arreglos en cada copia), pero también aumenta el mantenimiento (más código total). Por lo tanto, el costo de este truco es que está agregando más código.

Este tipo de técnica es útil para algo más que la inicialización y la limpieza. También es bueno para cuando desee que sea más fácil llamar a sus funciones (por ejemplo, podría usarlo en un asistente para que los botones "siguiente" y "anterior" no necesiten declaraciones de casos gigantes para decidir qué hacer para ir a la página siguiente / anterior

Brian
fuente
0

Si quieres modismos geniales, aquí está:

//-- the target class
class Resource { 
    def open () { // sensitive operation }
    def close () { // sensitive operation }
    //-- target method
    def doWork() { println "working";} }

//-- the execute around code
def static use (closure) {
    def res = new Resource();
    try { 
        res.open();
        closure(res)
    } finally {
        res.close();
    }
}

//-- using the code
Resource.use { res -> res.doWork(); }
Florín
fuente
Si mi apertura falla (por ejemplo, adquirir un bloqueo reentrante), se llama al cierre (por ejemplo, liberar un bloqueo reentrante a pesar de la falla de apertura correspondiente).
Tom Hawtin - tackline