Un metodo dovrebbe perdonare con gli argomenti passati? [chiuso]


21

Supponiamo di avere un metodo foo(String bar)che opera solo su stringhe che soddisfano determinati criteri; ad esempio, deve essere in minuscolo, non deve essere vuoto o contenere solo spazi bianchi e deve corrispondere al modello [a-z0-9-_./@]+. La documentazione per il metodo indica questi criteri.

Il metodo dovrebbe rifiutare qualsiasi deviazione da questi criteri o dovrebbe essere più indulgente con alcuni criteri? Ad esempio, se il metodo iniziale è

public void foo(String bar) {
    if (bar == null) {
        throw new IllegalArgumentException("bar must not be null");
    }
    if (!bar.matches(BAR_PATTERN_STRING)) {
        throw new IllegalArgumentException("bar must match pattern: " + BAR_PATTERN_STRING);
    }
    this.bar = bar;
}

E il secondo metodo di perdono è

public void foo(String bar) {
    if (bar == null) {
        throw new IllegalArgumentException("bar must not be null");
    }
    if (!bar.matches(BAR_PATTERN_STRING)) {
        bar = bar.toLowerCase().trim().replaceAll(" ", "_");
        if (!bar.matches(BAR_PATTERN_STRING) {
            throw new IllegalArgumentException("bar must match pattern: " + BAR_PATTERN_STRING);
        }
    }
    this.bar = bar;
}

La documentazione dovrebbe essere modificata per indicare che verrà trasformata e impostata sul valore trasformato, se possibile, o il metodo dovrebbe essere mantenuto il più semplice possibile e rifiutare qualsiasi deviazione? In questo caso, barpotrebbe essere impostato dall'utente di un'applicazione.

Il caso d'uso principale per questo sarebbe che gli utenti accedessero agli oggetti da un repository da un identificatore di stringa specifico. Ogni oggetto nel repository dovrebbe avere una stringa univoca per identificarlo. Questi repository potrebbero archiviare gli oggetti in vari modi (sql server, json, xml, binary, ecc.) E quindi ho cercato di identificare il minimo comune denominatore che corrispondesse alla maggior parte delle convenzioni di denominazione.


1
Questo probabilmente dipende fortemente dal tuo caso d'uso. Uno dei due potrebbe essere ragionevole, e ho anche visto delle classi che forniscono entrambi i metodi e fanno decidere all'utente. Potresti approfondire cosa dovrebbe fare questo metodo / classe / campo, in modo da poter offrire qualche consiglio reale?
Ixrec,

1
Conosci tutti quelli che chiamano il metodo? Come in, se lo cambi, puoi identificare in modo affidabile tutti i clienti? Se è così, andrei con il permissivo e il perdono come le preoccupazioni per le prestazioni lo consentono. Potrei anche cancellare la documentazione. In caso contrario, e fa parte di un'API della libreria, mi assicurerei che il codice abbia implementato esattamente l'API pubblicizzata, poiché altrimenti la modifica del codice in modo che corrisponda alla documentazione in futuro potrebbe generare segnalazioni di bug.
Jon Chesterfield,

7
Si potrebbe sostenere che Separation of Concerns afferma che, se necessario, si dovrebbe avere una foofunzione rigorosa che è rigorosa in quali argomenti accetta, e avere una seconda funzione di supporto che può provare a "ripulire" un argomento da utilizzare foo. In questo modo, ogni metodo ha meno da fare da solo e possono essere gestiti e integrati in modo più pulito. Se percorrendo questa strada, probabilmente sarebbe anche utile allontanarsi da un design particolarmente pesante; puoi usare qualcosa di simile Optionalinvece, e quindi avere le funzioni che consumano fooeccezioni di lancio, se necessario.
gntskn,

1
È come chiedere "qualcuno mi ha fatto del male, dovrei perdonarli?" Ovviamente ci sono circostanze in cui l'una o l'altra è appropriata. La programmazione potrebbe non essere così complicata come le relazioni umane, ma è sicuramente abbastanza complessa che una prescrizione generale come questa non funzionerà.
Kilian Foth,

2
@Boggin Vorrei anche indicarti il principio di robustezza riconsiderato . La difficoltà si presenta quando è necessario espandere l'implementazione e l'implementazione tollerante porta a un caso ambiguo con l'implementazione estesa.

Risposte:


47

Il tuo metodo dovrebbe fare quello che dice di fare.

Questo impedisce ai bug, sia dall'uso che dai manutentori di cambiare comportamento in seguito. Risparmia tempo perché i manutentori non devono dedicare troppo tempo a capire cosa sta succedendo.

Detto questo, se la logica definita non è facile da usare, dovrebbe forse essere migliorata.


8
Questa è la chiave Se il tuo metodo fa esattamente quello che dice di fare, il programmatore che usa il tuo metodo compenserà il loro caso d'uso specifico. Non fare mai qualcosa di non documentato con il metodo solo perché pensi che sia utile. Se è necessario modificarlo, scrivere un contenitore o modificare la documentazione.
Nelson,

Aggiungerei al commento di @ Nelson che il metodo non dovrebbe essere progettato nel vuoto. Se i programmatori dichiarano che lo useranno ma compenseranno e le loro compensazioni avranno un valore generale, considera di farne parte della classe. (Ad esempio, avere fooe fooForUncleanStringmetodi in cui quest'ultimo apporta le correzioni prima di passarlo al primo.)
Blrfl

20

Ci sono alcuni punti:

  1. L'implementazione deve fare ciò che afferma il contratto documentato e non dovrebbe fare altro.
  2. La semplicità è importante, sia per il contratto che per l'implementazione, sebbene di più per la prima.
  3. Cercare di correggere input errati aggiunge complessità, in modo controintuitivo non solo per il contratto e l'implementazione, ma anche per l'uso.
  4. Gli errori dovrebbero essere individuati in anticipo solo se ciò migliora la debuggabilità e non compromette troppo l'efficienza.
    Ricorda che ci sono affermazioni di debug per diagnosticare errori logici in modalità debug, che allevia principalmente qualsiasi problema di prestazioni.
  5. L'efficienza, per quanto tempo e denaro disponibili lo consentono senza compromettere troppo la semplicità, è sempre un obiettivo.

Se si implementa un'interfaccia utente, i messaggi di errore intuitivi (inclusi suggerimenti e altro aiuto) fanno parte di una buona progettazione.
Ma ricorda che le API sono per programmatori, non per utenti finali.


Un esperimento di vita reale nell'essere fuzzy e permissivo con l'input è HTML.
Il che ha portato tutti a farlo in modo leggermente diverso, e le specifiche, ora sono documentate, essendo un gigantesco tomo pieno di casi speciali.
Vedi la legge di Postel (" Sii prudente in ciò che fai, sii liberale in ciò che accetti dagli altri ") e un critico che lo tocca ( o uno di gran lunga migliore di cui MichaelT mi ha fatto conoscere ).


Un altro pezzo critico dell'autore di sendmail: il principio di robustezza riconsiderato

15

Il comportamento di un metodo dovrebbe essere chiaro, intuitivo, prevedibile e semplice. In generale, dovremmo essere molto restii a fare ulteriori elaborazioni sull'input di un chiamante. Tali ipotesi su ciò che il chiamante intendeva invariabilmente hanno molti casi limite che producono comportamenti indesiderati. Considera un'operazione semplice come unire il percorso del file. Molte (o forse anche la maggior parte) delle funzioni di unione dei percorsi dei file elimineranno silenziosamente tutti i percorsi precedenti se uno dei percorsi uniti sembra avere il root! Ad esempio, /abc/xyzunito con /evilsi tradurrà in solo /evil. Questo non è quasi mai quello che intendo quando mi unisco ai percorsi dei file, ma poiché non esiste un'interfaccia che non si comporta in questo modo, sono costretto ad avere bug o scrivere codice extra che copre questi casi.

Detto questo, ci sono rare occasioni in cui ha senso che un metodo sia "indulgente", ma dovrebbe sempre essere in potere del chiamante decidere quando e se queste fasi di elaborazione si applicano alla loro situazione. Quindi, quando hai identificato un passaggio di preelaborazione comune che desideri applicare agli argomenti in una varietà di situazioni, dovresti esporre interfacce per:

  • La funzionalità grezza, senza alcuna preelaborazione.
  • Il passo di preelaborazione da solo .
  • La combinazione di funzionalità grezza e preelaborazione.

L'ultimo è facoltativo; dovresti fornirlo solo se un gran numero di chiamate lo utilizzerà.

Esporre la funzionalità grezza offre al chiamante la possibilità di usarla senza la fase di preelaborazione quando è necessario. Esporre il passo del preprocessore da solo consente al chiamante di usarlo per situazioni in cui non sta nemmeno chiamando la funzione o quando vuole preelaborare alcuni input prima di chiamare la funzione (come quando vogliono passare prima in un'altra funzione). Fornire la combinazione consente ai chiamanti di invocare entrambi senza problemi, il che è utile principalmente se la maggior parte dei chiamanti lo utilizzerà in questo modo.


2
+1 per prevedibile. E un altro +1 (vorrei) per semplicità. Preferirei di gran lunga che tu mi aiutassi a individuare e correggere i miei errori piuttosto che cercare di nasconderli.
John M Gant,

4

Come altri hanno già detto, rendere la stringa corrispondente "perdonare" significa introdurre ulteriore complessità. Ciò significa più lavoro nell'attuazione della corrispondenza. Ora hai molti altri casi di test, per esempio. Devi fare un lavoro aggiuntivo per assicurarti che non ci siano nomi semanticamente uguali nello spazio dei nomi. Più complessità significa anche che c'è ancora molto da fare in futuro. Un meccanismo più semplice, come una bicicletta, richiede meno manutenzione rispetto a uno più complesso, come un'auto.

Quindi la corrispondenza della stringa indulgente vale tutto quel costo extra? Dipende dal caso d'uso, come altri hanno notato. Se le stringhe sono una sorta di input esterno su cui non hai alcun controllo e c'è un indubbio vantaggio nella corrispondenza indulgente, potrebbe valerne la pena. Forse l'input proviene da utenti finali che potrebbero non essere molto coscienziosi riguardo ai caratteri spaziali e alle maiuscole e hai un forte incentivo a rendere il tuo prodotto più facile da usare.

D'altra parte, se l'input provenisse, per esempio, da file di proprietà assemblati da tecnici, che dovrebbero capirlo "Fred Mertz" != "FredMertz", sarei più propenso a rendere più rigoroso l'abbinamento e risparmiare i costi di sviluppo.

Penso comunque che ci sia valore nel tagliare e ignorare gli spazi iniziali e finali - ho visto troppe ore sprecate nel debug di questo tipo di problemi.


3

Lei menziona alcuni dei contesti da cui proviene questa domanda.

Detto questo, vorrei che il metodo facesse solo una cosa, afferma i requisiti sulla stringa, lascialo eseguire in base a quello - non proverei a trasformarlo qui. Mantienilo semplice e chiaro; documentarlo e sforzarsi di mantenere la documentazione e il codice sincronizzati tra loro.

Se desideri trasformare i dati che provengono dal database utente in un modo più tollerante, metti quella funzionalità in un metodo di trasformazione separato e documenta la funzionalità collegata .

Ad un certo punto i requisiti della funzione devono essere misurati, chiaramente documentati e l'esecuzione deve continuare. Il "perdono", a suo punto, è un po 'muto, è una decisione di progettazione e direi che la funzione non muta il suo argomento. La mutazione della funzione nell'input nasconde parte della convalida che sarebbe richiesta al client. Avere una funzione che fa la mutazione aiuta il cliente a farlo bene.

La grande enfasi qui è la chiarezza e documentare cosa fa il codice .


-1
  1. Puoi nominare un metodo in base all'azione come doSomething (), takeBackUp ().
  2. Per facilitarne la manutenzione è possibile mantenere i contratti comuni e la convalida su diverse procedure. Chiamali come per i casi d'uso.
  3. Programmazione difensiva: la tua procedura gestisce un'ampia gamma di input tra cui (le cose minime che sono casi d'uso devono essere comunque coperte)
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.