Imutabilidade completa e programação orientada a objetos

43

Na maioria das linguagens OOP, os objetos geralmente são mutáveis ​​com um conjunto limitado de exceções (como, por exemplo, tuplas e seqüências de caracteres em python). Na maioria das linguagens funcionais, os dados são imutáveis.

Objetos mutáveis ​​e imutáveis ​​trazem uma lista completa de vantagens e desvantagens.

Existem linguagens que tentam se casar com ambos os conceitos, como por exemplo, scala, em que você (declarado explicitamente) dados mutáveis ​​e imutáveis ​​(por favor, corrija-me se estiver errado, meu conhecimento de scala é mais do que limitado).

Minha pergunta é: A imutabilidade completa (sic!) - que nenhum objeto pode sofrer mutação depois de criada - faz algum sentido em um contexto de POO?

Existem projetos ou implementações desse modelo?

Basicamente, a imutabilidade (completa) e os OOP são opostos ou ortogonais?

Motivação: no OOP, você normalmente opera com dados, alterando (mutando) as informações subjacentes, mantendo referências entre esses objetos. Por exemplo, um objeto de classe Personcom um membro fatherreferenciando outro Personobjeto. Se você alterar o nome do pai, isso ficará imediatamente visível para o objeto filho, sem necessidade de atualização. Sendo imutável, você precisaria construir novos objetos para pai e filho. Mas você teria muito menos confusão com objetos compartilhados, multi-threading, GIL, etc.

Hyperboreus
fonte
2
A imutabilidade pode ser simulada em uma linguagem OOP, expondo apenas os pontos de acesso ao objeto como métodos ou propriedades somente leitura que não alteram os dados. A imutabilidade funciona da mesma maneira nas linguagens OOP e em qualquer linguagem funcional, exceto que você pode estar perdendo alguns recursos da linguagem funcional.
Robert Harvey
5
Mutabilidade não é uma propriedade de linguagens OOP como C # e Java, nem é imutabilidade. Você especifica mutabilidade ou imutabilidade pela maneira como escreve a classe.
Robert Harvey
9
Sua presunção parece ser que a mutabilidade é um recurso essencial da orientação a objetos. Não é. Mutabilidade é simplesmente uma propriedade de objetos ou valores. A orientação a objetos abrange vários conceitos intrínsecos (encapsulamento, polimorfismo, herança etc.) que têm pouco ou nada a ver com mutação, e você ainda obteria os benefícios desses recursos. mesmo que você tenha tornado tudo imutável.
Robert Harvey
2
@ MichaelT A questão não é tornar as coisas específicas mutáveis, é sobre tornar as coisas imutáveis.

Respostas:

43

OOP e imutabilidade são quase completamente ortogonais entre si. No entanto, programação imperativa e imutabilidade não são.

OOP pode ser resumido por dois recursos principais:

  • Encapsulamento : não acessarei o conteúdo dos objetos diretamente, mas sim comunicarei através de uma interface específica ("métodos") com esse objeto. Essa interface pode ocultar dados internos de mim. Tecnicamente, isso é específico para programação modular em vez de OOP. O acesso a dados por meio de uma interface definida é aproximadamente equivalente a um tipo de dados abstrato.

  • Despacho dinâmico : quando eu chamo um método em um objeto, o método executado será resolvido em tempo de execução. (Por exemplo, no OOP baseado em classe, eu poderia chamar um sizemétodo em uma IListinstância, mas a chamada pode ser resolvida para uma implementação em uma LinkedListclasse). O envio dinâmico é uma maneira de permitir um comportamento polimórfico.

O encapsulamento faz menos sentido sem mutabilidade (não há estado interno que possa ser corrompido por interferências externas), mas ainda tende a facilitar as abstrações, mesmo quando tudo é imutável.

Um programa imperativo consiste em instruções que são executadas seqüencialmente. Uma declaração tem efeitos colaterais, como alterar o estado do programa. Com a imutabilidade, o estado não pode ser alterado (é claro, um novo estado pode ser criado). Portanto, a programação imperativa é fundamentalmente incompatível com a imutabilidade.

Agora acontece que o OOP sempre esteve historicamente conectado à programação imperativa (o Simula é baseado em Algol), e todas as linguagens OOP tradicionais têm raízes imperativas (C ++, Java, C #, ... estão todas enraizadas em C). Isso não implica que a OOP em si seja imperativa ou mutável; isso significa apenas que a implementação da OOP por essas linguagens permite a mutabilidade.

amon
fonte
2
Muito obrigado, especialmente pela definição dos dois principais recursos.
precisa
O Dynamic Dispatch não é um recurso essencial do OOP. Nem realmente é Encapsulamento (como você já admitiu).
parar de prejudicar Monica
5
@OrangeDog Sim, não há uma definição universalmente aceita de OOP, mas eu precisava de uma definição para trabalhar. Então, escolhi algo o mais próximo possível da verdade, sem escrever uma dissertação completa sobre o assunto. No entanto, considero o despacho dinâmico como a principal característica distintiva do POO de outros paradigmas. Algo que se parece com OOP, mas na verdade todas as chamadas foram resolvidas estaticamente, é realmente apenas programação modular com polimorfismo ad-hoc. Os objetos são um par de métodos e dados e, como tal, equivalem a fechamentos.
amon
2
"Objetos são um emparelhamento de métodos e dados". Tudo que você precisa é algo que não tem nada a ver com imutabilidade.
parar de prejudicar Monica
3
@CodeYogi A ocultação de dados é o tipo mais comum de encapsulamento. No entanto, a maneira como os dados são armazenados internamente por um objeto não é o único detalhe da implementação que deve ser oculto. É igualmente importante ocultar como a interface pública é implementada, por exemplo, se eu uso algum método auxiliar. Tais métodos auxiliares também devem ser privados, de um modo geral. Então, para resumir: encapsulamento é um princípio, enquanto ocultar dados é uma técnica de encapsulamento.
amon
25

Observe que existe uma cultura entre os programadores orientados a objetos em que as pessoas assumem que, se você estiver fazendo POO, a maioria dos seus objetos será mutável, mas isso é um problema separado se o OOP exige mutabilidade. Além disso, essa cultura parece estar mudando lentamente em direção a mais imutabilidade, devido à exposição das pessoas à programação funcional.

Scala é uma ilustração muito boa de que a mutabilidade não é necessária para a orientação a objetos. Enquanto Scala suporta mutabilidade, seu uso é desencorajado. O Idiomatic Scala é muito orientado a objetos e também quase inteiramente imutável. Permite principalmente a mutabilidade para compatibilidade com Java e, em certas circunstâncias, objetos imutáveis ​​são ineficientes ou complicados para trabalhar.

Compare uma lista Scala e uma lista Java , por exemplo. A lista imutável do Scala contém todos os mesmos métodos de objetos que a lista mutável do Java. Mais, de fato, porque Java usa funções estáticas para operações como classificação e Scala adiciona métodos de estilo funcional como map. Todas as características do OOP - encapsulamento, herança e polimorfismo - estão disponíveis de uma forma familiar aos programadores orientados a objetos e são usadas adequadamente.

A única diferença que você verá é que, quando você altera a lista, obtém um novo objeto como resultado. Isso geralmente requer que você use padrões de design diferentes dos usados ​​com objetos mutáveis, mas não exige que você abandone o OOP por completo.

Karl Bielefeldt
fonte
17

A imutabilidade pode ser simulada em uma linguagem OOP, expondo apenas os pontos de acesso ao objeto como métodos ou propriedades somente leitura que não alteram os dados. A imutabilidade funciona da mesma maneira nas linguagens OOP e em qualquer linguagem funcional, exceto que você pode estar perdendo alguns recursos da linguagem funcional.

Sua presunção parece ser que a mutabilidade é um recurso essencial da orientação a objetos. Mas a mutabilidade é simplesmente uma propriedade de objetos ou valores. A orientação a objetos engloba vários conceitos intrínsecos (encapsulamento, polimorfismo, herança etc.) que têm pouco ou nada a ver com mutação, e você ainda obteria os benefícios desses recursos, mesmo se tornasse tudo imutável.

Nem todas as linguagens funcionais também exigem imutabilidade. O Clojure possui uma anotação específica que permite que os tipos sejam mutáveis, e a maioria das linguagens funcionais "práticas" tem uma maneira de especificar tipos mutáveis.

Uma pergunta melhor a ser feita pode ser "A imutabilidade completa faz sentido na programação imperativa ?" Eu diria que a resposta óbvia a essa pergunta é não. Para alcançar a imutabilidade completa na programação imperativa, você teria que renunciar a coisas como forloops (já que seria necessário alterar uma variável de loop) em favor da recursão, e agora você está essencialmente programando de maneira funcional de qualquer maneira.

Robert Harvey
fonte
Obrigado. Você poderia elaborar um pouco o seu último parágrafo ("óbvio" pode ser um pouco subjetivo).
precisa
Já o fiz .... #
1111 Robert Harvey
1
@ Hyperboreus Existem muitas maneiras de obter polimorfismo. Subtipagem com despacho dinâmico, polimorfismo ad-hoc estático (também conhecido como sobrecarga de funções) e polimorfismo paramétrico (também conhecido como genéricos) são as formas mais comuns de fazer isso, e todas as formas têm seus pontos fortes e fracos. As linguagens modernas de POO combinam todas essas três maneiras, enquanto Haskell se baseia principalmente no polimorfismo paramétrico e no polimorfismo ad-hoc.
17174 amon
3
@RobertHarvey Você diz que precisa de mutabilidade porque precisa fazer um loop (caso contrário, você deve usar recursão). Antes de começar a usar o Haskell, há dois anos, pensei que também precisava de variáveis ​​mutáveis. Só estou dizendo que há outras maneiras de "fazer um loop" (mapear, dobrar, filtrar etc.). Depois de tirar o loop da tabela, por que mais você precisaria de variáveis ​​mutáveis?
Cimmanon
1
@RobertHarvey Mas esse é exatamente o ponto das linguagens de programação: o que é exposto a você e não o que está acontecendo sob o capô. O último é de responsabilidade do compilador ou intérprete, não do desenvolvedor do aplicativo. Caso contrário, volte ao montador.
precisa
5

Geralmente, é útil categorizar objetos como valores ou entidades que encapsulam, com a distinção de que, se algo é um valor, o código que contém uma referência a ele nunca deve ver seu estado mudar de forma que o próprio código não tenha iniciado. Por outro lado, o código que contém uma referência a uma entidade pode esperar que ela mude de maneiras além do controle do detentor de referência.

Embora seja possível usar o valor encapsulado usando objetos de tipos mutáveis ​​ou imutáveis, um objeto só pode se comportar como um valor se pelo menos uma das seguintes condições se aplicar:

  1. Nenhuma referência ao objeto será exposta a algo que possa alterar o estado nele encapsulado.

  2. O detentor de pelo menos uma das referências ao objeto conhece todos os usos para os quais qualquer referência existente pode ser colocada.

Como todas as instâncias de tipos imutáveis ​​atendem automaticamente ao primeiro requisito, é fácil usá-las como valores. Garantir que um dos requisitos seja atendido ao usar tipos mutáveis ​​é, por outro lado, muito mais difícil. Enquanto as referências a tipos imutáveis ​​podem ser passadas livremente como um meio de encapsular o estado nele encapsulado, a passagem pelo estado armazenado em tipos mutáveis ​​requer a construção de objetos invólucros imutáveis ​​ou a cópia do estado encapsulado por objetos de propriedade privada em outros objetos que são fornecido ou construído para o destinatário dos dados.

Os tipos imutáveis ​​funcionam muito bem para a passagem de valores e geralmente são pelo menos um pouco úteis para manipulá-los. Eles não são tão bons, no entanto, em lidar com entidades. A coisa mais próxima que se pode ter de uma entidade em um sistema com tipos puramente imutáveis ​​é uma função que, dado o estado do sistema, reportará os atributos de alguma parte dele ou produzirá uma nova instância de estado do sistema que é como uma fornecido um, exceto por uma parte específica do mesmo, que será diferente de alguma maneira selecionável. Além disso, se o objetivo de uma entidade é fazer a interface de algum código com algo que existe no mundo real, pode ser impossível para a entidade evitar a exposição de um estado mutável.

Por exemplo, se alguém recebe alguns dados através de uma conexão TCP, pode produzir um novo objeto "estado do mundo" que inclua esses dados em seu buffer sem afetar nenhuma referência ao antigo "estado do mundo", mas cópias antigas de o estado mundial que não inclui o último lote de dados estará com defeito e não deverá ser usado, pois não corresponderá mais ao estado do soquete TCP do mundo real.

supercat
fonte
4

Em c #, alguns tipos são imutáveis ​​como string.

Isso parece sugerir além disso que a escolha foi fortemente considerada.

Certamente, é realmente desempenho exigente o uso de tipos imutáveis, se você precisar modificar esse tipo centenas de milhares de vezes. Essa é a razão pela qual é sugerido o uso da StringBuilderclasse em vez da stringclasse nesses casos.

Fiz um experimento com um criador de perfil e usar o tipo imutável é realmente mais exigente de CPU e RAM.

Também é intuitivo se você considerar que, para modificar apenas uma letra em uma seqüência de 4000 caracteres, é necessário copiar todos os caracteres em outra área da RAM.

Revious
fonte
6
A modificação frequente de dados imutáveis ​​não precisa ser catastroficamente lenta, como ocorre com stringconcatenações repetidas . Para praticamente todos os tipos de dados / casos de uso, uma estrutura persistente eficiente pode ser (muitas vezes já foi) inventada. A maioria deles tem desempenho aproximadamente igual, mesmo que os fatores constantes às vezes sejam piores.
@ delnan Eu também acho que o último parágrafo da resposta é mais sobre um detalhe de implementação do que sobre (im) mutabilidade.
precisa
@ Hyperboreus: você acha que devo excluir essa parte? Mas como uma string pode mudar se for imutável? Quero dizer ... na minha opinião, mas com certeza posso estar errado, essa poderia ser a principal razão pela qual um objeto não é imutável.
Marious
1
@ Anterior De nenhuma maneira. Deixe-o, pois isso gera discussões e opiniões e pontos de vista mais interessantes.
precisa
1
@ Anterior Sim, a leitura seria mais lenta, embora não tão lenta quanto a alteração de uma string(a representação tradicional). Uma "string" (na representação de que estou falando) após 1000 modificações seria como uma string recém-criada (conteúdo do módulo); nenhuma estrutura de dados persistente útil ou amplamente usada diminui a qualidade após operações X. Fragmentação de memória não é um problema grave (você teria muitos alocações, sim, mas a fragmentação é bem um não-problema em coletores de lixo modernos)
0

A imutabilidade completa de tudo não faz muito sentido no OOP, ou na maioria dos outros paradigmas, por um motivo muito grande:

Todo programa útil tem efeitos colaterais.

Um programa que não faz nada mudar, é inútil. Você pode nem mesmo executá-lo, pois o efeito será idêntico.

Mesmo que você pense que não está mudando nada e esteja simplesmente resumindo uma lista de números que recebeu de alguma forma, considere que você precisa fazer algo com o resultado - imprima na saída padrão, grave em um arquivo, ou em qualquer lugar. E isso envolve alterar um buffer e alterar o estado do sistema.

Pode fazer muito sentido restringir a mutabilidade às partes que precisam ser capazes de mudar. Mas se absolutamente nada precisa mudar, você não está fazendo nada que valha a pena.

cHao
fonte
4
Não consigo entender como a sua resposta está relacionada à pergunta, pois não lidei com linguagens funcionais puras. Tomemos o erlang, por exemplo: dados imutáveis, nenhuma atribuição destrutiva, nenhuma confusão sobre os efeitos colaterais. Além disso, você tem estado em uma linguagem funcional, apenas que o estado "flui" através das funções, diferentemente das funções que operam no estado. O estado muda, mas não muda, mas um estado futuro substitui o estado atual. A imutabilidade não é se um buffer de memória é alterado ou não, mas se essas mutações são visíveis do lado de fora.
precisa
E um estado futuro substitui o atual como , exatamente? Em um programa OO, esse estado é uma propriedade de um objeto em algum lugar. Substituir o estado requer alterar um objeto (ou substituí-lo por outro, o que requer uma alteração nos objetos que se referem a ele (ou substituí-lo por outro, que ... eh. Você entendeu)). Você pode criar algum tipo de invasão monádica, onde cada ação acaba criando um aplicativo totalmente novo ... mas mesmo assim, o estado atual do programa precisa ser gravado em algum lugar.
Chao
7
-1. Isto está incorreto. Você está confundindo efeitos colaterais com mutações e, embora sejam frequentemente tratados da mesma forma por linguagens funcionais, eles são diferentes. Todo programa útil tem efeitos colaterais; nem todo programa útil tem mutação.
Michael Shaw
@ Michael: Quando se trata de POO, mutação e efeitos colaterais estão tão entrelaçados que não podem ser separados realisticamente. Se você não tem mutação, não pode ter efeitos colaterais sem grandes quantidades de hackers.
cHao 18/03
E, no
dmckee
-2

Eu acho que depende se a sua definição de OOP é que ele usa um estilo de passagem de mensagem.

As funções puras não precisam mudar nada, pois retornam valores que você pode armazenar em novas variáveis.

var brandNewVariable = pureFunction(foo);

Com o estilo de passagem de mensagens, você diz a um objeto para armazenar novos dados, em vez de perguntar quais novos dados você deve armazenar em uma nova variável.

sameOldObject.changeMe(foo);

É possível ter objetos e não modificá-los, transformando seus métodos em funções puras que vivem no interior do objeto e não no exterior.

var brandNewVariable = nonMutatingObject.askMe(foo);

Mas não é possível misturar estilo de passagem de mensagem e objetos imutáveis.

presley
fonte