Primitive vs Class per rappresentare un oggetto dominio semplice?

13

Quali sono le linee guida generali o le regole pratiche per quando utilizzare un oggetto specifico del dominio o una semplice stringa o numero?

Esempi:

  • Età class vs Integer?
  • FirstName class vs String?
  • UniqueID vs String
  • Classe PhoneNumber vs String vs Long?
  • DomainName class vs String?

Penso che la maggior parte dei professionisti OOP dirà sicuramente classi specifiche per PhoneNumber e DomainName. Più regole intorno a ciò che le rende valide e come confrontarle rendono le classi semplici più facili e sicure da gestire. Ma per i primi tre c'è più dibattito.

Non ho mai incontrato una classe "Age" ma si potrebbe sostenere che abbia senso dato che deve essere non negativo (ok, so che si può discutere di età negative ma è un buon esempio che è quasi equivalente a un intero primitivo ).

String è comune rappresentare "Nome" ma non è perfetto perché una stringa vuota è una stringa valida ma non un nome valido. Il confronto sarebbe di solito fatto ignorando il caso. Certo, ci sono metodi per verificare la presenza di vuoti, fare confronti senza distinzione tra maiuscole e minuscole, ecc. Ma richiede al consumatore di farlo.

La risposta dipende dall'ambiente? Mi occupo principalmente di software enterprise / di alto valore che vivrà e verrà mantenuto per forse più di un decennio.

Forse sto pensando troppo a questo, ma mi piacerebbe davvero sapere se qualcuno ha delle regole su quando scegliere classe vs primitiva.

    
posta noctonura 30.10.2017 - 07:42
fonte

7 risposte

18

What are general guidelines or rules of thumb for when to use a domain-speciifc object vs a plain String or number?

La linea guida generale è che desideri modellare il tuo dominio in una lingua specifica del dominio.

Considera: perché usiamo l'intero? Possiamo rappresentare tutti gli interi con stringhe altrettanto facilmente. O con i byte.

Se stessimo programmando in un linguaggio agnostico di dominio che includesse tipi primitivi per l'intero e età, quale sceglieresti?

In realtà, la natura "primitiva" di certi tipi è un incidente della scelta del linguaggio per la nostra implementazione.

I numeri, in particolare, di solito richiedono un contesto aggiuntivo. L'età non è solo un numero ma ha anche dimensioni (tempo), unità (anni?), regole di arrotondamento! L'aggiunta di età ha senso in un modo che non aggiunge un'età a un denaro.

Rendere i tipi distinti ci permette di modellare le differenze tra un indirizzo email non verificato e indirizzo email verificato .

L'incidente di come questi valori sono rappresentati in memoria è una delle parti meno interessanti. Il modello di dominio non interessa un CustomerId è un int, o una stringa, o un UUID / GUID o un nodo JSON. Vuole solo le affordances.

Ci interessa davvero se gli interi sono big endian o little endian? Ci interessa se il List che ci è stato passato è un'astrazione su un array o un grafico? Quando scopriamo che l'aritmetica a doppia precisione è inefficiente e che è necessario passare a una rappresentazione in virgola mobile, dovrebbe interessare il modello di dominio ?

Parnas, nel 1972 , ha scritto

We propose instead that one begins with a list of difficult design decisions or design decisions which are likely to change. Each module is then designed to hide such a decision from the others.

In un certo senso, i tipi di valore specifici del dominio che introduciamo sono moduli che isolano la nostra decisione su quale debba essere utilizzata la rappresentazione sottostante dei dati.

Quindi il lato positivo è la modularità: otteniamo un design in cui è più facile gestire l'ambito di un cambiamento. Il lato negativo è il costo: è più lavorare per creare i tipi personalizzati di cui hai bisogno, la scelta dei tipi corretti richiede l'acquisizione di una comprensione più approfondita del dominio. La quantità di lavoro richiesta per creare il modulo valore dipenderà dal tuo dialetto locale di Blub .

Altri termini nell'equazione potrebbero includere la durata prevista della soluzione (una modellazione attenta per lo script che verrà eseguito una volta che il rendimento dell'investimento è scadente), quanto il dominio è vicino alla competenza principale dell'azienda.

Un caso speciale che potremmo considerare è quello della comunicazione attraverso un confine . Non vogliamo trovarci in una situazione in cui le modifiche a un'unità schierabile richiedono modifiche coordinate con altre unità schierabili. Quindi messaggi tendono a concentrarsi maggiormente sulle rappresentazioni, senza considerare invarianti o comportamenti specifici del dominio. Non tenteremo di comunicare "questo valore deve essere strettamente positivo" nel formato del messaggio, ma piuttosto di comunicare la sua rappresentazione sul filo e applicare la convalida a quella rappresentazione al limite del dominio.

    
risposta data 30.10.2017 - 14:12
fonte
4

Stai pensando all'astrazione, ed è grandioso. Tuttavia, le cose che il tuo elenco mi sembrano essere per lo più attributi individuali di qualcosa di più grande (non tutte della stessa cosa, ovviamente).

Quello che sento è più importante è fornire un'astrazione che raggruppa gli attributi e dà loro una casa adeguata, come una classe Person, in modo che il programmatore non debba trattare con più attributi, ma piuttosto un livello più alto astrazione.

Una volta che hai questa astrazione non hai necessariamente bisogno di astrazioni aggiuntive per gli attributi che sono solo valori. La classe Person può contenere un BirthDate come data (primitiva), insieme a PhoneNumbers come elenco di stringhe (primitive) e un Name come stringa (primitiva).

Se si dispone di un numero di telefono con un codice di formattazione, ora si tratta di due attributi e meriterebbe una classe per il numero di telefono (poiché probabilmente comporterebbe anche un comportamento, come ottenere solo numeri e ottenere il numero di telefono formattato) .

Se hai un Nome come più attributi (es. prefissi / titoli, primo, ultimo, suffisso) che meriterebbe una classe (dato che ora hai alcuni comportamenti come ottenere il nome completo come stringa e ottenere pezzi più piccoli, titolo ecc.).

    
risposta data 30.10.2017 - 12:34
fonte
1

Dovresti modellare come hai bisogno, se vuoi semplicemente alcuni dati puoi usare i tipi di primitive ma se vuoi fare più operazioni su questi dati dovrebbe essere modellato in classi specifiche, ad esempio (sono d'accordo con @kiwiron nel suo commento) una classe Age non ha senso, ma sarebbe meglio sostituirla con una classe Birthdate e mettere questi metodi lì.

Il vantaggio di modellare questi concetti all'interno delle classi è che imponi la ricchezza del tuo modello, ad esempio, invece hai un metodo che accetta qualsiasi Data, un utente può riempirlo con qualsiasi data, data di inizio WW II o Guerra Fredda data di fine, di queste date ancora valida data di nascita. puoi sostituirlo con la data di nascita quindi, in questo caso, imponi il tuo metodo per accettare una data di nascita e non accettare alcuna data.

Per Firstname non vedo molti vantaggi, anche UniqueID, PhoneNumber un po ', puoi chiedere di darti il paese e la regione per esempio perché in generale PhoneNumber si riferisce a paesi e regioni, alcuni operatori usano numeri prefissati Suffix per classificare i numeri di Mobile, Office ..., così puoi aggiungere questi metodi a quella classe, lo stesso per DomainName.

Per maggiori dettagli ti consiglio di dare un'occhiata a Value Objects in DDD (Domain Driven Design).

    
risposta data 30.10.2017 - 11:18
fonte
1

Da cosa dipende se si ha o meno un comportamento (che è necessario mantenere e / o modificare) associato a tali dati.

In breve, se si tratta solo di una porzione di dati che viene sballottata senza un gran comportamento associato ad essa, usa un tipo primitivo o, per dati un po 'più complessi, una struttura dati (in gran parte priva di comportamento) (ad esempio una classe con solo getter e setter).

Se, invece, trovi che, per prendere decisioni, stai controllando o manipolando questi dati in vari punti del tuo codice, luoghi spesso non vicini tra loro - quindi trasformali in un oggetto appropriato definendo una classe e inserendo il comportamento pertinente all'interno di essa come metodi. Se per qualche ragione ritieni che non abbia senso definire una tale classe, un'alternativa è consolidare questo comportamento in una sorta di classe di "servizio" che opera su questi dati.

    
risposta data 30.10.2017 - 11:45
fonte
1

I tuoi esempi assomigliano molto più a proprietà o attributi di qualcos'altro. Ciò significa che sarebbe perfettamente ragionevole iniziare solo con il primitivo. A un certo punto dovrai usare una primitiva, quindi di solito risparmio la complessità per quando ne ho realmente bisogno.

Per esempio, diciamo che sto facendo un gioco e non devo preoccuparmi dell'invecchiamento dei personaggi. Usare un intero per questo ha perfettamente senso. L'età potrebbe essere utilizzata in semplici controlli per vedere se al personaggio è permesso fare qualcosa o no.

Come altri hanno sottolineato, l'età può essere un argomento complicato a seconda delle impostazioni locali. Se hai bisogno di riconciliare le età in diverse impostazioni locali, ecc., Ha perfettamente senso introdurre una classe Age che abbia DateOfBirth e Locale come proprietà. Quella classe Age può anche calcolare l'età attuale a qualsiasi data timestamp.

Quindi ci sono motivi per promuovere un primitivo in una classe, ma sono molto specifici dell'applicazione. Ecco alcuni esempi:

  • Hai una logica di convalida speciale che deve essere applicata ovunque venga utilizzato il tipo di informazioni (ad es. numeri di telefono, indirizzi email).
  • Hai una logica di formattazione speciale che viene utilizzata per il rendering da leggere per gli utenti (ad esempio indirizzi IP4 e IP6)
  • Hai un set di funzioni che riguardano tutti quel tipo di oggetto
  • Hai regole speciali per confrontare tipi di valore simili

In sostanza, se hai un sacco di regole su come un valore deve essere trattato che vuoi essere usato in modo coerente nel tuo progetto, crea una classe. Se riesci a convivere con valori semplici perché non è molto importante per la funzionalità, usa una primitiva.

    
risposta data 30.10.2017 - 14:07
fonte
1

Alla fine della giornata, un oggetto è solo un testimone che alcuni processi sono stati effettivamente eseguiti .

Un primitivo int significa che alcuni processi azzerarono 4 byte di memoria e quindi capovolgono i bit necessari per rappresentare un numero intero compreso tra -2.147.483.648 e 2.147.483.647; che non è una dichiarazione strong sul concetto di età. Allo stesso modo, un (non null) System.String significa che alcuni byte sono stati allocati e che i bit sono stati ruotati per rappresentare una sequenza di caratteri Unicode rispetto ad alcune codifiche.

Quindi, quando decidi come rappresentare il tuo oggetto dominio, dovresti pensare al processo per il quale funge da testimone. Se una FirstName in realtà dovrebbe essere una stringa non vuota, allora dovrebbe essere testimone che il sistema ha eseguito un processo che garantisce che almeno un carattere non di spazi bianchi sia stato creato in una sequenza di caratteri che ti sono stati consegnati.

Allo stesso modo, un oggetto Age dovrebbe probabilmente essere un testimone che alcuni processi hanno calcolato la differenza tra due date. Poiché ints non è generalmente il risultato del calcolo della differenza tra due date, esse sono ipso facto insufficienti per rappresentare le età.

Quindi, è abbastanza ovvio che quasi sempre vuoi creare una classe (che è solo un programma) per ogni oggetto dominio. Allora, qual'è il problema? Il problema è che C # (che adoro) e Java richiedono troppa cerimonia nella creazione di classi. Ma possiamo immaginare sintassi alternative che renderebbero la definizione di oggetti di dominio molto più semplice:

//hypothetical syntax
class Age = |start:date - end:date|.Years

(* ML-like syntax *)
type Age(x, y) = datediff(year, x, y)

//C#-like syntax with primary constructors and expression-bodied classes
class Age(DateTime x, DateTime y) => implicit operator int (Age a) => Abs((y - x).Years);

F #, ad esempio, ha effettivamente una bella sintassi per questo sotto forma di Pattern attivi :

//Impossible to produce an 'Age' without first computing the difference of two dates
//But, any pair (tuple) of dates is implicitly converted to an 'Age' when needed.

let (|Age|) (x, y) =  (date_diff y x).Days / 365

Il punto è che solo perché il tuo linguaggio di programmazione rende complicato definire gli oggetti del dominio non significa che in realtà sia sempre una cattiva idea farlo.

† Chiamiamo anche queste cose condizioni post , ma preferisco il termine "testimone" mentre serve come prova che alcuni processi possono e sono stati eseguiti.

    
risposta data 01.11.2017 - 05:38
fonte
0

Pensa a ciò che ottieni usando una classe rispetto a una primitiva. Se l'uso di una classe può farti

  • convalida
  • Formattazione
  • altri metodi utili
  • type safety (cioè con una classe PhoneNumber , dovrebbe essere impossibile assegnarlo a una stringa oa una stringa casuale (es. "cetriolo") dove mi aspetto un numero di telefono)
  • altri vantaggi

e questi vantaggi semplificano la vita, migliorano la qualità del codice, la leggibilità e superano il dolore derivante dall'utilizzo di tali elementi, fallo. Altrimenti, rimani con i primati. Se è vicino, fai una sentenza.

Comprendi anche che a volte una buona nomenclatura può semplicemente gestirla (cioè una stringa chiamata firstName rispetto a creare un'intera classe che include solo una proprietà stringa). Le classi non sono l'unico modo per risolvere le cose.

    
risposta data 30.10.2017 - 13:40
fonte