Clases estáticas como alternativa a inyección de dependencias

En las aplicaciones orientadas a objetos es frecuente que en algún momento necesites tener varias implementaciones de un mismo contrato. Esto no es ningún problema y puedes aprovechar el uso de interfaces o clases abstractas para definir el contrato y luego crear distintas implementaciones del mismo.

Al tener varias implementaciones, ahora toca buscar una forma de decidir qué implementación hay que utilizar en cada caso. Para esto hay varias fórmulas. Desde el clásico patrón factoría para delegar la creación de la implementación concreta a otra clase, hasta el uso de contenedores de inversión de control e inyección de dependencias. Hoy en día parece que las buenas prácticas te llevan a usar estos últimos, pero quizá andar inyectando dependencias no siempre sea la mejor opción.

Siguiendo con las «buenas prácticas», otra de las cosas que parece que deberíamos hacer es evitar a toda costa los métodos estáticos porque son menos flexibles y limitan el polimorfismo. Sin embargo, hay escenarios en los que un uso razonable de clases estáticas pueden ayudarnos a conseguir esa parte de comportamiento dinámico que necesitamos sin necesidad de introducir grandes cambios en el diseño de la aplicación, algo que es especialmente útil cuando estamos tratando con código legacy.

En este post vamos a partir de un ejemplo (basado en hechos reales) para ver cómo poco a poco podemos modificar una aplicación que parte de un diseño completamente rígido para dotarla de cierta flexibilidad sin asumir demasiados riesgos y sin añadir excesiva complejidad al diseño.

El problema

Partimos de una aplicación que realiza un proceso (que nos da más o menos igual), durante el cual se van ejecutando acciones modeladas en distintas clases. Esta aplicación es ejecutada manualmente por un usuario que va viendo en pantalla el estado de las operaciones y al que se le muestran de vez en cuando algunos mensajes.

El código podría ser de este estilo:

public class UserConfigurator {
  public void AddUser() {
    ProgressView.ShowWhile("Adding users...", () => {
      // ... 
    });

    MessageBox.ShowInfo("Users added");
  }
}

public class FileDeployer {
  public void DeployFiles() {
    if (!CanDeployFiles()) {
      MessageBox.ShowError("Could not deploy files");
      var helpView = new HelpView();
      helpView.LoadDocument("deploy-files.html");
      helpView.Show();
    }
    // ...
  }
}

Como éstas, habría unas cuantas clases más cada una encargada de realizar parte del proceso, comunicar al usuario el estado. Supongamos ahora que necesitamos modificar esta aplicación para que se pueda ejecutar en modo silencioso, sin mostrar ningún tipo de mensaje al usuario, para ser ejecutada de forma desatendida.

Siempre podríamos ir revisando el código con cariño e ir añadiendo ifs para sacar o no sacar ventanas, pero no parece una cosa muy agradable de hacer (ni de mantener).

Existen muchas alternativas, pero en general partimos de una situación relativamente incómoda, porque tenemos bastante código dedicado a mostrar cosas en pantalla, repartido por bastantes sitios de la aplicación, y el diseño original no usa inyección de dependencias o cualquiera de las técnicas típicas para resolver esto.

Ocultando el polvo

Para intentar mejorar la situación, lo primero que podemos hacer es intentar encapsular todo el subsistema de interfaz de usuario detrás de una fachada. En lugar de tener acciones a través del método ProcessView.ShowWhile, MessageBox.ShowInfo y MessageBox.ShowError, podemos construir algo así:

public static class UI {
  public static void ShowWhile(string message, Action action) {
    ProgressView.ShowWhile(message, action);
  }
  public static void ShowError(string message) {
    MessageBox.ShowError(messge);
  }
  public static void ShowInfo(string message) {
    MessageBox.ShowInfo(messge);
  }
  public static void ShowHelpDocument(string file) {
     var view = new HelpView();
     view.LoadDocument(file);
     view.Show(); 
  }
}

En la fachada encapsulamos todas las operaciones relativas a interfaz de usuario. Además, las vamos a encapsular como métodos estáticos. Esto nos permite evitar tener que gestionar el ciclo de vida de la fachada. Si fueran métodos de instancia y la fachada tuviera estado (algo que vamos a necesitar en un rato), tendríamos que andar garantizando que el estado de la fachada es el adecuado en todas las instancias y la cosa se complica. Es uno de esos casos en los que si puedes evitar clases y limitarte a módulos (que es lo que realmente es la clase estática), te ahorrarás complicaciones.

Con la fachada montada así, actualizar el código del resto de la aplicación es trivial:

public class UserConfigurator {
  public void AddUser() {
    UI.ShowWhile("Adding users...", () => {
      // ... 
    });

    UI.ShowInfo("Users added");
  }
}

public class FileDeployer {
  public void DeployFiles() {
    if (!CanDeployFiles()) {
      UI.ShowError("Could not deploy files");
      UI.ShowHelpDocument("deploy-files.html");
    }
    // ...
  }
}

Por el camino, al haber pasado algo de lógica a la fachada (como el caso del ShowHelpDocument), simplificamos algo el código de los clientes y gracias a la fachada tenemos encapsulado todo el código relativo a la comunicación con el usuario sacando ventanas.

Introduciendo polimorfismo

Ahora sólo necesitamos tener dos implementaciones de fachada, una que muestre mensajes al usuario y otra que se limite a escribir esos mensajes en un log en lugar de andar sacando ventanas. Para ello, vamos a empaquetar toda la lógica actual de la fachada en una clase y haremos que la fachada delegue en ella todas las operaciones:

public static class UI {

  private class WindowsUI {
    public void ShowWhile(string message, Action action) {
      ProgressView.ShowWhile(message, action);
    }
    public void ShowError(string message) {
      MessageBox.ShowError(messge);
    }
    public void ShowInfo(string message) {
      MessageBox.ShowInfo(messge);
    }
    public ShowHelpDocument(string file) {
       var view = new HelpView();
       view.LoadDocument(file);
       view.Show(); 
    }
  }

  private static readonly _ui = new WindowsUI();

  public static void ShowWhile(string message, Action action) {
    _ui.ShowWhile(message, action);
  }
  public static void ShowError(string message) {
    _ui.ShowError(messge);
  }
  public static void ShowInfo(string message) {
    _ui.ShowInfo(messge);
  }
  public static void ShowHelpDocument(string file) {
     _ui.ShowHelpDocument(file);
  }
}

Nuevamente, una refactorización trivial. He dejado la clase WindowsUI como privada a la fachada porque realmente no tiene sentido fuera de ella, pero si ten ponen nervioso las clases grandes, te lo puedes llevar fuera.

Ahora ya está bastante claro, sólo nos falta extraer un interface con las operaciones de WindowsUI para poder crear nuestra implementación silenciosa:

public static class UI {

  private interface IUI {
    void ShowWhile(string message, Action action);
    void ShowError(string message);
    void ShowInfo(string message);
    void ShowHelpDocument(string file);
  }

  private class WindowsUI: IUI {
    // Implementación que hemos visto antes
  }

  private class SilentUI : IUI {
    public void ShowWhile(string message, Action action) {
      Log.Info(message);
      action();
    }
    public void ShowError(string message) {
      Log.Error(message);
    }
    public void ShowInfo(string message) {
      Log.Info(message);
    }
    public void ShowHelpDocument(string file) {
      // Nada que hacer aquí
    }
  }

  private static readonly _ui = new WindowsUI();

  public static void EnableSilentMode() {
    _ui = new SilentUI();
  }

  public static void ShowWhile(string message, Action action) {
    _ui.ShowWhile(message, action);
  }
  public static void ShowError(string message) {
    _ui.ShowError(messge);
  }
  public static void ShowInfo(string message) {
    _ui.ShowInfo(messge);
  }
  public static void ShowHelpDocument(string file) {
    _ui.ShowHelpDocument(file);
  }
}

Sólo nos falta activar el modo silencioso al arrancar la aplicación en base a algún parámetro de línea de comandos:

static void Main(string[] args) {
  if (args.Contains("--silent")) {
    UI.EnableSilentMode();
  }
  // ...
}

Con esto ya podemos configurar de forma transparente a toda la aplicación si queremos que nuestro interfaz de usuario muestre ventanas o sea completamente silencioso y se limite a escribir en un fichero.

Resumen

Tendemos a guiarnos demasiado por las «buenas prácticas» y eso nos lleva a utilizar siempre las mismas soluciones para determinados problemas, aunque podamos encontrar soluciones alternativas más simples.

El ejemplo que hemos visto en este post es sólo eso, un ejemplo, y no pretendo en ningún caso decir que reniegues de tus contenedores de inyección de dependencias para usar sólo fachadas estáticas, entre otras cosas porque esta aproximación también tiene sus pegas (como la dificultad para seguir las dependencias de cada clase).

Sin embargo, cuando te encuentras con una aplicación que no está usando un contenedor, es bueno conocer alternativas que te permiten modificar la aplicación e introducir cierto comportamiento polimórfico sin tener que reescribir media aplicación para usar inyección de dependencias.

2 comentarios en “Clases estáticas como alternativa a inyección de dependencias

  1. Es una buena pregunta.

    Lo primero es plantearte qué quieres probar. En general, soy partidario de aislar la lógica que merece la pena ser testeada en funciones puras o en objetos sin dependencias sobre los que poder hacer tests de estado, y no intentar testear interacciones entre clases.

    Algo parecido a lo que comentaba aquí (https://blog.koalite.com/2013/07/refactorizando-tests-unitarios-hacia-tests-sin-dependencias/) y que desarrollaba en este otro post (https://blog.koalite.com/2017/11/no-pierdas-el-tiempo-escribiendo-tests/).

    Si quisieras realizar tests que cubrieran la interacción con la fachada, sería fácil modificándola para poder insertarle una implementación fake/mock/stub (lo que quieras) durante los tests:

    public static class UI {
      public static void Initialize(IUI ui) {
        _ui = ui;
      }
    }
    
    // En el test
    [Test]
    public void SomeTest() {
      // Arrange
      var testUI = new TestUI();
      UIFacade.Initialize(fake);
    
      // Act
      // someProcessThatCallsUI();
      
      // Assert
      Assert.Equals(testUI.Messages, new[] { 
        "Comprobando datos...",
        "Actualizada aplicación"
      });
    }
    

Comentarios cerrados.