Modello per una classe che fa solo una cosa


24

Diciamo che ho una procedura che fa cose :

void doStuff(initalParams) {
    ...
}

Ora scopro che "fare cose" è un'operazione piuttosto complicata. La procedura diventa grande, l'ho suddivisa in più procedure più piccole e presto mi rendo conto che avere qualche tipo di stato sarebbe utile mentre si fanno cose, quindi ho bisogno di passare meno parametri tra le piccole procedure. Quindi, lo fattore nella sua classe:

class StuffDoer {
    private someInternalState;

    public Start(initalParams) {
        ...
    }

    // some private helper procedures here
    ...
}

E poi lo chiamo così:

new StuffDoer().Start(initialParams);

o in questo modo:

new StuffDoer(initialParams).Start();

E questo è ciò che sembra sbagliato. Quando utilizzo l'API .NET o Java, non chiamo mai new SomeApiClass().Start(...);, il che mi fa sospettare che lo stia facendo male. Certo, potrei rendere privato il costruttore di StuffDoer e aggiungere un metodo di supporto statico:

public static DoStuff(initalParams) {
    new StuffDoer().Start(initialParams);
}

Ma poi avrei una classe la cui interfaccia esterna consiste in un solo metodo statico, che sembra strano.

Da qui la mia domanda: esiste un modello ben definito per questo tipo di classi

  • avere un solo punto di ingresso e
  • non ha uno stato "riconoscibile esternamente", cioè lo stato dell'istanza è richiesto solo durante l' esecuzione di quell'unico punto di ingresso?

Sono stato conosciuto per fare cose come bool arrayContainsSomestring = new List<string>(stringArray).Contains("somestring");quando tutto ciò che mi interessava era quella particolare informazione e i metodi di estensione LINQ non sono disponibili. Funziona bene e si adatta all'interno di una if()condizione senza la necessità di saltare attraverso i cerchi. Certo, vuoi un linguaggio spazzatura se stai scrivendo un codice del genere.
un CVn

Se è indipendente dalla lingua , perché aggiungere anche java e c # ? :)
Steven Jeuris l'

Sono curioso, qual è la funzionalità di questo "metodo unico"? Sarebbe di grande aiuto nel determinare quale sia l'approccio migliore nello scenario specifico, ma comunque una domanda interessante!
Steven Jeuris,

@StevenJeuris: mi sono imbattuto in questo problema in varie situazioni, ma nella fattispecie è un metodo che importa i dati nel database locale (li carica da un server web, fa alcune manipolazioni e li memorizza nel database).
Heinzi,

1
Se chiami sempre il metodo quando crei l'istanza, perché non renderla una logica di inizializzazione all'interno del costruttore?
NoChance,

Risposte:


29

C'è un modello chiamato Method Object in cui si estrae un singolo metodo grande con molte variabili / argomenti temporanei in una classe separata. Lo fai piuttosto che estrarre parti del metodo in metodi separati perché hanno bisogno dell'accesso allo stato locale (i parametri e le variabili temporanee) e non puoi condividere lo stato locale usando le variabili di istanza perché sarebbero locali a quel metodo (e i metodi estratti da esso) solo e resterebbero inutilizzati dal resto dell'oggetto.

Quindi, invece, il metodo diventa la sua classe, i parametri e le variabili temporanee diventano variabili di istanza di questa nuova classe, e quindi il metodo viene suddiviso in metodi più piccoli della nuova classe. La classe risultante di solito ha un solo metodo di istanza pubblica che esegue l'attività incapsulata dalla classe.


1
Questo suona esattamente come quello che sto facendo. Grazie, almeno adesso ho un nome. :-)
Heinzi

2
Link molto rilevante! Per me ha un profumo. Se il tuo metodo è così lungo, forse parti di esso potrebbero essere estratte in classi riutilizzabili o generalmente essere refactored in un modo migliore? Inoltre non ho mai sentito parlare di questo modello prima, né posso trovare molte altre risorse che mi fanno credere che in genere non sia usato così tanto.
Steven Jeuris,


1
@StevenJeuris Non credo sia un anti-pattern. Un anti-pattern sarebbe quello di ingombrare i metodi refactored con i parametri del lotto invece di memorizzare questo stato che è comune tra questi metodi in una variabile di istanza.
Alfredo Osorio,

@AlfredoOsorio: Beh, ingombra i metodi refactored con molti parametri. Vivono nell'oggetto, quindi è meglio che passarli tutti in giro, ma è peggio del refactoring a componenti riutilizzabili che richiedono solo un sottoinsieme limitato di parametri ciascuno.
Jan Hudec,

13

Direi che la classe in questione (utilizzando un unico punto di ingresso) è ben progettata. È facile da usare ed estendere. Abbastanza SOLIDO.

Stessa cosa per no "externally recognizable" state. È un buon incapsulamento.

Non vedo alcun problema con codice come:

var doer = new StuffDoer(initialParams);
var result = doer.Calculate(extraParams);

1
E, naturalmente, se non è necessario utilizzarlo di doernuovo, ciò si riduce a var result = new StuffDoer(initialParams).Calculate(extraParams);.
un CVn

Calculate dovrebbe essere rinominato in doStuff!
shabunc

1
Aggiungerei che se non vuoi inizializzare (nuova) la tua classe dove la usi (probabilmente vuoi usare una Factory) hai la possibilità di creare l'oggetto da qualche altra parte nella tua logica aziendale e di passare direttamente i parametri al singolo metodo pubblico che hai. L'altra opzione è usare una Factory e avere su di essa un metodo make che ottiene i parametri e crea l'oggetto con tutti i parametri attraverso il suo costruttore. Di quello che chiami il tuo metodo pubblico senza parametri da dove vuoi.
Patkos Csaba,

2
Questa risposta è eccessivamente semplicistica. Una StringComparerclasse che usi è new StringComparer().Compare( "bleh", "blah" )ben progettata? Che dire della creazione di uno stato quando non è assolutamente necessario? Va bene il comportamento è ben incapsulato (beh, almeno per l'utente della classe, più su questo nella mia risposta ), ma questo non lo rende "un buon design" per impostazione predefinita. Per quanto riguarda il tuo esempio "risultato doer". Questo avrebbe senso solo se tu dovessi mai usare doerseparato da result.
Steven Jeuris,

1
Non è un motivo per esistere, lo è one reason to change. La differenza potrebbe sembrare sottile. Devi capire cosa significa. Non creerà mai classi di un metodo
jgauffin

8

Dal punto di vista dei principi SOLIDI la risposta di jgauffin ha senso. Tuttavia, non dovresti dimenticare i principi generali di progettazione, ad esempio nascondere le informazioni .

Vedo diversi problemi con l'approccio dato:

  • Come hai sottolineato te stesso, le persone non si aspettano di utilizzare la parola chiave "nuova", quando l'oggetto creato non gestisce alcuno stato . Il tuo design riflette la sua intenzione. Le persone che usano la tua classe potrebbero essere confuse su quale stato gestisce e se le successive chiamate al metodo potrebbero comportare comportamenti diversi.
  • Dal punto di vista della persona che usa la classe lo stato interiore è ben nascosto, ma quando si desidera apportare modifiche alla classe o semplicemente capirla, si stanno rendendo le cose più complesse. Ho già scritto molto sui problemi che vedo con i metodi di divisione solo per renderli più piccoli , specialmente quando si sposta lo stato nell'ambito della classe. Stai modificando il modo in cui la tua API dovrebbe essere utilizzata solo per avere funzioni più piccole! Questo secondo me lo sta decisamente portando troppo lontano.

Alcuni riferimenti correlati

Probabilmente un argomento principale sta nella misura in cui estendere il principio della responsabilità singola . "Se lo porti all'estremo e costruisci classi che hanno un motivo per esistere, potresti finire con un solo metodo per classe. Ciò causerebbe una grande espansione di classi anche per i processi più semplici, facendo sì che il sistema sia difficile da capire e difficile da cambiare ".

Un altro riferimento rilevante relativo a questo argomento: "Dividi i tuoi programmi in metodi che eseguono un compito identificabile. Mantieni tutte le operazioni in un metodo allo stesso livello di astrazione ". - Kent Beck Key qui è "lo stesso livello di astrazione". Ciò non significa "una cosa", poiché viene spesso interpretato. Questo livello di astrazione dipende interamente dal contesto per il quale si sta progettando.

Quindi qual è l'approccio corretto?

Senza conoscere il tuo caso d'uso concreto è difficile da dire. C'è uno scenario in cui a volte (non spesso) uso un approccio simile. Quando voglio elaborare un set di dati, senza voler rendere disponibile questa funzionalità per l'intero ambito di classe. Ho scritto un post sul blog a proposito di come Lambdas può migliorare ulteriormente l'incapsulamento . Ho anche iniziato una domanda sull'argomento qui su Programmers . Il seguente è un ultimo esempio di dove ho usato questa tecnica.

new TupleList<Key, int>
{
    { Key.NumPad1, 1 },
            ...
    { Key.NumPad3, 16 },
    { Key.NumPad4, 17 },
}
    .ForEach( t =>
    {
        var trigger = new IC.Trigger.EventTrigger(
                        new KeyInputCondition( t.Item1, KeyInputCondition.KeyState.Down ) );
        trigger.ConditionsMet += () => AddMarker( t.Item2 );
        _inputController.AddTrigger( trigger );
    } );

Dato che il codice molto "locale" all'interno del codice ForEachnon viene riutilizzato da nessun'altra parte, posso semplicemente mantenerlo nella posizione esatta in cui è rilevante. Delineare il codice in modo tale che il codice che si basa l'uno sull'altro sia fortemente raggruppato insieme lo rende più leggibile secondo me.

Possibili alternative

  • In C # potresti invece usare metodi di estensione. Operate quindi direttamente sull'argomento che passate a questo metodo "una cosa".
  • Verifica se questa funzione in realtà non appartiene a un'altra classe.
  • Renderlo una funzione statica in una classe statica . Questo è probabilmente l'approccio più appropriato, come si riflette anche nelle API generali a cui hai fatto riferimento.

Ho trovato un possibile nome per questo anti-modello come indicato in un'altra risposta, Poltergeists .
Steven Jeuris,

4

Direi che è abbastanza buono, ma che la tua API dovrebbe essere ancora un metodo statico su una classe statica, poiché questo è ciò che l'utente si aspetta. Il fatto che il metodo statico utilizzi newper creare l'oggetto helper e fare il lavoro è un dettaglio di implementazione che dovrebbe essere nascosto a chiunque lo chiami.


Wow! Sei riuscito a gestirlo molto più conciso di me. :) Grazie. (ovviamente vado un po 'oltre dove potremmo non essere d'accordo, ma sono d'accordo con la premessa generale)
Steven Jeuris

2
new StuffDoer().Start(initialParams);

Si è verificato un problema con gli sviluppatori senza informazioni che potrebbero utilizzare un'istanza condivisa e utilizzarla

  1. più volte (ok se l'esecuzione precedente (forse parziale) non rovina l'esecuzione successiva)
  2. da più thread (non OK, hai esplicitamente detto che ha uno stato interno).

Quindi questo richiede una documentazione esplicita che non è thread-safe e che se è leggero (crearlo velocemente, non lega risorse esterne né molta memoria), va bene istanziare al volo.

Nasconderlo in un metodo statico aiuta in questo, perché il riutilizzo dell'istanza non avviene mai.

Se ha un'inizializzazione costosa, potrebbe (non sempre) essere utile preparare l'inizializzazione una volta e usarla più volte creando un'altra classe che contenga solo stato e la renda clonabile. L'inizializzazione creerebbe lo stato in cui verrebbe memorizzato doer. start()lo clonerebbe e lo passerebbe ai metodi interni.

Ciò consentirebbe anche altre cose come lo stato persistente di esecuzione parziale. (Se occorrono fattori lunghi ed esterni, ad esempio un'interruzione dell'alimentazione elettrica, si potrebbe interrompere l'esecuzione.) Ma queste cose fantasiose aggiuntive di solito non sono necessarie, quindi non ne vale la pena.


Punti buoni. Spero non ti dispiaccia la pulizia.
Steven Jeuris,

2

Oltre alla mia altra risposta più elaborata e personalizzata , sento che la seguente osservazione merita una risposta separata.

Ci sono indicazioni che potresti seguire l'anti-schema Poltergeists .

I poltergeist sono classi con ruoli molto limitati e cicli di vita efficaci. Spesso avviano processi per altri oggetti. La soluzione refactored include una riallocazione di responsabilità verso oggetti di lunga durata che eliminano i Poltergeist.

Sintomi E Conseguenze

  • Percorsi di navigazione ridondanti.
  • Associazioni transitorie.
  • Classi apolidi.
  • Oggetti e classi temporanei di breve durata.
  • Classi a singola operazione che esistono solo per "seminare" o "invocare" altre classi attraverso associazioni temporanee.
  • Classi con nomi di operazioni "simili al controllo" come start_process_alpha.

Soluzione refactored

Gli Acchiappafantasmi risolvono i Poltergeist rimuovendoli del tutto dalla gerarchia di classe. Dopo la loro rimozione, tuttavia, la funzionalità "fornita" dal poltergeist deve essere sostituita. Questo è facile con una semplice regolazione per correggere l'architettura.


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.