Quanto è necessario seguire pratiche di programmazione difensive per codice che non sarà mai reso disponibile al pubblico?

45

Sto scrivendo un'implementazione Java di un gioco di carte, quindi ho creato un tipo speciale di raccolta che sto chiamando una zona. Tutti i metodi di modifica della raccolta di Java non sono supportati, ma esiste un metodo nell'API di zona, move(Zone, Card) , che trasferisce una scheda dalla data zona a se stessa (realizzata mediante tecniche package-private). In questo modo, posso garantire che nessuna carta venga estratta da una zona e svanisca semplicemente; possono essere spostati solo in un'altra zona.

La mia domanda è, quanto è necessario questo tipo di codifica difensiva? È "corretto" e sembra la prassi giusta, ma non è come se l'API di zona diventasse parte di una biblioteca pubblica. È solo per me, quindi è come se stessi proteggendo il mio codice da me stesso quando potrei essere più efficiente semplicemente usando le Collezioni standard.

Quanto lontano dovrei prendere questa idea di zona? Qualcuno può darmi qualche consiglio su quanto dovrei pensare di preservare i contratti nelle classi che scrivo, specialmente per quelli che non saranno realmente disponibili al pubblico?

    
posta codebreaker 04.12.2013 - 20:14
fonte

6 risposte

72

Non ho intenzione di affrontare il problema di progettazione - solo la questione se fare le cose "correttamente" in un'API non pubblica.

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

Questo è esattamente il punto. Forse ci sono programmatori là fuori che ricordano le sfumature di ogni classe e metodo che hanno mai scritto e mai erroneamente chiamano con il contratto sbagliato. Io non sono uno di loro. Spesso dimentico che il codice che ho scritto dovrebbe funzionare entro poche ore dalla sua scrittura. Dopo aver pensato di averlo fatto bene una volta, la tua mente tenderà a cambiare marcia al problema su cui stai lavorando ora .

Hai gli strumenti per combatterlo. Questi strumenti includono (in nessun ordine particolare) convenzioni, test unitari e altri test automatici, controllo delle precondizioni e documentazione. Io stesso ho riscontrato che i test unitari sono inestimabili perché entrambi ti costringono a pensare a come verrà utilizzato il tuo contratto ea fornire la documentazione più tardi su come è stata progettata l'interfaccia.

    
risposta data 04.12.2013 - 21:30
fonte
25

Di solito seguo alcune semplici regole:

  • Prova a programmare sempre per contratto .
  • Se un metodo è pubblicamente disponibile o riceve input da il mondo esterno , applica alcune misure difensive (ad es. IllegalArgumentException ).
  • Per tutto il resto che è accessibile solo internamente, usa le asserzioni (ad esempio assert input != null ).

Se un cliente è davvero coinvolto, troverà sempre un modo per rendere il codice non corretto. Possono sempre farlo attraverso la riflessione, almeno. Ma questa è la bellezza di design per contratto . Non approvi tale uso del tuo codice e quindi non puoi garantire che funzioni in tali scenari.

Per quanto riguarda il tuo caso specifico, se Zone non dovrebbe essere usato e / o utilizzato da estranei, rendere il pacchetto di classe-private (e possibilmente final ), o preferibilmente, utilizzare le raccolte Java già ti fornisce Sono testati e non è necessario reinventare la ruota. Nota che questo non ti impedisce di usare asserzioni attraverso il tuo codice per assicurarti che tutto funzioni come previsto.

    
risposta data 04.12.2013 - 21:22
fonte
16

La programmazione difensiva è una cosa molto buona.
Fino a quando non inizia a intralciare la scrittura del codice. Quindi non è una buona cosa.

Parlando un po 'più pragmaticamente ...

Sembra che tu abbia ragione a prendere le cose troppo lontano. La sfida (e la risposta alla tua domanda) sta nel capire quali siano le regole o le esigenze del programma.

Usando l'API del tuo gioco di carte come esempio, ci sono alcuni ambienti in cui tutto ciò che può essere fatto per prevenire l'inganno è fondamentale. Grandi quantità di denaro reale possono essere coinvolte, quindi ha senso inserire un numero elevato di controlli per assicurarsi che non possano verificarsi imbrogli.

D'altra parte, è necessario essere consapevoli dei principi SOLID, specialmente della singola responsabilità. Chiedere alla classe container di controllare in modo efficace dove stanno andando le carte potrebbe essere un po 'troppo. Potrebbe essere preferibile disporre di un livello di controllo / controller tra il contenitore di schede e la funzione che riceve le richieste di spostamento.

In relazione a tali preoccupazioni, è necessario comprendere quali componenti della propria API sono esposti pubblicamente (e quindi vulnerabili) rispetto a ciò che è privato e meno esposto. Non sono un sostenitore totale di un "rivestimento esterno duro con un interno morbido", ma il miglior ritorno del tuo sforzo è quello di rafforzare l'esterno della tua API.

Non penso che l'utente finale di una libreria sia critico quanto a una determinazione sulla quantità di programmazione difensiva che hai messo in atto. Anche con i moduli che scrivo per mio uso personale, ho comunque messo una misura del controllo in atto per assicurarmi che in futuro io non abbia commesso un errore involontario nel chiamare la libreria.

    
risposta data 04.12.2013 - 21:32
fonte
13

La codifica difensiva non è solo una buona idea per il codice pubblico. È un'idea grande per qualsiasi codice che non sia immediatamente gettato via. Certo, sai come dovrebbe essere chiamato ora , ma non hai idea di quanto bene ti ricorderai di questi sei mesi da quando tornerai al progetto.

La sintassi di base di Java offre parecchie difese avanzate rispetto a un linguaggio di livello inferiore o interpretato come C o Javascript rispettivamente. Supponendo che tu chiami chiaramente i tuoi metodi e non abbia "sequenziamento dei metodi" esterno, puoi probabilmente cavartela semplicemente specificando argomenti come un tipo di dati corretto e includendo un comportamento ragionevole se i dati digitati correttamente possono ancora essere non validi.

(A parte, se le carte devono sempre essere in zona, penso che tu abbia una migliore probabilità di successo facendo in modo che tutte le carte in gioco siano referenziate da una collezione globale al tuo oggetto di Gioco, e sia Zona una proprietà di ogni carta, ma dal momento che non so cosa fanno le tue Zone se non tenere le carte, è difficile sapere se è appropriato.)

    
risposta data 04.12.2013 - 21:32
fonte
1

Per prima cosa crea una classe che mantiene un elenco di Zone in modo da non perdere una Zona o le carte in essa contenute. È quindi possibile verificare che un trasferimento sia all'interno di ZoneList. Questa classe sarà probabilmente una sorta di singleton, in quanto avrai bisogno di una sola istanza, ma potresti desiderare set di Zone in un secondo momento, quindi tieni aperte le opzioni.

In secondo luogo, non disporre di Raccolta di strumenti Zone o ZoneList o altro, a meno che non ci si aspetti di averne bisogno. Cioè, se una Zona o una ZonaLista verranno passate a qualcosa che si aspetta una Collezione, allora implementarla. Puoi disabilitare una serie di metodi facendoli lanciare un'eccezione (UnimplementedException, o qualcosa del genere) o facendoli semplicemente non fare nulla. (Pensa seriamente prima di usare la seconda opzione, se lo fai perché è facile scoprire che ti mancano i bachi che avresti potuto catturare in precedenza.)

Ci sono domande reali su cosa sia "corretto". Ma una volta capito cosa vorresti fare in quel modo. Tra due anni ti sarai dimenticato di tutto questo e, se proverai a usare il codice, diventerai davvero infastidito dal ragazzo che lo ha scritto in modo così poco intuitivo e non ha spiegato nulla.

    
risposta data 04.12.2013 - 21:20
fonte
1

La codifica difensiva nella progettazione delle API generalmente riguarda la convalida dell'input e la selezione accurata di un meccanismo di gestione degli errori adeguato. Vale anche la pena di menzionare altre risposte.

Questo non è in realtà ciò di cui parla il tuo esempio. Sei lì a limitare la tua superficie API, per un motivo molto specifico. Come GlenH7 menziona, quando il set di carte deve essere usato in un gioco reale, con un '' usato '' e 'inutilizzato') mazzo, un tavolo e le mani per esempio, si vuole assolutamente mettere dei check in atto per assicurarsi che ogni carta del set sia presente una volta e una sola volta.

Che hai progettato questo con "zone", è una scelta arbitraria. A seconda dell'implementazione (una zona può essere solo una mano, un mazzo o un tavolo nell'esempio sopra), potrebbe benissimo essere un progetto completo.

Tuttavia, questa implementazione sembra un tipo derivato di un set di carte più Collection<Card> -like, con un'API meno restrittiva. Ad esempio, quando vuoi costruire un calcolatore del valore della mano, o un'IA, sicuramente vuoi essere libero di scegliere quale e quante di ciascuna carta si itera.

Quindi è bene esporre un'API così restrittiva, se l'unico obiettivo di tale API è assicurarsi che ogni carta si trovi sempre in una zona.

    
risposta data 04.12.2013 - 22:09
fonte