O que há de errado com a validação de dados e o que o princípio de substituição de Liskov tem a ver com isso?



Se às vezes você se perguntar: "tudo é bom para mim nesse método?" E escolha entre "o que acontecerá se" e "é melhor verificar apenas por precaução"? Bem-vindo ao ...

Correção: Como observaram lorc e 0xd34df00d , o que é discutido abaixo é chamado de tipos dependentes. Você pode ler sobre eles aqui . Bem, abaixo está o texto fonte com meus pensamentos sobre isso.

Durante o desenvolvimento, geralmente é necessário verificar a validade dos dados para algum algoritmo. Formalmente, isso pode ser descrito da seguinte forma: suponha que obtemos uma certa estrutura de dados, verificamos seu valor quanto à conformidade com um determinado intervalo de valores permitidos (ODZ) e a transmitimos. Posteriormente, a mesma estrutura de dados pode ser submetida à mesma verificação. Se a estrutura permanecer inalterada, verificar novamente sua validade é obviamente uma ação desnecessária.

Embora a validação possa realmente ser longa, o problema não está apenas no desempenho. Muito mais desagradável é a responsabilidade extra. O desenvolvedor não tem certeza se é necessário verificar novamente a estrutura quanto à validade. Além da verificação desnecessária, pelo contrário, podemos assumir a ausência de qualquer verificação, assumindo incorretamente que a estrutura foi verificada anteriormente.

Portanto, defeitos são permitidos em métodos que esperam uma estrutura comprovada e não funcionam corretamente com uma estrutura cujo valor está fora de um determinado intervalo de valores aceitáveis.

Aí reside um problema mais profundo e não óbvio. De fato, uma estrutura de dados válida é um subtipo da estrutura original. Desse ponto de vista, o problema com um método que aceita apenas objetos válidos é equivalente ao seguinte código em um idioma fictício:

class Parent { ... } class Child : Parent { ... } ... void processValidObject(Parent parent) { if (parent is Child) { // process } else { // error } } 

Concorde que agora o problema está muito mais claro. Diante de nós está uma violação canônica do princípio da substituição de Liskov. Leia por que violar o princípio da substituição é ruim, por exemplo, aqui .

O problema de transferência de objetos inválidos pode ser resolvido criando um subtipo para a estrutura de dados original. Por exemplo, você pode criar objetos por meio de uma fábrica que retorne um objeto de subtipo válido ou nulo de acordo com a estrutura original. Se alterarmos a assinatura dos métodos que esperam uma estrutura válida para que eles aceitem apenas um subtipo, o problema desaparecerá. Além da confiança de que o sistema funciona exatamente, o número de validações por centímetro quadrado de código diminuirá. Outra vantagem é que, com essas ações, transferimos a responsabilidade de validar dados do desenvolvedor para o compilador.

No Swift, no nível da sintaxe, o problema de verificar nulo é resolvido. A idéia é separar tipos em tipos anuláveis ​​e não anuláveis. Ao mesmo tempo, é fabricado na forma de açúcar, de forma que o programador não precise declarar um novo tipo. Ao declarar um tipo de variável, ClassName garante que a variável seja diferente de zero e ao declarar ClassName? a variável é nula. Ao mesmo tempo, existe covariância entre tipos, ou seja, você pode passar um objeto do tipo ClassName para métodos que aceitam ClassName?

Essa ideia pode ser expandida para o DLD definido pelo usuário. O fornecimento de objetos com metadados contendo DLD armazenados no tipo eliminará os problemas descritos acima. Seria bom obter suporte para essa ferramenta em uma linguagem, mas esse comportamento também é implementado em linguagens OO "comuns", como Java ou C #, usando herança e uma fábrica.

A situação com a validação de dados é outra confirmação de que as entidades no OOP não são retiradas do mundo real, mas de necessidades de engenharia.

UPD: conforme observado corretamente nos comentários, vale a pena criar subtipos apenas se obtivermos confiabilidade adicional e reduzirmos o número de validações idênticas.

Além disso, o artigo não possui um exemplo. Deixe alguns caminhos de arquivo chegar até nós na entrada. Em alguns casos, nosso sistema funciona com todos os arquivos e, em alguns casos, apenas com arquivos aos quais temos acesso. Em seguida, queremos transferi-los para diferentes subsistemas, que também funcionam com arquivos acessíveis e inacessíveis. Além disso, esses subsistemas transferem arquivos ainda mais, onde novamente não está claro se o arquivo está disponível ou não. Assim, em qualquer lugar duvidoso, uma verificação de acesso aparecerá ou poderá ser esquecida pelo contrário. Por esse motivo, o sistema se tornará mais complicado devido a ambigüidades e verificações generalizadas. Mas essas verificações carregam o disco e geralmente são pesadas. Você pode armazenar esta verificação em cache em um campo booleano, mas isso não nos salvará do fato da necessidade de verificação. Sugiro mudar a responsabilidade de verificar do desenvolvedor para o compilador.

Source: https://habr.com/ru/post/pt476220/


All Articles