Como se decide se um tipo de objeto de dados deve ser projetado para ser imutável?

23

Adoro o "padrão" imutável por causa de seus pontos fortes e, no passado, achei benéfico projetar sistemas com tipos de dados imutáveis ​​(alguns, a maioria ou até todos). Frequentemente, quando faço isso, me pego escrevendo menos bugs e a depuração é muito mais fácil.

No entanto, meus colegas em geral evitam o imutável. Eles não são inexperientes (longe disso), mas escrevem objetos de dados da maneira clássica - membros privados com um getter e um setter para cada membro. Então, geralmente seus construtores não aceitam argumentos, ou talvez apenas tomem alguns argumentos por conveniência. Com frequência, a criação de um objeto se parece com isso:

Foo a = new Foo();
a.setProperty1("asdf");
a.setProperty2("bcde");

Talvez eles façam isso em todos os lugares. Talvez eles nem definam um construtor que use essas duas strings, por mais importantes que sejam. E talvez eles não alterem o valor dessas strings mais tarde e nunca precisem. Claramente, se essas coisas são verdadeiras, o objeto seria melhor projetado como imutável, certo? (o construtor pega as duas propriedades, sem setters).

Como você decide se um tipo de objeto deve ser projetado como imutável? Existe um bom conjunto de critérios para julgá-lo?

Atualmente, estou debatendo se é necessário mudar alguns tipos de dados em meu próprio projeto para imutáveis, mas precisaria justificá-los para meus colegas, e os dados nos tipos podem (MUITO raramente) mudar - e nesse momento é claro que você pode alterar da maneira imutável (crie uma nova, copiando as propriedades do objeto antigo, exceto as que você deseja alterar). Mas não tenho certeza se esse é apenas o meu amor por imutáveis ​​aparecendo, ou se há uma necessidade real / benefício deles.

Ricket
fonte
1
Programa em Erlang e todo o problema está resolvido, tudo é imutável
Zachary K
1
@ZacharyK Na verdade, eu estava pensando em mencionar algo sobre programação funcional, mas me abstive devido à minha experiência limitada com linguagens de programação funcional.
Ricket

Respostas:

27

Parece que você está se aproximando de trás para a frente. Deve-se usar como padrão imutável. Apenas torne um objeto mutável se você precisar / simplesmente não puder fazê-lo funcionar como um objeto imutável.

Brian Knoblauch
fonte
11

O principal benefício de objetos imutáveis ​​é garantir a segurança da linha. Em um mundo em que múltiplos núcleos e threads são a norma, esse benefício se tornou muito importante.

Mas usar objetos mutáveis ​​é muito conveniente. Eles têm um bom desempenho e, desde que você não os modifique a partir de threads separados, e você tenha um bom entendimento do que está fazendo, eles são bastante confiáveis.

Robert Harvey
fonte
15
O benefício de objetos imutáveis ​​é que eles são mais fáceis de raciocinar. No ambiente multithread, isso é muito mais fácil; em algoritmos simples de thread único, ainda é mais fácil. Por exemplo, você nunca precisa se preocupar se o estado é consistente, se o objeto passou todas as mutações necessárias para serem usadas em um determinado contexto, etc. Os melhores objetos mutáveis ​​permitem que você também se preocupe pouco com o estado exato: por exemplo, caches.
9000:
-1, concordo com @ 9000. A segurança do encadeamento é secundária (considere objetos que parecem imutáveis, mas que possuem um estado mutável interno devido a, por exemplo, memorização). Além disso, apresentar desempenho mutável é uma otimização prematura e é possível defender qualquer coisa se você precisar que o usuário "saiba o que está fazendo". Se eu soubesse o que estava fazendo o tempo todo, nunca escreveria um programa com um bug.
Doval
4
@Doval: Não há nada para discordar. 9000 está absolutamente correto; objetos imutáveis são mais fáceis de raciocinar. Isso é parcialmente o que os torna tão úteis para a programação simultânea. A outra parte é que você não precisa se preocupar com a mudança de estado. A otimização prematura é irrelevante aqui; o uso de objetos imutáveis ​​é sobre design, não desempenho, e a má escolha de estruturas de dados antecipadamente é uma pessimização prematura.
Robert Harvey
@RobertHarvey Não sei ao certo o que você quer dizer com "má escolha da estrutura de dados antecipadamente". Existem versões imutáveis ​​da maioria das estruturas de dados mutáveis ​​por aí, fornecendo desempenho semelhante. Se você precisar de uma lista e tiver a opção de usar uma lista imutável e uma lista mutável, vá com a imutável até ter certeza de que é um gargalo no aplicativo e a versão mutável terá um desempenho melhor.
Doval
2
@Doval: Eu li Okasaki. Essas estruturas de dados não são comumente usadas, a menos que você esteja usando uma linguagem que ofereça suporte completo ao paradigma funcional, como Haskell ou Lisp. E eu discuto a noção de que estruturas imutáveis ​​são a escolha padrão; a grande maioria dos sistemas de computação de negócios ainda é projetada em torno de estruturas mutáveis ​​(ou seja, bancos de dados relacionais). Começar com estruturas de dados imutáveis ​​é uma boa ideia, mas ainda é uma torre muito marfim.
Robert Harvey
6

Existem duas maneiras principais de decidir se um objeto é imutável.

a) Com base na natureza do objeto

É fácil capturar essas situações porque sabemos que esses objetos não serão alterados depois que forem construídos. Por exemplo, se você tem uma RequestHistoryentidade e, por natureza, as entidades não mudam depois que ela é construída. Esses objetos podem ser projetados diretamente como classes imutáveis. Lembre-se de que o objeto de solicitação é mutável, pois pode alterar seu estado e a quem é atribuído etc., ao longo do tempo, mas o histórico de solicitações não muda. Por exemplo, havia um elemento de histórico criado na semana passada quando passou do estado enviado para o estado atribuído E essa entidade do histórico nunca pode ser alterada. Portanto, este é um caso imutável clássico.

b) Com base na escolha do projeto, fatores externos

Isso é semelhante ao exemplo java.lang.String. As strings podem realmente mudar ao longo do tempo, mas, por design, elas o tornaram imutável devido a fatores de cache / pool de strings / simultaneidade. Da mesma forma, o cache / simultaneidade, etc, pode desempenhar um bom papel ao tornar um objeto imutável se o cache / simultaneidade e o desempenho relacionado forem vitais no aplicativo. Mas essa decisão deve ser tomada com muito cuidado após analisar todos os impactos.

A principal vantagem dos objetos imutáveis ​​é que eles não são submetidos ao padrão de queda de ervas daninhas. Se o objeto não sofrer nenhuma alteração ao longo da vida útil, isso facilita muito a codificação e a manutenção.

java_mouse
fonte
4

Atualmente, estou debatendo se é necessário mudar alguns tipos de dados em meu próprio projeto para imutáveis, mas precisaria justificá-los para meus colegas, e os dados nos tipos podem (MUITO raramente) mudar - e nesse momento é claro que você pode alterar da maneira imutável (crie uma nova, copiando as propriedades do objeto antigo, exceto as que você deseja alterar).

Minimizar o estado de um programa é altamente benéfico.

Pergunte a eles se eles desejam usar um tipo de valor mutável em uma de suas classes para armazenamento temporário de uma classe cliente.

Se eles disserem sim, pergunte por quê? O estado mutável não pertence a um cenário como este. Forçá-los a criar o estado ao qual realmente pertence e tornar o estado dos seus tipos de dados o mais explícito possível são excelentes motivos.

Brian
fonte
4

A resposta é um pouco dependente do idioma. Seu código se parece com Java, onde esse problema é o mais difícil possível. Em Java, os objetos podem ser passados ​​apenas por referência e o clone é completamente quebrado.

Não existe uma resposta simples, mas com certeza você deseja tornar objetos de pequeno valor imutável. O Java tornou as Strings corretamente imutáveis, mas a Data e o Calendário incorretamente tornaram mutáveis.

Portanto, torne imutáveis ​​os objetos de pequeno valor e implemente um construtor de cópias. Esqueça tudo sobre o Cloneable, ele é tão mal projetado que é inútil.

Para objetos de maior valor, se for inconveniente torná-los imutáveis, facilite a cópia.

Kevin Cline
fonte
Soa muito como como escolher entre a pilha e heap ao escrever C ou C ++ :)
Ricket
@Ricket: não tanto IMO. A pilha / pilha depende da vida útil do objeto. É muito comum em C ++ ter objetos mutáveis ​​na pilha.
kevin Cline
1

E talvez eles não alterem o valor dessas strings mais tarde e nunca precisem. Claramente, se essas coisas são verdadeiras, o objeto seria melhor projetado como imutável, certo?

Um tanto contra-intuitivo, nunca precisar mudar as cordas mais tarde é um argumento muito bom de que não importa se os objetos são imutáveis ​​ou não. O programador já está tratando-os como efetivamente imutáveis, independentemente de o compilador aplicá-lo ou não.

A imutabilidade não costuma doer, mas nem sempre ajuda. A maneira mais fácil de saber se seu objeto pode se beneficiar da imutabilidade é se você precisar fazer uma cópia do objeto ou adquirir um mutex antes de alterá-lo . Se isso nunca muda, a imutabilidade realmente não compra nada para você e, às vezes, torna as coisas mais complicadas.

Você faz ter um bom ponto sobre o risco de construir um objeto em um estado inválido, mas isso é realmente uma questão separada da imutabilidade. Um objeto pode ser mutável e sempre em um estado válido após a construção.

A exceção a essa regra é que, como o Java não suporta parâmetros nomeados nem parâmetros padrão, às vezes pode ser difícil projetar uma classe que garanta um objeto válido usando apenas construtores sobrecarregados. Não muito com o caso de duas propriedades, mas também há algo a ser dito sobre consistência, se esse padrão for frequente com classes semelhantes, mas maiores, em outras partes do seu código.

Karl Bielefeldt
fonte
Acho curioso que as discussões sobre mutabilidade falhem em reconhecer a relação entre imutabilidade e as maneiras pelas quais um objeto pode ser exposto ou compartilhado com segurança. Uma referência a um objeto de classe profundamente imutável pode ser compartilhada com segurança com código não confiável. Uma referência a uma instância de uma classe mutável pode ser compartilhada se todos os que possuem uma referência puderem confiar em nunca modificar o objeto ou expô-lo ao código que pode fazê-lo. Uma referência a uma instância de classe que pode ser modificada geralmente não deve ser compartilhada. Se apenas uma referência a um objeto existe em qualquer lugar do universo ... #
226
... e nada consultou seu "código hash de identidade", usou-o para bloquear ou acessou seus " Objectrecursos ocultos "; alterar um objeto diretamente não seria semanticamente diferente de substituir a referência por uma referência a um novo objeto que fosse idêntico, exceto pela alteração indicada. Uma das maiores fraquezas semânticas em Java, IMHO, é que ele não possui meios pelos quais o código possa indicar que uma variável deve ser apenas a única referência não efêmera a algo; as referências devem ser passáveis ​​apenas para métodos que não podem manter uma cópia após o retorno.
Supercat 22/11
0

Eu posso ter uma visão de nível excessivamente baixo disso e provavelmente porque estou usando C e C ++, que não tornam exatamente tão simples tornar tudo imutável, mas vejo os tipos de dados imutáveis ​​como um detalhe de otimização para escrever mais funções eficientes desprovidas de efeitos colaterais e podem fornecer recursos com muita facilidade, como desfazer sistemas e edição não destrutiva.

Por exemplo, isso pode ser extremamente caro:

/// @return A new mesh whose vertices have been transformed
/// by the specified transformation matrix.
Mesh transform(Mesh mesh, Matrix4f matrix);

... se Meshnão foi projetado para ser uma estrutura de dados persistente e foi, em vez disso, um tipo de dados que exigia copiá-lo completamente (o que poderia abranger gigabytes em algum cenário), mesmo que tudo o que faremos seja alterar um parte dela (como no cenário acima, onde apenas modificamos as posições dos vértices).

É nesse momento que procuro a imutabilidade e projeto a estrutura de dados para permitir que partes não modificadas sejam copiadas e contadas de referência superficialmente, para permitir que a função acima seja razoavelmente eficiente sem ter que copiar em profundidade malhas inteiras enquanto ainda é capaz de escrever o função livre de efeitos colaterais, o que simplifica drasticamente a segurança da rosca, a segurança da exceção, a capacidade de desfazer a operação, aplicá-la de maneira não destrutiva etc.

No meu caso, é muito caro (pelo menos do ponto de vista da produtividade) tornar tudo imutável, então eu o guardo para as classes que são caras demais para serem copiadas na íntegra. Essas classes geralmente são estruturas de dados pesadas, como malhas e imagens, e eu geralmente uso uma interface mutável para expressar alterações a elas através de um objeto "construtor" para obter uma nova cópia imutável. E não estou fazendo tanto para tentar obter garantias imutáveis ​​no nível central da classe, mas para me ajudar a usar a classe em funções que podem estar livres de efeitos colaterais. Meu desejo de tornar Meshimutável acima não é diretamente tornar imutáveis ​​as malhas, mas permitir facilmente escrever funções livres de efeitos colaterais que introduzem uma malha e produzem uma nova sem pagar uma enorme memória e custo computacional em troca.

Como resultado, tenho apenas quatro tipos de dados imutáveis ​​em toda a minha base de código, e são todas pesadas estruturas de dados, mas eu as uso intensamente para me ajudar a escrever funções livres de efeitos colaterais. Essa resposta pode ser aplicável se, como eu, você estiver trabalhando em um idioma que não torne tão fácil tornar tudo imutável. Nesse caso, você pode mudar seu foco para evitar que a maior parte de suas funções evite efeitos colaterais; nesse momento, convém tornar imutáveis ​​estruturas de dados selecionadas, tipos PDS, como um detalhe de otimização para evitar cópias caras e caras. Enquanto isso, quando eu tenho uma função como esta:

/// @return The v1 * v2.
Vector3f vec_mul(Vector3f v1, Vector3f v2);

... então não tenho tentação de tornar vetores imutáveis, pois são baratos o suficiente para apenas copiar na íntegra. Não há nenhuma vantagem de desempenho a ser obtida aqui, transformando Vectors em estruturas imutáveis ​​que podem copiar partes não modificadas de baixa profundidade. Tais custos superariam o custo de apenas copiar todo o vetor.


fonte
-1
Foo a = new Foo();
a.setProperty1("asdf");
a.setProperty2("bcde");

Se o Foo tiver apenas duas propriedades, será fácil escrever um construtor que aceite dois argumentos. Mas suponha que você adicione outra propriedade. Então você precisa adicionar outro construtor:

public Foo(String a, String b, SomethingElse c)

Neste ponto, ainda é gerenciável. Mas e se houver 10 propriedades? Você não quer um construtor com 10 argumentos. Você pode usar o padrão do construtor para construir instâncias, mas isso adiciona complexidade. A essa altura, você estará pensando "por que não adicionei setters para todas as propriedades como as pessoas normais fazem"?

Mike Baranczak
fonte
4
Um construtor com dez argumentos é pior que o NullPointerException que ocorre quando a propriedade7 não foi definida? O padrão do construtor é mais complexo que o código para verificar propriedades não inicializadas em cada método? Melhor verificar uma vez quando o objeto é construído.
Kevin Cline
2
Como foi dito décadas atrás exatamente para este caso, "Se você tem um procedimento com dez parâmetros, provavelmente perdeu alguns".
9000:
1
Por que você não quer um construtor com 10 argumentos? Em que ponto você desenha a linha? Eu acho que um construtor com 10 argumentos é um preço pequeno a pagar pelos benefícios da imutabilidade. Além disso, você tem uma linha com 10 itens separados por vírgulas (opcionalmente, distribuídos em várias linhas ou até 10 linhas, se parecer melhor) ou você tem 10 linhas de qualquer maneira ao configurar cada propriedade individualmente ...
Ricket
1
@Ricket: Porque aumenta o risco de colocar os argumentos na ordem errada . Se você estiver usando setters, ou o padrão do construtor, isso é bastante improvável.
Mike Baranczak
4
Se você tem um construtor com 10 argumentos, talvez seja hora de encapsular esses argumentos em uma classe (ou classes) própria e / ou dar uma olhada crítica no design de sua classe.
Adam Lear