Podemos realmente usar a imutabilidade no OOP sem perder todos os principais recursos do OOP?


11

Vejo os benefícios de tornar objetos no meu programa imutáveis. Quando estou realmente pensando profundamente em um bom design para o meu aplicativo, muitas vezes chego naturalmente a muitos dos meus objetos sendo imutáveis. Muitas vezes chega ao ponto em que eu gostaria de ter todos os meus objetos imutáveis.

Esta pergunta lida com a mesma idéia, mas nenhuma resposta sugere qual é uma boa abordagem para a imutabilidade e quando realmente usá-la. Existem bons padrões de design imutáveis? A idéia geral parece ser "tornar objetos imutáveis, a menos que você precise absolutamente mudar", o que é inútil na prática.

Minha experiência é que a imutabilidade direciona meu código cada vez mais ao paradigma funcional e essa progressão sempre acontece:

  1. Começo a precisar de estruturas de dados persistentes (no sentido funcional), como listas, mapas etc.
  2. É extremamente inconveniente trabalhar com referências cruzadas (por exemplo, nó de árvore que faz referência a seus filhos enquanto filhos referenciam seus pais), o que me faz não usar referências cruzadas, o que novamente torna minhas estruturas de dados e código mais funcionais.
  3. A herança para para fazer algum sentido e eu começo a usar a composição.
  4. Todas as idéias básicas de OOP, como encapsulamento, começam a desmoronar e meus objetos começam a parecer funções.

Neste ponto, praticamente não estou mais usando nada do paradigma OOP e posso apenas mudar para uma linguagem puramente funcional. Assim, minha pergunta: existe uma abordagem consistente para o bom design imutável de OOP ou sempre é que, quando você leva a idéia imutável ao máximo de seu potencial, você sempre acaba programando em uma linguagem funcional que não precisa mais de nada do mundo da OOP? Existem boas diretrizes para decidir quais classes devem ser imutáveis ​​e quais devem permanecer mutáveis ​​para garantir que a POO não desmorone?

Apenas por conveniência, darei um exemplo. Vamos ter uma ChessBoardcoleção imutável de peças de xadrez imutáveis ​​(estendendo a classe abstrataPiece) Do ponto de vista do POO, uma peça é responsável por gerar movimentos válidos a partir de sua posição no tabuleiro. Mas, para gerar os movimentos, a peça precisa de uma referência ao seu tabuleiro, enquanto o tabuleiro precisa ter referência às suas peças. Bem, existem alguns truques para criar essas referências cruzadas imutáveis, dependendo da sua linguagem OOP, mas eles são difíceis de gerenciar, é melhor não ter uma peça para referenciar seu quadro. Mas então a peça não pode gerar seus movimentos, pois não sabe o estado do tabuleiro. Em seguida, a peça se torna apenas uma estrutura de dados contendo o tipo e a posição da peça. Você pode usar uma função polimórfica para gerar movimentos para todos os tipos de peças. Isso é perfeitamente possível na programação funcional, mas quase impossível no OOP sem verificações de tipo de tempo de execução e outras práticas ruins de OOP ...


3
Gosto da pergunta básica, mas tenho dificuldade com os detalhes. Por exemplo, por que a herança pára para fazer sentido quando você maximiza a imutabilidade?
Martin Ba

1
Seus objetos não têm métodos?
Pare de prejudicar Monica


4
"Do ponto de vista do POO, uma peça é responsável por gerar movimentos válidos a partir de sua posição no tabuleiro". - definitivamente não, projetar uma peça como essa provavelmente prejudicaria o SRP.
Doc Brown

1
@lishaak Você "luta para explicar o problema da herança em termos simples" porque não é um problema; design deficiente é um problema. A herança é a própria essência do POO, o sine qua non, se você preferir. Muitos dos chamados "problemas com herança" são realmente problemas com idiomas que não possuem uma sintaxe de substituição explícita e são muito menos problemáticos em idiomas melhor projetados. Sem métodos virtuais e polimorfismo, você não tem OOP; você tem programação procedural com sintaxe de objeto engraçado. Portanto, não é de admirar que você não esteja vendo os benefícios do OO, se estiver evitando!
Mason Wheeler

Respostas:


24

Podemos realmente usar a imutabilidade no OOP sem perder todos os principais recursos do OOP?

Não vejo porque não. Faz isso há anos que o Java 8 fica funcional, de qualquer maneira. Já ouviu falar de Strings? Agradável e imutável desde o início.

  1. Começo a precisar de estruturas de dados persistentes (no sentido funcional), como listas, mapas etc.

Também precisei deles o tempo todo. A invalidação de meus iteradores porque você modificou a coleção enquanto eu estava lendo é apenas rude.

  1. É extremamente inconveniente trabalhar com referências cruzadas (por exemplo, nó de árvore que faz referência a seus filhos enquanto filhos referenciam seus pais), o que me faz não usar referências cruzadas, o que novamente torna minhas estruturas de dados e código mais funcionais.

Referências circulares são um tipo especial de inferno. A imutabilidade não salvará você disso.

  1. A herança para para fazer algum sentido e eu começo a usar a composição.

Bem, aqui estou com você, mas não vejo o que isso tem a ver com imutabilidade. A razão de eu gostar de composição não é porque eu amo o padrão de estratégia dinâmica, é porque me permite alterar meu nível de abstração.

  1. Todas as idéias básicas de OOP, como encapsulamento, começam a desmoronar e meus objetos começam a parecer funções.

Estremeço ao pensar qual é a sua idéia de "OOP como encapsulamento". Se envolver getters e setters, basta parar de chamar esse encapsulamento, porque não é. Isso nunca foi. É manual Programação Orientada a Aspectos. A chance de validar e um local para colocar um ponto de interrupção é boa, mas não é um encapsulamento. O encapsulamento está preservando meu direito de não saber ou me importar com o que está acontecendo lá dentro.

Seus objetos devem parecer funções. Eles são sacos de funções. São sacos de funções que se movem juntas e se redefinem juntas.

A programação funcional está em voga no momento e as pessoas estão lançando alguns conceitos errados sobre OOP. Não deixe que isso o confunda, acreditando que este é o fim do POO. Funcional e POO podem conviver muito bem.

  • A programação funcional está sendo formal sobre atribuições.

  • OOP está sendo formal sobre ponteiros de função.

Realmente isso. Dykstra nos disse que gotoera prejudicial, por isso fomos formais e criamos programação estruturada. Assim, esses dois paradigmas são sobre como encontrar maneiras de fazer as coisas, evitando as armadilhas que surgem ao se fazer essas coisas problemáticas casualmente.

Deixe-me mostrar-lhe algo:

f n (x)

Essa é uma função. Na verdade, é um continuum de funções:

f 1 (x)
f 2 (x),
...
f n (x)

Adivinha como expressamos isso nas línguas OOP?

n.f(x)

Esse pouco nescolhe qual implementação fé usada E decide quais são algumas das constantes usadas nessa função (que significa francamente a mesma coisa). Por exemplo:

f 1 (x) = x + 1
f 2 (x) = x + 2

É a mesma coisa que os fechamentos fornecem. Onde os fechamentos se referem ao seu escopo, os métodos de objeto se referem ao estado da instância. Objetos podem fazer fechamentos um melhor. Um fechamento é uma única função retornada de outra função. Um construtor retorna uma referência a um pacote inteiro de funções:

g 1 (x) = x 2 + 1
g 2 (x) = x 2 + 2

Sim, você adivinhou:

n.g(x)

feg são funções que mudam juntas e se movem juntas. Então, colocamos na mesma bolsa. Isto é o que realmente é um objeto. Manter nconstante (imutável) significa apenas que é mais fácil prever o que eles farão quando você os chamar.

Agora isso é apenas a estrutura. A maneira como penso sobre OOP é um monte de coisinhas que falam com outras coisinhas. Esperemos que um pequeno grupo seleto de pequenas coisas. Quando codifico, imagino-me como o objeto. Eu olho as coisas do ponto de vista do objeto. E tento ser preguiçoso para não trabalhar demais no objeto. Recebo mensagens simples, trabalho nelas um pouco e envio mensagens simples apenas para meus melhores amigos. Quando termino com esse objeto, pulo para outro e vejo as coisas da perspectiva dele.

Os cartões de responsabilidade de classe foram os primeiros a me ensinar a pensar dessa maneira. Cara, eu estava confuso sobre eles naquela época, mas caramba, se eles ainda não são relevantes hoje.

Vamos ter um ChessBoard como uma coleção imutável de peças de xadrez imutáveis ​​(estendendo a classe abstrata Piece). Do ponto de vista do POO, uma peça é responsável por gerar movimentos válidos a partir de sua posição no tabuleiro. Mas, para gerar os movimentos, a peça precisa de uma referência ao seu tabuleiro, enquanto o tabuleiro precisa ter referência às suas peças.

Arg! Novamente com as desnecessárias referências circulares.

Que tal: A ChessBoardDataStructuretransforma cabos xy em referências de peças. Essas peças têm um método que pega x, ye um particular ChessBoardDataStructuree o transforma em uma coleção de novos tipos de ChessBoardDataStructures. Em seguida, enfia isso em algo que pode escolher a melhor jogada. Agora ChessBoardDataStructurepode ser imutável e as peças também. Desta forma, você só tem um peão branco na memória. Existem apenas várias referências a ele nos locais xy certos. Orientado a objetos, funcional e imutável.

Espere, já não conversamos sobre xadrez?



1
E por último mas não menos importante, o Tratado de Orlando .
Jörg W Mittag

1
@Euphoric: Eu acho que é uma formulação bastante padrão que corresponde às definições padrão para "programação funcional", "programação imperativa", "programação lógica" etc. Caso contrário, C é uma linguagem funcional porque você pode codificar FP nela, uma linguagem lógica, porque você pode codificar programação lógica nela, uma linguagem dinâmica, porque você pode codificar digitação dinâmica nela, uma linguagem de ator, porque você pode codificar um sistema de ator e assim por diante. E Haskell é maior linguagem imperativa do mundo desde que você pode realmente nomear efeitos colaterais, armazená-los em variáveis, passá-los como ...
Jörg W Mittag

1
Penso que podemos usar idéias semelhantes às do artigo genial de Matthias Felleisen sobre expressividade da linguagem. Mover um programa OO de, digamos, Java para C♯ pode ser feito apenas com transformações locais, mas movê-lo para C requer uma reestruturação global (basicamente, você precisa introduzir um mecanismo de envio de mensagens e redirecionar todas as chamadas de funções por ele), portanto Java e C♯ podem expressar OO e C pode "apenas" codificá-lo.
Jörg W Mittag

1
@lishaak Porque você está especificando para coisas diferentes. Isso o mantém em um nível de abstração e evita a duplicação do armazenamento de informações posicionais que, de outra forma, poderiam se tornar inconsistentes. Se você simplesmente se ressentir da digitação extra, cole-a em um método para digitar apenas uma vez. Uma peça não precisa se lembrar de onde está, se você disser onde está. Agora, as peças armazenam apenas informações como cores, movimentos e imagem / abreviação, sempre verdadeiras e imutáveis.
candied_orange

2

Os conceitos mais úteis introduzidos no mainstream pelo OOP, na minha opinião, são:

  • Modularização adequada.
  • Encapsulamento de dados.
  • Separação clara entre interfaces públicas e privadas.
  • Mecanismos claros para extensibilidade de código.

Todos esses benefícios também podem ser obtidos sem os detalhes tradicionais da implementação, como herança ou mesmo classes. A idéia original de Alan Kay de um "sistema orientado a objetos" usava "mensagens" em vez de "métodos" e estava mais próxima de Erlang do que, por exemplo, em C ++. Veja o Go, que elimina muitos detalhes tradicionais da implementação de OOP, mas ainda parece razoavelmente orientado a objetos.

Se você usar objetos imutáveis, ainda poderá usar a maioria dos presentes tradicionais de OOP: interfaces, envio dinâmico, encapsulamento. Você não precisa de setters, e muitas vezes nem precisa de getters para objetos mais simples. Você também pode colher os benefícios da imutabilidade: sempre tem certeza de que um objeto não mudou nesse meio tempo, nenhuma cópia defensiva, nenhuma corrida de dados e é fácil tornar os métodos puros.

Veja como Scala tenta combinar imutabilidade e abordagens de FP com OOP. É certo que não é a linguagem mais simples e elegante. É razoavelmente praticamente bem-sucedido, no entanto. Além disso, dê uma olhada no Kotlin, que fornece muitas ferramentas e abordagens para um mix semelhante.

O problema usual de tentar uma abordagem diferente da que os criadores de idiomas tinham em mente N anos atrás é a "incompatibilidade de impedâncias" da biblioteca padrão. Os ecossistemas Java e .NET da OTOH atualmente têm um suporte razoável da biblioteca padrão para estruturas de dados imutáveis; é claro que também existem bibliotecas de terceiros.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.