Metodo virtuale vuoto sui metodi astratti della classe base VS

4

Non sono riuscito a trovare una domanda che non fosse specifica per alcuni casi, quindi cercherò di renderla molto generica.

Abbiamo bisogno di una classe base di estrazione per un insieme di documenti, per esempio. Ogni documento ha le sue proprietà specifiche, ma in definitiva sono documenti. Quindi vogliamo fornire operazioni di estrazione comuni per tutti loro.

Anche se sono tutti documenti, come ho detto, sono in qualche modo diversi. Alcuni potrebbero avere alcune proprietà, ma alcuni potrebbero non farlo.

Immaginiamo di avere la classe astratta base Document e le classi FancyDocument e NotSoFancyDocument che ereditano da essa. Il FancyDocument ha un SectionA , il NotSoFancyDocument no.

Detto questo, cosa difenderebbe come il modo migliore di implementarlo? Ecco le due opzioni:

  • Svuota metodi virtuali nella classe base

I metodi virtuali vuoti nella classe base consentirebbero al programmatore di ignorare solo i metodi che hanno senso per i diversi tipi di documenti. Avremmo quindi un comportamento predefinito sulla classe base astratta, che restituirebbe default per i metodi, in questo modo:

public abstract class Document
{
    public virtual SectionA GetDocumentSectionA()
    {
        return default(SectionA);
    }
}

public class FancyDocument : Document
{
    public override SectionA GetDocumentSectionA()
    {
        // Specific implementation            
    }
}

public class NotSoFancyDocument : Document
{
    // Does not implement method GetDocumentSectionA because it doesn't have a SectionA
}
  • Metodi vuoti concreti o metodi concreti che lanciano un NotImplementedException

Poiché NotSoFancyDocument non ha un SectionA , ma gli altri lo fanno, potremmo semplicemente restituire default per il metodo in esso, oppure potrebbe lanciare un NotImplementedException . Ciò dipenderà dal modo in cui è stato scritto il programma e da altre cose. Potremmo inventare qualcosa di simile:

//// Return the default value

public abstract class Document
{
    public abstract SectionA GetDocumentSectionA();
}

public class FancyDocument : Document
{
    public override SectionA GetDocumentSectionA()
    {
        // Specific implementation
    }
}

public class NotSoFancyDocument : Document
{
    public override SectionA GetDocumentSectionA()
    {
        return default(SectionA);
    }
}

o

//// Throw an exception

public abstract class Document
{
    public abstract SectionA GetDocumentSectionA();
}

public class FancyDocument : Document
{
    public override SectionA GetDocumentSectionA()
    {
        // Specific implementation
    }
}

public class NotSoFancyDocument : Document
{
    public override SectionA GetDocumentSectionA()
    {
        throw new NotImplementedException("NotSoFancyDocument does not have a section A");
    }
}

Personalmente, penso che l'approccio al metodo astratto sia migliore, dal momento che significa "Ehi, ho bisogno che tu sia in grado di ottenere una SezioneA come documento. Non mi interessa come." mentre il metodo virtuale significa "Ehi, io ho questa SezioneA qui. Se non è abbastanza buono per te, sentiti libero di cambiare il modo in cui lo ottengo".

Penso che il primo sia un segno di odore di programmazione orientato agli oggetti.

Quali sono le tue opinioni su questo?

    
posta Smur 07.03.2014 - 19:55
fonte

3 risposte

4

In questo caso la classe base non dovrebbe sapere nulla di SectionA. La classe derivata dovrebbe implementare le proprietà extra di cui quel tipo ha bisogno.

Per alcune operazioni in cui un'altra classe deve estrarre informazioni indipendentemente dal tipo di documento, il metodo sulla classe base dovrebbe essere idealmente virtuale con un'implementazione di base e consentire alle classi derivate di sovrascriverlo se necessario (ad esempio ToPlainText che sarebbe solo uscita tutte le sezioni di un documento sarebbe su Document , FancyDocument può sovrascrivere l'implementazione per anche l'output SectionA ).

Per le istanze in cui un'altra classe non si preoccupa del tipo di documento, ma si preoccupa se ha certe proprietà che usano le interfacce. IDocument avrebbe tutte le sezioni comuni e Document lo implementerebbe. IDocumentWithSectionA sarebbe inheer IDocument e FancyDocument lo implementerebbe. Questo ti consente quindi di ricavare un altro NeitherFancyNorNotFancyDocument con SectionA che può anche implementare IDocumentsWithSectionA .

Ovviamente avresti nomi più utili di IDocumentWithSectionA , ma dipende dal caso d'uso.

TL; DR usa la classe astratta per ciò che dovrebbe essere comune a tutti i documenti e alcune funzionalità comuni, usa le interfacce come il contratto che dice cosa ha un documento.

    
risposta data 10.03.2014 - 11:30
fonte
0

Come indicato nei commenti, nessuna di queste opzioni è molto buona. Questo è probabilmente il motivo per cui stai facendo questa domanda.

La soluzione con metodi virtuali vuoti non fornisce informazioni su ciò che è supportato da un'istanza di documento specifica. Inoltre, ti costringe ad aggiungere tutti i metodi possibili nella classe base. Ciò impone l'aggiornamento di tutti i client come whel, quando si decide che la classe base necessita di metodi aggiuntivi.

La soluzione che genera eccezioni è forse anche peggiore, poiché i client delle classi di documenti dovranno gestire una possibile eccezione per ogni metodo. Ciò porta a un codice terribile e inaffidabile.

Il problema con cui stai lottando è che le classi di documenti non possono prevedere il risultato desiderato. Questa è la logica dell'applicazione e non dovrebbe essere implementata nel livello del modello. Almeno non in un modo che porta a un livello di modello riutilizzabile.

Suggerirei di utilizzare le interfacce per descrivere le serie di funzionalità necessarie. Esempio di Fo, un'interfaccia IDocument , un'interfaccia IFancy e un'interfaccia INotSoFancy . Queste interfacce possono derivare o meno da IDocument o un'altra interfaccia comune, dato che stai parlando di capabillities, non di oggetti.

Utilizzando questa soluzione basata sull'interfaccia, il codice client può determinare se l'istanza del documento specificato supporta una capacità capabitica desiderata (tentando di eseguire il cast dell'istanza del documento sull'interfaccia desiderata). Il codice cliente può agire di conseguenza.

    
risposta data 09.03.2014 - 10:23
fonte
0

Sono completamente d'accordo con la risposta di Chao e quindi non lo ripeterò.

Penso che il vero problema qui sia che è necessario porsi alcune domande più fondamentali sul dominio del problema. Ricorda che tutto il codice è scritto per risolvere un problema.

Perché stiamo scrivendo una classe base Document in primo luogo?

Perché deriviamo dalla classe Document ?

Che cosa speriamo di ottenere dal fatto di non ottenere una semplice interfaccia?

In questo caso, sembra che ci siano due esigenze di base. C'è la necessità di condividere il codice tra i tipi di documento e c'è bisogno di ottenere SectionA da FancyDocument s.

Esistono diversi modi per risolvere questo problema e ne hai scelti due. Un approccio alternativo che non hai considerato è quello di utilizzare la composizione anziché l'ereditarietà. In molti casi i problemi di ereditarietà sono il risultato di uno strumento sbagliato per il lavoro.

Considera quanto segue come opzione alternativa:

sealed class Document 
{ 
    // some stuff 
}

class Fancy
{
    public Document Document { get; set; }
    public SectionA GetDocumentSectionA();
    // other fancy methods.
}

class NotFancy
{
    public Document Document { get; set; }
    // other non-fancy methods.
}

Ora puoi vedere che la delineazione tra i tipi di documento è molto più chiara. Questo svantaggio di questo approccio è che non è possibile memorizzare entrambe le classi nello stesso contenitore Document strongmente tipizzato. Se questo è importante per te, allora potresti prendere in considerazione l'utilizzo di un'interfaccia o potresti invece utilizzare l'ereditarietà.

    
risposta data 10.04.2014 - 07:33
fonte