Primitive vs Class per rappresentare un semplice oggetto di dominio?


14

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

Esempi:

  • Classe di età vs intero?
  • Classe FirstName vs String?
  • UniqueID vs String
  • PhoneNumber class vs String vs Long?
  • Classe DomainName vs String?

Penso che la maggior parte dei professionisti di OOP direbbe sicuramente classi specifiche per PhoneNumber e DomainName. Più regole su ciò che li rende validi e su come confrontarli rendono le classi semplici più facili e sicure da affrontare. Ma per i primi tre c'è più dibattito.

Non mi sono mai imbattuto in una classe "Età" ma si potrebbe obiettare che abbia senso dato che deve essere non negativo (okay so che si può discutere per età negative ma è un buon esempio che è quasi equivalente a un intero primitivo).

String è comune per 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. Sicuramente ci sono metodi per verificare la presenza di vuoti, fare confronti senza distinzione tra maiuscole e minuscole, ecc. Ma richiede che il consumatore lo faccia.

La risposta dipende dall'ambiente? Mi occupo principalmente di software aziendale / di alto valore che vivrà e sarà 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 la classe rispetto alla primitiva.


2
La classe di età non è una buona idea se può essere sostituita da un numero intero. Se prende una data di nascita nel costruttore e ha i metodi getCurrentAge () e getAgeAt (Data data), allora la classe ha un significato. Ma non può essere sostituito da un numero intero.
kiwiron,

5
"A volte una stringa non è una stringa. Modellala di conseguenza.". C'è un buon post di Mark Seemann sull'ossessione primitiva: blog.ploeh.dk/2015/01/19/…
Serhii Shushliapin,

4
In realtà una classe di età ha un senso in un modo strano. La definizione occidentale comune di Età è zero alla nascita e aggiungi 1 al tuo prossimo compleanno. La metodologia dell'Asia orientale è che tu abbia 1 alla nascita e 1 sia aggiunto il giorno di Capodanno. Pertanto, a seconda delle impostazioni locali, l'età può essere calcolata in modo diverso. Ad esempio da en.wikipedia.org/wiki/East_Asian_age_reckoning people may be one or two years older in Asian reckoning than in the western age system
Peter M

@PeterM, queste sono buone informazioni! Date / orari e ora età. Dovrebbero essere concetti semplici, ma questo dimostra che c'è molta più complessità nel mondo rispetto a ciò che siamo abituati a gestire.
Machado,

6
Vedo molto sotto questa domanda, ma la risposta non è poi così complicata. Si utilizza una Ageclasse anziché un numero intero quando è necessario il comportamento aggiuntivo che tale classe fornirebbe.
Robert Harvey,

Risposte:


20

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

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

Considera: perché usiamo numeri interi? Possiamo rappresentare tutti gli interi con stringhe altrettanto facilmente. O con byte.

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

Ciò a cui si riduce davvero è la natura "primitiva" di alcuni tipi è un incidente della scelta del linguaggio per la nostra implementazione.

I numeri, in particolare, richiedono solitamente un contesto aggiuntivo. L'età non è solo un numero ma ha anche dimensioni (tempo), unità (anni?), Regole di arrotondamento! L'aggiunta di età insieme ha senso in un modo in cui l'aggiunta di un'età a un denaro no.

Rendere distinti i tipi ci consente di modellare le differenze tra un indirizzo e-mail non verificato e un indirizzo e-mail verificato .

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

Ci interessa davvero se i numeri interi sono big endian o little endian? Ci interessa se il Listpassaggio è un'astrazione su un array o un grafico? Quando scopriamo che l'aritmetica della doppia precisione è inefficiente e che dobbiamo passare a una rappresentazione in virgola mobile, dovrebbe interessarsi il modello di dominio ?

Parnas, nel 1972 , scrisse

Proponiamo invece che si inizi con un elenco di decisioni di progettazione difficili o decisioni di progettazione che potrebbero cambiare. Ogni modulo è quindi progettato per nascondere tale decisione agli altri.

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

Quindi il lato positivo è la modularità: otteniamo un design in cui è più semplice gestire l'ambito di una modifica. Il rovescio della medaglia è il costo - è più lavoro per creare i tipi su misura di cui hai bisogno, scegliere i tipi corretti richiede acquisire una comprensione più profonda del dominio. La quantità di lavoro richiesta per creare il modulo valore dipenderà dal dialetto locale di Blub .

Altri termini nell'equazione potrebbero includere la durata prevista della soluzione (attenta modellizzazione per gli script script che verranno eseguiti una volta che si ottiene un pessimo ritorno sull'investimento), quanto il dominio è vicino alla competenza principale dell'azienda.

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


Ottimo punto su come nascondere (o astrarre) quelle cose che sono difficili e che potrebbero cambiare.
user949300,

4

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

Ciò che ritengo più importante è fornire un'astrazione che raggruppa gli attributi insieme e dia loro una casa adeguata, come una classe Person, in modo che il programmatore client non debba occuparsi di più attributi, ma piuttosto di un'astrazione di livello superiore.

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 nome come stringa (primitiva).

Se hai un numero di telefono con un codice di formattazione, ora sono due attributi e meriterebbero una classe per il numero di telefono (poiché probabilmente avrebbe anche un comportamento, come ottenere numeri solo rispetto a un numero di telefono formattato).

Se hai un nome come più attributi (ad esempio prefissi / titoli, primo, ultimo, suffisso) che meriterebbero una classe (come ora hai alcuni comportamenti come ottenere il nome completo come una stringa contro ottenere pezzi più piccoli, titolo ecc. .).


1

Dovresti modellare come ti serve, se vuoi semplicemente alcuni dati puoi usare i tipi di primitive ma se vuoi fare più operazioni su questi dati dovrebbero essere modellati in classi specifiche, ad esempio (sono d'accordo con @kiwiron nel suo commento) un La classe di età non ha senso, ma sarebbe meglio sostituirla con una classe di data di nascita e mettere questi metodi lì.

Il vantaggio di modellare questi concetti all'interno delle classi è che stai applicando 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 della Seconda Guerra Mondiale o data di fine della guerra fredda, di queste date è ancora valida una data di nascita. puoi sostituirlo con Birthdate, quindi in questo caso, imponi il tuo metodo per accettare un Birthdate per non accettare alcuna Data.

Per il nome non vedo molti vantaggi, anche UniqueID, PhoneNumber un po ', puoi chiederlo per darti il ​​paese e la regione, ad esempio perché in generale PhoneNumber si riferisce a paesi e regioni, alcuni operatori usano Suffissi di numero predefiniti per classificare Mobile , Office ... numeri, quindi 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).


1

Ciò da cui dipende è se si dispone o meno di comportamenti (che è necessario mantenere e / o modificare) associati a tali dati.

In breve, se è solo un pezzo di dati che viene lanciato senza molto comportamento associato ad esso, usa un tipo primitivo o, per pezzi di dati un po 'più complessi, una struttura di dati (in gran parte senza comportamento) (ad esempio una classe con solo getter e setter).

Se, d'altra parte, scopri che, al fine di prendere decisioni, stai controllando o manipolando questi dati in vari punti del tuo codice, posti spesso non vicini l'uno all'altro - quindi trasformali in un oggetto adeguato definendo una classe e inserendo il comportamento rilevante al suo interno come metodi. Se per qualche motivo 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.


1

I tuoi esempi assomigliano molto di più alle proprietà o agli attributi di qualcos'altro. Ciò significa che sarebbe perfettamente ragionevole iniziare solo con la primitiva. Ad un certo punto dovrai usare una primitiva, quindi di solito conservo la complessità per quando ne ho davvero bisogno.

Ad esempio, supponiamo che stia facendo un gioco e non devo preoccuparmi dell'invecchiamento dei personaggi. Usare un numero intero per questo ha perfettamente senso. L'età potrebbe essere usata 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 del Paese. Se hai bisogno di conciliare età in luoghi diversi, ecc., Allora ha perfettamente senso introdurre una Ageclasse che ha DateOfBirthe Localecome proprietà. Quella classe di età può anche calcolare l'età attuale in qualsiasi data e ora.

Quindi ci sono ragioni per promuovere una primitiva 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 e-mail).
  • Hai una logica di formattazione speciale che viene utilizzata per il rendering da leggere per gli umani (ad esempio indirizzi IP4 e IP6)
  • Hai un set di funzioni che si riferiscono tutte a quel tipo di oggetto
  • Hai regole speciali per confrontare tipi di valore simili

Fondamentalmente, se hai un sacco di regole su come un valore deve essere trattato che vuoi usare in modo coerente durante il tuo progetto, crea una classe. Se riesci a vivere con valori semplici perché non è davvero fondamentale per la funzionalità, usa una primitiva.


1

Alla fine della giornata, un oggetto è solo una testimonianza del fatto che alcuni processi sono stati effettivamente eseguiti .

Una primitiva intsignifica che alcuni processi hanno azzerato 4 byte di memoria, e quindi hanno capovolto i bit necessari per rappresentare un numero intero compreso tra -2.147.483.648 e 2.147.483.647; che non è una forte affermazione sul concetto di età. Allo stesso modo, un (non nullo) System.Stringsignifica che alcuni byte sono stati allocati e i bit sono stati capovolti per rappresentare una sequenza di caratteri Unicode rispetto ad alcune codifiche.

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

Allo stesso modo, un Ageoggetto dovrebbe probabilmente essere testimone del fatto che alcuni processi hanno calcolato la differenza tra due date. Dato che intsgeneralmente non sono il risultato del calcolo della differenza tra due date, 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 di dominio. Allora, qual'è il problema? Il problema è che C # (che adoro) e Java richiedono troppe cerimonie per la creazione di classi. Ma possiamo immaginare sintassi alternative che renderebbero molto più semplice la definizione di oggetti di dominio:

//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, in realtà ha una bella sintassi per questo sotto forma di Active Patterns :

//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 ingombrante definire oggetti di dominio non significa che in realtà non sia mai una cattiva idea farlo.


† Chiamiamo anche queste cose come condizioni , ma preferisco il termine "testimone" in quanto serve come prova che un processo può e ha funzionato.


0

Pensa a cosa ottieni usando una classe contro una primitiva. Se usare una classe può darti

  • convalida
  • formattazione
  • altri metodi utili
  • tipo di sicurezza (ad es. con una PhoneNumberclasse, dovrebbe essere impossibile per me assegnarlo a una stringa oa una stringa casuale (ad es. "cetriolo") dove mi aspetto un numero di telefono)
  • qualsiasi altro vantaggio

e questi benefici ti semplificano la vita, migliorano la qualità del codice, la leggibilità e superano il dolore dell'uso di tali cose, fallo. Altrimenti, mantieni i primitivi. Se è vicino, fai una chiamata di giudizio.

Comprendi anche che a volte un buon nome può semplicemente gestirlo (cioè una stringa chiamata firstNamevs. creazione di un'intera classe che avvolge solo una proprietà stringa). Le lezioni non sono l'unico modo per risolvere le cose.

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.