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 Person
com um membro father
referenciando outro Person
objeto. 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.
fonte
Respostas:
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
size
método em umaIList
instância, mas a chamada pode ser resolvida para uma implementação em umaLinkedList
classe). 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.
fonte
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.
fonte
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
for
loops (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.fonte
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:
Nenhuma referência ao objeto será exposta a algo que possa alterar o estado nele encapsulado.
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.
fonte
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
StringBuilder
classe em vez dastring
classe 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.
fonte
string
concatenaçõ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.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)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.
fonte
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.
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.
É 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.
Mas não é possível misturar estilo de passagem de mensagem e objetos imutáveis.
fonte