Imutabilidade completa e programação orientada a objetos

41

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

Ambos os objetos mutáveis e imutáveis trazem toda uma lista de vantagens e desvantagens próprias.

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

Minha pergunta é: A completa (sic!) imutabilidade, ou seja, nenhum objeto pode sofrer mutação depois de criado - faz algum sentido em um contexto OOP?

Existem projetos ou implementações de tal modelo?

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

Motivação: Em OOP, você normalmente opera os dados em , alterando (alterando) as informações subjacentes, mantendo referências entre esses objetos. Por exemplo. um objeto da classe Person com um membro father referenciando outro objeto Person . Se você alterar o nome do pai, isso é 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 kerfuffle com objetos compartilhados, multi-threading, GIL, etc.

    
por Hyperboreus 17.03.2014 / 20:10
fonte

7 respostas

41

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

A OOP pode ser resumida por dois recursos principais:

  • Encapsulamento : Eu não irei acessar o conteúdo de objetos diretamente, mas sim me comunicar através de uma interface específica (“métodos”) com este objeto. Essa interface pode ocultar dados internos de mim. Tecnicamente, isso é específico para programação modular em vez de OOP. Acessar dados através 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, em OOP baseada em classe, posso chamar um método size em uma instância IList , mas a chamada pode ser resolvida para uma implementação em uma classe LinkedList ). O despacho dinâmico é uma maneira de permitir o comportamento polimórfico.

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

Um programa imperativo consiste em declarações que são executadas sequencialmente. Uma declaração tem efeitos colaterais como mudar 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.

Acontece agora que a OOP tem sido historicamente sempre conectada com programação imperativa (Simula é baseada em Algol), e todas as principais linguagens OOP possuem raízes imperativas (C ++, Java, C #,… estão todas enraizadas em C). Isto não implica que a OOP em si seja imperativa ou mutável, isto significa apenas que a implementação da OOP por estas linguagens permite a mutabilidade.

    
por 17.03.2014 / 20:54
fonte
25

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

O Scala é uma boa ilustração de que a mutabilidade não é necessária para a orientação a objetos. Enquanto o Scala suporta mutabilidade, seu uso é desencorajado. Scala idiomática é muito orientada a objetos e também quase inteiramente imutável. Ele permite principalmente a mutabilidade para compatibilidade com Java e porque, 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 de Scala contém todos os mesmos métodos de objeto que a lista mutável de Java. Mais, na verdade, porque o Java usa funções estáticas para operações como sort e o Scala adiciona métodos de estilo funcional como map . Todas as características da OOP - encapsulamento, herança e polimorfismo - estão disponíveis em um formato familiar aos programadores orientados a objetos e usados apropriadamente.

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

    
por 17.03.2014 / 21:56
fonte
17

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

Sua presunção parece ser que a mutabilidade é uma característica central 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 pouco ou nada têm a ver com a mutação, e você ainda obteria os benefícios desses recursos, mesmo se você tornasse tudo imutável.

Nem todas as linguagens funcionais exigem imutabilidade. O Clojure tem 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 se fazer pode ser "Será que a imutabilidade completa faz sentido na programação imperativa ?" Eu diria que a resposta óbvia para essa pergunta é não. Para alcançar a imutabilidade completa na programação imperativa, você teria que abrir mão de coisas como for loops (já que você teria que mudar uma variável de loop) em favor da recursão, e agora você está essencialmente programando de uma maneira funcional. p>     

por 17.03.2014 / 20:29
fonte
5

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

Embora seja possível usar o valor de encapsulamento 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 proprietário de pelo menos uma das referências ao objeto conhece todos os usos aos quais qualquer referência existente pode ser colocada.

Como todas as instâncias de tipos imutáveis satisfazem automaticamente o primeiro requisito, é fácil usá-las como valores. Garantir que qualquer requisito seja atendido ao usar tipos mutáveis é, por contraste, muito mais difícil. Considerando que as referências aos tipos imutáveis podem ser passadas livremente como um meio de encapsular o estado encapsulado nele, passar em torno do estado armazenado em tipos mutáveis requer a construção de objetos invólucros imutáveis, ou então copiar o estado encapsulado por objetos privados em outros objetos que são fornecidos ou construídos para o destinatário dos dados.

Os tipos imutáveis funcionam muito bem para passar valores e, geralmente, são pelo menos utilizáveis para manipulá-los. Eles não são tão bons, no entanto, em lidar com entidades. O mais próximo que uma pessoa pode ter de uma entidade em um sistema com tipos puramente imutáveis é uma função que, dado o estado do sistema, relatará os atributos de alguma parte deles, ou produzirá uma nova instância de estado do sistema que é como um fornecido um, exceto por alguma parte particular do mesmo, que será diferente de alguma forma 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 do estado mutável.

Por exemplo, se alguém recebe alguns dados através de uma conexão TCP, pode-se produzir um novo objeto "state of the world" que inclui esses dados em seu buffer sem afetar quaisquer referências ao antigo "estado do mundo", mas cópias antigas do estado mundial que não incluam o último lote de dados estarão com defeito e não devem ser usadas, pois não corresponderão mais ao estado do soquete TCP do mundo real.

    
por 18.03.2014 / 22:11
fonte
4

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

Isto parece sugerir que a escolha foi strongmente considerada.

Com certeza, é realmente exigente o uso de tipos imutáveis se você tiver que modificar esse tipo centenas de milhares de vezes. Essa é a razão pela qual é sugerido usar a classe StringBuilder em vez da classe string nestes casos.

Eu fiz um experimento com um profiler e usar o tipo imutável é muito mais exigente em CPU e RAM.

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

    
por 17.03.2014 / 20:34
fonte
0

A imutabilidade completa de tudo não faz muito sentido em OOP, ou na maioria dos outros paradigmas, por uma grande razão:

Todo programa útil tem efeitos colaterais.

Um programa que não faz com que nada mude, é inútil. Você pode também não executá-lo, já que o efeito será idêntico.

Mesmo se você acha que não está mudando nada, e está simplesmente resumindo uma lista de números que recebeu de alguma forma, considere que precisa fazer algo com o resultado - se você imprimi-lo na saída padrão, escreva-o para um arquivo ou qualquer outro lugar. E isso envolve a mutação de um buffer e a alteração do estado do sistema.

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

    
por 17.03.2014 / 23:06
fonte
-2

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

Funções puras não precisam mudar nada porque elas retornam valores que você pode armazenar em novas variáveis.

var brandNewVariable = pureFunction(foo);

Com o estilo de passar 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 transformá-los, tornando seus métodos funções puras que vivem no interior do objeto, e não fora dele.

var brandNewVariable = nonMutatingObject.askMe(foo);

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

    
por 17.03.2014 / 23:31
fonte