Quão necessário é seguir práticas de programação defensivas para códigos que nunca serão disponibilizados publicamente?

45

Estou escrevendo uma implementação Java de um jogo de cartas, então criei um tipo especial de Coleção que estou chamando de Zona. Todos os métodos de modificação da Coleção do Java não são suportados, mas há um método na API de Zona, move(Zone, Card) , que move uma Placa da Zona em questão para si mesma (realizada por técnicas privadas de pacote). Dessa forma, posso garantir que nenhum cartão seja retirado de uma zona e simplesmente desaparecer; eles só podem ser movidos para outra zona.

Minha pergunta é: quão necessário é esse tipo de codificação defensiva? É "correto" e parece ser a prática correta, mas não é como se a API de regiões fosse fazer parte de alguma biblioteca pública. É só para mim, então é como se eu estivesse protegendo meu código de mim mesmo quando provavelmente poderia ser mais eficiente usando apenas coleções padrão.

Até onde devo levar esta ideia da Zona? Alguém pode me dar algum conselho sobre o quanto eu deveria pensar em preservar os contratos nas aulas que escrevo, especialmente para aqueles que não estão realmente disponíveis publicamente?

    
por codebreaker 04.12.2013 / 20:14
fonte

6 respostas

72

Não vou abordar o problema de design - apenas a questão de fazer as coisas "corretamente" em uma API não pública.

it's just for me, so it's kind of like I'm protecting my own code from myself

Esse é exatamente o ponto. Talvez haja programadores por aí que lembrem as nuances de todas as classes e métodos que já escreveram e nunca os chamem erroneamente com o contrato errado. Eu não sou um deles. Muitas vezes esqueço como o código que escrevi deve funcionar poucas horas depois de escrevê-lo. Depois que você achar que acertou uma vez, sua mente tenderá a mudar as marchas para o problema em que você está trabalhando agora .

Você tem ferramentas para combater isso. Essas ferramentas incluem convenções (sem nenhuma ordem específica), testes de unidade e outros testes automatizados, verificação de pré-condição e documentação. Eu mesmo achei os testes de unidade inestimáveis porque eles forçaram você a pensar em como seu contrato será usado e fornecer documentação mais tarde sobre como a interface foi projetada.

    
por 04.12.2013 / 21:30
fonte
25

Eu costumo seguir algumas regras simples:

  • Tente sempre programar por contrato .
  • Se um método estiver disponível publicamente ou receber informações do mundo externo , aplique algumas medidas defensivas (por exemplo, IllegalArgumentException ).
  • Para tudo o mais que só é acessível internamente, use asserções (por exemplo, assert input != null ).

Se um cliente estiver realmente envolvido, sempre encontrará uma maneira de fazer com que seu código se comporte mal. Eles sempre podem fazer isso através da reflexão, pelo menos. Mas essa é a beleza do design por contrato . Você não aprova o uso desse código e, portanto, não pode garantir que ele funcionará nesses cenários.

Quanto ao seu caso específico, se Zone não for usado e / ou acessado por pessoas de fora, torne a classe package-private (e possivelmente final ) ou, de preferência, use as coleções Java já fornece a você. Eles são testados e você não precisa reinventar a roda. Observe que isso não impede que você use asserções em todo o código para garantir que tudo funcione como esperado.

    
por 04.12.2013 / 21:22
fonte
16

A programação defensiva é uma coisa muito boa.
Até começar a atrapalhar a escrita do código. Então não é uma coisa tão boa.

Falando um pouco mais pragmaticamente ...

Parece que você está no limite de levar as coisas longe demais. O desafio (e a resposta à sua pergunta) está em entender quais são as regras ou requisitos do programa.

Usando sua API de jogos de cartas como exemplo, há alguns ambientes em que tudo o que pode ser feito para evitar fraudes é fundamental. Grandes quantias de dinheiro real podem estar envolvidas, por isso, faz sentido colocar um grande número de verificações no local para garantir que a fraude não ocorra.

Por outro lado, você precisa permanecer atento aos princípios do SOLID, especialmente a responsabilidade única. Pedir à classe contêiner para efetivamente auditar onde os cartões estão indo pode ser um pouco demais. Talvez seja melhor ter uma camada de auditoria / controlador entre o contêiner de cartões e a função que recebe as solicitações de movimentação.

Relacionado a essas preocupações, você precisa entender quais componentes de sua API estão expostos publicamente (e, portanto, vulneráveis) versus o que é privado e menos exposto. Eu não sou um defensor total de um "revestimento exterior duro com um interior macio", mas o melhor retorno de seu esforço é endurecer o exterior da sua API.

Eu não acho que o usuário final pretendido de uma biblioteca seja tão crítico quanto a determinação de quanto de programação defensiva você coloca em prática. Mesmo com módulos que eu escrevo para meu próprio uso, eu ainda coloco uma medida de verificação no lugar para ter certeza de que o futuro eu não cometeu algum erro inadvertido ao chamar a biblioteca.

    
por 04.12.2013 / 21:32
fonte
13

Codificação defensiva não é apenas uma boa ideia para código público. É uma ótima idéia para qualquer código que não seja imediatamente descartado. Claro, você sabe como deve ser chamado agora , mas você não tem ideia de como você vai se lembrar disso daqui a seis meses quando voltar ao projeto.

A sintaxe básica do Java oferece muita defesa em comparação com uma linguagem interpretada de nível inferior, como C ou Javascript, respectivamente. Supondo que você nomeie seus métodos com clareza e não tenha "sequenciamento de método" externo, provavelmente conseguirá simplesmente especificar argumentos como um tipo de dados correto e incluir um comportamento sensato se os dados corretamente definidos ainda puderem ser inválidos.

(Deixando de lado, se as Cartas sempre tiverem que ser na zona, eu acho que você fica melhor com todas as cartas em jogo referenciadas por uma coleção global para o seu objeto de jogo, e ter Zone be uma propriedade de cada cartão, mas como não sei o que suas Zonas fazem além de ter cartões, é difícil saber se isso é apropriado.

    
por 04.12.2013 / 21:32
fonte
1

Primeiro, crie uma classe que mantenha uma lista de zonas para que você não perca uma zona ou as cartas nela. Você pode então verificar se uma transferência está dentro de sua ZoneList. Esta classe provavelmente será uma espécie de singleton, já que você só precisará de uma instância, mas você pode querer conjuntos de Zones depois, então mantenha suas opções abertas.

Em segundo lugar, não faça Zone ou ZoneList implementar a Coleção ou qualquer outra coisa, a menos que você precise dela. Ou seja, se uma Zone ou ZoneList for passada para algo que espera uma coleção, então a implementa. Você pode desabilitar vários métodos fazendo com que eles lançem uma exceção (UnimplementedException, ou algo parecido) ou simplesmente fazer com que eles não façam nada. (Pense bem antes de usar a segunda opção. Se você fizer isso porque é fácil, você perceberá que está perdendo bugs que você poderia ter pego no início.)

Existem questões reais sobre o que é "correto". Mas uma vez que você descubra o que é, você vai querer fazer as coisas dessa maneira. Em dois anos você terá esquecido tudo isso, e se você tentar usar o código, ficará muito aborrecido com o cara que escreveu de maneira tão contraintuitiva e não explicou nada.

    
por 04.12.2013 / 21:20
fonte
1

A codificação defensiva no design da API geralmente é sobre a validação da entrada e a seleção cuidadosa de um mecanismo adequado de tratamento de erros. Coisas que outras respostas mencionam também são dignas de nota.

Este não é o seu exemplo. Você está limitando sua superfície da API por um motivo muito específico. Como GlenH7 menciona, quando o conjunto de cartas é para ser usado em um jogo real, com um ('usado') e 'não usado') deck, uma mesa e mãos, por exemplo, você definitivamente deseja colocar cheques no lugar para se certificar de que cada carta do jogo está presente uma vez e apenas uma vez.

Você projetou isso com "zonas", é uma escolha arbitrária. Dependendo da implementação (uma zona pode ser apenas uma mão, um deck ou uma mesa no exemplo acima), pode muito bem ser um projeto completo.

No entanto, essa implementação soa como um tipo derivado de um conjunto de cartões com mais Collection<Card> , com uma API menos restritiva. Por exemplo, quando você quer construir uma calculadora de valor de mão, ou uma IA, você certamente quer ser livre para escolher em qual e quantas cartas você irá iterar.

Por isso, é bom expor uma API tão restritiva, se o único objetivo dessa API é garantir que cada cartão esteja sempre em uma zona.

    
por 04.12.2013 / 22:09
fonte