2010-06-21 7 views
16

consideran este código:¿Cómo puede el código SwingWorker hacerse comprobable

public void actionPerformed(ActionEvent e) { 
    setEnabled(false); 
    new SwingWorker<File, Void>() { 

     private String location = url.getText(); 

     @Override 
     protected File doInBackground() throws Exception { 
      File file = new File("out.txt"); 
      Writer writer = null; 
      try { 
       writer = new FileWriter(file); 
       creator.write(location, writer); 
      } finally { 
       if (writer != null) { 
        writer.close(); 
       } 
      } 
      return file; 
     } 

     @Override 
     protected void done() { 
      setEnabled(true); 
      try { 
       File file = get(); 
       JOptionPane.showMessageDialog(FileInputFrame.this, 
        "File has been retrieved and saved to:\n" 
        + file.getAbsolutePath()); 
       Desktop.getDesktop().open(file); 
      } catch (InterruptedException ex) { 
       logger.log(Level.INFO, "Thread interupted, process aborting.", ex); 
       Thread.currentThread().interrupt(); 
      } catch (ExecutionException ex) { 
       Throwable cause = ex.getCause() == null ? ex : ex.getCause(); 
       logger.log(Level.SEVERE, "An exception occurred that was " 
        + "not supposed to happen.", cause); 
       JOptionPane.showMessageDialog(FileInputFrame.this, "Error: " 
        + cause.getClass().getSimpleName() + " " 
        + cause.getMessage(), "Error", JOptionPane.ERROR_MESSAGE); 
      } catch (IOException ex) { 
       logger.log(Level.INFO, "Unable to open file for viewing.", ex); 
      } 
     } 
    }.execute(); 

url es un JTextField y 'creador' es una interfaz inyectado para escribir el archivo (de modo que parte se encuentra bajo prueba). La ubicación en la que se escribe el archivo está codificada a propósito porque pretende ser un ejemplo. Y java.util.logging se usa simplemente para evitar una dependencia externa.

¿Cómo clasificaría esto para que sea comprobable por unidad (incluido el abandono de SwingWorker si fuera necesario, pero luego reemplazando su funcionalidad, al menos como se usa aquí).

Por la forma en que lo miro, el doInBackground está básicamente bien. La mecánica fundamental es crear un escritor y cerrarlo, que es casi demasiado simple de probar y el verdadero trabajo está bajo prueba. Sin embargo, el método done es problemático, incluido su acoplamiento con el método actionPerformed, la clase principal y la coordinación de la activación y desactivación del botón.

Sin embargo, separar eso no es obvio. Inyectar algún tipo de SwingWorkerFactory hace que la captura de los campos de la GUI sea mucho más difícil de mantener (es difícil ver cómo sería una mejora en el diseño). El JOpitonPane y el Escritorio tienen toda la "bondad" de Singletons, y el manejo de excepciones hace que sea imposible ajustar el get fácilmente.

Entonces, ¿cuál sería una buena solución para poner este código a prueba?

+0

Código reformado; por favor revertir si es incorrecto. – trashgod

+1

No es una respuesta completa: pero si te gusta el código de calidad, no te acerques a 'SwingWorker'. En general factorizar las cosas. Donde tenga una API que use statics/singletons, introduzca una interfaz con una implementación que use la API estática "real" y otra para la burla (posiblemente otra para la auditoría). –

+0

@Tom, si tiene tiempo para escribir el esquema de un diseño alternativo a SwingWorker (o si conoce una mejor implementación alternativa) sería muy apreciado. – Yishai

Respuesta

10

En mi humilde opinión, eso es complicado para una clase anónima. Mi enfoque sería refactorizar la clase anónima a algo como esto:

public class FileWriterWorker extends SwingWorker<File, Void> { 
    private final String location; 
    private final Response target; 
    private final Object creator; 

    public FileWriterWorker(Object creator, String location, Response target) { 
     this.creator = creator; 
     this.location = location; 
     this.target = target; 
    } 

    @Override 
    protected File doInBackground() throws Exception { 
     File file = new File("out.txt"); 
     Writer writer = null; 
     try { 
      writer = new FileWriter(file); 
      creator.write(location, writer); 
     } 
     finally { 
      if (writer != null) { 
       writer.close(); 
      } 
     } 
     return file; 
    } 

    @Override 
    protected void done() { 
     try { 
      File file = get(); 
      target.success(file); 
     } 
     catch (InterruptedException ex) { 
      target.failure(new BackgroundException(ex)); 
     } 
     catch (ExecutionException ex) { 
      target.failure(new BackgroundException(ex)); 
     } 
    } 

    public interface Response { 
     void success(File f); 
     void failure(BackgroundException ex); 
    } 

    public class BackgroundException extends Exception { 
     public BackgroundException(Throwable cause) { 
      super(cause); 
     } 
    } 
} 

Eso permite que el archivo escribiendo funcionalidad a ensayar independiente de una interfaz gráfica de usuario

Entonces, el actionPerformed se convierte en algo parecido a esto:

public void actionPerformed(ActionEvent e) { 
    setEnabled(false); 
    Object creator; 
    new FileWriterWorker(creator, url.getText(), new FileWriterWorker.Response() { 
     @Override 
     public void failure(FileWriterWorker.BackgroundException ex) { 
      setEnabled(true); 
      Throwable bgCause = ex.getCause(); 
      if (bgCause instanceof InterruptedException) { 
       logger.log(Level.INFO, "Thread interupted, process aborting.", bgCause); 
       Thread.currentThread().interrupt(); 
      } 
      else if (cause instanceof ExecutionException) { 
       Throwable cause = bgCause.getCause() == null ? bgCause : bgCause.getCause(); 
       logger.log(Level.SEVERE, "An exception occurred that was " 
        + "not supposed to happen.", cause); 
       JOptionPane.showMessageDialog(FileInputFrame.this, "Error: " 
        + cause.getClass().getSimpleName() + " " 
        + cause.getMessage(), "Error", JOptionPane.ERROR_MESSAGE); 
      } 
     } 

     @Override 
     public void success(File f) { 
      setEnabled(true); 
      JOptionPane.showMessageDialog(FileInputFrame.this, 
       "File has been retrieved and saved to:\n" 
       + file.getAbsolutePath()); 
      try { 
       Desktop.getDesktop().open(file); 
      } 
      catch (IOException iOException) { 
       logger.log(Level.INFO, "Unable to open file for viewing.", ex); 
      } 
     } 
    }).execute(); 
} 

Además, la instancia de FileWriterWorker.Response se puede asignar a una variable y probarse independientemente de FileWriterWorker.

+0

Hay algunas ideas/expresiones idiomáticas agradables en ese fragmento de código. Gracias, voy a masticarlo un poco. – Yishai

+0

Me encanta esto. – gdbj

-1

Solución fácil: un temporizador simple es lo mejor; Lanza su temporizador, inicia su acción realizada, y en el tiempo de espera el botón debe estar habilitado y así sucesivamente.

Aquí es un exemple muy Littel con un java.util.Timer:

package goodies; 

import java.util.Timer; 
import java.util.TimerTask; 
import javax.swing.JButton; 

public class SWTest 
{ 
    static class WithButton 
    { 
    JButton button = new JButton(); 

    class Worker extends javax.swing.SwingWorker<Void, Void> 
    { 
     @Override 
     protected Void doInBackground() throws Exception 
     { 
     synchronized (this) 
     { 
      wait(4000); 
     } 
     return null; 
     } 

     @Override 
     protected void done() 
     { 
     button.setEnabled(true); 
     } 
    } 

    void startWorker() 
    { 
     Worker work = new Worker(); 
     work.execute(); 
    } 
    } 

    public static void main(String[] args) 
    { 
     final WithButton with; 
     TimerTask verif; 

     with = new WithButton(); 
     with.button.setEnabled(false); 
     Timer tim = new Timer(); 
     verif = new java.util.TimerTask() 
     { 
     @Override 
     public void run() 
     { 
      if (!with.button.isEnabled()) 
      System.out.println("BAD"); 
      else 
      System.out.println("GOOD"); 
      System.exit(0); 
     }}; 
     tim.schedule(verif, 5000); 
     with.startWorker(); 
    } 
} 

solución de Expertos supone: un trabajador Swing es un RunnableFuture, en su interior un FutureTask Embebido en un exigible, por lo que puede use su propio ejecutor para ejecutarlo (el RunableFuture). Para hacer eso, necesitas un SwingWorker con una clase de nombre, no anónima. Con su propio ejecutor y una clase de nombre, puede probar todo lo que quiera, dice el supuesto experto.

+1

No entiendo por qué usaría un temporizador para volver a habilitar el botón si el proceso podría no haber finalizado en ese momento, o mantener el botón deshabilitado innecesariamente. – Yishai

+0

Lo siento, es mi mal inglés. No es "debe estar habilitado", pero "es habilitar", supongo. Edito mi respuesta con algún código de ejemplo de Java. Espero que sea lo mejor. – Istao

8

La implementación actual combina problemas de subprocesos, UI y escritura de archivos, y como ha descubierto, el acoplamiento dificulta la prueba aislada de los componentes individuales.

Esta es una respuesta bastante larga, pero se reduce a extraer estas tres preocupaciones de la implementación actual en clases separadas con una interfaz definida.

Factor cabo de aplicación lógica

Para empezar, se centran en la lógica de la aplicación principal y mover que en una clase/interfaz separada. Una interfaz permite una burla más fácil y el uso de otros marcos de oscilación de rosca. La separación significa que puede probar la lógica de su aplicación de manera completamente independiente de las otras preocupaciones.

interface FileWriter 
{ 
    void writeFile(File outputFile, String location, Creator creator) 
     throws IOException; 
    // you could also create your own exception type to avoid the checked exception. 

    // a request object allows all the params to be encapsulated in one object. 
    // this makes chaining services easier. See later. 
    void writeFile(FileWriteRequest writeRequest); 
} 

class FileWriteRequest 
{ 
    File outputFile; 
    String location; 
    Creator creator; 
    // constructor, getters etc.. 
} 


class DefualtFileWriter implements FileWriter 
{ 
    // this is basically the code from doInBackground() 
    public File writeFile(File outputFile, String location, Creator creator) 
     throws IOException 
    { 
      Writer writer = null; 
      try { 
       writer = new FileWriter(outputFile); 
       creator.write(location, writer); 
      } finally { 
       if (writer != null) { 
        writer.close(); 
       } 
      } 
      return file; 
    } 
    public void writeFile(FileWriterRequest request) { 
     writeFile(request.outputFile, request.location, request.creator); 
    } 
} 

separar la interfaz de usuario

Con la lógica de la aplicación ahora separada, que luego factorizar el éxito y el tratamiento de errores. Esto significa que la IU puede probarse sin realmente escribir el archivo. En particular, el manejo de errores se puede probar sin necesidad de provocar esos errores. Aquí, los errores son bastante simples, pero a menudo algunos errores pueden ser muy difíciles de provocar. Al separar el manejo de errores, también existe la posibilidad de reutilización o reemplazo de cómo se manejan los errores. P.ej. usando un JXErrorPane después.

interface FileWriterHandler { 
    void done(); 
    void handleFileWritten(File file); 
    void handleFileWriteError(Throwable t); 
} 

class FileWriterJOptionPaneOpenDesktopHandler implements FileWriterHandler 
{ 
    private JFrame owner; 
    private JComponent enableMe; 

    public void done() { enableMe.setEnabled(true); } 

    public void handleFileWritten(File file) { 
     try { 
     JOptionPane.showMessageDialog(owner, 
        "File has been retrieved and saved to:\n" 
        + file.getAbsolutePath()); 
     Desktop.getDesktop().open(file); 
     } 
     catch (IOException ex) { 
      handleDesktopOpenError(ex); 
     } 
    } 

    public void handleDesktopOpenError(IOException ex) { 
     logger.log(Level.INFO, "Unable to open file for viewing.", ex);   
    } 

    public void handleFileWriteError(Throwable t) { 
     if (t instanceof InterruptedException) { 
       logger.log(Level.INFO, "Thread interupted, process aborting.", ex); 
       // no point interrupting the EDT thread 
     } 
     else if (t instanceof ExecutionException) { 
      Throwable cause = ex.getCause() == null ? ex : ex.getCause(); 
      handleGeneralError(cause); 
     } 
     else 
     handleGeneralError(t); 
    } 

    public void handleGeneralError(Throwable cause) { 
     logger.log(Level.SEVERE, "An exception occurred that was " 
        + "not supposed to happen.", cause); 
     JOptionPane.showMessageDialog(owner, "Error: " 
        + cause.getClass().getSimpleName() + " " 
        + cause.getMessage(), "Error", JOptionPane.ERROR_MESSAGE); 
    } 
} 

separar enhebrar

Por último, también podemos separar las preocupaciones de roscado con un FileWriterService. El uso de FileWriteRequest hace que la codificación sea más simple.

interface FileWriterService 
{ 
    // rather than have separate parms for file writing, it is 
    void handleWriteRequest(FileWriteRequest request, FileWriter writer, FileWriterHandler handler); 
} 

class SwingWorkerFileWriterService 
    implements FileWriterService 
{ 
    void handleWriteRequest(FileWriteRequest request, FileWriter writer, FileWriterHandler handler) { 
     Worker worker = new Worker(request, fileWriter, fileWriterHandler); 
     worker.execute(); 
    } 

    static class Worker extends SwingWorker<File,Void> { 
     // set in constructor 
     private FileWriter fileWriter; 
     private FileWriterHandler fileWriterHandler; 
     private FileWriterRequest fileWriterRequest; 

     protected File doInBackground() { 
      return fileWriter.writeFile(fileWriterRequest); 
     } 
     protected void done() { 
      fileWriterHandler.done(); 
      try 
      { 
       File f = get(); 
       fileWriterHandler.handleFileWritten(f); 
      } 
      catch (Exception ex) 
      {     
       // you could also specifically unwrap the ExecutorException here, since that 
       // is specific to the service implementation using SwingWorker/Executors. 
       fileWriterHandler.handleFileError(ex); 
      } 
     } 
    } 

} 

Cada parte del sistema es comprobable por separado - la lógica de la aplicación, la presentación (el éxito y el manejo de errores) y la implementación de roscado es también una preocupación por separado.

Esto puede parecer como una gran cantidad de interfaces, pero la implementación es sobre todo cortar y pegar de su código original. Las interfaces proporcionan la separación que se necesita para que estas clases sean comprobables.

No soy muy entusiasta de SwingWorker, así que mantenerlos detrás de una interfaz ayuda a mantener el desorden que producen fuera del código. También le permite usar una implementación diferente para implementar los subprocesos de IU/fondo por separado. Por ejemplo, para usar Spin, solo necesita proporcionar una nueva implementación de FileWriterService.

Cuestiones relacionadas