Patrón de diseño para valores interdependientes

8

Resumen: ¿Existe un buen patrón de diseño para reducir la duplicación de información entre valores estrechamente interdependientes?

En mi línea de trabajo, es bastante común tener una relación entre cantidades, de modo que pueda derivar una de las cantidades si conoce las otras. Un ejemplo podría ser la ley del gas ideal :

Pv = RT

Podrías imaginar crear una clase para representar el estado de un gas ideal. La clase tendría 3 propiedades, naturalmente Pressure, Temperaturey SpecificVolumecada uno de un tipo apropiado.

Para el usuario de un objeto de esta clase, parece natural esperar que si establece valores para ambos Pressurey Temperature, podría leer un valor SpecificVolumey esperar que el objeto lo haya calculado por usted.

Del mismo modo, si establece valores para ambos Pressurey SpecificVolume, podría leer Temperature, etc.

Sin embargo, implementar realmente esta clase requiere cierta duplicación de información. Debería programar explícitamente todas las variaciones de la ecuación, tratando una variable diferente como dependiente en cada caso:

  1. T = P * v / R

  2. P = R * T / v

  3. v = R * T / P

que parece violar el principio SECO . Aunque cada uno expresa la misma relación, estos casos requieren codificación y pruebas independientes.

En casos reales, la lógica en la que estoy pensando es más compleja que este ejemplo, pero presenta el mismo problema básico. Por lo tanto, tendría un valor real si pudiera expresar la lógica solo una vez, o al menos menos veces.

Tenga en cuenta que una clase como esta probablemente también tenga que ocuparse de asegurarse de que se inicializó correctamente antes de que se lean los valores, pero creo que es una consideración secundaria.


Aunque di un ejemplo matemático, la pregunta no se limita solo a las relaciones matemáticas entre los datos. Ese solo parecía ser un ejemplo simple para hacer el punto.

UuDdLrLrSs
fuente
55
Tenga en cuenta que su ejemplo no muestra ninguna forma de duplicación. DRY solo se aplica cuando recrea activamente el mismo comportamiento. En su caso de ejemplo, cada variación de la ecuación resuelve un valor diferente y, por lo tanto, es un comportamiento diferente. La forma Pv = RT de la ecuación es una generalización inútil por sí misma desde un punto de vista OO.
T. Sar
No veo ninguna duplicación.
Deja de dañar a Monica el
1
También está la cuestión de hacer que los estados "ilegales" sean imposibles, es decir, aquellos con Pv/T != R. No estoy seguro de si esa idea podría ayudarlo a resolver los problemas subyacentes, o si eso incluso lo complica.
Bernhard Hiller
Como dijo T. Sar, no hay repetición aquí. Cualquier otra cosa que sugiera que Martin Maat provocará más problemas que resolverá. Básicamente, su motor de búsqueda puede adaptar automáticamente una fórmula para calcular el valor que falta en otro, a menos que encuentre una biblioteca que haga eso (quién sabe ...) simplemente renunciar a eso, no vale la pena.
Walfrat
Como señalaron las respuestas, los lenguajes imperativos tienen algunos problemas. Por el contrario, un lenguaje declarativo / lógico como Prolog lo hace trivial. Revisa Wikipedia . Se puede hacer con un lenguaje imperativo, "solo" requiere más trabajo.
Armaghast el

Respuestas:

12

No hay problema desde una perspectiva OO. Podría tener una clase estática que ofrezca métodos de GasLaw

GetVolume(R, T, P) 
GetPressure(R, T, v)

etcétera

y no habría un problema de duplicación. No hay objeto, no hay miembros de datos. Solo comportamientos que son todos diferentes.

Puede considerar la ley del gas como "una cosa", pero las operaciones son todas distintas. No hay nada de malo en tener un método para cada uno de los casos de uso de la ley.

Martin Maat
fuente
3
Esto realmente no aborda la cuestión de cómo internamente en una clase así reducir la duplicación de información / lógica. La interfaz externa no es realmente un problema.
UuDdLrLrSs
No estoy de acuerdo con que no aborde la pregunta. La respuesta es clara para mí que Martin dice que no hagas lo que estás pidiendo, porque claramente hay una manera mucho más fácil de entender para lograr exactamente lo mismo.
Dunk
10

Esto no es realmente posible en la forma en que pides.

Considere: tengo un objeto de gas ideal g. Si configuro explícitamente las tres temperaturas, la presión y el volumen específico, y luego obtengo la temperatura nuevamente, como:

IdealGas g;
g.setTemperature(t1);
g.setPressure(p1);
g.setVolume(v1);
assert(g.getTemperature() == what); // ?

deberia:

  1. dame el valor t1que configuré originalmente?
  2. calcular el valor usando la presión y el volumen específico?
  3. calcular el valor usando presión y volumen específico solo cuando los configuro después de la temperatura?
  4. darme el valor original si está lo suficientemente cerca del calculado (deberíamos permitir errores de redondeo), y calcular como # 2 o # 3 de lo contrario?

Si debe hacer esto, necesita rastrear mucho estado para cada miembro: ¿no está inicializado, establecido explícitamente por el código del cliente o almacenando en caché un valor calculado? ¿El valor establecido o almacenado en caché ha sido invalidado por otro miembro que se estableció más adelante?

Obviamente, el comportamiento es difícil de predecir al leer el código del cliente. Este es un diseño pobre, porque siempre será difícil entender por qué obtuviste la respuesta que obtuviste. La complejidad es una señal de que este problema no es adecuado para OO, o al menos que un objeto de gas ideal con estado es una mala elección de abstracción.

Inútil
fuente
8

Primero, no creo que viole el Principio DRY porque las 3 fórmulas calculan valores diferentes. El hecho de que sea una dependencia entre los 3 valores es porque lo ves como una ecuación matemática y no como una asignación de variable programática.

Sugiero implementar su caso con una clase inmutable IdealGas de la siguiente manera

public class IdealGas {
    private static final double R = 8.3145;

    private final Pressure            pressure;
    private final Temperature         temperature;
    private final SpecificVolume      specificVolume;

    public IdealGas(Pressure pressure, Temperature temperature)
    {
        this.pressure = pressure;
        this.temperature = temperature;
        this.specificVolume = calculateSpecificVolume(pressure.getValue(), temperature.getValue());
    }


    public IdealGas(Pressure pressure, SpecificVolume specificVolume)
    {
        this.pressure = pressure;
        this.specificVolume = specificVolume;
        this.temperature = calculateTemperature(pressure.getValue(), specificVolume.getValue());
    }

    public IdealGas(Temperature temperature, SpecificVolume specificVolume)
    {
        this.temperature = temperature;
        this.specificVolume = specificVolume;
        this.pressure = calculatePressure(temperature.getValue(), specificVolume.getValue());
    }

    private SpecificVolume calculateSpecificVolume(double pressure, double temperature)
    {
        return new SpecificVolume(R * temperature / pressure);
    }

    private Temperature calculateTemperature(double pressure, double specificVolume)
    {
        return new Temperature(pressure * specificVolume / R);
    }

    private Pressure calculatePressure(double temperature, double specificVolume)
    {
        return new Pressure(R * temperature / specificVolume);
    }

    public Pressure getPressure()
    {
        return pressure;
    }

    public Temperature getTemperature()
    {
        return temperature;
    }

    public SpecificVolume getSpecificVolume()
    {
        return specificVolume;
    }
}

Déjame explicarte la implementación. La clase IdealGas encapsula las 3 propiedades Presión, Temperatura y Volumen específico como valores finales para la inmutabilidad. Cada vez que llame a getXXX no se realizará ningún cálculo. El truco es tener 3 constructores para las 3 combinaciones de 2 parámetros dados. En cada constructor calcula la tercera variable que falta. El cálculo se realiza una vez, en el momento de la construcción y se asigna al tercer atributo. Debido a que esto se hace en constructor, el tercer atributo puede ser final e inmutable. Por inmutabilidad, supongo que las clases Presión, Temperatura y Volumen específico también son inmutables.

La única parte restante es tener captadores para todos los atributos. No existen establecedores porque pasa parámetros a los constructores. Si necesita cambiar un atributo, cree una nueva instancia de IdealGas con los parámetros deseados.

En mi ejemplo, las clases Presión, Temperatura y Volumen específico son envoltorios simples de un valor doble. El código de muestra está en Java, pero se puede generalizar.

Este enfoque puede generalizarse, pasar todos los datos relacionados a un constructor, calcular datos relacionados en el constructor y tener solo captadores.

catta
fuente
2
Resolver esto con la inmutabilidad y la inyección del constructor es la respuesta correcta aquí. En el lenguaje OO, las tres variaciones diferentes de la ecuación se traducen en tres constructores diferentes. Cada operación en IdealGas produce otro objeto (valor). +1 Esta debería ser la respuesta aquí.
Greg Burghardt
+1 por proporcionar una solución
keuleJ
5

¡Esta es una pregunta realmente interesante! En principio, tiene razón en que está duplicando información porque se usa la misma ecuación en los tres casos, solo con diferentes incógnitas.

Pero en un lenguaje de programación convencional típico no hay soporte incorporado para resolver ecuaciones. Las variables en la programación son siempre cantidades conocidas (en el momento de la ejecución), por lo que a pesar de las similitudes superficiales, las expresiones en los lenguajes de programación no son comparables con las ecuaciones matemáticas.

En una aplicación típica, una ecuación como la que usted describe simplemente se escribiría como tres expresiones separadas y usted vive con la duplicación. Pero las bibliotecas de resolución de ecuaciones existen y podrían usarse en tal caso.

Sin embargo, esto es más que un patrón, es un paradigma de programación completo, llamado resolución de restricciones, y existen lenguajes de programación dedicados como Prolog para este tipo de problemas.

JacquesB
fuente
3

Esto es una irritación común al codificar modelos matemáticos en software. Ambos usan una notación muy similar para modelos simples como la ley de los gases, pero los lenguajes de programación no son matemáticas y muchas cosas que puedes dejar de lado mientras trabajas en el mundo de las matemáticas deben hacerse explícitamente en el software.

No se repite a ti mismo. No existe un concepto de ecuación, transformación algebraica o "resolución para x" en la mayoría de los lenguajes de programación. Sin una representación de software de todos esos conceptos, no hay forma de que el software llegue a T = P * v / Rla Pv = RTecuación dada .

Si bien puede parecer una limitación irrazonable de los lenguajes de programación cuando se miran modelos simples como la ley de los gases, incluso las matemáticas un poco más avanzadas permiten ecuaciones que no tienen soluciones algebraicas cerradas, o ningún método conocido que pueda derivar la solución de manera eficiente. Para la arquitectura de software no podría depender de ella en el caso más complejo.

¿Y el caso simple por sí mismo? No estoy seguro de que valga la pena tener que leer la especificación de la función en el lenguaje de programación. Los modelos tienden a ser reemplazados, no modificados y generalmente solo tienen algunas variables para resolver.

Patricio
fuente
1

La clase 3 tendría propiedades, de forma natural Pressure, Temperaturey SpecificVolume.

No, dos son suficientes. Pero que sean privadas y exponer métodos como pressure(), temperature(), y specificVolume(). Uno de ellos, que no corresponde a las dos propiedades privadas, debe tener la lógica apropiada. Así podemos eliminar la duplicación de datos.

Debería haber tres constructores con parámetros (P, T), (P, v)y (T, v). Uno de ellos, que corresponde a las dos propiedades privadas, simplemente actúa como establecedores. Otros dos deben tener la lógica apropiada. Por supuesto, la lógica se escribe tres veces (dos veces aquí y una vez en el párrafo anterior), pero no son duplicados. Incluso si los considera duplicados, son necesarios.

¿Estas tres expresiones expresan la misma relación?

Sí matemáticamente y no OO-ly. En OO, los objetos son ciudadanos de primera clase, no expresiones. Para hacer expresiones de ciudadanos de primera clase (o para hacer que la relación se exprese de la misma manera) necesitamos escribir una clase, por ejemplo, Expressiono Equationque no sea una tarea fácil (puede haber algunas bibliotecas).

La pregunta no se limita solo a las relaciones matemáticas entre los datos.

La relación tampoco es un ciudadano de primera clase. Para eso es posible que necesitemos escribir una clase Relationshipque tampoco sea fácil. Pero vivir con duplicados es más fácil.

Si decide vivir con duplicados y si los parámetros en la relación son altos, considere usar el patrón Builder . Incluso si los parámetros son menos considera usar Fábricas Estáticas para evitar limitaciones de sobrecarga del constructor.

Durgadass S
fuente
1

Podrías imaginar crear una clase para representar el estado de un gas ideal.

Una clase debe representar el comportamiento de un gas ideal. Una instancia de clase, un objeto, representa el estado.

Esto no es recoger liendres. El diseño de la clase debe ser desde una perspectiva de comportamiento y funcionalidad. El comportamiento se expone públicamente de manera que un objeto instanciado (sí, eso es superfluo) alcanza un estado mediante el comportamiento de ejercicio.

Entonces:

Gas.HeatToFarenheit( 451 );

Y lo siguiente es simplemente una tontería e imposible en el mundo real. Y tampoco lo hagas en código:

Gas.MoleculeDistiance = 2.34;  //nanometers

El comportamiento hace que la pregunta de "información duplicada" sea discutible

Afectar las mismas propiedades de estado a través de diferentes métodos no es duplicación. Lo siguiente también afecta la temperatura, pero no es un duplicado de lo anterior:

Gas.PressurizeTo( 34 );  //pascals
radarbob
fuente
0

Posiblemente podría usar la generación de código para generar las diversas formas de la ecuación a partir de la forma única que especifique.

Puedo imaginar que sería bastante complicado para fórmulas más complejas. Pero para el simple que das solo tienes que

  • mover variables de un lado a otro por división
  • voltear los lados izquierdo y derecho de la ecuación

Me imagino que si agrega multiplicación, suma y resta a esa lista y su fórmula base tiene cada variable solo una vez, entonces podría usar la manipulación de cadenas para generar automáticamente todas las versiones de la fórmula con el código repetitivo apropiado para usar la correcta dadas las variables conocidas .

Wolfram Alpha tiene varias herramientas de manipulación de ecuaciones, por ejemplo.

¡¡Sin embargo!! a menos que tenga grandes listas de estas clases para producir, no puedo ver ese código como un uso efectivo de su tiempo.

Solo necesita generar este código una vez por función y es probable que sus funciones sean demasiado complejas para resolverlas simplemente como su ejemplo.

Manualmente la codificación de cada versión es probable que sea el método más rápido y fiable de generar las funciones y aunque se puede imaginar una solución en la que el código de forma iterativa adivinar los valores y la prueba de la verdad, creo que haces necesidad de generar y compilar las funciones para calcular la variable desconocida con una cantidad razonable de poder de procesamiento.

Ewan
fuente
0

Creo que estás pensando demasiado en esto, considera la siguiente solución:

double computeIdealGasLaw(double a, double b, double c){
    return a * b / c;
}
// example
//T = P * v/R
double P = ....;
double v = ....;
// R is a constant;
double T = computeIdealGasLaw(P, v, R); 


//P = R * T/v
double T = ....;
double v = ....;
// R is a constant;
double P = computeIdealGasLaw(R, T, v); 

//v = R * T/P
double T = ....;
double P = ....;
// R is a constant;
double v = computeIdealGasLaw(R, T, P); 

Solo necesita definir una función, sin duplicación, incluso podría usar esto para una implementación de clase interna, o simplemente podría usar esta función simplemente cambiar qué parámetros usar dónde.

También puede usar este mismo patrón para cualquier situación en la que tenga una función en la que los valores se intercambien.

Si está utilizando un lenguaje estáticamente tipado y tuvo que lidiar con una función donde los valores podrían ser de diferentes tipos, podría usar la programación de plantillas (nota, usando la sintaxis de C ++)

template<class A, class B, class C, class D>
D computeIdealGasLaw(A a, B b, C c){
    return D(a * b / c);
}
cuando
fuente