Escribir un constructor bueno / legible que necesita muchos cálculos para completar sus campos

7

Tengo una clase que tiene varios campos que solo pueden rellenarse consecutivamente mediante muchos cálculos. El primer campo se puede configurar muy fácilmente. Para completar el segundo campo, tomamos el contenido del primer campo y calculamos mucho. Los campos 3, 4 y 5 deben calcularse juntos desde el campo 1 y el campo 2 (no es posible calcular 3, 4 y 5 aislados unos de otros sin una pérdida de rendimiento severa). Y el sexto campo se calcula a partir de los campos 1, 3 y 4.

Usaré la sintaxis de C # para cada ejemplo de código.

class Foo
{
    private Type1 f1; // obtained directly from input parameter of constructor
    private Type2 f2; // from f1 we can calculate f2
    private Type3 f3; // from f1 and f2 we can calculate the triple (f3,f4,f5)
    private Type4 f4; // from f1 and f2 we can calculate the triple (f3,f4,f5)
    private Type5 f5; // from f1 and f2 we can calculate the triple (f3,f4,f5)
    private Type6 f6; // from f1, f3 and f4 we can calculate f6

    public Foo(Type1 input)
    {
        ... // What is a good way to write it?
    }
}

Me gustaría obtener alguna información con respecto a una buena manera de implementar dicho constructor. Soy bastante nuevo en la ingeniería de software de la vida real (antes hice algunos proyectos de sandbox en la universidad) y aún no sé cuáles son los aspectos más importantes.

Estas son las dos formas que tengo en mente. Por favor, coméntalos, cuéntame sobre los pros y los contras en los que no he pensado, ¡o da ejemplos de cómo escribirías a un constructor así!

Primera versión: métodos estáticos sin efectos secundarios

public Foo(Type1 input)
{
    f1 = input;
    f2 = GetF2(f1);
    Tuple<Type3, Type4, Type5> bar = GetF3F4F5(f1, f2);
    f3 = bar.Item1;
    f4 = bar.Item2;
    f5 = bar.Item3;
    f6 = GetF6(f1, f3, f4);
}

private static Type2 GetF2(Type1 field1)
{
    Type2 field2 = new Type2();

    ... // calculating a lot, setting the correct value for field2

    return field2;
}

private static Tuple<Type3, Type4, Type5> GetF3F4F5(Type1 field1, Type2 field2)
{
    Type3 field3 = new Type3();
    Type4 field4 = new Type4();
    Type5 field5 = new Type5();

    ... // calculating a lot, setting the correct values for field3, 4 and 5

    return new Tuple<Type3, Type4, Type5>(field3, field4, field5);
}

private static Type6(Type1 field1, Type3 field3, Type4 field4)
{
    Type6 field6 = new Type6();

    ... // calculating a lot, setting the correct value for field6

    return field6;
}

pro:

  • En cada línea del constructor, uno sabe exactamente qué campos ya están establecidos y cuáles no, lo que lo hace claramente legible.
  • Los cálculos se encapsulan en métodos sin efectos secundarios, lo que los hace fácilmente intercambiables.
  • Cada método muestra claramente qué campos son necesarios para el cálculo.

con:

  • La creación de tuplas tiene cierta sobrecarga.
  • Puede parecer inútil para el lector que una variable esté definida, inicializada y devuelta solo para establecer un campo en este valor, en lugar de establecer el campo directamente.

Segunda versión: métodos no estáticos que manipulan los campos directamente

public Foo(Type1 input)
{
    f1 = input;
    GetF2();
    GetF3F4F5();
    GetF6();
}

private void GetF2()
{
    f2 = new Type2();

    ... // calculating a lot with f1, setting the correct value for f2
}

private void GetF3F4F5()
{
    f3 = new Type3();
    f4 = new Type4();
    f5 = new Type5();

    ... // calculating a lot with f1 and f2, setting the correct values for f3, f4 and f5
}

private void Type6()
{
    f6 = new Type6();

    ... // calculating a lot with f1, f3, f4, setting the correct value for f6
}

pro:

  • Ligeramente más corto.
  • Sin sobrecarga de tuplas.
  • Probablemente sea un poco más rápido porque falta el desvío "inicializar > retorno- > conjunto". (solo una conjetura)

con:

  • Si toma alguna línea en el constructor, no puede decir qué campos ya están inicializados y cuáles no. (O tal vez puedas debido a los nombres de los métodos, pero para mí no es tan claro como en la primera versión).
  • Los métodos no indican qué campos usan para el cálculo.
pregunta Kjara 18.07.2016 - 14:20

3 respuestas

9

El primer enfoque es mucho, mucho mejor que el segundo enfoque.

La calidad más importante del software es la legibilidad, ya que la cantidad de veces que escribimos y reescribimos una línea de código es mucho menor que la cantidad de veces que podemos leer una línea de código. (¿Alguna vez escribe un código y luego nunca lo lee de nuevo? Espero que no. Y antes de volver a escribir una línea de código, seguramente lo leerá al menos una vez, ¿verdad?)

La penalización de rendimiento asociada con la devolución de múltiples valores de una llamada de método es completamente irrelevante, especialmente porque, por su propia admisión, los cálculos involucrados son extensos. Por lo tanto, devolver los resultados no solo es muy barato, sino también mucho más barato que otras operaciones que ya sabe que tiene que hacer.

Además, dado que los métodos de cálculo serán privados, el compilador puede incluso optimizarlos, por lo que es posible que no haya ninguna penalización de rendimiento en absoluto.

(En cualquier caso, si realmente desea evitar la creación de la tupla, y ya que está usando la sintaxis de C #, también puede usar los parámetros out ).

(Además, ten en cuenta que el segundo enfoque ni siquiera sería una opción si esperabas que tu objeto fuera inmutable.)

Hay dos tipos de problemas de rendimiento: problemas algorítmicos y tipo de problema de "guardar los ciclos del reloj".

Las preocupaciones sobre el rendimiento algorítmico tienen que ver con el uso de quicksort en lugar de la clasificación por burbujas, el uso de la búsqueda binaria en lugar de la búsqueda lineal, la desincronización de las operaciones para que puedan realizarse en paralelo en lugar de secuencialmente, mejorando los protocolos de comunicación para no requerir redondeo. viajes, etc.

Los problemas de rendimiento del tipo "guardar el reloj" se refieren a piratear, ajustar y retorcer cosas para guardar ciclos de reloj aquí y allá.

Mi consejo para usted sería que nunca considere un problema de rendimiento del tipo "guardar el ciclo del reloj" como "pro" o "con" cuando decida cómo estructurar el software. Este tipo de preocupación nunca hará una diferencia, y en el caso extremadamente improbable de que algún día se descubra que haga una diferencia, siempre habrá una manera muy fácil de solucionarlo.

Si realmente desea una sugerencia para un enfoque alternativo, considere el método de fábrica :

  1. Haga que su constructor sea privado y haga que acepte exactamente un parámetro para que se inicialice cada variable miembro. (f1, f2, f3, f4, f5 y f6.)

  2. Escriba una función estática pública que acepte el parámetro Type1 input , realice todos los cálculos necesarios almacenando los resultados en variables locales denominadas f1, f2, f3, f4, f5 y f6 y termina con return new Foo( f1, f2, f3, f4, f5, f6); .

respondido por el Mike Nakis 18.07.2016 - 15:23
2

Considere separar el uso de la construcción .

Foo foo = new FooInjector(f1).build();

Ahora hacer Foo inmutable es trivial. Ahora puedo ver el comportamiento de Foo sin que me distraiga la construcción de f #. Sure Foo tiene un constructor largo para manejar pero está oculto en FooInjector , no se extiende a lo largo de la base del código.

Si encuentra muchas clases (además de Foo ) debe hacer esto para acceder a la familia f # que le gustaría ver en introducir parámetro objeto refactorización.

También vería por qué un constructor no debería estar haciendo un trabajo real, pero eso ya tiene una respuesta aquí .

    
respondido por el candied_orange 18.07.2016 - 15:16
1

No veo que importe. Todo el procesamiento es interno / privado a la clase foo . Es un efecto secundario masivo de todos modos; el constructor está configurando el estado del objeto Foo . Si Foo no puede confiar en sí mismo, ¿qué hacer? ¿No mantienes el estado y vuelves a calcular todos los objetos fx cada vez?

No obstante, me gusta la primera opción, no por la gestión de efectos secundarios, sino porque los parámetros que pasan explícitamente expresan las dependencias para construir un objeto. Hace que el orden de construcción requerido y los objetos participantes sean inequívocos. Y no me obliga a leer mucho código, como lo hace el segundo ejemplo, para averiguar qué está pasando.

    
respondido por el radarbob 18.07.2016 - 14:57

Lea otras preguntas en las etiquetas