Padrão de design para valores interdependentes

8

Resumo: Existe um bom padrão de design para reduzir a duplicação de informações entre valores fortemente interdependentes?

Na minha linha de trabalho, é bastante comum ter um relacionamento entre quantidades, de modo que você possa derivar uma das quantidades se conhecer as outras. Um exemplo poderia ser a lei dos gases ideais :

Pv = RT

Você pode imaginar criar uma classe para representar o estado de um gás ideal. A classe teria 3 propriedades, naturalmente Pressure, Temperaturee SpecificVolumecada um de um tipo apropriado.

Para o usuário de um objeto dessa classe, parece natural esperar que, se você definir valores para ambos Pressuree Temperature, poderá ler um valor para SpecificVolumee esperar que o objeto calcule isso para você.

Da mesma forma, se você definir valores para ambos Pressuree SpecificVolume, poderá ler Temperature, etc.

Para realmente implementar essa classe, no entanto, é necessária alguma duplicação de informações. Você precisaria programar explicitamente todas as variações da equação, tratando uma variável diferente como dependente em cada caso:

  1. T = P * v / R

  2. P = R * T / v

  3. v = R * T / P

o que parece violar o princípio DRY . Embora cada um expresse a mesma relação, esses casos requerem codificação e teste independentes.

Em casos reais, a lógica em que estou pensando é mais complexa que este exemplo, mas apresenta o mesmo problema básico. Portanto, haveria um valor real se eu pudesse expressar a lógica apenas uma vez, ou pelo menos menos vezes.

Observe que uma classe como essa provavelmente também teria que lidar para garantir que fosse inicializada corretamente antes que os valores fossem lidos, mas acho que essa é uma consideração secundária.


Embora eu tenha dado um exemplo matemático, a questão não se limita apenas às relações matemáticas entre os dados. Isso apenas parecia ser um exemplo simples de entender.

UuDdLrLrSs
fonte
5
Lembre-se de que seu exemplo não mostra nenhuma forma de duplicação. O DRY só se aplica quando você está recriando ativamente o mesmo comportamento. No seu exemplo de exemplo, toda variação da equação resolve para um valor diferente - e, portanto, é um comportamento diferente. A forma Pv = RT da equação é uma generalização inútil, por si só, do ponto de vista de OO.
507 Sar
Não vejo duplicação.
Pare de prejudicar Monica
1
Há também a questão de tornar impossível estados "ilegais", ou seja, aqueles com Pv/T != R. Não tenho certeza se essa ideia pode ajudá-lo a resolver os problemas subjacentes ou se isso ainda a complica.
Bernhard Hiller
Como disse T. Sar, não há repetição aqui. Qualquer outra coisa que sugerisse Martin Maat provavelmente provocará mais problemas do que resolverá. Basicamente, sua busca por mecanismo que possa adaptar automaticamente uma fórmula para calcular o valor que falta em outra, a menos que você encontre uma biblioteca que faça isso (quem sabe ...) simplesmente desista disso, não vale a pena.
Walfrat 28/07
Como as respostas apontaram, as línguas imperativas têm alguns problemas. Por contraste, uma linguagem declarativa / lógica como o Prolog a torna trivial. Verifique a Wikipedia . Isso pode ser feito com uma linguagem imperativa, "apenas" requer mais trabalho.
Armaghast #

Respostas:

12

Não há problema da perspectiva do OO. Você poderia ter uma classe estática que oferece métodos GasLaw

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

et cetera

e não haveria um problema de duplicação. Não há objeto, nem membros de dados. Apenas comportamentos que são todos diferentes.

Você pode considerar a lei do gás como "uma coisa", mas as operações são todas distintas. Não há nada errado em ter um método para cada um dos casos de uso da lei.

Martin Maat
fonte
3
Isso realmente não aborda a questão de quão internamente nessa classe reduzir a duplicação de informações / lógica. A interface externa não é realmente um problema.
UUDdLrLrSs
Eu discordo que não trata da questão. A resposta é clara para mim: Martin está dizendo: não faça o que você está perguntando, porque é claro que é muito mais fácil entender o modo de realizar exatamente a mesma coisa.
Dunk
10

Isso não é realmente possível da maneira que você pergunta.

Considere: eu tenho um objeto de gás ideal g. Se eu definir explicitamente todas as três temperaturas, pressão e volume específico e, em seguida, obtiver a temperatura novamente, como:

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

deveria:

  1. me dê o valor que t1eu originalmente defini?
  2. calcular o valor usando a pressão e o volume específico?
  3. calcular o valor usando pressão e volume específico somente quando eu os defino após a temperatura?
  4. forneça o valor original se estiver próximo o suficiente do calculado (devemos permitir erros de arredondamento) e calcule como # 2 ou # 3, caso contrário?

Se você precisar fazer isso, precisará rastrear muito estado para cada membro: ele não foi inicializado, definido explicitamente pelo código do cliente ou armazenando em cache um valor calculado? O valor explicitamente definido ou em cache foi invalidado por outro membro que está sendo definido posteriormente?

Obviamente, é difícil prever o comportamento da leitura do código do cliente. Esse é um design ruim, porque sempre será difícil descobrir por que você recebeu a resposta. A complexidade é um sinal de que esse problema é inadequado para OO, ou pelo menos que um objeto de gás ideal com estado é uma má escolha de abstração.

Sem utilidade
fonte
8

Primeiro, acho que você não viola o princípio DRY, porque as três fórmulas calculam valores diferentes. O fato de ser uma dependência entre todos os 3 valores é porque você a vê como uma equação matemática e não como uma atribuição de variável programática.

Sugiro implementar seu caso com uma classe imutável IdealGas da seguinte maneira

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;
    }
}

Deixe-me explicar a implementação. A classe IdealGas encapsulou as 3 propriedades Pressão, Temperatura e Volume Específico como valores finais para imutabilidade. Toda vez que você chamar getXXX, nenhum cálculo será feito. O truque é ter 3 construtores para todas as 3 combinações dos 2 parâmetros dados. Em todo construtor, você calcula a terceira variável ausente. A computação é feita uma vez, no tempo de construção e atribuída ao terceiro atributo. Como isso é feito no construtor, o terceiro atributo pode ser final e imutável. Para imutabilidade, presumo que as classes Pressure, Temperature e SpecificVolume também sejam imutáveis.

A única parte restante é ter getters para todos os atributos. Não existem setters porque você passa parâmetros para construtores. Se você precisar alterar um atributo, crie uma nova instância IdealGas com os parâmetros desejados.

No meu exemplo, as classes Pressure, Temperature e SpecificVolume são invólucros simples de um valor duplo. O código de amostra está em java, mas pode ser generalizado.

Essa abordagem pode ser generalizada, passar todos os dados relacionados para um construtor, computar dados relacionados no construtor e ter apenas getters.

catta
fonte
2
Resolver isso com imutabilidade e injeção de construtor é a resposta certa aqui. Na linguagem OO, as três variações diferentes da equação se traduzem em três construtores diferentes. Cada operação no IdealGas produz outro objeto (valor). +1 Esta deve ser a resposta aqui.
precisa
1 para proporcionar uma solução
keuleJ
5

Esta é uma pergunta realmente interessante! Você está correto em princípio ao duplicar informações porque a mesma equação é usada nos três casos, apenas com diferentes incógnitas.

Mas em uma linguagem de programação convencional típica, não há suporte interno para resolver equações. As variáveis ​​na programação são sempre quantidades conhecidas (no momento da execução); portanto, apesar das semelhanças superficiais, as expressões nas linguagens de programação não são comparáveis ​​às equações matemáticas.

Em um aplicativo típico, uma equação como você descreve apenas seria escrita como três expressões separadas e você vive com a duplicação. Mas as bibliotecas de solução de equações existem e podem ser usadas nesse caso.

Isso é mais do que um padrão, porém, é um paradigma de programação completo, chamado de solução de restrições, e existem linguagens de programação dedicadas como o Prolog para esse tipo de problema.

JacquesB
fonte
3

Isso é uma irritação comum ao codificar modelos matemáticos no software. Ambos usam uma notação muito semelhante para modelos simples, como a lei dos gases, mas as linguagens de programação não são matemática e muitas coisas que você pode deixar de fora enquanto trabalha no mundo da matemática precisam ser explicitamente explicadas em software.

Não está se repetindo. Não há conceito de equação, transformação algébrica ou "resolução de x" na maioria das linguagens de programação. Sem uma representação de software de todos esses conceitos, não há como chegar ao software, T = P * v / Rdada a Pv = RTequação.

Embora possa parecer uma limitação irracional das linguagens de programação quando se olha para modelos simples como a lei dos gases, mesmo as matemáticas um pouco mais avançadas permitem equações que não têm soluções algébricas fechadas ou nenhum método conhecido que possa derivar a solução com eficiência. Para a arquitetura de software, você não pode depender dela no caso mais complexo.

E o caso simples por si só? Não tenho certeza de que valeria a pena ter que ler a especificação do recurso na linguagem de programação. Os modelos tendem a ser substituídos, não modificados e geralmente têm apenas algumas variáveis ​​a serem resolvidas.

Patrick
fonte
1

A classe teria 3 propriedades, naturalmente Pressure, Temperaturee SpecificVolume.

Não, dois são suficientes. Mas fazê-los privados e expor métodos como pressure(), temperature(), e specificVolume(). Um deles, que não corresponde às duas propriedades particulares, deve ter a lógica apropriada. Assim, podemos eliminar a duplicação de dados.

Deve haver três construtores com parâmetros (P, T), (P, v)e (T, v). Um deles, correspondente às duas propriedades privadas, simplesmente atua como setter. Outros dois devem ter a lógica apropriada. É claro que a lógica é escrita três vezes (duas vezes aqui e uma vez no parágrafo anterior), mas elas não são duplicadas. Mesmo que você os considere duplicados, eles são necessários.

Essas três expressões expressam o mesmo relacionamento?

Sim matematicamente e não OO-ly. Nos objetos OO, os cidadãos de primeira classe não são expressões. Para fazer expressões cidadãos de primeira classe (ou estabelecer o relacionamento expresso pela mesma coisa), precisamos escrever uma classe, digamos, Expressionou Equationque não seja uma tarefa fácil (pode haver algumas bibliotecas).

A questão não se limita apenas às relações matemáticas entre os dados.

O relacionamento também não é um cidadão de primeira classe. Para isso, podemos precisar escrever uma classe Relationshipque também não seja fácil. Mas viver com duplicatas é mais fácil.

Se você decidir viver com duplicatas e se os parâmetros no relacionamento forem altos, considere usar o padrão Construtor . Mesmo se os parâmetros forem menos, considere o uso de Static Factories para evitar limitações de sobrecarga do construtor.

Durgadass S
fonte
1

Você pode imaginar criar uma classe para representar o estado de um gás ideal.

Uma classe deve representar o comportamento de um gás ideal. Uma instância de classe - um objeto - representa o estado.

Isso não é pegar lêndeas. O design da classe deve ser de uma perspectiva de comportamento e funcionalidade. O comportamento é exposto publicamente, de modo que um objeto instanciado (sim, isso é supérfluo) atinge um estado através do exercício do comportamento.

Assim:

Gas.HeatToFarenheit( 451 );

E o seguinte é simplesmente absurdo e impossível no mundo real. E não faça isso no código:

Gas.MoleculeDistiance = 2.34;  //nanometers

O comportamento torna a pergunta "informações duplicadas" discutível

Afetar as mesmas propriedades de estado por métodos diferentes não é duplicação. O seguinte também afeta a temperatura, mas não é uma duplicata do acima:

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

Possivelmente, você pode usar a geração de código para gerar as várias formas da equação a partir da única forma especificada.

Posso imaginar que seria bastante complicado para fórmulas mais complexas. Mas para o simples que você dá, você só precisa

  • mover variáveis ​​de um lado para o outro por divisão
  • virar os lados esquerdo e direito na equação

Eu posso imaginar se você adicionar multiplicação, adição e subtração a essa lista e sua fórmula base tiver cada variável apenas uma vez, então você poderá usar a manipulação de cadeias para gerar automaticamente todas as versões da fórmula com o código apropriado para usar a correta, dadas as variáveis ​​conhecidas .

O Wolfram Alpha possui várias ferramentas de manipulação de equações, por exemplo.

Contudo!! a menos que você tenha grandes listas dessas classes para produzir, não vejo esse código como um uso eficaz do seu tempo.

Você só precisa gerar esse código uma vez por função e é provável que suas funções sejam muito complexas para serem resolvidas da maneira mais simples que no seu exemplo.

Manualmente codificação de cada versão é provável que seja o método mais rápido e mais confiável de gerar as funções e embora você pode imaginar uma solução onde você código para iteratively acho valores e teste de verdade, eu acho que você fazer necessidade de gerar e compilar as funções para calcular a variável desconhecida com uma quantidade sensata de poder de processamento.

Ewan
fonte
0

Acho que você está pensando demais sobre isso, considere a seguinte solução:

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); 

Você só precisa definir uma função, sem duplicação, você pode até usá-lo para uma implementação de classe interna, ou você pode apenas usar essa função, apenas alterar quais parâmetros você usa onde.

Você também pode usar esse mesmo padrão para qualquer situação em que você tenha uma função na qual os valores sejam trocados.

Se você estiver usando um idioma estatístico e tiver que lidar com uma função em que os valores possam ser de tipos diferentes, poderá usar a programação de modelos (observe a sintaxe do C ++)

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