OOP: attivazione di tipi polimorfici e mantenimento dello stato in tutto il flusso di lavoro

4

Livello alto: Penso che il mio modello di progettazione sia difettoso. Nonostante implementando il polimorfismo, mi trovo a fare affidamento su istruzioni switch di grandi dimensioni basate sul tipo derivato all'interno della mia applicazione WPF. Dato che sto aggiungendo sempre più classi derivate dalla mia classe base astratta, non posso fare a meno di sentirmi come se il mio codice fosse brutto .. Codice di spaghetti se vuoi

Esempio: Diciamo che ho una classe base astratta chiamata: PDF.cs. Devo implementare 35 diverse classi derivate da PDF.cs. L'utente seleziona 1 una di queste classi derivate da un elenco a discesa nella pagina 1 dell'app. Mentre l'app procede da una pagina all'altra, l'utente aggiunge proprietà e valori unici al tipo di PDF che sta creando. Le pagine sono condivise tra i tipi, tuttavia vengono sottoposti a controlli diversi e vengono assegnate proprietà diverse a seconda del tipo selezionato nella pagina 1. All'utente è inoltre consentito navigare tra le pagine avanti e indietro, con lo stato di mantenimento dei dati.

La mia implementazione:

// Pdf was passed to this page, handling UI
public void UtilizeState(IPdfBase pdf) {
    switch (pdf.PdfType)
    {
        case PdfTypes.TypeA
            var aPdf = (a)pdf;
            this.CurrentPdf = aPdf;
            DoStuff_PdfA(aPdf);
            break;
        case PdfTypes.TypeB
            var bPdf = (b)pdf;
            this.CurrentPdf = bPdf;
            DoStuff_PdfB(bPdf);
            break;
    }

    // we could be moving backward, need to populate UI
    if(this.CurrentPdf.Reverse)
    {
        handleReverse(this.CurrentPdf);
    }
    else
    {
        handleNewPdf(this.CurrentPdf);
    {
}

// Moving to the next page
private void btnNext_Click(object sender, RoutedEventArgs e)
{       
        switch (CurrentPdf.PdfType)
        {
            case PdfType.TypeA:
                var aPdf = (PSI)CurrentPdf;
                Switcher.Switch(new UploadPdf(), aPdf);
                break;

            case PdfType.TypeB:
                var bPdf = (COD)CurrentPdf;
                Switcher.Switch(new UploadPdf2(), bPdf); // Some pdf's follow a different workflow..
                break;
        }
}

private void btnBack_Click(object sender, RoutedEventArgs e)
{
    // another switch...
}

Commenti: mano a mano che avanzo in questo progetto, le cose stanno diventando disordinate. Soprattutto con 3 enormi istruzioni switch per pagina, una per gestire i tipi di PDF in arrivo, un'altra per inviare i tipi di PDF all'indietro di una pagina, e un'altra ancora per inviare i tipi di PDF in avanti su una pagina. Il mio design è intrinsecamente sbagliato? Come gestiresti il passaggio di molti oggetti derivati, con regole aziendali e flussi di lavoro diversi, attraverso le pagine condivise?

    
posta Andrew Deacy 30.03.2017 - 00:30
fonte

2 risposte

4

Un suggerimento comune è mettere tutta la logica in ogni oggetto Pdf . Tuttavia, è necessario estendere l'interfaccia di Pdf e modificare l'implementazione di ogni oggetto Pdf ogni volta che si desidera supportare una nuova operazione. Richiede inoltre che ogni oggetto Pdf debba sapere tutto su come operare su di esso, piuttosto che sapere come memorizzare la propria rappresentazione. Il risultato è che si finisce con giganteschi oggetti Pdf che in pratica devono fare tutto.

Probabilmente vorrai utilizzare il Pattern visitatori per realizzare questo.

Modello visitatore standard

Aggiungi un nuovo metodo alla tua classe base Pdf :

class Pdf {
  ...
  public abstract void AcceptVisitor(IPdfVisitor visitor);
}

L'interfaccia IPdfVisitor è abbastanza semplice:

interface IPdfVisitor {
  void Visit(FirstPdf pdf);
  void Visit(SecondPdf pdf);
  void Visit(ThirdPdf pdf);
}

Ora, ogni oggetto classe Pdf implementato definisce la funzione AcceptVisitor allo stesso modo (ma è necessario scrivere l'implementazione in ogni classe).

class FirstPdf : Pdf {
  ...
  public override void AcceptVisitor(IPdfVisitor visitor) {
    visitor.Visit(this);
  }
}

Quindi, perché devi avere l'implementazione in ogni classe? Perché il compilatore conosce il tipo statico di ogni classe. Pertanto, quando chiami visitor.Visit(this) da un oggetto di tipo FirstPdf , chiamerà la funzione Visit che accetta un argomento di tipo FirstPdf . Ora, attui i visitatori per ogni "verbo" che vuoi implementare. Forse hai un SavePdfVisitor , un PrintPdfVisitor e un DisplayPdfVisitor , per esempio. Sembrerebbero così:

class SavePdfVisitor : IPdfVisitor {
  public void Visit(FirstPdf pdf) {
    // FirstPdf specific save logic
  }
  public void Visit(SecondPdf pdf) {
    // SecondPdf specific save logic
  }
  public void Visit(ThirdPdf pdf) {
    // ThirdPdf specific save logic
  }
}

Ora, il tuo metodo di salvataggio generico per Pdf è simile al seguente:

public void Save(Pdf pdf) {
  IPdfVisitor saveVisitor = new SavePdfVisitor();
  pdf.AcceptVisitor(saveVisitor);
}

L'oggetto pdf chiama Visit e passa al visitatore di salvataggio, senza bisogno di sapere quale tipo di operazione viene eseguita. Viene chiamato il metodo corretto di SavePdfVisitor. Come minimo, questo ti consente di mettere tutta la logica di salvataggio in un'unica posizione e utilizzare metodi privati per la funzionalità condivisa.

Un vantaggio di questo è che se si definisce un nuovo tipo di Pdf , quando si implementa il suo metodo AcceptVisitor , si otterrà un errore del compilatore a meno che non si aggiunga quel tipo all'interfaccia IPdfVisitor e quindi a ciascun degli oggetti visitatori. Questo garantisce che quando definisci un nuovo tipo di Pdf , non dimentichi di implementare la logica per tutte le operazioni che esegui su Pdf .

Pattern Visitatore migliore (nelle mie opinioni)

D'altra parte, se hai molta logica condivisa - per esempio, l'operazione di salvataggio è la stessa per molti tipi di Pdf ma è personalizzata per pochi - puoi definire i tuoi oggetti visitatori usando un classe base astratta invece di un'interfaccia come questa.

abstract class PdfVisitor {
  public abstract void Visit(Pdf pdf);
  public virtual void Visit(FirstPdf pdf) {
    Visit((Pdf)pdf);
  }
  public virtual void Visit(SecondPdf pdf) {
    Visit((Pdf)pdf);
  }
  public virtual void Visit(ThirdPdf pdf) {
    Visit((Pdf)pdf);
  }
}

class Pdf {
  ...
  public abstract void AcceptVisitor(PdfVisitor visitor);
}

class FirstPdf : Pdf {
  ...
  public override void AcceptVisitor(PdfVisitor visitor) {
    visitor.Visit(this);
  }
}
// Do the same for other Pdf classes

Tieni presente che PdfVisitor NON definisce l'implementazione di Visit(Pdf pdf) . Tuttavia, fornisce un'implementazione predefinita di Visit per tutti i tipi di implementazione concreta. Quella implementazione predefinita chiama solo Visit(Pdf pdf) . Quando implementi Visit(Pdf pdf) in ciascuno dei tuoi oggetti visitatore concreti, ciò definisce il comportamento predefinito per quel visitatore o azione. Ad esempio, supponi che DisplayPdfVisitor utilizzi la stessa logica per ogni tipo di Pdf . Lo implementa in questo modo:

class DisplayPdfVisitor : PdfVisitor {
  public override void Visit(Pdf pdf) {
    // Display logic here
  }
}

D'altra parte, se SavePdfVisitor fa la stessa cosa per tutti gli oggetti Pdf , ad eccezione del fatto che SecondPdf ha bisogno di un'implementazione speciale, fai questo:

class SavePdfVisitor : PdfVisitor {
  public override void Visit(Pdf pdf) {
    // default save implementation
  }
  public override void Visit(SecondPdf pdf) {
    // custom save implementation for SecondPdf type
  }
}

Ora se chiami pdf.AcceptVisitor(saveVisitor) su un Pdf oggetto di tipo SecondPdf e chiama visitor.Visit(this) , chiamerà il metodo che accetta un oggetto di tipo SecondPdf . Tuttavia, lo stesso percorso di codice per qualsiasi altro tipo di Pdf chiamerà solo l'implementazione predefinita.

Il vantaggio di questo approccio è che puoi definire una nuova operazione Pdf creando un visitatore che implementa solo un singolo metodo e si applicherà a tutti i tipi di oggetto. Puoi anche definire un nuovo tipo di oggetto Pdf e otterrà il comportamento predefinito su tutti i tuoi visitatori senza dover apportare alcuna modifica. Se crei una nuova classe Pdf , ad esempio FourthPdf , e ti accorgi che hai bisogno di una logica personalizzata per il tuo DisplayPdfVisitor , devi solo modificare la classe base:

abstract class PdfVisitor {
  // previous stuff
  public virtual void Visit(FourthPdf pdf) { 
    Visit((Pdf)pdf);
  }
}

E poi in DisplayPdfVisitor :

class DisplayPdfVisitor : PdfVisitor {
  // other implementation
  public override void Visit(FourthPdf pdf) {
    // custom FourthPdf display logic
  }
}

Non hai dovuto modificare SavePdfVisitor o PrintPdfVisitor o qualsiasi altra cosa. Applicano volentieri il comportamento di salvataggio e stampa predefinito agli oggetti FourthPdf , ma DisplayPdfVisitor li gestisce in modo diverso.

    
risposta data 30.03.2017 - 03:00
fonte
3

In generale, puoi risolvere questo tipo di cose modificando questo:

DoStuff_PdfA(aPdf);

a questo:

aPdf.DoStuff();

Ogni tipo derivato può sovrascrivere DoStuff per ottenere qualcosa di specifico per tipo.

Quando lo fai in questo modo, DoStuff non avrà accesso alle variabili membro nello scope corrente, quindi potresti doverlo passare una sorta di contesto:

aPdf.DoStuff(myContext);

o se vuoi qualcosa di veloce e sporco puoi imbrogliare e fare qualcosa del genere:

aPdf.DoStuff(this);

Se devi creare un worker per aPdf (ad es. creare un oggettoUploadPdf% specifico del tipo), inserisci una factory

var uploader = aPdf.GetUploader(uploaderFactory);

... e lascia che sia il sottotipo a risolverlo:

PdfUploader GetUploader(IUploaderFactory factory)
{
    return factory.Resolve<PdfTypeA>();
}
    
risposta data 30.03.2017 - 02:04
fonte