Ao projetar classes para manter seu modelo de dados que eu li, pode ser útil criar objetos imutáveis, mas em que momento o ônus das listas de parâmetros do construtor e cópias profundas se torna muito alto e você precisa abandonar a restrição imutável?
Por exemplo, aqui está uma classe imutável para representar uma coisa nomeada (estou usando a sintaxe C #, mas o princípio se aplica a todas as linguagens OO)
class NamedThing
{
private string _name;
public NamedThing(string name)
{
_name = name;
}
public NamedThing(NamedThing other)
{
this._name = other._name;
}
public string Name
{
get { return _name; }
}
}
Os itens nomeados podem ser construídos, consultados e copiados para novos itens nomeados, mas o nome não pode ser alterado.
Tudo isso é bom, mas o que acontece quando eu quero adicionar outro atributo? Eu tenho que adicionar um parâmetro ao construtor e atualizar o construtor de cópia; o que não é muito trabalhoso, mas os problemas começam, tanto quanto posso ver, quando quero tornar um objeto complexo imutável.
Se a classe contiver vários atributos e coleções, contendo outras classes complexas, parece-me que a lista de parâmetros do construtor se tornaria um pesadelo.
Então, em que ponto uma classe se torna complexa demais para ser imutável?
Respostas:
Quando eles se tornam um fardo? Muito rapidamente (especialmente se o seu idioma de escolha não fornecer suporte sintático suficiente para imutabilidade.)
A imutabilidade está sendo vendida como a bala de prata para o dilema multinúcleo e tudo isso. Mas a imutabilidade na maioria dos idiomas OO obriga a adicionar artefatos e práticas artificiais em seu modelo e processo. Para cada classe imutável complexa, você deve ter um construtor igualmente complexo (pelo menos internamente). Não importa como você o projete, ele apresenta um forte acoplamento (portanto, é melhor ter um bom motivo para apresentá-los).
Não é necessariamente possível modelar tudo em pequenas classes não complexas. Portanto, para grandes classes e estruturas, nós as particionamos artificialmente - não porque isso faça sentido em nosso modelo de domínio, mas porque temos que lidar com suas instâncias complexas e construtores de código.
É ainda pior quando as pessoas levam a idéia de imutabilidade longe demais em uma linguagem de propósito geral como Java ou C #, tornando tudo imutável. Como resultado, você vê pessoas forçando construções de expressão s em idiomas que não suportam essas coisas com facilidade.
Engenharia é o ato de modelar através de compromissos e trade-offs. Tornar tudo imutável por decreto porque alguém lê que tudo é imutável na linguagem funcional X ou Y (um modelo de programação completamente diferente), isso não é aceitável. Isso não é uma boa engenharia.
Coisas pequenas, possivelmente unitárias, podem ser imutáveis. Coisas mais complexas podem ser tornadas imutáveis quando faz sentido . Mas a imutabilidade não é uma bala de prata. A capacidade de reduzir erros, aumentar a escalabilidade e o desempenho, não é a única função da imutabilidade. É uma função de práticas de engenharia adequadas . Afinal, as pessoas escreveram software bom e escalável, sem imutabilidade.
A imutabilidade se torna um fardo muito rápido (isso aumenta a complexidade acidental) se for feita sem uma razão, quando for feita fora do que faz sentido no contexto de um modelo de domínio.
Eu, por exemplo, tento evitá-lo (a menos que esteja trabalhando em uma linguagem de programação com bom suporte sintático para isso).
fonte
Passei por uma fase de insistir em que as aulas fossem imutáveis sempre que possível. Os construtores tinham praticamente tudo, matrizes imutáveis, etc. etc. Achei que a resposta para sua pergunta é simples: em que momento as classes imutáveis se tornam um fardo? Muito rapidamente. Assim que você deseja serializar algo, você deve ser capaz de desserializar, o que significa que deve ser mutável; assim que você deseja usar um ORM, a maioria deles insiste em que as propriedades sejam mutáveis. E assim por diante.
Acabei substituindo essa política por interfaces imutáveis para objetos mutáveis.
Agora, o objeto tem flexibilidade, mas você ainda pode dizer ao código de chamada que ele não deve editar essas propriedades.
fonte
IComparable<T>
garante que seX.CompareTo(Y)>0
eY.CompareTo(Z)>0
, entãoX.CompareTo(Z)>0
. As interfaces têm contratos . Se o contrato paraIImmutableList<T>
especificar que os valores de todos os itens e propriedades devem ser "gravados em pedra" antes que qualquer instância seja exposta ao mundo externo, todas as implementações legítimas o farão. Nada impede que umaIComparable
implementação viole a transitividade, mas as implementações que o fazem são ilegítimas. Se umSortedDictionary
mau funcionamento quando dado um ilegítimoIComparable
, ... #IReadOnlyList<T>
serão imutáveis, uma vez que (1) esse requisito não está declarado na documentação da interface e (2) a implementação mais comumList<T>
, nem é somente leitura ? Não estou muito claro o que é ambíguo sobre meus termos: uma coleção é legível se os dados contidos puderem ser lidos. É somente leitura se puder prometer que os dados contidos não podem ser alterados, a menos que alguma referência externa seja mantida pelo código que os alteraria. É imutável se pode garantir que não pode ser alterado, ponto final.Eu não acho que haja uma resposta geral para isso. Quanto mais complexa é uma classe, mais difícil é argumentar sobre suas mudanças de estado e mais caro é criar novas cópias dela. Portanto, acima de algum nível (pessoal) de complexidade, será muito doloroso tornar uma classe imutável.
Observe que uma classe muito complexa ou uma longa lista de parâmetros de métodos são odores de design em si, independentemente da imutabilidade.
Portanto, normalmente, a solução preferida seria dividir essa classe em várias classes distintas, cada uma das quais pode ser transformada por mutação ou imutabilidade por si só. Se isso não for viável, pode ser alterado.
fonte
Você pode evitar o problema de cópia se armazenar todos os seus campos imutáveis em um interior
struct
. Isso é basicamente uma variação do padrão de lembrança. Então, quando você quiser fazer uma cópia, basta copiar a lembrança:fonte
Você tem algumas coisas trabalhando aqui. Conjuntos de dados imutáveis são ótimos para escalabilidade multithread. Essencialmente, você pode otimizar bastante sua memória para que um conjunto de parâmetros seja uma instância da classe - em qualquer lugar. Como os objetos nunca mudam, você não precisa se preocupar em sincronizar para acessar seus membros. É uma coisa boa. No entanto, como você aponta, quanto mais complexo o objeto, mais você precisará de alguma mutabilidade. Eu começaria com o raciocínio ao longo destas linhas:
Em idiomas que oferecem suporte apenas a objetos imutáveis (como Erlang), se houver alguma operação que pareça modificar o estado de um objeto imutável, o resultado final será uma nova cópia do objeto com o valor atualizado. Por exemplo, quando você adiciona um item a um vetor / lista:
Essa pode ser uma maneira sensata de trabalhar com objetos mais complicados. À medida que você adiciona um nó da árvore, por exemplo, o resultado é uma nova árvore com o nó adicionado. O método no exemplo acima retorna uma nova lista. No exemplo deste parágrafo
tree.add(newNode)
, retornaria uma nova árvore com o nó adicionado. Para os usuários, fica fácil trabalhar com eles. Para os escritores da biblioteca, torna-se entediante quando o idioma não suporta cópias implícitas. Esse limite é de sua própria paciência. Para os usuários da sua biblioteca, o limite mais sensato que encontrei é de três a quatro topos de parâmetros.fonte
Se você possui vários membros da classe final e não deseja que eles sejam expostos a todos os objetos que precisam criá-lo, é possível usar o padrão do construtor:
a vantagem é que você pode criar facilmente um novo objeto com apenas um valor diferente de um nome diferente.
fonte
newObject
.NamedThing
neste caso)Builder
é reutilizada, existe um risco real de que eu mencionei. Alguém pode estar construindo muitos objetos e decidir que, como a maioria das propriedades é a mesma, simplesmente reutilizarBuilder
e, de fato, vamos torná-lo um singleton global que é injetado em dependência! Ops. Principais erros introduzidos. Então, acho que esse padrão de instanciado versus estático misto é ruim.Na minha opinião, não vale a pena se preocupar em tornar as turmas pequenas imutáveis em idiomas como o que você está mostrando. Estou usando aqui pequeno e não é complexo , porque mesmo que você adicione dez campos a essa classe e realmente realize operações sofisticadas, duvido que serão necessários kilobytes e muito menos megabytes e muito menos gigabytes, portanto, qualquer função usando instâncias de seu A classe pode simplesmente fazer uma cópia barata de todo o objeto para evitar modificar o original, se quiser evitar causar efeitos colaterais externos.
Estruturas de dados persistentes
Onde eu acho que o uso pessoal para imutabilidade é a grande estrutura de dados central que agrega um monte de dados pequeninos, como instâncias da classe que você está mostrando, como uma que armazena um milhão
NamedThings
. Pertencendo a uma estrutura de dados persistente que é imutável e estando atrás de uma interface que permite apenas acesso somente leitura, os elementos que pertencem ao contêiner se tornam imutáveis sem que a classe de elemento (NamedThing
) precise lidar com ele.Cópias baratas
A estrutura de dados persistente permite que regiões dele sejam transformadas e tornadas únicas, evitando modificações no original sem precisar copiar a estrutura de dados em sua totalidade. Essa é a verdadeira beleza disso. Se você deseja escrever ingenuamente funções que evitam efeitos colaterais que inserem uma estrutura de dados que consome gigabytes de memória e modifica apenas o valor de memória de um megabyte, copie toda a loucura para evitar tocar na entrada e retornar um novo resultado. Ou copia gigabytes para evitar efeitos colaterais ou causar efeitos colaterais nesse cenário, fazendo com que você escolha entre duas opções desagradáveis.
Com uma estrutura de dados persistente, ele permite que você escreva uma função e evite fazer uma cópia de toda a estrutura de dados, exigindo apenas cerca de um megabyte de memória extra para a saída se sua função transformar apenas o valor de memória de um megabyte.
Carga
Quanto ao fardo, há um imediato, pelo menos no meu caso. Eu preciso dos construtores sobre os quais as pessoas estão falando ou "transientes", como eu os chamo, para poder expressar efetivamente transformações nessa estrutura de dados maciça sem tocá-la. Código como este:
... então tem que ser escrito assim:
Mas, em troca dessas duas linhas de código extras, agora é seguro chamar a função através de threads com a mesma lista original, não causa efeitos colaterais, etc. Também facilita muito tornar essa operação uma ação do usuário desfavorável, já que o desfazer pode apenas armazenar uma cópia rasa barata da lista antiga.
Segurança de exceção ou recuperação de erros
Nem todo mundo pode se beneficiar tanto quanto eu de estruturas de dados persistentes em contextos como esses (achei muito útil para eles em sistemas de desfazer e edição não destrutiva, que são conceitos centrais no meu domínio VFX), mas uma coisa se aplica a praticamente todo mundo a considerar é segurança de exceção ou recuperação de erros .
Se você deseja tornar segura a exceção da função de mutação original, ela precisa de lógica de reversão, para a qual a implementação mais simples requer a cópia de toda a lista:
Nesse ponto, a versão mutável com exceção de segurança é ainda mais cara em termos de computação e sem dúvida ainda mais difícil de escrever corretamente do que a versão imutável usando um "construtor". E muitos desenvolvedores de C ++ apenas negligenciam a segurança de exceção e talvez isso seja bom para o domínio deles, mas no meu caso, eu gostaria de garantir que meu código funcione corretamente mesmo no caso de uma exceção (mesmo escrevendo testes que deliberadamente lançam exceções para testar exceção) segurança), e isso torna necessário que eu seja capaz de reverter quaisquer efeitos colaterais que uma função cause a meio caminho da função, se alguma coisa for lançada.
Quando você deseja ser protegido contra exceções e se recuperar de erros normalmente sem que seu aplicativo falhe e queime, você deve reverter / desfazer quaisquer efeitos colaterais que uma função possa causar no caso de um erro / exceção. E aí o construtor pode realmente economizar mais tempo do programador do que o custo, juntamente com o tempo computacional, porque: ...
Então, voltando à questão fundamental:
Eles sempre são um fardo em idiomas que giram mais em torno da mutabilidade do que da imutabilidade, e é por isso que acho que você deve usá-los onde os benefícios superam significativamente os custos. Mas em um nível amplo o suficiente para estruturas de dados grandes o suficiente, acredito que há muitos casos em que é uma troca valiosa.
Também na minha, eu tenho apenas alguns tipos de dados imutáveis e todas elas são estruturas de dados enormes destinadas a armazenar um grande número de elementos (pixels de uma imagem / textura, entidades e componentes de um ECS e vértices / bordas / polígonos de uma malha).
fonte