Existe uma estratégia de design específica que possa ser aplicada para resolver a maioria dos problemas de ovos e galinhas ao usar objetos imutáveis?

17

Vindo de um background OOP (Java), estou aprendendo Scala sozinho. Embora eu possa ver facilmente as vantagens de usar objetos imutáveis ​​individualmente, estou tendo dificuldades para ver como é possível projetar um aplicativo inteiro como esse. Vou dar um exemplo:

Digamos que eu tenha objetos que representem "materiais" e suas propriedades (estou criando um jogo, então realmente tenho esse problema), como água e gelo. Eu teria um "gerente" que possui todas essas instâncias de materiais. Uma propriedade seria o ponto de congelamento e derretimento e o material para o qual congela ou derrete.

[EDIT] Todas as instâncias materiais são "singleton", como um Java Enum.

Quero que "água" diga que congela para "gelo" a 0 ° C e "gelo" para dizer que derrete para "água" a 1 ° C. Mas se água e gelo são imutáveis, eles não podem obter uma referência um para o outro como parâmetros construtores, porque um deles precisa ser criado primeiro e que não foi possível obter uma referência para o outro ainda não existente como parâmetro construtor. Eu poderia resolver isso fornecendo a ambos uma referência ao gerente, para que eles pudessem consultá-lo para encontrar a outra instância material de que precisam toda vez que forem solicitados por suas propriedades de congelamento / derretimento, mas então eu tenho o mesmo problema entre o gerente e os materiais, que eles precisam de uma referência um para o outro, mas só podem ser fornecidos no construtor para um deles, para que o gerente ou o material não possa ser imutável.

Não há maneira de contornar esse problema, ou preciso usar técnicas de programação "funcionais" ou algum outro padrão para resolvê-lo?

Sebastien Diot
fonte
2
para mim, do jeito que você afirma, não há água nem gelo. Há apenas h2omateriais
mosquito
1
Eu sei que isso faria mais "sentido científico", mas em um jogo é mais fácil modelá-lo como dois materiais diferentes com propriedades "fixas", em vez de um material com propriedades "variáveis", dependendo do contexto.
Sebastien Diot 12/09
Singleton é uma ideia idiota.
DeadMG
@DeadMG Bem, OK. Eles não são Java Singletons reais. Quero apenas dizer que não há sentido em criar mais de uma instância de cada uma, pois elas são imutáveis ​​e seriam iguais uma à outra. De fato, não terei nenhuma instância estática real. Meus objetos "raiz" são serviços OSGi.
Sebastien Diot
A resposta a esta outra questão parecem confirmar minha suspeita de que as coisas se complicam muito rapidamente com imutáveis: programmers.stackexchange.com/questions/68058/...
Sebastien Diot

Respostas:

2

A solução é trapacear um pouco. Especificamente:

  • Crie A, mas deixe sua referência a B não inicializada (como B ainda não existe).

  • Crie B e faça com que aponte para A.

  • Atualize A para apontar para B. Não atualize A ou B depois disso.

Isso pode ser feito explicitamente (exemplo em C ++):

struct List {
    int n;
    List *next;

    List(int n, List *next)
        : n(n), next(next);
};

// Return a list containing [0,1,0,1,...].
List *binary(void)
{
    List *a = new List(0, NULL);
    List *b = new List(1, a);
    a->next = b; // Evil, but necessary.
    return a;
}

ou implicitamente (exemplo em Haskell):

binary :: [Int]
binary = a where
    a = 0 : b
    b = 1 : a

O exemplo de Haskell usa avaliação lenta para obter a ilusão de valores imutáveis ​​mutuamente dependentes. Os valores começam como:

a = 0 : <thunk>
b = 1 : a

ae bambos são válidos para a cabeça normal, independentemente. Cada contras pode ser construído sem precisar do valor final da outra variável. Quando a conversão é avaliada, ela apontará para os mesmos bpontos de dados para.

Portanto, se você deseja que dois valores imutáveis ​​apontem um para o outro, é necessário atualizar o primeiro após a construção do segundo ou usar um mecanismo de nível superior para fazer o mesmo.


No seu exemplo particular, eu posso expressá-lo em Haskell como:

data Material = Water {temperature :: Double}
              | Ice   {temperature :: Double}

setTemperature :: Double -> Material -> Material
setTemperature newTemp (Water _) | newTemp <= 0.0 = Ice newTemp
                                 | otherwise      = Water newTemp
setTemperature newTemp (Ice _)   | newTemp >= 1.0 = Water newTemp
                                 | otherwise      = Ice newTemp

No entanto, estou deixando de lado a questão. Eu imagino que, em uma abordagem orientada a objetos, em que um setTemperaturemétodo seja anexado ao resultado de cada Materialconstrutor, você precisará que os construtores apontem um para o outro. Se os construtores forem tratados como valores imutáveis, você poderá usar a abordagem descrita acima.

Joey Adams
fonte
(supondo que entendesse a sintaxe de Haskell) Acho que minha solução atual é realmente muito semelhante, mas fiquei pensando se era a "certa" ou se existe algo melhor. Primeiro, crio um "identificador" (referência) para cada objeto (ainda não criado), depois crio todos os objetos, fornecendo os identificadores necessários e, finalmente, inicializo o identificador para os objetos. Os objetos são imutáveis, mas não os identificadores.
Sebastien Diot
6

No seu exemplo, você está aplicando uma transformação a um objeto, para que eu usasse algo como um ApplyTransform()método que retorne um BlockBaseao invés de tentar alterar o objeto atual.

Por exemplo, para alterar um IceBlock para WaterBlock aplicando um pouco de calor, eu chamaria algo como

BlockBase currentBlock = new IceBlock();
currentBlock = currentBlock.ApplyTemperature(1); 
// currentBlock is now a WaterBlock 

e o IceBlock.ApplyTemperature()método ficaria assim:

public class IceBlock() : BlockBase
{
    public BlockBase ApplyTemperature(int temp)
    {
        return (temp > 0 ? new WaterBlock((BlockBase)this) : this);
    }
}
Rachel
fonte
Essa é uma boa resposta, mas infelizmente apenas porque não mencionei que meus "materiais", na verdade meus "blocos", são todos únicos, então o novo WaterBlock () simplesmente não é uma opção. Esse é o principal benefício do imutável, você pode reutilizá-los infinitamente. Em vez de ter 500.000 blocos em memória ram, tenho 500.000 referências a 100 blocos. Muito mais barato!
Sebastien Diot
Então, que tal retornar em BlockList.WaterBlockvez de criar um novo bloco?
Rachel
Sim, é isso que eu faço, mas como faço para obter a lista de bloqueio? Obviamente, os blocos precisam ser criados antes da lista de blocos e, portanto, se o bloco for realmente imutável, ele não poderá receber a lista de blocos como parâmetro. Então, de onde ela tira a lista? Meu argumento geral é que, ao tornar o código mais complicado, você resolve o problema do ovo e da galinha em um nível, apenas para recuperá-lo no próximo. Basicamente, não vejo como criar um aplicativo inteiro com base na imutabilidade. Parece aplicável apenas aos "objetos pequenos", mas não aos contêineres.
Sebastien Diot
@Sebastien Eu estou pensando que BlockListé apenas uma staticclasse que é responsável pelas únicas instâncias de cada bloco, para que você não precisa criar uma instância de BlockList(eu estou acostumado a C #)
Rachel
@Sebastien: Se você usa Singeltons, paga o preço.
DeadMG
6

Outra maneira de interromper o ciclo é separar as preocupações de material e transmutação, em alguma linguagem inventada:

water = new Block("water");
ice = new Block("ice");

transitions = new Transitions([
    new transitions.temperature.Below(0.0, water, ice),
    new transitions.temperature.Above(0.0, ice, water),
]);
SingleNegationElimination
fonte
Foi difícil ler este aqui inicialmente, mas acho que é essencialmente a mesma abordagem que eu advoguei.
Aidan Cully
1

Se você usar uma linguagem funcional e quiser obter os benefícios da imutabilidade, deverá abordar o problema com isso em mente. Você está tentando definir um tipo de objeto "gelo" ou "água" que possa suportar uma variedade de temperaturas - para oferecer suporte à imutabilidade, você precisará criar um novo objeto sempre que a temperatura mudar, o que é um desperdício. Portanto, tente tornar os conceitos de tipo de bloco e temperatura mais independentes. Eu não conheço Scala (está na minha lista de aprendizado :-)), mas emprestando a resposta de Joey Adams em Haskell , sugiro algo como:

data Material = Water | Ice

blockForTemperature :: Double -> Material
blockForTemperature x = 
  if x < 0 then Ice else Water

ou talvez:

transitionForTemperature :: Material -> Double -> Material
transitionForTemperature oldMaterial newTemp = 
  case (oldMaterial, newTemp) of
    (Ice, _) | newTemp > 0 -> Water
    (Water, _) | newTemp <= 0 -> Ice

(Nota: eu não tentei executar isso, e meu Haskell está um pouco enferrujado.) Agora, a lógica de transição é separada do tipo de material, para que não desperdice tanta memória e (na minha opinião) é bastante um pouco mais funcionalmente orientado.

Aidan Cully
fonte
Na verdade, não estou tentando usar a "linguagem funcional" porque simplesmente não entendo! A única coisa que normalmente retiro de qualquer exemplo de programação funcional não trivial é: "Droga, eu era mais inteligente!" Está além de mim como isso pode fazer sentido para qualquer pessoa. Nos dias de meus alunos, lembro que o Prolog (baseado em lógica), o Occam (tudo é executado paralelamente por padrão) e até o assembler faziam sentido, mas Lisp era apenas uma loucura. Mas eu entendo o ponto de mudar o código que causa uma "alteração de estado" fora do "estado".
Sebastien Diot