I primitivi, come string
o int
, non hanno alcun significato in un dominio aziendale. Una conseguenza diretta di questo è che potresti erroneamente utilizzare un URL quando è previsto un ID prodotto o utilizzare la quantità quando ti aspetti il prezzo .
Questo è anche il motivo per Calisthenics Object sfida caratteristiche primitive wrapping come una delle sue regole:
Rule 3: Wrap all primitives and Strings
In the Java language, int is a primitive, not a real object, so it obeys different rules than objects. It is used with a syntax that isn’t object-oriented. More importantly, an int on its own is just a scalar, so it has no meaning. When a method takes an int as a parameter, the method name needs to do all of the work of expressing the intent. If the same method takes an Hour as a parameter, it’s much easier to see what’s going on.
Lo stesso documento spiega che c'è un ulteriore vantaggio:
Small objects like Hour or Money also give us an obvious place to put behavior that would otherwise have been littered around other classes.
In effetti, quando vengono utilizzati i primitivi, di solito è estremamente difficile rintracciare l'esatta posizione del codice relativo a tali tipi, che spesso porta a una severa duplicazione del codice . Se è presente la classe Price: Money
, è naturale trovare il controllo dell'intervallo all'interno. Se, invece, viene utilizzato un int
(peggio, un double
) per archiviare i prezzi dei prodotti, chi dovrebbe convalidare l'intervallo? Il prodotto? Lo sconto? Il carrello?
Infine, un terzo beneficio non menzionato nel documento è la possibilità di cambiare relativamente facilmente il tipo sottostante. Se oggi il mio ProductId
ha short
come tipo sottostante e successivamente ho bisogno di usare int
, è probabile che il codice da cambiare non si estenda all'intero code base.
L'inconveniente - e lo stesso argomento vale per ogni regola di esercizio di Calisthenics degli oggetti - è che se diventa rapidamente troppo travolgente per creare una classe per ogni cosa . Se Product
contiene ProductPrice
che eredita da PositivePrice
che eredita da Price
che a sua volta eredita da Money
, questo non è l'architettura pulita, ma piuttosto un pasticcio completo dove, al fine di trovare una sola cosa, un il maintainer dovrebbe aprire qualche dozzina di file ogni volta.
Un altro punto da considerare è il costo (in termini di righe di codice) della creazione di classi aggiuntive. Se i wrapper sono immutabili (come dovrebbero essere, di solito), significa che, se prendiamo C #, devi avere, all'interno del wrapper almeno:
- La proprietà getter,
- Il suo campo di supporto,
- Un costruttore che assegna il valore al campo di supporto,
- Un% personalizzato
ToString()
,
- Commenti della documentazione XML (che rende molto di linee),
- Un
Equals
e un GetHashCode
sostituisce (anche un sacco di LOC).
e alla fine, se pertinente:
- DebuggerDisplay attributo,
- Un override di
==
e !=
operatori,
- Alla fine un sovraccarico dell'operatore di conversione implicita per convertire senza problemi da e verso il tipo incapsulato,
- Contratti di codice (incluso l'invariante, che è un metodo piuttosto lungo, con i suoi tre attributi),
- Diversi convertitori che verranno utilizzati durante la serializzazione XML, la serializzazione JSON o l'archiviazione / caricamento di un valore in / da un database.
Un centinaio di LOC per un semplice wrapper lo rende abbastanza proibitivo, motivo per cui si può essere completamente sicuri della redditività a lungo termine di tale wrapper. La nozione di ambito spiegata da Thomas Junk è particolarmente rilevante qui. Scrivere un centinaio di LOC per rappresentare un ProductId
utilizzato su tutto il tuo codice base sembra abbastanza utile. Scrivere una classe di queste dimensioni per un pezzo di codice che rende tre righe all'interno di un singolo metodo è molto più discutibile.
Conclusione:
-
Fare primitive avvolgere in classi che hanno un significato in un dominio di business dell'applicazione quando (1) aiuta a ridurre gli errori, (2) riduce il rischio di duplicazioni codice o (3) aiuta la modifica del tipo di fondo in seguito .
-
Non avvolgere automaticamente ogni primitiva che trovi nel tuo codice: ci sono molti casi in cui l'uso di string
o int
è perfettamente corretto.
In pratica, in public string CreateNewThing()
, restituire un'istanza di ThingId
class invece di string
potrebbe essere d'aiuto, ma potresti anche:
-
Restituisce un'istanza di Id<string>
class, ovvero un oggetto di tipo generico che indica che il tipo sottostante è una stringa. Hai il vantaggio della leggibilità, senza lo svantaggio di dover mantenere molti tipi.
-
Restituisce un'istanza di Thing
class. Se l'utente ha solo bisogno dell'ID, questo può essere fatto facilmente con:
var thing = this.CreateNewThing();
var id = thing.Id;