Dovremmo definire i tipi per tutto?


141

Recentemente ho avuto un problema con la leggibilità del mio codice.
Avevo una funzione che eseguiva un'operazione e restituiva una stringa che rappresenta l'ID di questa operazione per riferimento futuro (un po 'come OpenFile in Windows che restituisce un handle). L'utente utilizzerà questo ID in un secondo momento per avviare l'operazione e monitorarne il completamento.

L'ID doveva essere una stringa casuale a causa di problemi di interoperabilità. Questo ha creato un metodo con una firma molto poco chiara in questo modo:

public string CreateNewThing()

Ciò rende poco chiaro l'intento del tipo restituito. Ho pensato di avvolgere questa stringa in un altro tipo che ne chiarisca il significato in questo modo:

public OperationIdentifier CreateNewThing()

Il tipo conterrà solo la stringa e verrà utilizzato ogni volta che viene utilizzata questa stringa.

È ovvio che il vantaggio di questo modo di operare è una maggiore sicurezza del tipo e un intento più chiaro, ma crea anche molto più codice e codice che non è molto idiomatico. Da un lato mi piace la maggiore sicurezza, ma crea anche molto disordine.

Pensi che sia una buona pratica avvolgere tipi semplici in una classe per motivi di sicurezza?


4
Potresti essere interessato a leggere su piccoli tipi. Ci ho giocato un po 'e non lo consiglierei, ma è un approccio divertente a cui pensare. darrenhobbs.com/2007/04/11/tiny-types
Jeroen Vannevel

40
Una volta che usi qualcosa come Haskell, vedrai quanto aiuta l'uso dei tipi. I tipi aiutano a garantire un programma corretto.
Nate Symer,

3
Perfino un linguaggio dinamico come Erlang beneficia di un sistema di tipi, motivo per cui ha un sistema di tipi e strumenti di controllo dei tipi familiari a Haskeller nonostante sia un linguaggio dinamico. Linguaggi come C / C ++ in cui il tipo reale di qualsiasi cosa è un numero intero che concordiamo con il compilatore per trattare in un determinato modo, o Java in cui quasi si hanno tipi se si decide di fingere che le classi siano tipi presenti o un problema di sovraccarico semantico nel lingua (è una "classe" o un "tipo"?) o un'ambiguità di tipo (è un "URL", "String" o "UPC"?). Alla fine, usa tutti gli strumenti che puoi per chiarire le ambiguità.
zxq9,

7
Non sono sicuro da dove venga l'idea che C ++ abbia un sovraccarico sui tipi. Con C ++ 11, nessuno dei produttori di compilatori ha riscontrato un problema nel dare a ogni lambda il proprio tipo unico. Ma TBH non puoi davvero aspettarti di trovare molte informazioni in un commento sul "sistema di tipi C / C ++" come se i due condividessero un sistema di tipi.
MSalters

2
@JDB - no, non lo fa. Nemmeno simile.
Davor Ždralo,

Risposte:


152

I primitivi, come stringo int, non hanno alcun significato in un dominio aziendale. Una conseguenza diretta di ciò è che si può erroneamente utilizzare un URL quando è previsto un ID prodotto o utilizzare la quantità quando si prevede il prezzo .

Questo è anche il motivo per cui la sfida di Object Calisthenics include le primitive che si avvolgono come una delle sue regole:

Regola 3: avvolgi tutti i primitivi e le stringhe

Nel linguaggio Java, int è un oggetto primitivo, non reale, quindi obbedisce a regole diverse rispetto agli oggetti. Viene utilizzato con una sintassi non orientata agli oggetti. Ancora più importante, un int da solo è solo uno scalare, quindi non ha alcun significato. Quando un metodo accetta un int come parametro, il nome del metodo deve fare tutto il lavoro per esprimere l'intento. Se lo stesso metodo accetta un'ora come parametro, è molto più facile vedere cosa sta succedendo.

Lo stesso documento spiega che esiste un ulteriore vantaggio:

Piccoli oggetti come Hour o Money ci danno anche un posto ovvio per mettere comportamenti che altrimenti sarebbero stati disseminati attorno ad altre classi .

Infatti, quando si usano le primitive, di solito è estremamente difficile tenere traccia della posizione esatta del codice correlato a quei tipi, spesso portando a una duplicazione del codice grave . Se esiste una Price: Moneyclasse, è naturale trovare il controllo della gamma all'interno. Se, invece, viene utilizzato un int(peggio, a double) per memorizzare i prezzi dei prodotti, chi dovrebbe validare l'intervallo? Il prodotto? Lo sconto? Il carrello?

Infine, un terzo vantaggio non menzionato nel documento è la possibilità di cambiare relativamente facilmente il tipo sottostante. Se oggi il mio ProductIdha shortcome tipo sottostante e successivamente devo usare intinvece, è probabile che il codice da modificare non si estenda all'intera base di codice.

Lo svantaggio - e lo stesso argomento si applica a ogni regola dell'esercizio di Calisthenics dell'oggetto - è che se diventa rapidamente troppo travolgente per creare una classe per tutto . Se Productcontiene ProductPricequali eredita da PositivePricecui eredita da Pricecui a sua volta eredita Money, questa non è un'architettura pulita, ma piuttosto un disordine completo in cui per trovare una singola cosa, un manutentore 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 almeno all'interno del wrapper:

  • Il getter di proprietà,
  • Il suo campo di appoggio,
  • Un costruttore che assegna il valore al campo di supporto,
  • Un'usanza ToString(),
  • Commenti sulla documentazione XML (che compongono molte righe),
  • A Equalse a GetHashCodeoverride (anche un sacco di LOC).

ed eventualmente, quando pertinente:

  • Un attributo DebuggerDisplay ,
  • Una sostituzione 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 la memorizzazione / caricamento di un valore in / da un database.

Un centinaio di LOC per un semplice wrapper lo rende abbastanza proibitivo, motivo per cui potresti essere completamente sicuro della redditività a lungo termine di tale wrapper. La nozione di portata spiegata da Thomas Junk è particolarmente rilevante qui. Scrivere un centinaio di LOC per rappresentare un ProductIdusato in tutta la tua base di codice sembra abbastanza utile. Scrivere una classe di queste dimensioni per un pezzo di codice che rende tre righe in un singolo metodo è molto più discutibile.

Conclusione:

  • Avvolgi le primitive in classi che hanno un significato in un dominio aziendale dell'applicazione quando (1) aiuta a ridurre gli errori, (2) riduce il rischio di duplicazione del codice o (3) aiuta a cambiare il tipo sottostante in un secondo momento.

  • Non racchiudere automaticamente tutte le primitive che trovi nel tuo codice: ci sono molti casi in cui l'utilizzo stringo intva perfettamente bene.

In pratica, in public string CreateNewThing(), restituire un'istanza di ThingIdclasse invece che stringpotrebbe aiutare, ma puoi anche:

  • Restituisce un'istanza di Id<string>classe, ovvero un oggetto di tipo generico che indica che il tipo sottostante è una stringa. Hai il vantaggio della tua leggibilità, senza l'inconveniente di dover mantenere molti tipi.

  • Restituisce un'istanza di Thingclasse. Se l'utente ha solo bisogno dell'ID, questo può essere fatto facilmente con:

    var thing = this.CreateNewThing();
    var id = thing.Id;
    

33
Penso che il potere di cambiare liberamente il tipo sottostante sia importante qui. È una stringa oggi, ma forse un GUID o domani a lungo. La creazione del wrapper di tipo nasconde tali informazioni portando a un minor numero di modifiche lungo la strada. ++ Buona risposta qui.
RubberDuck,

4
Nel caso di denaro, assicurati che la valuta sia inclusa nell'oggetto Money o che l'intero programma utilizzi lo stesso (che quindi dovrebbe essere documentato).
Paŭlo Ebermann,

5
@Blrfl: mentre ci sono casi validi in cui è necessaria un'eredità profonda, di solito vedo tale eredità nel codice sovrastampato scritto senza attenzione alla leggibilità, derivante dall'attitudine compulsiva pay-by-LOC. Quindi il carattere negativo di questa parte della mia risposta.
Arseni Mourzenko,

3
@ArTs: questo è l'argomento "condanniamo lo strumento perché alcune persone lo usano nel modo sbagliato".
Blrfl

6
@MainMa Un esempio per cui il significato avrebbe potuto evitare un errore costoso: en.wikipedia.org/wiki/Mars_Climate_Orbiter#Cause_of_failure
cbojar

60

Vorrei usare l' ambito come regola empirica: più stretto è lo scopo di generare e consumare tale values, meno probabilità è necessario creare un oggetto che rappresenti questo valore.

Supponi di avere il seguente pseudocodice

id = generateProcessId();
doFancyOtherstuff();
job.do(id);

quindi l'ambito è molto limitato e non vedrei alcun senso nel renderlo un tipo. Ma supponiamo che tu generi questo valore in un livello e lo passi a un altro livello (o anche a un altro oggetto), quindi avrebbe perfettamente senso creare un tipo per quello.


14
Un ottimo punto. I tipi espliciti sono uno strumento importante per comunicare l'intento , che è particolarmente importante oltre i confini di oggetti e servizi.
Avner Shahar-Kashtan

+1 anche se se non hai ancora eseguito il refactoring di tutto il codice doFancyOtherstuff()in una subroutine, potresti pensare che il job.do(id)riferimento non sia abbastanza locale da mantenere come una semplice stringa.
Mark Hurd,

Questo è assolutamente vero! È il caso in cui hai un metodo privato in cui sai che il mondo esterno non utilizzerà mai il metodo. Successivamente, puoi pensare un po 'di più quando la classe è protetta e successivamente quando la classe è pubblica ma non fa parte di un'API pubblica. Scope scope scope e molta arte per posizionare la linea nella buona posizione tra tipi molto primitivi e molto personalizzati.
Samuel,

34

I sistemi di tipo statico mirano a prevenire usi errati dei dati.

Ci sono ovvi esempi di tipi che fanno questo:

  • non puoi ottenere il mese di un UUID
  • non puoi moltiplicare due stringhe.

Ci sono esempi più sottili

  • non puoi pagare qualcosa usando la lunghezza della scrivania
  • non è possibile effettuare una richiesta HTTP utilizzando il nome di qualcuno come URL.

Potremmo essere tentati di utilizzare doublesia per il prezzo che per la lunghezza, oppure utilizzare stringa sia per un nome che per un URL. Ma farlo sovverte il nostro fantastico sistema di tipi e consente a questi abusi di superare i controlli statici della lingua.

Essere confuso da un chilo di secondo a Newton-secondi può avere scarsi risultati in fase di esecuzione .


Questo è particolarmente un problema con le stringhe. Spesso diventano il "tipo di dati universale".

Siamo abituati principalmente alle interfacce di testo con i computer e spesso estendiamo queste interfacce umane (UI) alle interfacce di programmazione (API). Pensiamo a 34.25 come ai personaggi 34.25 . Pensiamo a una data come ai personaggi 05-03-2015 . Pensiamo a un UUID come ai personaggi 75e945ee-f1e9-11e4-b9b2-1697f925ec7b .

Ma questo modello mentale danneggia l'astrazione delle API.

Le parole o la lingua, come sono scritte o parlate, non sembrano avere alcun ruolo nel mio meccanismo di pensiero.

Albert Einstein

Allo stesso modo, le rappresentazioni testuali non dovrebbero svolgere alcun ruolo nella progettazione di tipi e API. Attenti al string! (e altri tipi "primitivi" eccessivamente generici)


I tipi comunicano "quali operazioni hanno senso".

Ad esempio, una volta ho lavorato su un client a un'API REST HTTP. REST, eseguito correttamente, utilizza entità ipermediali, che hanno collegamenti ipertestuali che puntano a entità correlate. In questo client, non solo sono state digitate le entità (ad es. Utente, Account, Abbonamento), ma sono stati anche digitati i collegamenti a tali entità (UserLink, AccountLink, SubscriptionLink). I collegamenti erano poco più che involucri Uri, ma i tipi separati rendevano impossibile provare a usare un AccountLink per recuperare un Utente. Se tutto fosse stato semplice Uri, o peggio ancora, stringquesti errori si sarebbero trovati solo in fase di esecuzione.

Allo stesso modo, nella tua situazione, hai dati che vengono utilizzati per un solo scopo: identificare un Operation. Non dovrebbe essere usato per nient'altro e non dovremmo cercare di identificare Operations con stringhe casuali che abbiamo inventato. La creazione di una classe separata aggiunge leggibilità e sicurezza al tuo codice.


Naturalmente, tutte le cose buone possono essere usate in eccesso. Tener conto di

  1. quanta chiarezza aggiunge al tuo codice

  2. quanto spesso viene utilizzato

Se un "tipo" di dati (in senso astratto) viene usato frequentemente, per scopi distinti e tra interfacce di codice, è un ottimo candidato per essere una classe separata, a spese della verbosità.


21
"le stringhe spesso diventano il tipo di dati" universale "", questa è l'origine del moniker "linguaggi tipicamente digitati".
MSalters

@MSalters, ah ah, molto bello.
Paul Draper,

4
E ovviamente, cosa 05-03-2015 significa quando interpretato come una data ?
un CVn

10

Pensi che sia una buona pratica avvolgere tipi semplici in una classe per motivi di sicurezza?

A volte.

Questo è uno di quei casi in cui è necessario valutare i problemi che potrebbero derivare dall'uso di un stringinvece di uno più concreto OperationIdentifier. Qual è la loro gravità? Qual è la loro verosimiglianza?

Quindi è necessario considerare il costo dell'utilizzo dell'altro tipo. Quanto è doloroso usare? Quanto lavoro deve fare?

In alcuni casi, ti risparmierai tempo e fatica avendo un bel tipo concreto contro cui lavorare. In altri, non ne varrà la pena.

In generale, penso che questo genere di cose dovrebbe essere fatto più di quanto non sia oggi. Se hai un'entità che significa qualcosa nel tuo dominio, è bene averla come tipo proprio, poiché è più probabile che l'entità cambi / cresca con l'azienda.


10

Sono generalmente d'accordo sul fatto che molte volte dovresti creare un tipo per primitive e stringhe, ma poiché le risposte sopra suggeriscono di creare un tipo nella maggior parte dei casi, elencherò alcuni motivi per cui / quando non:

  • Prestazione. Devo fare riferimento alle lingue attuali qui. In C #, se un ID client è breve e lo racchiudi in una classe, hai appena creato un sacco di sovraccarico: memoria, poiché ora è 8 byte su un sistema a 64 bit e velocità da ora è allocato sull'heap.
  • Quando si fanno ipotesi sul tipo. Se un ID client è breve e hai una logica che lo comprime in qualche modo, di solito fai già ipotesi sul tipo. In tutti quei posti ora devi rompere l'astrazione. Se è un punto, non è un grosso problema, se è dappertutto, potresti trovare quella metà del tempo che stai usando la primitiva.
  • Perché non tutte le lingue hanno dattiloscritto. E per le lingue che non lo fanno e per il codice già scritto, apportare una modifica del genere potrebbe essere un grosso compito che potrebbe anche introdurre bug (ma tutti hanno una suite di test con una buona copertura, giusto?).
  • In alcuni casi riduce la leggibilità. Come lo stampa? Come posso convalidare questo? Devo controllare null? Sono tutte domande che richiedono di approfondire la definizione del tipo.

3
Sicuramente se il tipo di wrapper è uno struct piuttosto che una classe, allora un short(per esempio) ha lo stesso costo incartato o non imballato. (?)
MathematicalOrchid

1
Non sarebbe un buon progetto per un tipo incapsulare la propria convalida? O per creare un tipo di supporto per convalidarlo?
Nick Udell,

1
Imballare o mescolare inutilmente è un anti-modello. Le informazioni dovrebbero fluire in modo trasparente attraverso il codice dell'applicazione, tranne nei casi in cui la trasformazione è esplicitamente e realmente necessaria . I tipi sono buoni poiché in genere sostituiscono una piccola quantità di buon codice, situato in un unico posto, per grandi quantità di scarsa implementazione schizzata in tutto il sistema.
Thomas W,

7

No, non dovresti definire i tipi (classi) per "tutto".

Ma, come affermano altre risposte, è spesso utile farlo. Dovresti sviluppare - consapevolmente, se possibile - un sentimento o un senso di eccessivo attrito dovuto all'assenza di un tipo o di una classe adatti, mentre scrivi, collaudi e mantieni il tuo codice. Per me, l'inizio dell'eccessivo attrito è quando voglio consolidare più valori primitivi come un singolo valore o quando devo convalidare i valori (cioè determinare quale di tutti i possibili valori del tipo primitivo corrispondono a valori validi del 'tipo implicito').

Ho trovato considerazioni come ciò che hai posto nella tua domanda di essere responsabile di troppa progettazione nel mio codice. Ho preso l'abitudine di evitare deliberatamente di scrivere più codice del necessario. Spero che tu stia scrivendo buoni test (automatici) per il tuo codice - se lo sei, puoi facilmente riformattare il tuo codice e aggiungere tipi o classi se così facendo fornisci benefici netti per lo sviluppo e la manutenzione in corso del tuo codice .

La risposta di Telastyn e la risposta di Thomas Junk entrambi fanno un ottimo punto sul contesto e l'uso del relativo codice. Se stai usando un valore all'interno di un singolo blocco di codice (ad es. Metodo, loop, usingblocco), allora va bene usare solo un tipo primitivo. Va anche bene usare un tipo primitivo se stai usando un insieme di valori ripetutamente e in molti altri luoghi. Ma più frequentemente e ampiamente stai usando un insieme di valori, e meno strettamente tale insieme di valori corrisponde ai valori rappresentati dal tipo primitivo, più dovresti considerare l'incapsulamento dei valori in una classe o tipo.


5

In superficie sembra che tutto ciò che devi fare è identificare un'operazione.

Inoltre dici cosa dovrebbe fare un'operazione:

per avviare l'operazione [e] per monitorarne la fine

Lo dici in un modo come se questo fosse "solo come viene usata l'identificazione", ma direi che sono proprietà che descrivono un'operazione. A me sembra una definizione di tipo, c'è persino uno schema chiamato "schema di comando" che si adatta molto bene. .

Dice

il modello di comando [...] viene utilizzato per incapsulare tutte le informazioni necessarie per eseguire un'azione o attivare un evento in un secondo momento .

Penso che questo sia molto simile rispetto a quello che vuoi fare con le tue operazioni. (Confronta le frasi che ho messo in grassetto in entrambe le virgolette) Invece di restituire una stringa, restituisci un identificatore per un Operationin senso astratto, ad esempio un puntatore a un oggetto di quella classe in oop.

Per quanto riguarda il commento

Sarebbe wtf-y chiamare questa classe un comando e quindi non avere la logica al suo interno.

No non lo farebbe. Ricorda che i modelli sono molto astratti , in realtà così astratti da essere in qualche modo meta. Cioè, spesso astraggono la programmazione stessa e non alcuni concetti del mondo reale. Il modello di comando è un'astrazione di una chiamata di funzione. (o metodo) Come se qualcuno avesse premuto il pulsante Pausa subito dopo aver passato i valori dei parametri e subito prima dell'esecuzione, per essere ripreso in seguito.

Quanto segue sta prendendo in considerazione l'oop, ma la motivazione alla base dovrebbe valere per qualsiasi paradigma. Mi piace dare alcuni motivi per cui inserire la logica nel comando può essere considerato una cosa negativa.

  1. Se avessi tutta quella logica nel comando, sarebbe gonfia e disordinata. Penseresti "vabbè, tutte queste funzionalità dovrebbero essere in classi separate". Si potrebbe refactoring la logica di comando.
  2. Se avessi tutta quella logica nel comando, sarebbe difficile testare e metterti in mezzo durante il test. "Merda, perché devo usare questo comando senza senso? Voglio solo testare se questa funzione sputa o meno 1, non voglio chiamarlo più tardi, voglio testarlo ora!"
  3. Se avessi tutta quella logica nel comando, non avrebbe molto senso riportare indietro al termine. Se si pensa a una funzione da eseguire in modo sincrono per impostazione predefinita, non ha senso ricevere una notifica al termine dell'esecuzione. Forse stai avviando un altro thread perché la tua operazione è in realtà il rendering di avatar in formato cinema (potrebbe essere necessario un thread aggiuntivo su raspberry pi), forse devi recuperare alcuni dati da un server, ... qualunque cosa sia, se c'è una ragione per riferire quando finito, potrebbe essere perché c'è un po 'di asincrono (è una parola?) in corso. Penso che eseguire un thread o contattare un server sia una logica che non dovrebbe essere al tuo comando. Questo è in qualche modo un meta argomento e forse controverso. Se pensi che non abbia senso, per favore dimmelo nei commenti.

Ricapitolando: il modello di comando consente di avvolgere la funzionalità in un oggetto, da eseguire in seguito. Per motivi di modularità (la funzionalità esiste indipendentemente dall'esecuzione tramite comando o meno), la testabilità (la funzionalità deve essere verificabile senza comando) e tutte quelle altre parole che esprimono essenzialmente la richiesta di scrivere un buon codice, non si inserirà la logica effettiva nel comando.


Poiché i modelli sono astratti, può essere difficile inventare buone metafore del mondo reale. Ecco un tentativo:

"Ehi nonna, potresti per favore premere il pulsante di registrazione sulla TV alle 12, quindi non mi mancheranno i simpson sul canale 1?"

Mia nonna non sa cosa succede tecnicamente quando preme quel pulsante di registrazione. La logica è altrove (nella TV). E questa è una buona cosa. La funzionalità è incapsulata e le informazioni sono nascoste dal comando, è un utente dell'API, non necessariamente parte della logica ... oh, accidenti, sto ancora chiacchierando di parole, meglio finire ora questa modifica.


Hai ragione, non ci ho pensato in quel modo. Ma il modello di comando non è adatto al 100% perché la logica dell'operazione è incapsulata in un'altra classe e non posso inserirlo all'interno dell'oggetto che è il comando. Sarebbe wtf-y chiamare questa classe un comando e quindi non avere la logica al suo interno.
Ziv,

Questo ha molto senso. Considera di restituire un'operazione, non un OperationIdentifier. Questo è simile al CPL TPL in cui si restituisce un'attività o un'attività <T>. @Ziv: dici "la logica dell'operazione è incapsulata in un'altra classe". Ma questo significa davvero che non puoi fornirlo anche da qui?
Moby Disk

@Ziv Mi permetto di dissentire. Dai un'occhiata ai motivi che ho aggiunto alla mia risposta e vedi se hanno senso per te. =)
null

5

Idea dietro il confezionamento di tipi primitivi,

  • Per stabilire una lingua specifica del dominio
  • Limitare gli errori commessi dagli utenti passando valori errati

Ovviamente sarebbe molto difficile e non pratico farlo ovunque, ma è importante avvolgere i tipi dove richiesto,

Ad esempio se hai la classe Order,

Order
{
   long OrderId { get; set; }
   string InvoiceNumber { get; set; }
   string Description { get; set; }
   double Amount { get; set; }
   string CurrencyCode { get; set; }
}

Le proprietà importanti per cercare un ordine sono principalmente OrderId e InvoiceNumber. E l'Importo e il CurrencyCode sono strettamente correlati, dove se qualcuno cambia il CurrencyCode senza cambiare l'Importo, l'ordine non può più essere considerato valido.

Quindi, in questo scenario ha senso solo il wrapping di OrderId, InvoiceNumber e l'introduzione di un composito per la valuta, e probabilmente il wrapping della descrizione non ha senso. Quindi il risultato preferito potrebbe apparire come

    Order
    {
       OrderIdType OrderId { get; set; }
       InvoiceNumberType InvoiceNumber { get; set; }
       string Description { get; set; }
       Currency Amount { get; set; }
    }

Quindi non c'è motivo di avvolgere tutto, ma le cose che contano davvero.


4

Opinione impopolare:

Di solito non dovresti definire un nuovo tipo!

Definire un nuovo tipo per avvolgere una primitiva o una classe di base è talvolta chiamato definire uno pseudo-tipo. IBM lo descrive come una cattiva pratica qui (si concentrano specificamente sull'uso improprio dei generici in questo caso).

I pseudo tipi rendono inutili le funzionalità di libreria comuni.

Le funzioni matematiche di Java possono funzionare con tutte le primitive numeriche. Ma se si definisce una nuova classe Percentuale (racchiudendo un doppio che può essere compreso nell'intervallo 0 ~ 1) tutte queste funzioni sono inutili e devono essere racchiuse da classi che (anche peggio) devono conoscere la rappresentazione interna della classe Percentuale .

Gli pseudo tipi diventano virali

Quando crei più librerie, spesso scoprirai che questi pseudotipi diventano virali. Se usi la classe Percentuale sopra menzionata in una libreria, dovrai convertirla al limite della libreria (perdendo tutta la sicurezza / significato / logica / altra ragione che hai avuto per creare quel tipo) o dovrai creare queste classi accessibile anche all'altra biblioteca. Infettare la nuova libreria con i tuoi tipi in cui un semplice doppio avrebbe potuto essere sufficiente.

Porta via un messaggio

Finché il tipo che avvolgi non ha bisogno di molta logica di business, consiglierei di non inserirlo in una pseudo classe. Dovresti avvolgere una classe solo se ci sono seri vincoli aziendali. In altri casi, la denominazione corretta delle variabili dovrebbe fare molto per comunicare il significato.

Un esempio:

A uintpuò rappresentare perfettamente un ID utente, possiamo continuare a utilizzare gli operatori integrati di Java per uints (come ==) e non abbiamo bisogno della logica aziendale per proteggere lo "stato interno" dell'ID utente.


Essere d'accordo. tutta la filosofia di programmazione va bene, ma in pratica devi farlo funzionare anche
Ewan

Se hai visto pubblicità per i prestiti alle persone più povere del Regno Unito, scoprirai che la percentuale per avvolgere un doppio nell'intervallo 0..1 non funziona ...
gnasher729

1
In C / C ++ / Obiettivo C puoi usare un typedef, che ti dà solo un nuovo nome per un tipo primitivo esistente. D'altra parte, il tuo codice dovrebbe funzionare indipendentemente dal tipo sottostante, ad esempio se cambio OrderId da uint32_t a uint64_t (perché devo elaborare più di 4 miliardi di ordini).
gnasher729,

Non ti voterò per il tuo coraggio, ma gli pseudotipi e i tipi definiti nella risposta anticipata sono diversi. Questi sono tipi specifici per l'azienda. Gli psuedotipi sono tipi ancora generici.
0

@ gnasher729: C ++ 11 ha anche valori letterali definiti dall'utente che rendono il processo di avvolgimento molto dolce per l'utente: Distanza d = 36.0_mi + 42.0_km; . msdn.microsoft.com/en-us/library/dn919277.aspx
damix911

3

Come per tutti i suggerimenti, sapere quando applicare le regole è l'abilità. Se si creano i propri tipi in una lingua basata sul tipo, si ottiene il controllo del tipo. Quindi generalmente sarà un buon piano.

NamingConvention è la chiave per la leggibilità. I due combinati possono trasmettere chiaramente l'intenzione.
Ma /// è ancora utile.

Quindi sì, direi di creare molti tipi propri quando la loro vita è oltre un limite di classe. Considera anche l'uso di entrambi Structe Class, non sempre della classe.


2
Cosa ///significa?
Gabe,

1
/// è il commento di documentazione in C # che consente a intellisense di mostrare la documentazione della firma nel punto del metodo di chiamata. Considera il modo migliore per documentare il codice. Può essere coltivato con strumenti e offrire un comportamento di intellisenza. msdn.microsoft.com/en-us/library/tkxs89c5%28v=vs.71%29.aspx
phil soady
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.