Mai rendere i membri pubblici virtuali / astratti - davvero?


20

Negli anni 2000 un mio collega mi ha detto che è un anti-schema rendere virtuali o astratti i metodi pubblici.

Ad esempio, ha considerato una classe come questa non ben progettata:

public abstract class PublicAbstractOrVirtual
{
  public abstract void Method1(string argument);

  public virtual void Method2(string argument)
  {
    if (argument == null) throw new ArgumentNullException(nameof(argument));
    // default implementation
  }
}

Lo ha affermato

  • lo sviluppatore di una classe derivata che implementa Method1e sostituisce Method2deve ripetere la convalida dell'argomento.
  • nel caso in cui lo sviluppatore della classe base decida di aggiungere qualcosa intorno alla parte personalizzabile Method1o Method2successiva, non può farlo.

Invece il mio collega ha proposto questo approccio:

public abstract class ProtectedAbstractOrVirtual
{
  public void Method1(string argument)
  {
    if (argument == null) throw new ArgumentNullException(nameof(argument));
    this.Method1Core(argument);
  }

  public void Method2(string argument)
  {
    if (argument == null) throw new ArgumentNullException(nameof(argument));
    this.Method2Core(argument);
  }

  protected abstract void Method1Core(string argument);

  protected virtual void Method2Core(string argument)
  {
    // default implementation
  }
}

Mi ha detto che rendere i metodi (o le proprietà) pubblici virtuali o astratti è negativo quanto rendere pubblici i campi. Se si racchiudono i campi in proprietà, è possibile intercettare qualsiasi accesso a tali campi in un secondo momento, se necessario. Lo stesso vale per i membri pubblici virtuali / astratti: il loro avvolgimento come mostrato nella ProtectedAbstractOrVirtualclasse consente allo sviluppatore della classe base di intercettare tutte le chiamate che vanno ai metodi virtuali / astratti.

Ma non lo vedo come una linea guida di progettazione. Anche Microsoft non lo segue: dai un'occhiata alla Streamclasse per verificarlo.

Cosa ne pensi di quella linea guida? Ha senso o pensi che stia complicando eccessivamente l'API?


5
La creazione di metodi virtual consente l'override facoltativo. Il tuo metodo dovrebbe probabilmente essere pubblico, perché potrebbe non essere ignorato. Fare metodi abstract ti costringe a scavalcarli; probabilmente dovrebbero esserlo protected, perché non sono particolarmente utili in un publiccontesto.
Robert Harvey,

4
In realtà, protectedè molto utile quando si desidera esporre membri privati ​​della classe astratta a classi derivate. In ogni caso, non sono particolarmente preoccupato per l'opinione del tuo amico; scegli il modificatore di accesso che ha più senso per la tua situazione particolare.
Robert Harvey,

4
Il tuo collega stava sostenendo il Modello Metodo Modello . Possono esserci casi d'uso per entrambi i modi, a seconda di quanto siano interdipendenti i due metodi.
Greg Burghardt,

8
@GregBurghardt: sembra che il collega del PO suggerisca di utilizzare sempre il modello di metodo del modello, indipendentemente dal fatto che sia richiesto o meno. Questo è un tipico uso eccessivo di modelli: se uno ha un martello, prima o poi ogni problema inizia a sembrare un chiodo ;-)
Doc Brown

3
@PeterPerot: non ho mai avuto problemi a iniziare con semplici DTO con soli campi pubblici e quando un tale DTO ha richiesto membri con una logica aziendale, li ha trasformati in classi con proprietà. Sicuramente le cose sono diverse quando uno lavora come venditore di biblioteche e deve preoccuparsi di non cambiare le API pubbliche, quindi anche trasformare un campo pubblico in una proprietà pubblica con lo stesso nome può causare problemi.
Doc Brown,

Risposte:


30

Detto

che è un anti-pattern per rendere i metodi pubblici virtuali o astratti a causa dello sviluppatore di una classe derivata che implementa Method1 e sovrascrive Method2 deve ripetere la convalida dell'argomento

sta mescolando causa ed effetto. Presuppone che ogni metodo scavalcabile richiede una convalida argomento non personalizzabile. Ma è esattamente il contrario:

Se si vuole progettare un metodo in un modo in cui fornisce alcune convalide degli argomenti fissi in tutte le derivazioni della classe (o - più in generale - un personalizzabili e una parte non personalizzabile), quindi ha senso per fare il punto di ingresso non virtuale e fornisce invece un metodo virtuale o astratto per la parte personalizzabile che viene chiamata internamente.

Ma ci sono molti esempi in cui ha perfettamente senso avere un metodo virtuale pubblico, dal momento che non esiste una parte fissa non personalizzabile: guardare metodi standard come ToStringo Equalso GetHashCode- migliorerebbe il design della objectclasse per avere questi non pubblici e virtuale allo stesso tempo? Io non la penso così.

O, in termini di codice proprio: quando il codice nella classe base finalmente e intenzionalmente appare così

 public void Method1(string argument)
 {
    // nothing to validate here, all strings including null allowed
    this.Method1Core(argument);
 }

avere questa separazione tra Method1e Method1Corecomplica le cose senza una ragione apparente.


1
Nel caso del ToString()metodo, Microsoft avrebbe reso meglio non virtuale e introdotto un metodo modello virtuale ToStringCore(). Perché: per questo motivo: ToString()- nota per gli eredi . Dichiarano che ToString()non dovrebbe restituire null. Avrebbero potuto forzare questa richiesta implementando ToString() => ToStringCore() ?? string.Empty.
Peter Perot,

9
@PeterPerot: quella linea guida a cui hai collegato raccomanda anche di non tornare string.Empty, hai notato? E raccomanda molte altre cose che non possono essere applicate nel codice introducendo qualcosa come un ToStringCoremetodo. Quindi questa tecnica non è probabilmente lo strumento giusto per ToString.
Doc Brown,

3
@Theraot: sicuramente si possono trovare alcuni motivi o argomenti per cui ToString e Equals o GetHashcode avrebbero potuto essere progettati in modo diverso, ma oggi sono come sono (almeno penso che il loro design sia abbastanza buono per fare un buon esempio).
Doc Brown,

1
@PeterPerot "Ho visto un sacco di codice come anyObjectIGot.ToString()invece di anyObjectIGot?.ToString()" - come è rilevante? Il tuo ToStringCore()approccio impedirebbe la restituzione di una stringa null, ma genererebbe comunque un NullReferenceExceptionse l'oggetto è null.
Imil

1
@PeterPerot Non stavo cercando di trasmettere un argomento dall'autorità. Non è così tanto che utilizza Microsoft public virtual, ma ci sono casi in cui public virtualè ok. Potremmo discutere per una parte vuota non personalizzabile come rendere il codice a prova di futuro ... Ma questo non funziona. Tornare indietro e modificarlo potrebbe interrompere i tipi derivati. Pertanto, non guadagna nulla.
Theraot,

6

Farlo nel modo suggerito dal collega offre maggiore flessibilità all'implementatore della classe base. Ma da ciò deriva anche una maggiore complessità che in genere non è giustificata dai presunti benefici.

Ricorda che la maggiore flessibilità per l'implementatore della classe base viene a scapito di una minore flessibilità per la parte prevalente. Ricevono un comportamento imposto che potrebbe non interessare particolarmente. Per loro le cose sono appena diventate più rigide. Questo può essere giustificato e utile, ma tutto dipende dallo scenario.

La convenzione di denominazione per implementare questo (che io conosco) è di riservare il buon nome per l'interfaccia pubblica e di aggiungere il prefisso del metodo interno a "Do".

Un caso utile è quando l'azione eseguita necessita di qualche impostazione e di chiusura. Come aprire un flusso e chiuderlo dopo aver eseguito l'overrider. In generale, stesso tipo di inizializzazione e finalizzazione. È un modello valido da usare, ma sarebbe inutile imporne l'uso in tutti gli scenari astratti e virtuali.


1
Il prefisso del metodo Do è un'opzione. Microsoft utilizza spesso il metodo Postfix Core .
Peter Perot,

@Peter Perot. Non ho mai visto il prefisso Core in alcun materiale Microsoft, ma ciò potrebbe essere dovuto al fatto che ultimamente non ho prestato molta attenzione. Sospetto che abbiano iniziato a farlo di recente solo per promuovere il moniker Core, per farsi un nome per .NET Core.
Martin Maat,

No, è un vecchio cappello: BindingList. Inoltre ho trovato la raccomandazione da qualche parte, forse le loro Linee guida per la progettazione del framework o qualcosa di simile. E: è un postfisso . ;-)
Peter Perot,

Il punto è meno flessibilità per la classe derivata. La classe base è un confine di astrazione. La classe base indica al consumatore cosa fa la sua API pubblica e definisce l'API richiesta per raggiungere tali obiettivi. Se una classe derivata può sovrascrivere un metodo pubblico nella classe base, si corre un rischio maggiore di violare il principio di sostituzione di Liskov.
Adrian McCarthy,

2

In C ++, questo è chiamato modello di interfaccia non virtuale (NVI). (C'era una volta, si chiamava Metodo Metodo. Ciò era confuso, ma alcuni degli articoli più vecchi hanno quella terminologia.) NVI è promossa da Herb Sutter, che ne ha scritto almeno alcune volte. Penso che uno dei primi sia qui .

Se ricordo bene, la premessa è che una classe derivata non dovrebbe cambiare ciò che fa la classe base ma come lo fa.

Ad esempio, una forma potrebbe avere un metodo Sposta per riposizionare la forma. Un'implementazione concreta (es. Come Square e Circles) non dovrebbe sovrascrivere direttamente Move, perché Shape definisce cosa significa Moving (a livello concettuale). Un quadrato potrebbe avere dettagli di implementazione della differenza rispetto a un cerchio in termini di come la posizione viene rappresentata internamente, quindi dovranno sovrascrivere alcuni metodi per fornire la funzionalità di spostamento.

In semplici esempi, ciò si riduce spesso a un Move pubblico che delega tutto il lavoro a un ReallyDoTheMove virtuale privato, quindi sembra un sacco di spese generali senza alcun vantaggio.

Ma questa corrispondenza uno a uno non è un requisito. Ad esempio, è possibile aggiungere un metodo Animate all'API pubblica di Shape e implementarlo chiamando ReallyDoTheMove in un ciclo. Si finisce con due API di metodi pubblici non virtuali che si basano entrambe su un metodo astratto privato. Le tue cerchie e i tuoi quadrati non devono svolgere alcun lavoro aggiuntivo, né l'animazione può essere ignorata .

La classe base definisce l'interfaccia pubblica utilizzata dai consumatori e definisce un'interfaccia di operazioni primitive di cui ha bisogno per implementare quei metodi pubblici. I tipi derivati ​​sono responsabili di fornire implementazioni di tali operazioni primitive.

Non sono a conoscenza di differenze tra C # e C ++ che potrebbero cambiare questo aspetto del design di classe.


Buona scoperta! Ora ricordo che ho scoperto esattamente che il tuo secondo link punta a (o una sua copia), negli anni 2000. Ricordo che stavo cercando ulteriori prove dell'affermazione del mio collega e che non ho trovato nulla nel contesto C #, ma C ++. Questo. È. Si! :-) Ma, tornato in terra C #, sembra che questo schema non sia molto usato. Forse le persone hanno capito che l'aggiunta della funzionalità di base in un secondo momento può interrompere anche le classi derivate e che l'uso rigoroso di TMP o NVIP invece dei metodi virtuali pubblici non ha sempre senso.
Peter Perot,

Nota per sé: è utile sapere che questo modello ha un nome: NVIP.
Peter Perot,
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.