¿Cómo se mantienen bajos los argumentos y se mantienen separadas las dependencias de terceros?

13

Yo uso una biblioteca de terceros. Me pasan un POJO que, para nuestros propósitos y propósitos, probablemente se implementa de la siguiente manera:

public class OurData {
  private String foo;
  private String bar;
  private String baz;
  private String quux;
  // A lot more than this

  // IMPORTANT: NOTE THAT THIS IS A PACKAGE PRIVATE CONSTRUCTOR
  OurData(/* I don't know what they do */) {
    // some stuff
  }

  public String getFoo() {
    return foo;
  }

  // etc.
}

Por muchas razones, que incluyen, entre otras, encapsular su API y facilitar las pruebas unitarias, quiero ajustar sus datos. ¡Pero no quiero que mis clases principales dependan de sus datos (nuevamente, por razones de prueba)! Así que ahora tengo algo como esto:

public class DataTypeOne implements DataInterface {
  private String foo;
  private int bar;
  private double baz;

  public DataTypeOne(String foo, int bar, double baz) {
    this.foo = foo;
    this.bar = bar;
    this.baz = baz;
  }
}

public class DataTypeTwo implements DataInterface {
  private String foo;
  private int bar;
  private double baz;

  public DataTypeOne(String foo, int bar, double baz, String quux) {
    this.foo = foo;
    this.bar = bar;
    this.baz = baz;
    this.quux = quux;
  }
}

Y luego esto:

public class ThirdPartyAdapter {
  public static makeMyData(OurData data) {
    if(data.getQuux() == null) {
      return new DataTypeOne(
        data.getFoo(),
        Integer.parseInt(data.getBar()),
        Double.parseDouble(data.getBaz()),
      );
    } else {
      return new DataTypeTwo(
        data.getFoo(),
        Integer.parseInt(data.getBar()),
        Double.parseDouble(data.getBaz()),
        data.getQuux();
      );
  }
}

Esta clase de adaptador se combina con las otras pocas clases que DEBEN conocer sobre la API de terceros, lo que limita su omnipresencia en el resto de mi sistema. Sin embargo ... ¡esta solución es BRUTA! En Clean Code, página 40:

Más de tres argumentos (poliádicos) requieren una justificación muy especial, y de todos modos no deberían usarse.

Cosas que he considerado:

  • Crear un objeto de fábrica en lugar de un método auxiliar estático
    • No resuelve el problema de tener miles de millones de argumentos
  • Crear una subclase de DataTypeOne y DataTypeTwo que tenga un constructor dependiente
    • Todavía tiene un constructor poliádico protegido
  • Cree implementaciones completamente separadas que se ajusten a la misma interfaz
  • Múltiple de las ideas anteriores simultáneamente

¿Cómo debe manejarse esta situación?


Tenga en cuenta que esta no es una situación de capa anticorrupción . No hay nada malo con su API. Los problemas son:

  • No quiero que mis estructuras de datos tengan import com.third.party.library.SomeDataStructure;
  • No puedo construir sus estructuras de datos en mis casos de prueba
  • Mi solución actual da como resultado recuentos de argumentos muy muy altos. Quiero mantener bajos los argumentos, SIN pasar sus estructuras de datos.
  • Esa pregunta es " ¿qué es una capa anticorrupción?". Mi pregunta es " ¿cómo puedo usar un patrón, cualquier patrón, para resolver este escenario?"

Tampoco estoy pidiendo código (de lo contrario, esta pregunta estaría en SO), solo estoy pidiendo una respuesta suficiente para que pueda escribir el código de manera efectiva (que esa pregunta no proporciona).

durron597
fuente
Si hay varios POJO de terceros, puede valer la pena escribir un código de prueba personalizado que use un Mapa con algunas convenciones (por ejemplo, nombre las teclas int_bar) como su entrada de prueba. O use JSON o XML con algún código intermediario personalizado. En efecto, una especie de DSL para probar com.thirdparty.
user949300
La cita completa de Clean Code:The ideal number of arguments for a function is zero (niladic). Next comes one (monadic), followed closely by two (dyadic). Three arguments (triadic) should be avoided where possible. More than three (polyadic) requires very special justification — and then shouldn’t be used anyway.
Lilienthal
11
La adhesión ciega a un patrón o pauta de programación es su propio antipatrón .
Lilienthal
2
"encapsular su API y facilitar la prueba de la unidad". Suena como esto podría ser un caso de sobreexamen y / o daño de diseño inducido por la prueba para mí (o indicativo de que podría diseñar esto de manera diferente para empezar). Pregúntese esto: ¿esto realmente hace que su código sea más fácil de entender, cambiar y reutilizar? Pondría mi dinero en "no". ¿Cuán realista es probable que alguna vez intercambies esta biblioteca? Probablemente no muy. Si lo cambia, ¿esto realmente hace que sea más fácil colocar uno completamente diferente en su lugar? De nuevo, apostaría por el "no".
jpmc26
1
@JamesAnderson Acabo de reproducir la cita completa porque me pareció interesante, pero no me quedó claro desde el fragmento si se refería a funciones en general o constructores específicamente. No quise respaldar el reclamo y, como dijo jpmc26, mi próximo comentario debería darle alguna indicación de que no lo estaba haciendo. No estoy seguro de por qué sientes la necesidad de atacar a los académicos, pero el uso de polisílabos no hace que alguien sea un elitista académico encaramado en su torre de marfil sobre las nubes.
Lilienthal

Respuestas:

10

La estrategia que he usado cuando hay varios parámetros de inicialización es crear un tipo que solo contenga los parámetros para la inicialización

public class DataTypeTwoParameters {
    public String foo;  // use getters/setters instead if it's appropriate
    public int bar;
    public double baz;
    public String quuz;
}

Luego, el constructor para DataTypeTwo toma un objeto DataTypeTwoParameters, y DataTypeTwo se construye a través de:

DataTypeTwoParameters p = new DataTypeTwoParameters();
p.foo = "Hello";
p.bar = 4;
p.baz = 3;
p.quuz = "World";

DataTypeTwo dtt = new DataTypeTwo(p);

Esto brinda muchas oportunidades para dejar en claro cuáles son todos los parámetros que entran en DataTypeTwo y qué significan. También puede proporcionar valores predeterminados razonables en el constructor DataTypeTwoParameters para que solo los valores que deben establecerse puedan realizarse en cualquier orden que le guste al consumidor de la API.

Erik
fuente
Enfoque interesante ¿Dónde pondrías un relevante Integer.parseInt? ¿En un setter o fuera de la clase de parámetros?
durron597
55
Fuera de la clase de parámetros. La clase de parámetros debe ser un objeto "tonto" y no debe intentar hacer otra cosa que expresar cuáles son las entradas requeridas y sus tipos. El análisis debe hacerse en otros lugares, como: p.bar = Integer.parseInt("4").
Erik
77
Esto suena como un objeto de parámetro patrón
mosquito
99
... o antipatrón.
Telastyn
1
... o usted podría cambiar el nombre DataTypeTwoParametersa DataTypeTwo.
user253751
14

Realmente tiene dos preocupaciones separadas aquí: ajustar una API y mantener bajo el recuento de argumentos.

Cuando se ajusta una API, la idea es diseñar la interfaz como si fuera desde cero, sin conocer nada más que los requisitos. Dices que no hay nada de malo en su API, luego, en el mismo aliento, enumera varias cosas que están mal con su API: comprobabilidad, capacidad de construcción, demasiados parámetros en un objeto, etc. Escribe la API que deseas tener. Si eso requiere varios objetos en lugar de uno, hazlo. Si requiere ajustar un nivel más alto, a los objetos que crean el POJO, hazlo.

Luego, una vez que tenga su API deseada, el recuento de parámetros ya no será un problema. Si es así, hay una serie de patrones comunes a considerar:

  • Un objeto de parámetro, como en la respuesta de Erik .
  • El patrón constructor , donde crea un objeto de construcción separado, luego llama a una serie de establecedores para establecer los parámetros individualmente, luego crea su objeto final.
  • El patrón prototipo , donde clona subclases de su objeto deseado con los campos ya establecidos internamente.
  • Una fábrica con la que ya estás familiarizado.
  • Alguna combinación de lo anterior.

Tenga en cuenta que estos patrones de creación a menudo terminan llamando a un constructor poliádico, que debe considerar correcto cuando está encapsulado. El problema con los constructores poliádicos no es llamarlos una vez, es cuando te ves obligado a llamarlos cada vez que necesitas construir un objeto.

Tenga en cuenta que, por lo general, es mucho más fácil y más fácil pasar a la API subyacente almacenando una referencia al OurDataobjeto y reenviando las llamadas al método, en lugar de intentar volver a implementar sus componentes internos. Por ejemplo:

public class DataTypeTwo implements DataInterface {
  private OurData data;

  public DataTypeOne(OurData data) {
    this.data = data;
  }

   public String getFoo() {
    return data.getFoo();
  }

  public int getBar() {
    return Integer.parseInt(data.getBar());
  }
  ...
}
Karl Bielefeldt
fuente
Primera mitad de esta respuesta: genial, muy útil, +1. Segunda mitad de esta respuesta: "pasar a la API subyacente almacenando una referencia al OurDataObjeto": esto es lo que estoy tratando de evitar, al menos en la clase base, para garantizar que no haya dependencia.
durron597
1
Es por eso que solo lo haces en una de tus implementaciones de DataInterface. Creas otra implementación para tus objetos simulados.
Karl Bielefeldt
@ durron597: sí, pero ya sabes cómo resolver ese problema si realmente te molesta.
Doc Brown
1

Creo que podrías estar interpretando la recomendación del tío Bob demasiado estrictamente. Para las clases normales, con lógica y métodos y constructores y demás, un constructor poliádico se parece mucho al olor a código. Pero para algo que es estrictamente un contenedor de datos que expone campos y es generado por lo que es esencialmente un objeto Factory, no creo que sea tan malo.

Usted puede utilizar el modelo de objetos de parámetros, como se sugiere en un comentario, puede envolver estos parámetros del constructor para usted, cuál es su tipo de datos local envoltura es es ya , en esencia, un objeto de parámetro. Todo lo que hará su objeto Parameter es empaquetar los parámetros (¿Cómo lo creará? ¿Con un constructor poliádico?) Y luego desempacarlos un segundo más tarde en un objeto que sea casi idéntico.

Si no desea exponer los setters para sus campos y llamarlos, creo que está bien apegarse a un constructor poliádico dentro de una fábrica bien definida y encapsulada.

Avner Shahar-Kashtan
fuente
El problema es que la cantidad de campos en mi estructura de datos ha cambiado varias veces y probablemente volverá a cambiar. Lo que significa que necesito refactorizar el constructor en todos mis casos de prueba. El patrón de parámetros con valores predeterminados razonables parece un mejor camino a seguir; Tener una versión mutable que se guarda en la forma inmutable podría facilitarme la vida de muchas maneras.
durron597