Command Pattern - Uso corretto?

2

Ho uno scenario in cui ho bisogno di fare alcuni passi specifici in un ordine specifico. Per maggiore chiarezza, fai un semplice esempio.

Diciamo che ho un Student e qualche Student Properties che incapsula in una classe StudentDetails .

Una classe semplicistica StudentDetails :

class StudentDetails {
  private String name;
  private Integer standard;
  private String grade;
  private List<StudentSubjectDetails> subjectDetails;
  // other attributes, if needed
}

I need to apply some Business Logic on this Student and process it in a specific way. For simplicity consider following steps:

  1. Filter Students which are in standard 10 or above.

  2. Filter students with Grade "A" or above.

  3. Process these students. Apply some logic or store their data in a database.

Questo è uno scenario abbastanza semplice, ma l'idea è di separare i passaggi logici oi comandi in una classe separata in modo che possano essere riutilizzati in altri processi aziendali simili.

The concern I see is with the StudentDetails object which is being used by the different steps.

Tipicamente una classe dovrebbe avere tutti gli attributi logicamente correlati insieme & quegli attributi dovrebbero essere usati insieme. Ma in questo caso diversi processi potrebbero essere interessati a differenti attributi. Quindi stanno usando solo attributi specifici della classe alla volta.

Va bene? o c'è un modo migliore disponibile?

    
posta AgentX 29.04.2016 - 06:41
fonte

3 risposte

2

Ignora "pattern" e "anti-pattern" per ora. Implementa tutto ciò che ti serve in base alle tue esigenze.

In particolare, se hai bisogno di istanziare un'espressione di filtro booleano che agisce su StudentDetails a runtime (cioè la composizione delle regole non è hard-coded) e poi usare in qualche modo i record filtrati, allora il tuo l'approccio è corretto e non dovresti preoccuparti di "anti-schemi" per ora.

Come promemoria, questo approccio ha un costo elevato di manutenzione del codice, in termini di numero di classi necessarie. E questi non sono generici: sono progettati specificamente per filtrare StudentDetails e non possono essere utilizzati per filtrare altri tipi di informazioni. Potresti aver bisogno di molte dozzine di classi per implementare questa funzione di espressione del filtro booleano istanziabile in runtime.

Prima di implementare, controlla se il framework (s) e la libreria (e) che stai usando fornisce qualcosa di simile. Non riutilizzare la struttura esistente è forse il peggior anti-modello di tutti. In particolare, ti hanno fatto leggere quanto segue:

Se hai un milione di record in SQL, dovresti fare tutto in SQL, per quanto possibile. Altrimenti, se qualcuno esegue una query, aspettandosi al massimo 10 record (o solo 1), un filtro implementato all'interno dell'applicazione finirà per dover scaricare il milione di record. Se questo è il caso, ci sono diversi servizi che ti aiutano a convertire la tua espressione booleana in istruzioni SQL.

Per iniziare, avrai qualcosa di simile a questo:

interface StudentBoolFilter
{
    bool Check(StudentDetails student);
}

interface StudentProcessor
{
    void Process(StudentDetails student);
}

Quindi avrai bisogno di molte classi di meccanica:

class StudentAndFilter : StudentBoolFilter
{
    List<StudentBoolFilter> filters;
    void Add(StudentBoolFilter childFilter);
    bool Check(StudentDetails student)
    {
         if (filters.Count == 0)
         {
             throw new Exception("typically wrong for business software to encounter an AND node without any children.");
         }
         foreach (StudentBoolFilter childFilter in filters)
         {
             if (!childFilter.check(student)) return false;
         }
         return true;
    }
}

Allo stesso modo hai bisogno di StudentOrFilter (riduzione booleana N-ary), StudentNotFilter (unario) e così via.

Dal lato dell'elaborazione, avrete bisogno di un processore preconfezionato (sink) e consentire al programmatore dell'applicazione di implementare ulteriori classi di processore (cioè consentire alle classi client di implementare StudentProcessor ).

Questo processore (sink) scrive il% co_de ricevuto in una lista.

class StudentListAppender : StudentProcessor
{
    IList<StudentDetails> targetList;
    StudentListAppender(List<StudentDetails> targetList)
    {
        if (targetList.IsReadOnly) throw new Exception("Cannot write to read-only list!");
        this.targetList = targetList;
    }
    void Process(StudentDetails student)
    {
        targetList.Add(student);
    }
}

Infine, hai bisogno di un "driver" (una classe che invocherà tutto ciò che hai implementato finora) e presumo che questo "driver" prenderà il suo input da una lista. Nota che questo processore non implementa un'interfaccia.

class StudentListProcessor
{
    StudentListProcessor(IList<StudentDetails> students, StudentBoolFilter filter, StudentProcessor processor)
    {
        // Store the three; do nothing.
        // This class only stores one filter, but because
        // a filter can be composite, and a tree of filters
        // can be constructed at runtime, this gives unlimited 
        // possibility.
    }
    void Run()
    {
        foreach (StudentDetails student in students)
        {
            if (!filter.Check(student))
            {
                continue;
            }
            processor.Process(student);
        }
    }
}

Ricorda che questo è solo un modo per implementare questa funzionalità. In particolare, a causa della presenza della classe StudentDetails , questa implementazione appartiene allo stile "push" del filtraggio degli elenchi. (La StudentListProcessor è la classe che "spinge" gli oggetti attraverso la struttura del filtro booleano e infine al sink / processore.)

C'è un'altra implementazione che usa lo stile "pull". Un'implementazione in stile pull funzionerà come StudentListProcessor (un'interfaccia che ha IEnumerator (ottiene l'elemento corrente), Current e MoveNext ).

L'implementazione di questo stile di attrazione insieme al filtro è leggermente più coinvolgente.

In C #, ogni filtro deve interrogare il filtro precedente per un elemento nell'implementazione di Reset . Se l'oggetto ricevuto non valuta true nel filtro corrente, il filtro corrente deve continuare a chiamare MoveNext del filtro precedente e MoveNext per ottenere l'elemento successivo.

In Java, c'è un problema più grande, perché chiamare Current richiede anche le stesse operazioni di hasNext in C # - non può rispondere alla query MoveNext senza ricevere effettivamente un elemento che abbia soddisfatto tutte le condizioni booleane in fasi precedenti.

    
risposta data 29.04.2016 - 12:40
fonte
1

Sono dell'opinione che lo schema di comando non si applica qui. L'intento del modello di comando è di raccogliere molte parti di dati, quindi fornirle a una classe di processore Wikipedea . Questo può facilmente diventare un anti-pattern con l'oggetto command che diventa poco più di un contenitore per (quasi) variabili globali.

Nel mio mondo, quello che hai è una classe StudentDetails. Questo dovrebbe contenere tutti gli attributi che si applicano allo studente e allo studente da solo. Nel tuo esempio, nome e subjectDetails fanno questo bene. La classe dovrebbe mostrare metodi comportamentali che dipendono solo dagli attributi della classe. grado e standard sembrano campi calcolati che richiedono dati da un database esterno. A rigor di termini non appartengono a uno studente, ma in pratica possono essere costosi da calcolare, quindi metterli in cache nell'oggetto StudentDetails può fornire una grossa vincita se vengono utilizzati più volte.

Il filtraggio degli elenchi di studenti su qualsiasi criterio non è un lavoro per la tua classe StudentDetails. Hai bisogno di una classe di livello superiore (CourseProcessor ?, StudentRanker?) In grado di scorrere un elenco di studenti, calcolare i criteri di selezione e selezionare il sottoinsieme di StudentDetails che vuole.Questa classe deve essere riusabile in qualsiasi parte del tuo codice.

Infine, probabilmente devi considerare attentamente la strategia di persistenza del tuo database. Campi come grado e standard sono scarsi candidati per la persistenza del database, poiché possono variare nel tempo in quanto le prestazioni degli studenti migliorano o peggiorano.

Questa è stata una buona domanda. Mi è piaciuto.

    
risposta data 29.04.2016 - 10:04
fonte
0

Da una prospettiva di Domain Driven Design, direi che ogni comando dovrebbe acquisire l'intento dell'utente e portare solo i tipi di informazioni che vanno con quella particolare azione dell'utente. Quindi la domanda guida è "in che modo i tuoi esperti di dominio categorizzano mentalmente questo"?

Se si tratta di qualcosa come "Generare la lista di studenti Honor Roll", probabilmente è un comando singolo con poche informazioni come "time frame" e "year-cohort".

Quindi, quando qualcuno ha bisogno di uno nuovo, come "Genera la lista degli studenti che non hanno successo", questo è un comando separato che ricorda il primo ma non dovrebbe condividere molto codice. Cerca invece il riutilizzo del codice all'interno di qualsiasi riceve e gestisce il comando, o meglio ancora all'interno delle entità del tuo dominio.

L'idea di inserire comandi all'interno di altri comandi mi rende nervoso, soprattutto perché complica la questione di cose come i confini delle transazioni o la registrazione di controllo.

    
risposta data 29.04.2016 - 07:23
fonte

Leggi altre domande sui tag