Dados imutáveis são realmente nescessários?

Continuando a discussão do Testes de Unidade são realmente nescessários?:

Você não evitou o problema, apenas encapsulou embaixo do tapete. :wink:

Essa coisa de querer proteger os dados, nada mais é do que uma consequência direta da mutabilidade irrestrita. Nada de ruim pode acontecer com dados imutáveis, por isso não precisam ser “protegidos” e podem ser compartilhados livremente.

Mas no mundo real o que importa é justamente o custo, e não a teoria do que é possível de ser feito com OO.

com testes escritos como devem ser.

Como falei na outra thread. Teste unitário é fundamental, mas nem sempre é possível ou viável em todo o sistema. TDD por outro lado, não deve ser algo imposto, quem gostar usa. Minha impressão é que programadores experientes em linguagens funcionais, como Erlang e Clojure, já tem imutabilidade pra ajudar a criação de software com design correto e incremental, portanto eles não precisariam de TDD.

2 curtidas

Desde que o objeto esteja bem encapsulado e testado, por quê não daria pra ser compartilhado, sendo que uma das principais premissas da OOP é reuso?

De fato, muitos patterns conhecidos em OOP acabam por ser desnecessários em linguagem funcional por motivos óbvios, mas evitar o estado a todo custo também tem seu custo em alguns cenários: muitos sistemas são feitos em OOP devido ao paradigma facilitar a abstração dos problemas, facilitar reuso, definição de tipos, etc. Sem contar que muitas equipes têm profissionais com experiência em OOP apenas, então não adianta tentar socar o paradigma funcional (de uma hora pra outra) numa equipe assim que vai ficar uma zona e mais caro no final.

Contudo, entretanto, todavia :), acho fantástico o paradigma funcional e a forma como resolvo problemas que seriam bem mais ardilosos utilizando OOP. Alguns projetos que tenho começado recentemente, tenho feito com Erlang/Elixir e até o momento não sinto falta alguma de OOP.

TDD não deve jamais ser imposto, alguns naquela thread apenas colocaram a experiência desenvolvendo TDD, como este contribuiu para diminuir custos e também atingir bons resultados de forma iterativa. Por isso que quando existe uma vaga pra desenvolvedor, muitas empresas colocam se fazem TDD ou não, desta forma o candidato fica livre pra escolher.

Mas assim como você diz que TDD não deve ser algo imposto, e eu concordo, programação funcional também não deve ser imposta e vendida aos 4 ventos como a solução para todas as coisas.
Eu uso TDD mesmo quando estou fazendo coisas em Elixir ou Clojure, pois TDD é um mindset que me ajuda a guiar o design do software, não importa se é imperativo, funcional, lógico, etc.

Resumindo: eu acho que nenhum paradigma é a prova de programador. Quando fazemos uma coisa mal feita num paradigma, nada nos impede de continuar fazendo mal feito em outro paradigma. A coisa mal feita continua, só muda de padrão :slight_smile:

Se o programador é forçado a passar objetos como parâmetros em métodos, ele estará passando dados irrelevantes junto com dados necessarios para a função funcionar. Todo esse ocultamento e dados excessivos só torna mais difícil saber o que exatamente o método faz, sem falar que devido a herança, métodos e dados ficam espalhados em diferentes classes, dificultando o raciocínio (e o teste) das invariantes do objeto. Por tudo isso, na minha opinião encapsulamento destrói os benefícios da transparência referencial.

como foi falado na outra thread, front-end não precisa evitar estado mutável a todo custo. Mas software critico geralmente sim.

Sim, só pra software critico. Mas programadores OO também podem se beneficiar de aprender uma linguagem funcional visto que mesmo em linguagens sem transparência referencial, eles podem adotar um estilo de programação que evita estado mutável escondido para criar apps front-end ainda melhores.

Interessante. Eu mesmo não vejo desenvolvedores Erlang e Clojure falando de TDD. Mas pensando bem, o que eles falariam. Se eu uso uma biblioteca de terceiro, não me importa se o desenvolvedor usou TDD, só que tenha testes e estão passando.

1 curtida

Pelo que entendo, transparência referencial não está ligada apenas a linguagens funcionais. Nestas, devido à natureza da imutabilidade, é mais natural que se desenvolvam propriedades de transparência referencial, abrindo assim portas para as chamadas “funções puras”.

Mas isto não é impossível em OOP, pois nada impede-me de criar uma função:

int sumThree(int number) {
    return number + 3;
}

Se eu chamar sumThree(3) diversas vezes sempre me retorna 6. Entendo que isto é a propriedade de transparência referencial, certo?

Acho que perde-se a transparência referencial quando dentro da função temos acesso a escopo externo, global, então daí não temos mais controle algum do que pode acontecer, como você bem disse. Mesmo em linguagens funcionais, como Haskell, é permitido ter partes onde se tem mutabilidade, embora no Haskell isso é bem definido separando código mutável de imutável.

Escopo global tem de ser evitado quase sempre (uns dirão que sempre!), cabendo um estudo do que se vai desenvolver e decidir quando abrir mão de tal prática.

Quanto ao encapsulamento, não me referi apenas à forma naive de “getters and setters”, mas encapsulamento de objeto e estado, garantindo que o estado é gerenciável apenas dentro de um objeto. Um exemplo disso é o React com Redux, onde você tem componentes que são declarativos e um “grande objeto” que controla estado, responsável por gerenciar o estado a aplicar mudanças. Posso estar exagerando, mas ao meu entender, vejo que esse é um exemplo do “mix” entre OO e funcional.

E sobre herança, acho que é bem divulgado na comunidade que devíamos ao invés sempre favorecer composição, pois facilita testes e track do estado. Herança é outra coisa que tem de ser usada com muita cautela mesmo, com exceção em cenários como por exemplo extensibilidade, que é uma das propriedades da OOP (mas mesmo assim tento evitar herança ao máximo).

De novo, acho (no meu limitado conhecimento) que transparência referencial não é propriedade de uma linguagem ou paradigma específico apenas, mas se torna difícil garantir a mesma em linguagem OO se utilizarmos mutabilidade a rodo, escopo global e herança. Podem existir N casos que a falta de transparência referencial não será de todo um problema, é questão de avaliar.

E concordo contigo, paradigma funcional aplicado em linguagens imperativas com certeza traz muitos benefícios que ajudam a criar apps mais robustas.


Sobre a pergunta da thread: não sei onde você quer chegar exatamente, mas não vejo pq querer evitar mutabilidade a todo custo mesmo em sistemas críticos, sendo que muitas linguagens funcionais que não são 100% puras também têm sem quesito mutável quando necessário. De uma forma ou de outra, sistemas de qualquer categoria (backend, frontend, crítico, enterprise, não-enterprise, etc) vão precisar de estado, mesmo que seja na base de dados. Se puder explanar isso melhor, agradeço pq realmente não vejo como evitar a mutabilidade da forma como você tem argumentado.

Não sou experiente em Clojure, e me parece que a comunidade não investe mesmo em testes (digo pelos poucos projetos que vi). Não sei se pelo fato da S-expression já ajudar em algo pelo fato de validar conjunto de expressões, enfim…
Já em Elixir, vejo que a comunidade investe e clama muito por testes e principalmente TDD, visto que os doctests existem pra ajudar a escrever testes de forma incremental enquanto você documenta o código. Mas, de novo, isso sou eu. Não sou pago pra empurrar TDD no site do GUJ, portanto acho que nisto já estamos conversados e eu entendo seu ponto sobre testes :slight_smile:

Sim, mas um compilador não pode substituir um pelo outro, como poderia numa linguagem que garante TR. Vai que dentro do método tem algum código que altera o estado de alguma variável de instância do objeto?

Eu acho que não é possível ter OO com apenas um objeto né? :slight_smile:

Encapsulamento ajuda o programador a garantir as invariantes de um objeto, porque vc pode regular todo o acesso ao estado do objeto através de seus métodos. Composição de objetos não me ajuda porque é difícil garantir invariantes quando elas envolvem mais de 1 objeto.

Se for uma propriedade da linguagem, isso significa que o compilador pode afirmar algumas coisas sobre seu código, ex: substituir uma função pelo valor direto, pode rodar em paralelo, etc. Se a linguagem não garante TR isso não tem como.

Porque não? Estado mutavel é a razão para a maior parte do software hoje em dia ser tão complexo.

De uma forma ou de outra, sistemas de qualquer categoria (backend, frontend, crítico, enterprise, não-enterprise, etc) vão precisar de estado, mesmo que seja na base de dados. Se puder explanar isso melhor, agradeço pq realmente não vejo como evitar a mutabilidade da forma como você tem argumentado.

O problema não é estado, e sim estado mutável.

Todo projeto em Clojure que uso possui testes unitários. Mas TDD não é muito usado em linguagens funcionais, e pra explicar porque, ninguém melhor do que um especialista em TDD:

When we use TDD to develop a tell-don’t-ask system, we start at the high level and write tests using mocks to make sure we are issuing the correct “tells”. We proceed from the top of the system to the bottom of the system. The last tests we write are for the utilities at the very bottom.

In an ask-don’t-tell system, data starts at the bottom and flows upwards. The operation of each function depends on the data fed to it by the lower level functions. There is no mocking framework. So we write tests that start at the bottom, and we work our way up the the top.

Ou seja, não exatamente por ser S-expressions, mas pelo fato de Lisp ser uma linguagem funcional (isto é, as expressões são avaliadas de baixo pra cima, com a saída de cada função servindo de entrada para a próxima), o processo de desenvolvimento já é incremental e bottom-up por natureza.

Cara, lê o artigo até o fim. :smiley:

Is TDD necessary in Clojure?
If you follow the code in the Orbit example, you’ll note that I wrote tests for all the computations, but did not write tests for the Swing-Gui. This is typical of the way that I work. I try to test all business rules, but I “fiddle” with the GUI until I like it.

If you look carefully you’ll find that amidst the GUI functions there are some “presentation” functions that could have been tested, but that I neglected to write with TDD[2]. These functions were the worst to get working. I continuously encountered NPEs and Illegal Cast exceptions while trying to get them to work.

My conclusion is that Clojure without TDD is just as much a nightmare as Java or Ruby without TDD.

Meu ponto é que o mindset TDD é diferente dependendo do paradigma usado. Como o especialista disse, TDD em linguagens OO é o oposto de linguagens funcionais.

Sobre clojure sem TDD ser um pesadelo, é a opinião pessoal dele. O uncle bob é especialista em TDD, não é autoridade em clojure. Em 2010, quando o artigo foi escrito, ele ainda estava começando na linguagem. :wink:

No meu caso ajuda na maioria das vezes. Desde que meus objetos estejam bem testados, consigo eliminar essa invariância uma vez que utilizando injeção de dependências meus componentes são testados isoladamente com sua própria API.

Minha questão não é o compilador, mas a “técnica” de transparência referencial não é encontrada apenas em linguagens funcionais. De acordo com Wikipedia:

An expression e is referentially transparent if and only if e can be replaced with its evaluated result without affecting the behavior of the program.

Diversas linguagens modernas utilizam técnicas como memoization, lazy evaluation para otimizar calls a essas “funções puras”, mas meu ponto não é nem este. Afinal, como você mesmo disse dos benefícios que o paradigma funcional poderia trazer para OO, esse é um deles, aplicando TR de forma a deixar os componentes idempotentes.

Exatamente, era isso o que eu queria dizer.

Esse artigo é interessante, eu li ele alguns anos atrás antes mesmo de mergulhar em linguagens funcionais e quando comecei a fazer TDD em linguagens funcionais, fiquei feliz por saber que não estava sozinho, pois no final ele mesmo afirma que Clojure sem TDD é um pesadelo, assim como em outras linguagens.

O mindset TDD é sempre o mesmo. O que muda são as técnicas, o fluxo, e mesmo sendo tão antigo o artigo como é, hoje em dia existem bibliotecas em diversas linguagens funcionais que facilitam o teste top-down, como exemplo esta que encontrei em Clojure.

Em Elixir, por exemplo, e eu acho que Elixir e Clojure até têm lá suas semelhanças, é possível fazer mock testing com libs externas, mas o mais comum e recomendado é utilizando a própria feature de behaviours.


De novo, não quero eternizar isto dizendo o que é melhor ou pior. Apenas entender o motivo do argumento de que com OO é impossível ter coisa bem feita, idempotente, isolada e escalável para software crítico. Diversas linguagens OO carregam features funcionais, como lazy evaluation, map, reduce, pipes, etc o que permitem um desenvolvimento “declarativo” quando necessário.

Ou seja, a opinião do especialista é válida contanto que reforce suas convicções pessoais! Fantástico! :smiley:

Sobre os tais sistemas críticos feitos em OO…

Se estamos falando de sistemas onde uma falha pode acarretar perda massiva de vidas humanas, como sistemas de defesa e controle de usinas, então existem restrições na indústria sobre o uso de linguagens com alocação dinâmica de memória e garbage collectors. O que inclui Java em sua forma tradicional e praticamente TODAS as linguagens funcionais, inclusive Clojure. A maior parte desses sistemas é feita em C ou ADA (nenhuma relação com gangs). Ambas as linguagens não exigem imutabilidade, nem possuem qualquer facilidade nesse sentido. Por outro lado, ADA possui um vasto ecossistema de ferramentas para provar código, análise estática, etc.
Veja que nos manuais da NASA e do DoD não há nada que mencione transparência referencial como um pré-requisito suficiente nem necessário para a construção dessa classe de sistemas. A programação funcional sequer é considerada. O consenso na indústria ainda é que o método de construção é o fator primordial de qualidade em sistemas críticos e não a linguagem de programação. Eles estão satisfeitos programando estruturado em linguagens imperativas…

Eliminar invariância?

Invariantes

Por que você quer eliminar as invariantes do objeto??? Você tem que testar elas, não eliminar.

Estou falando do problema que é testar as invariantes que envolvem mais de 1 objeto. Se vc testa os objetos isoladamente, não testa invariantes que envolvem mais de 1 objeto. Então você mudou o problema.

Esse trecho não diz nada sobre TR ser encontrado em linguagens não-funcionais.

Mas eu entendo o que vc quer dizer, que TR é uma boa prática que o usuário de qualquer linguagem pode seguir. Mas ela não é característica de nenhuma linguagem OO. Programadores OO em geral nem sabem o que é TR. :slight_smile:

A questão não é o número de técnicas que a linguagem disponibiliza, senão Scala seria um sucesso. Nenhuma linguagem não funcional é comum programação lazy. Simplesmente pq não há sinergia entre ter estado mutável e lazy evaluation. O inventor da linguagem pode até disponibilizar a técnica, mas é só isso mesmo.

Os beneficios do paradigma funcional para OO são limitados uma vez que depende da competência e disciplina de toda a equipe pra estar constantemente lembrando de evitar estado mutável. Basta alguém anular o seu método sumThree numa subclasse de maneira errada, e acabou a TR que você achou que tinha.

Engraçado porque não tem motivo nenhum pra hoje, os banco de dados serem mutáveis, isso é do tempo em que armazenamento era recurso escasso. Hoje em dia armazenamento é infinito na nuvem, as pessoas nem tem mais disquetes e cd-roms nos computadores. :slight_smile:

Não é clojure sem TDD que é um pesadelo, mas sim clojure sem programação bottom-up. TDD é programação top-down.

Palavras tem significado. Se tudo muda e continua sendo TDD, eu suspeito que o termo (TDD) deixou de ter valor prático e talvez precisamos de um novo termo que possa melhor capturar a essência do que esta sendo comunicado?

Nenhum momento eu disse ser impossível, apenas dificil e portanto improvável dado a realidade do mercado. Mas com uma combinação de programadores ninja e rock stars, de preferencia com experiência em linguagens funcionais, tudo é possível. :wink:

Desde quando orientação cima - baixo é uma convicção minha? O próprio texto fala em top-down e bottom-up, que são coisas diferentes. Se você quer chamar coisas diferentes do mesmo nome porque o especialista disse, fique à vontade.

Você sabe que nem C nem ADA são orientadas a objetos né? :grin:

Poderia simplesmente ter dito que não tem sistemas criticos em OO, só relacional, funcional e estruturado.

Verdade, interpretei mal a tua primeira frase quando você disse que encapsulamento ajuda a garantir as invariantes de um objeto. Viajei total quando me referi ao termo, obrigado pelo toque.

ADA possui OO há mais de 10 anos. O DoD investigou a possibilidade de adotar OO em projetos de defesa. As linguagens funcionais sofrem da mesma restrição que as OO (alocação dinâmica, binding dinâmico). Mas isso na verdade não te interessa. O que te interessa é papaguear o que você leu no blog da esquina, como se fossem suas idéias…

1 curtida

Por favor, nos poupe dessa papagaiada de NASA e DoD. Você ainda não respondeu porque governos, corporações, bancos, telecoms, etc. só usam OO pra fazer CRUD. Na hora do vamos ver, é relacional ou funcional mesmo.

Eu só queria saber quem é o grande nome do OO? Assim como os nomes Oracle, Ericson e Neymar estão para o paradigma relacional, funcional e a seleção brasileira. Da pra ser ou tá difícil?

No DDD, a entidade que é o aggregate root é responsável por garantir as invariantes, mas ela não pode conter outras entidades, apenas value objects. Isso pq se existir uma entidade dentro do aggregate root que é root em outro agregado, não teria como garantir todas as invariantes dos dois agregados em conjunto. Então um dos aggregate roots teria que ser “desligado”? Enfim, parece complicado usar composição nesse caso.

De onde você tirou essa informação? Simplesmente, não corresponde à realidade do mercado. Eu tenho conhecimento direto de dezenas de sistemas bancários feitos em Java, incluindo conta corrente, análise de risco, internet banking. Em telecom, conheço plataformas de OSS e BSS, Billing, Charging e Inventário feitas em Java e/ou C++.

Confesso que nunca encontrei em campo um sistema feito todo em Lisp, Erlang ou afins. E olha que eu trabalhei na empresa que você citou (mas errou o nome).

Na verdade, a maior parte das plataformas complexas envolve mais de uma linguagem…

Você está falando sem ter a experiência real. Saia para o mundo e olhe a verdade.

Não faz sentido a mesma Entidade ser parte de dois Aggregate Roots. Seria um design errado.

Conhece dezenas mas não pode citar um grande nome da OO?

:sleeping::sleeping::sleeping::sleeping:

Só 1 vamos lá, vc consegue.