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

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 do Java's Collection não são suportados, mas há um método na API da região move(Zone, Card), que move um cartão da região em questão para si (realizado por técnicas de pacote privado). Dessa forma, posso garantir que nenhum cartão seja retirado de uma zona e simplesmente desapareça; eles só podem ser movidos para outra zona.

Minha pergunta é: quão necessário é esse tipo de codificação defensiva? É "correto" e parece a prática correta, mas não é como se a API da Zona fizesse parte de alguma biblioteca pública. É só para mim, é 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 essa ideia da Zona? Alguém pode me dar algum conselho sobre o quanto devo pensar em preservar os contratos nas aulas que escrevo, especialmente para aqueles que realmente não estarão disponíveis ao público?

quebra-código
fonte
4
= ~ s / necessário / recomendado / gi
GrandmasterB /
2
Os tipos de dados devem estar corretos por construção, ou então no que você está construindo? Eles devem ser encapsulados de tal maneira que, mutáveis ​​ou não, eles só possam estar em estados válidos. Somente se for impossível aplicar isso estaticamente (ou excessivamente difícil) você deve gerar um erro de tempo de execução.
quer
1
Nunca diga nunca. A menos que seu código nunca seja usado, você nunca poderá saber com certeza onde seu código será finalizado. ;)
Izkata
1
O comentário de @codebreaker GrandmasterB é uma expressão de substituição. Significa: substitua "necessário" por "recomendado".
Ricardo Souza
1
O Código Codeless nº 116, Confiar em Ninguém, provavelmente é particularmente apropriado aqui.

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.

é só para mim, é como se eu estivesse protegendo meu próprio código de mim mesmo

Esse é exatamente o ponto. Talvez existam programadores que se lembram das nuances de todas as classes e métodos que eles escreveram e nunca os chamam erroneamente com o contrato errado. Eu não sou um deles. Costumo esquecer como o código que escrevi deve funcionar poucas horas depois de escrevê-lo. Depois de pensar que acertou uma vez, sua mente tenderá a mudar de direção para o problema no qual está trabalhando agora .

Você tem ferramentas para combater isso. Essas ferramentas incluem (em nenhuma ordem específica) convenções, 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 ambos o forçam a pensar em como o seu contrato será usado e fornecem a documentação posteriormente sobre como a interface foi projetada.

Michael K
fonte
Bom saber. No passado, eu costumava programar da maneira mais eficiente possível, então às vezes tenho dificuldade em me acostumar com idéias como essa. Estou feliz por estar indo na direção certa.
Codebreaker
15
"Eficientemente" pode significar muitas coisas diferentes! Na minha experiência, os novatos (não que eu esteja dizendo que você é um) geralmente ignoram a eficiência com que eles serão capazes de apoiar o programa. O código geralmente gasta muito mais tempo na fase de suporte do ciclo de vida do produto do que na fase "escrever novo código", então acho que essa é uma eficiência que deve ser considerada com cuidado.
precisa
2
Eu definitivamente concordo. De volta à faculdade, eu nunca tive que pensar sobre isso.
Codebreaker
25

Eu costumo seguir algumas regras simples:

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

Se um cliente estiver realmente interessado nisso, sempre encontrará uma maneira de fazer com que seu código se comporte mal. Eles sempre podem fazê-lo através da reflexão, pelo menos. Mas essa é a beleza do design por contrato . Você não aprova esse uso do seu código e, portanto, não pode garantir que ele funcione em tais cenários.

Quanto ao seu caso específico, se Zonenão deve ser usado e / ou acessado por pessoas de fora, torne a classe privada do pacote (e possivelmente final), ou preferencialmente, use as coleções que o Java já fornece. 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 conforme o esperado.

afsantos
fonte
1
+1 por mencionar Design por contrato. Se você não pode proibir completamente o comportamento (e isso é difícil de fazer), pelo menos, você deixa claro que não há garantias sobre o mau comportamento. Também gosto de lançar um IllegalStateException ou UnsupportedOperationException.
user949300
@ user949300 Claro. Eu gosto de acreditar que essas exceções foram introduzidas com um propósito significativo. Honrar contratos parece se encaixar nesse papel.
afsantos
16

A programação defensiva é uma coisa muito boa.
Até que comece 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) consiste em entender quais são as regras ou requisitos de negócios do programa.

Usando a API do jogo de cartas como exemplo, existem alguns ambientes em que tudo o que pode ser feito para evitar trapaças é crítico. Grandes quantidades de dinheiro real podem estar envolvidas; portanto, faz sentido colocar um grande número de verificações para garantir que não ocorram trapaças.

Por outro lado, você precisa manter-se atento aos princípios do SOLID, especialmente à responsabilidade única. Pedir à classe de contêiner para auditar efetivamente para 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 do cartão e a função que recebe as solicitações de movimentação.


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

Não acho que o usuário final pretendido de uma biblioteca seja tão crítico quanto uma determinação sobre a quantidade de programação defensiva que você implementa. Mesmo com os módulos que eu escrevo para meu próprio uso, ainda coloco uma medida de verificação para garantir que meu futuro não cometa algum erro inadvertido ao chamar a biblioteca.


fonte
2
+1 em "Até que comece a atrapalhar a escrita do código". Especialmente para projetos pessoais de curto prazo, a codificação defensiva pode levar muito mais tempo do que vale a pena.
Corey #
2
Concordo, embora eu gostaria de acrescentar que é uma coisa boa poder / poder / programar defensivamente, mas também é crucial poder programar de maneira prototipada. A capacidade de fazer as duas coisas permitirá que você escolha a ação mais apropriada, que é muito melhor do que muitos programadores que conheço que são capazes de apenas programar defensivamente (mais ou menos).
David Mulder
13

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

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

(De um lado, se as Cartas sempre tiverem que estar na zona, acho que você fica melhor ao fazer com que todas as cartas em jogo sejam referenciadas por uma coleção global para o seu objeto Jogo, e a Zona seja uma propriedade de Mas como não sei o que suas Zonas fazem além de guardar as cartas, é difícil saber se isso é apropriado.)

DougM
fonte
1
Eu considerei a zona uma propriedade do cartão, mas como meus cartões funcionam melhor como objetos imutáveis, decidi que dessa maneira era melhor. Obrigado pelo conselho.
Codebreaker
3
@codebreaker Uma coisa que pode ajudar nesse caso é encapsular o cartão em outro objeto. Um ás de espadas é o que é. O local não define sua identidade e um cartão provavelmente deve ser imutável. Talvez uma Zona contenha cartas: talvez uma CardDescriptorque contenha uma carta, sua localização, status voltado para cima / baixo ou até rotação para jogos que se importam com isso. Todas essas são propriedades mutáveis ​​que não alteram a identidade de um cartão.
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 há uma transferência na sua ZoneList. Essa classe provavelmente será uma espécie de singleton, pois você precisará apenas de uma instância, mas poderá querer conjuntos de zonas mais tarde, portanto, mantenha suas opções em aberto.

Segundo, não tenha o Zone ou o ZoneList implementado Collection ou qualquer outra coisa, a menos que você precise. Ou seja, se uma zona ou zonelist será passado para algo que espera uma coleção, em seguida, implementá-lo. Você pode desativar vários métodos fazendo com que eles gerem uma exceção (UnimplementedException, ou algo assim) ou fazendo com que eles simplesmente não façam nada. (Pense bem antes de usar a segunda opção. Se você fizer isso porque é fácil, descobrirá que está perdendo bugs que poderia ter detectado desde o início.)

Existem perguntas reais sobre o que é "correto". Mas depois de descobrir o que é, você vai querer fazer as coisas dessa maneira. Em dois anos, você terá esquecido tudo isso e, se tentar usar o código, ficará muito irritado com o cara que o escreveu de maneira tão contra-intuitiva e não explicou nada.

RalphChapin
fonte
2
Sua resposta se concentra um pouco demais no problema em questão, em vez das perguntas mais amplas que o OP está fazendo sobre a programação defensiva em geral.
Na verdade, eu passo as Zonas para métodos que usam Coleções, portanto a implementação é necessária. Uma espécie de registro de zonas dentro do jogo é uma ideia interessante.
Codebreaker
@ GlenH7: Acho que trabalhar com exemplos específicos geralmente ajuda mais do que a teoria abstrata. O OP forneceu um interessante, então eu fui com isso.
precisa saber é o seguinte
1

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

Na verdade, esse não é o seu exemplo. Você está limitando sua superfície da API, por um motivo muito específico. Como o GlenH7 menciona, quando o conjunto de cartas deve ser usado em um jogo real, com um baralho ('usado' e 'não utilizado'), uma mesa e mãos, por exemplo, você definitivamente deseja colocar cheques para garantir que cada O cartão do conjunto está presente uma vez e apenas uma vez.

Que 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 tabela no exemplo acima), pode muito bem ser um projeto completo.

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

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

CodeCaster
fonte