Quando l'ossessione primitiva non è un odore di codice?


22

Di recente ho letto molti articoli che descrivono l'ossessione primitiva come un odore di codice.

Ci sono due vantaggi nell'evitare l'ossessione primitiva:

  1. Rende il modello di dominio più esplicito. Ad esempio, posso parlare con un analista aziendale di un codice postale anziché di una stringa che contiene un codice postale.

  2. Tutta la validazione è in un posto invece che attraverso l'applicazione.

Ci sono molti articoli là fuori che descrivono quando si tratta di un odore di codice. Ad esempio, posso vedere il vantaggio di rimuovere l'ossessione primitiva per un codice postale come questo:

public class Address
{
    public ZipCode ZipCode { get; set; }
}

Ecco il costruttore di ZipCode:

public ZipCode(string value)
    {
        // Perform regex matching to verify XXXXX or XXXXX-XXXX format
        _value = value;
    }

Si sarebbe infrangere la DRY principio di mettere che la logica di validazione in tutto il mondo viene utilizzato un codice postale.

Tuttavia, per quanto riguarda i seguenti oggetti:

  1. Data di nascita: controlla che sia superiore alla mente e inferiore alla data odierna.

  2. Stipendio: verifica che maggiore o uguale a zero.

Creeresti un oggetto DateOfBirth e un oggetto Salary? Il vantaggio è che puoi parlarne quando descrivi il modello di dominio. Tuttavia, questo è un caso di ingegnerizzazione eccessiva in quanto non c'è molta convalida. Esiste una regola che descrive quando e quando non rimuovere l'ossessione primitiva o dovresti sempre farlo se possibile?

Immagino di poter creare un alias di tipo anziché una classe, il che aiuterebbe con il punto uno sopra.


8
"Infrangeresti il ​​principio DRY mettendo quella logica di validazione ovunque venga usato un codice postale." Quello non è vero. La convalida deve essere effettuata non appena i dati vengono inseriti nel modulo . Se esiste più di un "punto di ingresso", la convalida dovrebbe essere in un'unità riutilizzabile e non deve essere (né dovrebbe essere) il DTO ...
Timothy Truckle,

1
In che modo stai dando "mente" e "data odierna" al DateOfBirthcostruttore per verificarne la presenza?
Caleth,

11
Un altro vantaggio della creazione di tipi personalizzati è la sicurezza dei tipi. Se hai Salarye Distanceoggetti non puoi accidentalmente usarli in modo intercambiabile. Puoi se sono entrambi di tipo double.
Scroog1

3
@ w0051977 La tua dichiarazione (come ho capito) implicava che qualsiasi altra cosa oltre ad avere la convalida nel costruttore DTO avrebbe violato il DRY. In effetti la convalida dovrebbe essere al di fuori del DTO ...
Timothy Truckle

2
Per me è tutta una questione di portata. Se dai ai primitivi un ampio ambito, allora ci sono numerosi modi in cui possono essere usati in modo improprio e maltrattati. Quindi in genere si desidera dare loro un ambito più ristretto, e un modo per farlo è quello di progettare una classe che rappresenta un concetto usando un primitivo, memorizzato privatamente come un interno, per implementarlo. Ora la portata della primitiva è ristretta ed è improbabile che venga utilizzata in modo improprio / maltrattato, e puoi mantenere efficacemente gli invarianti. Ma se lo scopo della primitiva era limitato all'inizio, questo potrebbe essere eccessivo e introdurre un sacco di ulteriore accoppiamento e codice da mantenere.

Risposte:


17

L'ossessione primitiva utilizza tipi di dati primitivi per rappresentare idee di dominio.

Il contrario sarebbe la "modellazione di domini", o forse "l'ingegnerizzazione".

Creeresti un oggetto DateOfBirth e un oggetto Salary?

L'introduzione di un oggetto Stipendio può essere una buona idea per il seguente motivo: i numeri raramente sono indipendenti nel modello di dominio, hanno quasi sempre una dimensione e un'unità. Normalmente non modelliamo nulla di utile se aggiungiamo una lunghezza a un tempo o una massa e raramente otteniamo buoni risultati mescolando metri e piedi.

Per quanto riguarda DateOfBirth, probabilmente - ci sono due problemi da considerare. Innanzitutto, la creazione di una Data non primitiva ti dà un posto al centro di tutte le strane preoccupazioni intorno alla matematica della data. Molte lingue ne forniscono una pronta all'uso; DateTime , java.util.Date . Si tratta di domini implementazioni agnostici di date, ma sono non primitive.

In secondo luogo, DateOfBirthnon è davvero un appuntamento; qui negli Stati Uniti, la "data di nascita" è un costrutto culturale / finzione legale. Tendiamo a misurare la data di nascita dalla data locale di nascita di una persona; Bob, nato in California, potrebbe avere una data di nascita "precedente" rispetto ad Alice, nata a New York, anche se è il più giovane dei due.

Esiste una regola che descrive quando e quando non rimuovere l'ossessione primitiva o dovresti sempre farlo se possibile.

Certamente non sempre; ai confini, le applicazioni non sono orientate agli oggetti . È abbastanza comune vedere i primitivi usati per descrivere i comportamenti nei test .


1
Il primo commento dopo la citazione in alto sembra essere un non-sequitur. Inoltre, riafferma semplicemente l'oggetto della domanda. Altrimenti è una buona risposta, ma trovo che ciò sia davvero fonte di distrazione.
JimmyJames,

né C # DateTime né java.util.Date sono tipi sottostanti appropriati per DateOfBirth.
Kevin Cline,

Forse sostituirlo java.util.Dateconjava.time.LocalDate
Koray Tugay il

7

Ad essere onesti: dipende.

C'è sempre il rischio di ingegnerizzare eccessivamente il codice. Quanto saranno utilizzati DateOfBirth e Stipendio? Li userete solo in tre classi strettamente accoppiate o saranno utilizzate in tutta l'applicazione? Li "incapsuleresti" semplicemente nel loro proprio Tipo / Classe per far valere quel vincolo, o potresti pensare a più vincoli / funzioni che effettivamente appartengono a questo?

Prendiamo ad esempio lo stipendio: hai delle operazioni con "Stipendio" (ad es. Gestione di valute diverse o forse una funzione toString ())? Considera cosa è / fa lo stipendio quando non lo consideri un semplice primitivo, e c'è una buona possibilità che lo stipendio sia la sua stessa classe.


Un alias di tipo è una buona alternativa?
w0051977,

@ w0051977 sono d'accordo con charonx e il tipo alias potrebbe essere un'alternativa
techagrammer

@ w0051977 un alias di tipo può essere un'alternativa se l'obiettivo principale è quello di imporre la tipizzazione rigorosa, per indicare esplicitamente quale sia un determinato valore (Stipendio) per evitare un'assegnazione accidentale di "dollari float" (all'ora? Settimana? Mese?) a "stipendio galleggiante" (al mese? Anno?). Dipende davvero dalle tue esigenze.
CharonX,

@CharonX, credo che un decimale dovrebbe essere usato per uno stipendio e non per un float. Sei d'accordo?
w0051977,

@ w0051977 Se si dispone di un buon tipo decimale, quello sarebbe preferibile, sì. (Sto lavorando al progetto di un C ++ in questo momento, in modo da booleani, interi e galleggianti sono in prima linea nella mia mente)
CharonX

5

Una possibile regola empirica può dipendere dal livello del programma. Per il Domain (DDD) aka Entities Layer (Martin, 2018), questo potrebbe anche essere "per evitare primitivi per qualsiasi cosa che rappresenti un concetto di dominio / business". Le giustificazioni sono quelle dichiarate dal PO: un modello di dominio più espressivo, la convalida delle regole aziendali, che rende espliciti i concetti impliciti (Evans, 2004).

Un alias di tipo può essere un'alternativa leggera (Ghosh, 2017) e, se necessario, refactorizzato in una classe di entità. Per esempio, si può richiedere che prima SalaryBE >=0, e poi decidere di non consentire $100.33333e qualsiasi cosa sopra $10,000,000(che sarebbe in bancarotta il cliente). L'uso del Nonnegativeprimitivo per rappresentare Salarye altri concetti complicherebbe questo refactoring.

Evitare i primitivi può anche aiutare a evitare un eccesso di ingegneria. Supponiamo di dover combinare lo stipendio e la data di nascita in una struttura di dati: ad esempio, per avere meno parametri del metodo o passare i dati tra i moduli. Quindi possiamo usare una tupla con type (Salary, DateOfBirth). In effetti, una tupla con primitivi, (Nonnegative, Nonnegative)non è informativa, mentre alcuni gonfiati class EmployeeDatanasconderebbero i campi richiesti tra gli altri. La firma nel dire calcPension(d: (Salary, DateOfBirth))è più focalizzato rispetto al calcPension(d: EmployeeData), che viola la segregazione principio dell'interfaccia. Allo stesso modo, uno specialista class SalaryAndDateOfBirthsembra imbarazzante ed è probabilmente un eccesso. Successivamente, potremo scegliere di definire una classe di dati; le tuple e i tipi di dominio elementale ci consentono di rinviare tali decisioni.

In uno strato esterno (ad es. GUI) può avere senso "spogliare" le entità fino ai loro primitivi costituenti (ad es. Per inserirle in un DAO). Questo impedisce perdite di astrazioni di dominio negli strati esterni, come sostenuto da Martin (2018).

Riferimenti
E. Evans, "Domain-Driven Design", 2004
D. Ghosh, "Modellazione di domini funzionali e reattivi", 2017
RC Martin, "Clean architecture", 2018


+1 per tutti i riferimenti.
w0051977,

4

Meglio soffrire di ossessione primitiva o essere un astronauta dell'architettura ?

Entrambi i casi sono patologici, in un caso hai troppe poche astrazioni, che portano alla ripetizione e scambiano facilmente una mela con un'arancia, e nell'altro ti sei dimenticato di smetterla già e di iniziare a fare le cose, rendendo difficile fare qualsiasi cosa .

Come quasi sempre, vuoi la moderazione, una via di mezzo spero ben considerata.

Ricorda che una proprietà ha un nome, oltre a un tipo. Inoltre, decomporre un indirizzo nelle sue parti costitutive potrebbe essere troppo restrittivo se fatto sempre allo stesso modo. Non tutto il mondo è nel centro di New York.


3

Se tu avessi una classe di stipendio, potrebbe avere metodi come ApplyRaise.

D'altra parte la tua classe ZipCode non deve avere una convalida interna per evitare di duplicare la convalida ovunque tu possa avere una classe ZipCodeValidator che potrebbe essere iniettata, quindi se il tuo sistema deve essere eseguito su indirizzi statunitensi e britannici puoi semplicemente iniettare il validatore corretto e quando devi gestire anche gli indirizzi AUS puoi semplicemente aggiungere un nuovo validatore.

Un'altra preoccupazione è se devi scrivere i dati in un database tramite EntityFramework, allora dovrà sapere come gestire Salary o ZipCode.

Non esiste una risposta chiara su dove tracciare la linea tra le classi intelligenti, ma dirò che tendo a spostare la logica di business, come la convalida, verso le classi di business logic con le classi di dati come dati puri come sembra per lavorare meglio con EntityFramework.

Per quanto riguarda l'uso degli alias di tipo, il nome del membro / proprietà dovrebbe fornire tutte le informazioni necessarie sui contenuti, quindi non userò gli alias di tipo.


Un alias di tipo è una buona alternativa?
w0051977,

2

(Qual è la domanda probabilmente davvero)

Quando l'uso del tipo primitivo non è un odore di codice?

(Risposta)

Quando il parametro non contiene regole, usa un tipo primitivo.

Usa il tipo primitivo per artisti del calibro di:

htmlEntityEncode(string value)

Usa oggetto per artisti del calibro di:

numberOfDaysSinceUnixEpoch(SimpleDate value)

Quest'ultimo esempio ha regole in esso, cioè, l'oggetto SimpleDateè composto da Year, Monthe Day. Attraverso l'uso di Object in questo caso, il concetto di SimpleDateessere valido può essere incapsulato all'interno dell'oggetto.


1

A parte gli esempi canonici di indirizzi e-mail o codici postali forniti altrove in questa domanda, dove trovo che il refactoring lontano da Primitive Obsession possa essere particolarmente utile è con gli ID entità (vedi https://andrewlock.net/using-strongly-typed-entity -ids-to-avoid-primitive-obsession-part-1 / per un esempio di come farlo in .NET).

Ho perso il conto del numero di volte in cui un bug si è insinuato perché un metodo aveva una firma come questa:

int leaveId = 12345;
int submitterId = 23456;
int approverId = 34567;

SubmitLeaveApplication(leaveId, approverId, submitterId);

public void SubmitLeaveApplication(int leaveId, int submitterId, int approverId) {
  // implementation here
}

Compilare bene, e se non si è rigorosi con i test dell'unità, potrebbe passare anche quello. Tuttavia, refactoring quegli ID entità in classi specifiche del dominio, e hey presto, errori di compilazione:

LeaveId leaveId = 12345;
SubmitterId submitterId = 23456;
ApproverId approverId = 34567;

SubmitLeaveApplication(leaveId, approverId, submitterId);

public void SubmitLeaveApplication(LeaveId leaveId, SubmitterId submitterId, ApproverId approverId) {
  // implementation here
}

Immagina che il metodo abbia scalato fino a 10 o più parametri, tutti inti tipi di dati (non importa l' odore del codice dell'elenco dei parametri lunghi ). Diventa ancora peggio quando usi qualcosa come AutoMapper per scambiare tra oggetti di dominio e DTO e un refactoring che fai non viene rilevato dalla mappatura automagica.


0

Infrangeresti il ​​principio DRY mettendo quella logica di validazione ovunque venga usato un codice postale.

D'altra parte, quando si ha a che fare con molti paesi diversi e con i loro diversi sistemi di codici postali, ciò significa che non è possibile convalidare un codice postale se non si conosce il paese in questione. Quindi la tua ZipCodeclasse deve anche memorizzare il paese.

Ma poi memorizzi separatamente il Paese sia come parte del Address(di cui fa parte anche il codice postale), sia come parte del codice postale (per la convalida)?

  • Se lo fai, stai violando anche DRY. Anche se non la definisci una violazione DRY (perché ogni istanza ha uno scopo diverso), occupa comunque inutilmente memoria aggiuntiva, oltre ad aprire la porta ai bug quando i valori dei due paesi sono diversi (che logicamente non dovrebbero mai essere).
    • Oppure, in alternativa, ti porta alla necessità di sincronizzare i due punti dati per assicurarti che siano sempre gli stessi, il che suggerisce che dovresti davvero archiviare questi dati in un unico punto, vanificando così lo scopo.
  • Se non lo fai, allora non è una ZipCodeclasse ma una Addressclasse, che conterrà di nuovo un string ZipCodeche significa che siamo tornati al punto di partenza.

Ad esempio, posso parlare con un analista aziendale di un codice postale anziché di una stringa che contiene un codice postale.

Il vantaggio è che puoi parlarne quando descrivi il modello di dominio.

Non capisco la tua affermazione di fondo secondo cui quando un'informazione ha un determinato tipo di variabile, sei in qualche modo obbligato a menzionare quel tipo ogni volta che parli con un analista aziendale.

Perché? Perché non riesci a parlare semplicemente del "codice postale" e ometti completamente il tipo specifico? Che tipo di discussioni stai avendo con il tuo analista aziendale (non tecnico!) In cui il tipo di proprietà è essenziale per la conversazione?

Da dove vengo io, i codici postali sono sempre numerici. Quindi abbiamo una scelta, potremmo memorizzarlo come into come string. Tendiamo a usare una stringa perché non ci si può aspettare operazioni matematiche sui dati, ma mai un analista aziendale mi ha detto che doveva essere una stringa. Tale decisione è lasciata allo sviluppatore (o probabilmente all'analista tecnico, anche se nella mia esperienza non si occupano direttamente del grintoso spirito).

Un analista aziendale non si preoccupa del tipo di dati, purché l'applicazione faccia quello che dovrebbe fare.


La convalida è una bestia difficile da affrontare, perché si basa su ciò che gli umani si aspettano.

Per uno, non sono d'accordo con l'argomento di validazione come un modo per mostrare perché l'ossessione primitiva dovrebbe essere evitata, perché non sono d'accordo che i dati (come verità universale) debbano sempre essere validati in ogni momento.

Ad esempio, cosa succede se si tratta di una ricerca più complicata? Invece di un semplice controllo del formato, cosa succede se la convalida comporta il contatto con un'API esterna e l'attesa di una risposta? Vuoi davvero forzare la tua applicazione a chiamare questa API esterna per ogni ZipCodeoggetto che crei un'istanza?
Forse è un requisito aziendale rigoroso, e quindi è ovviamente giustificabile. Ma questa non è una verità universale. Ci saranno molti casi d'uso in cui questo è più un peso che una soluzione.

Come secondo esempio, quando inserisci il tuo indirizzo in un modulo, è comune inserire il tuo codice postale prima del tuo paese. Sebbene sia bello avere un feedback di convalida immediato nell'interfaccia utente, in realtà sarebbe un ostacolo per me (come utente) se l'applicazione mi avvisasse di un formato zipcode "errato", perché la vera fonte del problema è (ad esempio) che il mio paese non è il paese selezionato per impostazione predefinita, quindi la convalida è avvenuta per il paese sbagliato.
È un messaggio di errore sbagliato, che distrae l'utente e causa inutili confusioni.

Proprio come la validazione perpetua non è una verità universale, né i miei esempi. È contestuale . Alcuni domini dell'applicazione richiedono la convalida dei dati sopra ogni altra cosa. Altri domini non inseriscono la convalida così in alto nell'elenco delle priorità perché la seccatura che ne deriva è in conflitto con le loro priorità effettive (ad esempio l'esperienza dell'utente o la capacità di archiviare inizialmente i dati difettosi in modo che possano essere corretti invece di non permettere mai che siano immagazzinato)

Data di nascita: controlla che sia maggiore della mentalità e inferiore alla data di oggi.
Stipendio: verifica che maggiore o uguale a zero.

Il problema con queste convalide è che sono incomplete, ridondanti o indicative di un problema molto più grande .

Controllare che una data sia maggiore della mentalità è ridondante. La mentalità significa letteralmente che è la data più piccola possibile. Inoltre, dove disegni la linea di pertinenza? Qual è il punto di prevenire DateTime.MinDatema permettere DateTime.MinDate.AddSeconds(1)? Stai criptando un valore particolare che non è particolarmente sbagliato rispetto a molti altri valori.

Il mio compleanno è il 2 gennaio 1978 (non lo è, ma supponiamo che lo sia). Ma supponiamo che i dati nella tua applicazione siano sbagliati e invece dice che il mio compleanno è:

  • 1 gennaio 1978
  • 1 gennaio 1722
  • 1 gennaio 2355

Tutte queste date sono sbagliate. Nessuno di loro è "più giusto" dell'altro. Ma la tua regola di validazione catturerebbe solo uno di questi tre esempi.

Hai anche completamente omesso il contesto di come stai usando questi dati. Se questo viene utilizzato, ad esempio, in un bot di promemoria di compleanno, direi che la convalida è inutile poiché non vi sono conseguenze particolarmente negative per la compilazione della data sbagliata.
D'altra parte, se si tratta di dati governativi e hai bisogno della data di nascita per autenticare l'identità di qualcuno (e la mancata osservanza porta a conseguenze negative, ad esempio negare a qualcuno la sicurezza sociale), allora la correttezza dei dati è fondamentale e devi essere pienamente convalidare i dati. La validazione proposta che hai ora non è adeguata.

Per uno stipendio, c'è un po 'di buon senso in quanto non può essere negativo. Ma se realisticamente ti aspetti che vengano immessi dati senza senso, ti suggerirei di indagare sulla fonte di questi dati senza senso. Perché se non ci si può fidare di inserire dati sensibili, non ci si può fidare di loro per inserire dati corretti .

Se invece lo stipendio viene calcolato dalla tua applicazione e in qualche modo è possibile finire con un numero negativo (e corretto)), un approccio migliore sarebbe quello Math.Max(myValue, 0)di trasformare i numeri negativi in ​​0, anziché fallire la convalida. Perché se la tua logica ha deciso che il risultato è un numero negativo, fallire la convalida significa che dovrà ripetere il calcolo, e non c'è motivo di pensare che verrà fuori con un numero diverso la seconda volta.
E se viene fornito un numero diverso, ciò porta di nuovo a sospettare che il calcolo non sia coerente e quindi non ci si possa fidare.

Questo non vuol dire che la validazione non sia utile. Ma la convalida inutile è negativa, sia perché richiede uno sforzo che non risolve davvero un problema, sia per dare alle persone un falso senso di sicurezza.


La data di nascita di qualcuno può effettivamente essere passata oltre la data corrente, se un bambino è nato proprio ora in un fuso orario che è già saltato al giorno successivo. E un ospedale potrebbe archiviare le "date di nascita previste" in un database che potrebbe essere mesi in futuro. Vorresti un tipo diverso per quello?
gnasher729,

@ gnasher729: Non sono abbastanza sicuro di seguirlo, sembra che tu sia d'accordo con me (la convalida è contestuale e non universalmente corretta), ma il fraseggio del tuo commento suggerisce che pensi che non sia d'accordo. O sto leggendo male?
Flater
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.