Quando una classe rappresenta una proprietà che potrebbe non essere valida, come deve essere eseguita la validazione?

1

Ho una classe Product che ha tra l'altro un attributo Ean13 che incapsula un codice EAN13 .

Ecco un prototipo della classe Product:

@Entity
@Table(name = "tb_produtos")
public class Product implements Serializable {


    public Product() {
    }


    @Id
    @GeneratedValue
    private Integer id;

    @ManyToOne
    @JoinColumn(name = "ID_FABRICANTE")
    private Manufacturer manufacturer;

    @Column(name = "DESCRICAO")
    private String description;

    @Column(name = "URL")
    private String url;

    @Embedded
    private Ean13 ean;

    @Transient
    private Keywords keywords;

    ... getters and setters.
}

Inizialmente, implemento la classe EAN come segue:

@Embeddable
public class Ean13 {
    private static final RuntimeException notValidEanException = new RuntimeException("NOT VALID EAN CODE");

    @Column(name = "ean_code", nullable = true, length = 13)
    private String code;

    public Ean13() {
    }

    public Ean13(String code) {
        validate(code);
        this.code = code;
    }

    private void validate(String code) {
        if (code == null || code.length() != 13) {
            throw notValidEanException;
        }
        if (!CharMatcher.DIGIT.matchesAllOf(code)) {
            throw notValidEanException;
        }
        String codeWithoutVd = code.substring(0, 12);
        int pretendVd = Integer.valueOf(code.substring(12, 13));
        int e = sumEven(codeWithoutVd);
        int o = sumOdd(codeWithoutVd);
        int me = o * 3;
        int s = me + e;
        int dv = getEanVd(s);
        if (!(pretendVd == dv)) {
            throw notValidEanException;
        }
    }

    private int getEanVd(int s) {
        return 10 - (s % 10);
    }

    //mover estes metodos para outra classe. 
    //TODO: Java 8. 
    private int sumEven(String code) {
        int sum = 0;
        for (int i = 0; i < code.length(); i++) {
            if (isEven(i)) {
                sum += Character.getNumericValue(code.charAt(i));
            }
        }
        return sum;
    }


    private int sumOdd(String code) {
        int sum = 0;
        for (int i = 0; i < code.length(); i++) {
            if (!isEven(i)) {
                sum += Character.getNumericValue(code.charAt(i));
            }
        }
        return sum;
    }


    private boolean isEven(int i) {
        return i % 2 == 0;
    }

    @Override
    public String toString() {
        return code;
    }
}

Come puoi vedere, il modo in cui implemento il costruttore genera una RuntimeException quando l'oggetto viene istanziato con un codice non valido.

Una spiegazione sulla classe del prodotto:

In questo caso specifico userò questa classe per conservare informazioni sui prodotti in un'applicazione crawler, che eseguono la scansione di alcuni siti Web e raccolgono informazioni su questi prodotti. Una di queste informazioni è il codice EAN. In questo caso il prodotto e l'oggetto EAN effettueranno un'istanza in una classe di servizio. Alcuni codici EAN che le applicazioni catturano dal sito Web non sono codici EAN validi (può essere un altro codice o qualche altra stringa). All'inizio voglio salvare questo prodotto senza alcun codice fino a quando non ho creato un modo per memorizzarlo. Quindi all'inizio devo convalidare questo codice EAN prima di persisterlo.

Come puoi vedere nel codice sopra, implemento la convalida nel metodo 'validate' che viene chiamato dal costruttore. Nel modo in cui viene implementato, la convalida funzionerà più o meno come segue:

Product p = new Product ();
try {
    Ean13 e = new Ean13("somecode");
    p.setEan13 (e);
} catch (InvalidEanCodeRuntimeException e) {
    //Log invalid ean code and product information. 
}
persist(p);

Ma ci sarebbero altri modi per convalidarlo.

Posso rendere implementare un metodo pubblico nella classe Ean13 come segue:

Product p = new Product ();
Ean13 ean = new Ean13 ("somecode");
if (ean.isValidEan ()){
    p.setEan13(ean);
}

o potrebbe ancora essere eseguito come segue, inserendo la convalida in una classe di utilità esterna:

Product p = new Product ();
String somecode = "somecode";
if (CodeUtil.isValidEan13 (somecode)){
    p.setEan13(new Ean13(somecode));
}

Certamente ci sono molti altri modi per implementarlo. Ma vorrei implementare questa convalida nel modo più corretto, pulito ed elegante possibile e / o promuovere una discussione sui modi per implementare questo tipo di convalida.

    
posta alexpfx 27.12.2015 - 23:31
fonte

3 risposte

3

Dipende da cosa il tuo programma farà con quegli oggetti. Ad esempio, se si sta implementando un programma di scansione di codici a barre e in caso di errore di scansione, è necessario scrivere il codice (non valido!) Che lo scanner ha rilevato in un file di registro o visualizzarlo all'utente, posso immagina uno scenario in cui abbia senso costruire ed elaborare Ean oggetto anche se non è valido. Se è il tuo caso, dovrebbe essere ovvio che hai bisogno di un metodo isValid per qualcosa sulla falsariga di:

  Ean eanCode;
  while(true)
  {
      eanCode = new Ean(ScanCode());
      if(eanCode.isValid())
         break;
      LogScanError(eanCode);
  }

Tuttavia, se il tuo programma si aspetta sempre che Ean oggetti sia valido, e non c'è assolutamente bisogno di oggetti non validi, fai la validazione nel costruttore e lancia un'eccezione nel caso in cui fallisca. L'esempio sopra potrebbe essere implementato come

  Ean eanCode;
  bool scanOk=false;
  while(!scanOk)
  {
      String code = ScanCode();
      try
      {
          eanCode = new Ean(code);
          scanOk=true;
      }
      catch(ValidationException ex)
      {
          LogScanError(code);
      }
  }

Questa seconda variante ha un "odore di codice" leggero, poiché utilizza eccezioni per il controllo del flusso, che è spesso visto come una cattiva pratica.

Quindi preferirei la variante isValid se hai bisogno dei codici Ean "non validi" in più di un posto nel tuo programma in modo ragionevole, e la "variante del costruttore" se i codici Ean non validi sono una rara eccezione e l'utilizzo può essere aggirato implementando un codice come sopra.

    
risposta data 28.12.2015 - 21:36
fonte
1

Beh, c'è sempre una scuola per cui devi sempre istanziare i tuoi oggetti in uno stato valido, quindi la tua soluzione è in realtà una best practice. Se rendi il tuo oggetto immutabile, troverai molto difficile perdere le cose in quel modo.

Spostare la convalida al di fuori del costruttore potrebbe causare problemi se si dimentica di chiamarlo. Altri sviluppatori potrebbero credere che l'oggetto sia stato creato con successo attraverso il suo costruttore e iniziare a usarlo direttamente.

D'altro canto, rendere pubblico il metodo isValid() potrebbe essere una buona idea per riutilizzare la funzionalità di convalida del codice EAN stesso (nell'IU, ad esempio).

    
risposta data 28.12.2015 - 15:56
fonte
1

Osservando il tuo codice, ti suggerirei di spostare la convalida nel costruttore. Può essere considerata una cattiva pratica avere troppa logica nel costruttore, quindi, quindi se stai facendo un sacco di lavoro allora ti consiglio di guardare il modello di fabbrica.

Puoi avere un costruttore privato e un metodo statico pubblico Create con un parametro String che genera se non è valido e restituisce l'oggetto che ti aspetti.

Ean ean = Ean.create("123");

Questo ha più senso, in quanto è un comportamento strano per un costruttore fallire e non avere alcun oggetto.

    
risposta data 28.12.2015 - 16:46
fonte