Come implementare una proprietà sulla classe A che fa riferimento a una proprietà di un oggetto figlio della classe A


9

Abbiamo questo codice che, una volta semplificato, assomiglia a questo:

public class Room
{
    public Client Client { get; set; }

    public long ClientId
    {
        get
        {
            return Client == null ? 0 : Client.Id;
        }
    }
}

public class Client 
{
    public long Id { get; set; }
}

Ora abbiamo tre punti di vista.

1) Questo è un buon codice perché la Clientproprietà deve essere sempre impostata (cioè non nulla) in modo Client == nullche non si verifichi mai e il valore Id 0indica comunque un ID falso (questa è l'opinione dello scrittore del codice ;-))

2) Non puoi fare affidamento sul chiamante per sapere che 0è un valore falso per Ide quando la Clientproprietà deve sempre essere impostata dovresti inserire un exceptionnel getquando la Clientproprietà sembra essere nulla

3) Quando la Clientproprietà deve essere sempre impostata, è sufficiente tornare Client.Ide lasciare che il codice generi NullRefun'eccezione quando la Clientproprietà risulta essere nulla.

Quale di questi è più corretto? O c'è una quarta possibilità?


5
Non una risposta ma solo una nota: non gettare mai eccezioni dai vincitori di proprietà.
Brandon,

3
Per approfondire il punto di Brandon: stackoverflow.com/a/1488488/569777
MetaFight

1
Certo: l'idea generale ( msdn.microsoft.com/en-us/library/… , stackoverflow.com/questions/1488472/… , tra gli altri) è che le proprietà dovrebbero fare il meno possibile, essere leggere e dovrebbero rappresentare la pulizia , stato pubblico del tuo oggetto, con gli indicizzatori una leggera eccezione. È anche un comportamento imprevisto vedere eccezioni in una finestra di controllo per una proprietà durante il debug.
Brandon,

1
Non è necessario (è necessario) scrivere il codice che genera un getter di proprietà. Ciò non significa che è necessario nascondere e ingoiare eventuali eccezioni che si verificano a causa di un oggetto che entra in uno stato non supportato.
Ben

4
Perché non puoi semplicemente fare Room.Client.Id? Perché vuoi avvolgerlo in Room.ClientId? Se è per cercare un client, perché non qualcosa come Room.HasClient {get {return client == null; }} che è più semplice e consente comunque alle persone di fare il normale Room.Client.Id quando hanno effettivamente bisogno dell'id?
James,

Risposte:


25

Odora che dovresti limitare il numero di stati in cui la tua Roomclasse può trovarsi.

Il fatto stesso che stai chiedendo cosa fare quando Clientè null è un indizio che lo Roomspazio degli stati è troppo grande.

Per semplificare le cose, non permetterei che la Clientproprietà di qualsiasi Roomistanza sia mai nulla. Ciò significa che il codice all'interno Roompuò tranquillamente presumere che Clientnon sia mai nullo.

Se per qualche motivo in futuro Clientdiventa nullresistere all'impulso di sostenere quello stato. Ciò aumenterà la complessità della manutenzione.

Invece, consentire al codice di fallire e fallire velocemente. Dopotutto, questo non è uno stato supportato. Se l'applicazione si trova in questo stato, hai già attraversato una linea di non ritorno. L'unica cosa ragionevole da fare è quindi chiudere l'applicazione.

Ciò potrebbe accadere (in modo del tutto naturale) a seguito di un'eccezione di riferimento null non gestita.


2
Bello. Mi piace il pensiero "Per semplificare le cose, non permetterei che la proprietà Client di qualsiasi istanza Room sia mai nulla". È davvero meglio che chiedersi cosa fare quando è null
Michel,

@Michel se in seguito decidi di modificare tale requisito, puoi sostituire il nullvalore con un NullObject(o meglio NullClient) che funzionerebbe ancora con il tuo codice esistente e ti permetterà di definire il comportamento per un client inesistente, se necessario. La domanda è se il caso "no client" fa parte o meno della logica aziendale.
null,

3
Totalmente corretto. Non scrivere un singolo carattere di codice per supportare uno stato non supportato. Lascialo lanciare un NullRef.
Ben

@Ben Disagree; Le linee guida degli Stati membri affermano esplicitamente che i getter di proprietà non dovrebbero generare eccezioni. E permettere a null riferimenti di essere lanciati quando si potrebbe lanciare un'eccezione più significativa invece è semplicemente pigro.
Andy,

1
@Anche capire il motivo della linea guida ti permetterà di sapere quando non si applica.
Ben,

21

Solo alcune considerazioni:

a) Perché esiste un getter specifico per ClientId quando esiste già un getter pubblico per l'istanza del client stesso? Non vedo perché le informazioni che il ClientId è un longdevono essere scolpite nella pietra nella firma di Room.

b) Per quanto riguarda la seconda opinione, è possibile introdurre una costante Invalid_Client_Id.

c) Per quanto riguarda l'opinione uno e tre (ed essendo il mio punto principale): una stanza deve sempre avere un cliente? Forse è solo una semantica, ma questo non suona bene. Forse sarebbe più appropriato avere classi completamente separate per Room e Client e un'altra classe che le unisce. Forse appuntamento, prenotazione, occupazione? (Dipende da cosa stai effettivamente facendo.) E da quella classe puoi imporre i vincoli "deve avere una stanza" e "deve avere un cliente".


5
+1 per "Non vedo, ad esempio, perché le informazioni che il ClientId è lungo devono essere scolpite nella pietra nella firma di Room"
Michel

Il tuo inglese va bene. ;) Solo leggendo questa risposta, non avrei saputo che la tua lingua madre non è l'inglese senza che tu lo dicessi.
jpmc26,

I punti B e C sono buoni; RE: a, è un metodo conveniente, e il fatto che la stanza abbia un riferimento al Cliente potrebbe essere solo un dettaglio di implementazione (si potrebbe sostenere che il Cliente non dovrebbe essere pubblicamente visibile)
Andy

17

Non sono d'accordo con tutte e tre le opinioni. Se Clientnon puoi mai esserlo null, allora non renderlo nemmeno possibilenull !

  1. Imposta il valore di Clientnel costruttore
  2. Lancia un ArgumentNullExceptionnel costruttore.

Quindi il tuo codice sarebbe qualcosa del tipo:

public class Room
{
    private Client theClient;

    public Room(Client client) {
        if(client == null) throw new ArgumentNullException();
        this.theClient = client;
    }

    public Client Client { 
        get { return theClient; }
        set 
        {
            if (value == null) 
                throw new ArgumentNullException();
            theClient = value;
        }
    }

    public long ClientId
    {
        get
        {
            return theClient.Id;
        }
    }
}
public class Client 
{
    public long Id { get; set; }
}

Questo non è correlato al tuo codice, ma probabilmente sarebbe meglio usare una versione immutabileRoom creando theClient readonly, e quindi se il client cambia, creando una nuova stanza. Ciò migliorerà la sicurezza del thread del tuo codice oltre alla sicurezza nulla degli altri aspetti della mia risposta. Vedi questa discussione su mutable vs immutable


1
Questi non dovrebbero essere ArgumentNullExceptions?
Mathieu Guindon,

4
No. Il codice del programma non dovrebbe mai generare un NullReferenceException, viene generato dalla VM .Net "quando si tenta di dereferenziare un riferimento a un oggetto null" . Dovresti lanciare un ArgumentNullException, che viene lanciato "quando un riferimento null (Nothing in Visual Basic) viene passato a un metodo che non lo accetta come argomento valido".
Pharap,

2
@Pharap Sono un programmatore Java che tenta di scrivere codice C #, ho pensato che avrei fatto errori del genere.
durron597,

@ durron597 Avrei dovuto immaginarlo dall'uso del termine 'final' (in questo caso la parola chiave C # corrispondente è readonly). Avrei appena modificato, ma sentivo che un commento sarebbe servito a spiegare meglio il perché (per i futuri lettori). Se non avessi già dato questa risposta, darei la stessa identica soluzione. Anche l'immutabilità è una buona idea, ma non è sempre facile come si spera.
Pharap,

2
Le proprietà generalmente non dovrebbero generare eccezioni; invece di un setter che genera ArgumentNullException, fornire invece un metodo SetClient. Qualcuno ha pubblicato il link a una risposta in merito alla domanda.
Andy,

5

La seconda e la terza opzione dovrebbero essere evitate: il getter non dovrebbe colpire il chiamante con un'eccezione su cui non ha alcun controllo.

Dovresti decidere se il Cliente può mai essere nullo. In tal caso, è necessario fornire un modo per un chiamante di verificare se è nullo prima di accedervi (ad esempio, bool proprietà ClientIsNull).

Se decidi che il client non può mai essere nullo, rendilo un parametro richiesto al costruttore e genera l'eccezione se viene passato un null.

Infine, la prima opzione contiene anche un odore di codice. Dovresti lasciare che il Cliente gestisca la propria proprietà ID. Sembra eccessivo codificare un getter in una classe container che chiama semplicemente un getter sulla classe contenuta. Basta esporre il Cliente come una proprietà (altrimenti, finirai per duplicare tutto ciò che un Cliente già offre ).

long clientId = room.Client.Id;

Se il Cliente può essere nullo, allora dovrai almeno dare la responsabilità al chiamante:

if (room.Client != null){
    long clientId = room.Client.Id;
    /* other code follows... */
}

1
Penso che "finirai per duplicare tutto ciò che un cliente offre già" deve essere in grassetto o almeno in corsivo.
Pharap,

2

Se una proprietà client null è uno stato supportato, prendere in considerazione l'utilizzo di un oggetto NullObject .

Ma molto probabilmente questo è uno stato eccezionale, quindi dovresti rendere impossibile (o semplicemente non molto conveniente) finire con un null Client:

public Client Client { get; private set; }

public Room(Client client)
{
    Client = client;
}

public void SetClient(Client client)
{
    if (client == null) throw new ArgumentNullException();
    Client = client;
}

Se tuttavia non è supportato, non perdere tempo con soluzioni a metà cottura. In questo caso:

return Client == null ? 0 : Client.Id;

Hai "risolto" qui NullReferenceException ma a un prezzo eccezionale! Ciò costringerebbe tutti i chiamanti Room.ClientIda verificare 0:

var aRoom = new Room(aClient);
var clientId = aRoom.ClientId;
if (clientId == 0) RestartRoombookingWizard();
else ContinueRoombooking(clientid);

Bug strani possono insorgere con altri sviluppatori (o te stesso!) Dimenticando di controllare il valore di ritorno "ErrorCode" in un determinato momento.
Gioca sicuro e fallisci velocemente. Consenti a NullReferenceException di essere lanciato e "aggraziandolo" con grazia da qualche parte più in alto nello stack di chiamate invece di consentire al chiamante di rovinare ancora di più le cose ...

In una nota diversa, se sei così appassionato di esporre Cliente anche ClientIdpensare a TellDontAsk . Se chiedi troppo agli oggetti invece di dire loro cosa vuoi ottenere, potresti finire di accoppiare tutto insieme, rendendo il cambiamento più difficile in seguito.


Questo NotSupportedExceptionè usato in modo improprio, dovresti usare ArgumentNullExceptioninvece un .
Pharap,

1

A partire da c # 6.0, che ora è rilasciato, dovresti semplicemente fare questo,

public class Room
{
    public Room(Client client)
    {
        this.Client = client;
    }

    public Client Client { get; }
}

public class Client
{
    public Client(long id)
    {
        this.Id = id;
    }

    public long Id { get; }
}

Aumentare una proprietà di Clientto Roomè una palese violazione dell'incapsulamento e del principio DRY.

Se devi accedere a Iddi a Clientdi un Roomche puoi fare,

var clientId = room.Client?.Id;

Si noti l'uso del condizionale operatore Null , clientIdsarà un long?, se Clientè null, clientIdsarà null, in caso contrario clientIdavrà il valore di Client.Id.


ma il tipo di clientidè lungo nullable allora?
Michel,

@Michel bene, se una stanza deve avere un cliente, metti un controllo nullo nel ctor. Quindi var clientId = room.Client.Idsarà sia sicuro che long.
Jodrell,
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.