2009-06-03 8 views
9

Considerando il seguente codice di esempio:Pro e contro delle "nuove" proprietà in C#/.Net?

// delivery strategies 
public abstract class DeliveryStrategy { ... } 
public class ParcelDelivery : DeliveryStrategy { ... } 
public class ShippingContainer : DeliveryStrategy { ... } 

e la seguente classe di esempio Order:

// order (base) class 
public abstract class Order 
{ 
    private DeliveryStrategy delivery; 

    protected Order(DeliveryStrategy delivery) 
    { 
     this.delivery = delivery; 
    } 

    public DeliveryStrategy Delivery 
    { 
     get { return delivery; } 
     protected set { delivery = value; } 
    } 
} 

Quando ho derivare un nuovo tipo di classe di ordine, esso erediterà la proprietà di consegna di tipo DeliveryStrategy.

Ora, quando si è visto che CustomerOrders devono essere consegnati utilizzando la strategia ParcelDelivery, potremmo considerare 'nuovo' ing la proprietà di consegna nella classe OrdineCliente:

public class CustomerOrder : Order 
{ 
    public CustomerOrder() 
     : base(new ParcelDelivery()) 
    { } 

    // 'new' Delivery property 
    public new ParcelDelivery Delivery 
    { 
     get { return base.Delivery as ParcelDelivery; } 
     set { base.Delivery = value; } 
    } 
} 

(L'OrdineCliente necessita ovviamente di assicurarsi che sia compatibile (polimorfo) con Ordine)

Ciò consente l'utilizzo diretto della strategia ParcelDelivery su CustomerOrder senza la necessità di eseguire il cast.

Prenderesti in considerazione l'utilizzo di questo modello? perché perché no?

Aggiornamento: ho trovato questo modello, invece di usare i generici, perché voglio usarlo per più proprietà. Non voglio utilizzare argomenti di tipo generico per tutte queste proprietà

+2

Finché il metodo di shadowing non ha precondizioni più rigide e non post-condizioni più deboli, ritengo che questa pratica sia perfettamente accettabile. –

+0

Nel primo caso, perché dovresti comunque eseguire il DeliveryStrategy? Il punto di utilizzo del modello di strategia è che i comportamenti implementano un'interfaccia e quindi hanno tutti gli stessi metodi, quindi non è necessario eseguire il cast. –

risposta

6

Penso che questo sia un buon modello.Rende più semplice l'uso esplicito dei tipi derivati ​​eliminando la necessità di eseguire il cast del risultato e non "spezza" il comportamento della classe base. In realtà, un modello simile è utilizzato in alcune classi della BCL, per esempio guardare la gerarchia di classe DbConnection:

  • DbConnection.CreateCommand() restituisce un DbCommand
  • SqlConnection.CreateCommand() nasconde l'implementazione di base utilizzando 'nuovo' per restituire un SqlCommand.
  • (altre implementazioni DbConnection fanno lo stesso)

Quindi, se si manipola l'oggetto di connessione tramite una variabile DbConnection, CreateCommand restituirà un DbCommand; se lo si manipola tramite una variabile SqlConnection, CreateCommand restituirà un SqlCommand, evitando il cast se lo si assegna a una variabile SqlCommand.

+0

Accettato come risposta, per mostrare casi simulati dal framework .Net (anche se non usano 'new' in SqlConnection!) –

+1

"sebbene non usino 'new' in SqlConnection": in realtà sono quasi certo che loro do: è l'unico modo per definire un metodo con lo stesso nome e parametro della classe base, ma con un tipo di ritorno diverso. Immagino che tu abbia controllato con Reflector, ma il codice mostrato da Reflector non è il vero codice sorgente: può solo mostrare ciò che è stato effettivamente generato in IL, e apparentemente il 'nuovo' modificatore non emette alcun IL (o almeno Reflector non lo fa lo mostro). –

+0

l'uso di nuovo nel codice sorgente impedisce semplicemente al compilatore di avvertirti che lo sta facendo comunque per te (che potrebbe cambiare ovviamente) – ShuggyCoUk

5

È possibile utilizzare i generici.

// order (base) class 
public abstract class Order<TDeliveryStrategy> where TDeliveryStrategy : DeliveryStrategy 
{ 
    private TDeliveryStrategy delivery; 

    protected Order(TDeliveryStrategy delivery) 
    { 
     this.delivery = delivery; 
    } 

    public TDeliveryStrategy Delivery 
    { 
     get { return delivery; } 
     protected set { delivery = value; } 
    } 
} 

public class CustomerOrder : Order<ParcelDelivery> 
{ 
    public CustomerOrder() 
     : base(new ParcelDelivery()) 
    { } 
} 
11

Preferirei fare il tipo generico:

public abstract class Order<TDelivery> where TDelivery : Delivery 
{ 
    public TDelivery Delivery { ... } 
    ... 
} 

public class CustomerOrder : Order<ParcelDelivery> 
{ 
    ... 
} 

questo modo si garantisce la sicurezza di tipo a tempo di compilazione, invece di lasciarlo fino a tempo di esecuzione. Inoltre impedisce la situazione di:

CustomerOrder customerOrder = new CustomerOrder(); 
Order order = customerOrder; 
order.Delivery = new NonParcelDelivery(); // Succeeds! 

ParcelDelivery delivery = customerOrder.Delivery; // Returns null 

Ouch.

Io considero new come di solito un'ultima risorsa. Introduce una maggiore complessità sia in termini di implementazione che di utilizzo.

Se non si vuole scendere lungo il percorso generico, introdurrei una proprietà veramente nuova (con un nome diverso).

+0

Non riesco ad aspettare C# 4 con controvarianza nei tipi restituiti. – erikkallen

+0

Tipi di restituzione covarianti, tipi di parametri controvarianti :) Tuttavia, questo è solo per le interfacce e solo in determinati scenari, per quanto ne so ... –

+0

@Jon: ho reso protetto la proprietà setter, per questo caso esatto. La classe base e la classe derivata devono collaborare per eliminare questo problema. –

1

C'è qualche motivo per cui è necessario modificare il tipo di reso? Se non c'è, allora vorrei suggerire solo facendo la proprietà di consegna virtuale in modo che deve essere definito per classi ereditate invece:

public abstract class Order 
{ 
    protected Order(DeliveryStrategy delivery) 
    { 
     Delivery = delivery; 
    } 

    public virtual DeliveryStrategy Delivery { get; protected set; } 
} 

public class CustomerOrder : Order 
{ 
    public CustomerOrder() 
     : base(new ParcelDelivery()) 
    { } 

    public DeliveryStrategy Delivery { get; protected set; } 
} 

Se si richiede il cambiamento di tipo di ritorno, allora mi chiedo perché si farebbe bisogno di quella drastica modifica di un comportamento sul tipo di ritorno. Indipendentemente da ciò, se è così, allora questo non funzionerà per voi.

Quindi, per rispondere direttamente alla tua domanda, utilizzerei solo lo schema che hai descritto se è necessario che il tipo di ritorno sia diverso dalla classe base e molto parsimonioso (vorrei analizzare il mio modello a oggetti per vedere se c'è qualcos'altro che potrei fare prima). Se questo non è il caso, allora userei lo schema che ho descritto sopra.

3

L'utilizzo della parola chiave 'new' per nascondere le proprietà scrivibili dalla classe base è una cattiva idea secondo me. La nuova parola chiave ti consente di nascondere un membro di una classe base in una classe derivata, piuttosto che sovrascriverla. Ciò significa che le chiamate a tali membri che utilizzano un riferimento di classe base continuano ad accedere al codice della classe base, non al codice di classe derivato. C# ha la parola chiave 'virtuale', che consente alle classi derivate di implementare l'implementazione in realtà a override, anziché semplicemente nasconderlo. C'è un articolo ragionevolmente buono here che parla delle differenze.

Nel tuo caso, sembra che tu stia cercando di usare il metodo nascosto come modo per introdurre property covariance in C#. Tuttavia, ci sono problemi con questo approccio.

Spesso, il valore di una classe base è consentire agli utenti del codice di trattare i tipi in modo polimorfico. Cosa succede con la tua implementazione se qualcuno imposta la proprietà Delivery usando un riferimento alla classe base? La classe derivata si interromperà se la proprietà Delivery non è un'istanza di ParcelDelivery? Questi sono i tipi di domande che devi porci su questa scelta di implementazione.

Ora, se la proprietà Delivery nella classe base non ha fornito un setter, si ha una situazione leggermente diversa. Gli utenti della classe base possono solo recuperare la proprietà e non impostarla. Poiché instradi la proprietà all'accesso alla classe base, l'accesso tramite la classe base continua a funzionare. Tuttavia, se la classe derivata non è sigillata, le classi che la ereditano potrebbero introdurre gli stessi tipi di problemi nascondendo la proprietà Delivery con una propria versione.

Come alcuni degli altri post hanno già menzionato, è possibile utilizzare i generici come metodo per ottenere diversi tipi di proprietà di consegna. L'esempio di Jon è abbastanza bravo a dimostrarlo. C'è un problema con l'approccio dei generici, se è necessario derivare da CustomerOrder e modificare la proprietà Delivery in un nuovo tipo.

C'è un'alternativa ai generici. Devi considerare se vuoi davvero una proprietà impostabile nel tuo caso. Se ti sbarazzi del setter nella proprietà Delivery, i problemi introdotti usando la classe Order vanno direttamente via. Poiché si imposta la proprietà di consegna utilizzando i parametri del costruttore, è possibile garantire che tutti gli ordini abbiano il giusto tipo di strategia.

1

considerare questo approccio:

public interface IOrder 
{ 
    public DeliveryStrategy Delivery 
    { 
     get; 
    } 
} 

// order (base) class 
public abstract class Order : IOrder 
{ 
    protected readonly DeliveryStrategy delivery; 

    protected Order(DeliveryStrategy delivery) 
    { 
     this.delivery = delivery; 
    } 

    public DeliveryStrategy Delivery 
    { 
     get { return delivery; } 
    } 
} 

quindi utilizzare

public class CustomerOrder : Order 
{ 
    public CustomerOrder() 
     : base(new ParcelDelivery()) 
    { } 

    public ParcelDelivery Delivery 
    { 
     get { return (ParcelDelivery)base.Delivery; } 
    } 


    DeliveryStrategy IOrder.Delivery 
    { 
     get { return base.Delivery} 
    } 
} 

Questa è ancora lontano dall'essere perfetto (il tuo esempio non mostra perché classe di base ha bisogno di conoscere la strategia di consegna del tutto e sarebbe molto più sensato essere generico con un vincolo ma questo almeno ti permette di usare lo stesso nome per la proprietà e ottenere la sicurezza del tipo

Come nel tuo esempio è stato inutile, se qualcosa è mai non il tipo giusto non dovresti mascherarlo con null che dovresti lanciare quando la tua invariante è stata violata.

i campi di sola lettura sono sempre preferibili laddove possibile. Rendono chiara l'immutabilità.

+0

Mi piace l'alternativa offerta dalle interfacce, ma credo che questo modello renda il modello più complesso (con due proprietà di consegna sulla stessa classe). –

+0

In assenza di variazioni di co/contr, devi farlo tu stesso dal consumatore (diversi nomi/cast) o dal fornitore (due proprietà reciproche come la mia o ombreggiate) In tutti gli scenari del provider ci saranno due proprietà (semplicemente non la vedi con 'new') – ShuggyCoUk

1

La tua soluzione non fa quello che pensi che faccia. Sembra funzionare ma non chiama il tuo metodo "nuovo". Considerate le seguenti modifiche al codice per aggiungere un po 'di uscita per vedere quale metodo si chiama:

// order (base) class 
public abstract class Order 
{ 
    private DeliveryStrategy delivery; 

    protected Order(DeliveryStrategy delivery) 
    { 
     this.delivery = delivery; 
    } 

    public DeliveryStrategy Delivery 
    { 
     get 
     { 
      Console.WriteLine("Order"); 
      return delivery; 
     } 
     protected set { delivery = value; } 
    } 
} 

public class CustomerOrder : Order 
{ 
    public CustomerOrder() 
     : base(new ParcelDelivery()) 
    { } 

    // 'new' Delivery property 
    public new ParcelDelivery Delivery 
    { 
     get 
     { 
      Console.WriteLine("CustomOrder"); 
      return base.Delivery as ParcelDelivery; 
     } 
     set { base.Delivery = value; } 
    } 
} 

Poi il seguente frammento di codice di utilizzare effettivamente la classe CustomOrder:

Order o = new CustomerOrder(); 
var d = o.Delivery; 

Sarebbe uscita l' "Ordine ". Il nuovo metodo spezza specificamente il polimorfismo. Crea una nuova proprietà di consegna su CustomOrder che non fa parte della classe base dell'Ordine. Pertanto, quando si utilizza CustomOrder come se fosse un ordine, non si chiama la nuova proprietà Delivery poiché esiste solo su CustomOrder e non fa parte della classe Order.

Quello che stai cercando di fare è sovrascrivere un metodo che non è sovrascrivibile. Se vuoi dire la proprietà di essere override, renderlo astratto:

// order (base) class 
public abstract class Order 
{ 
    private DeliveryStrategy delivery; 

    protected Order(DeliveryStrategy delivery) 
    { 
     this.delivery = delivery; 
    } 

    public abstract DeliveryStrategy Delivery 
    { 
     get { return delivery; } 
     protected set { delivery = value; } 
    } 
} 

public class CustomerOrder : Order 
{ 
    public CustomerOrder() 
     : base(new ParcelDelivery()) 
    { } 

    public override ParcelDelivery Delivery 
    { 
     get { return base.Delivery as ParcelDelivery; } 
     set { base.Delivery = value; } 
    } 
} 
+0

Fa wat mi aspetto che faccia. Cambia Console.WriteLine ("Ordine") in Console.WriteLine (delivery.GetType(). GetName()) (o alcuni di essi) e vedrai ;-) –

+0

delivery.GetType() ti darà ParcelDelivery perché tu inoltrato al costruttore dell'Ordine, non perché stai chiamando la nuova proprietà Delivery della sottoclasse. Se si desidera utilizzare la riflessione per vedere dove ci si trova, quale metodo si sta effettivamente chiamando, modificare la linea di scrittura in: Console.WriteLine (System.Reflection.MethodBase.GetCurrentMethod(). DeclaringType.FullName); – ZombieDev

0

Utilizzando new a shadow membri virtuali di una classe base è una cattiva idea, perché i tipi di sub-derivati ​​non saranno in grado di ignorare in modo corretto. Se esistono membri della classe che si desidera ombreggiare nelle classi derivate, i membri della classe base non devono essere dichiarati come abstract o virtual, ma devono semplicemente chiamare un membro protected abstract o protected virtual. Un tipo derivato può ombreggiare il metodo della classe base con uno che chiama il membro appropriato protected e inserisce il risultato in modo appropriato.