Modello C # per gestire in modo pulito le "funzioni libere", evitando le classi statiche di "utility bag" di tipo Helper


9

Recentemente stavo rivedendo alcune classi statiche "utility bag" in stile Helper che fluttuavano attorno ad alcune grandi basi di codice C # con cui lavoro, cose fondamentalmente come il seguente frammento molto condensato:

// Helpers.cs

public static class Helpers
{
    public static void DoSomething() {}
    public static void DoSomethingElse() {}
}

I metodi specifici che ho esaminato sono

  • per lo più estranei l'uno all'altro,
  • senza stato esplicito persistito tra le invocazioni,
  • piccolo e
  • ciascuno consumato da una varietà di tipi non correlati.

Modifica: quanto sopra non intende essere un elenco di presunti problemi. È un elenco di caratteristiche comuni dei metodi specifici che sto esaminando. È contesto per aiutare le risposte a fornire soluzioni più pertinenti.

Proprio per questa domanda, mi riferirò a questo tipo di metodo come GLUM (metodo generale di utilità leggero). La connotazione negativa di "glum" è parzialmente intesa. Mi dispiace se questo si presenta come un gioco di parole stupido.

Anche mettendo da parte il mio scetticismo predefinito sui GLUM, non mi piacciono le seguenti cose:

  • Una classe statica viene utilizzata esclusivamente come spazio dei nomi.
  • L'identificatore di classe statica è sostanzialmente insignificante.
  • Quando viene aggiunto un nuovo GLUM, (a) questa classe "bag" viene toccata senza motivo o (b) viene creata una nuova classe "bag" (che di solito non è un problema; ciò che è negativo è che il nuovo le classi statiche spesso ripetono semplicemente il problema di non correlazione, ma con meno metodi).
  • La meta-denominazione è inevitabilmente terribile, non standard, e di solito internamente incoerente, che si tratti di Helpers, Utilitieso qualsiasi altra cosa.

Qual è un modello ragionevolmente buono e semplice per refactoring questo, preferibilmente affrontando le preoccupazioni di cui sopra e preferibilmente con un tocco il più leggero possibile?

Dovrei probabilmente enfatizzare: tutti i metodi con cui ho a che fare sono accoppiati l'uno con l'altro. Non sembra esserci un modo ragionevole per scomporli in borse di metodi di classe statica a grana fine ma ancora multi-membro.


1
Informazioni su GLUMs ™: Penso che l'abbreviazione di "leggero" potrebbe essere rimosso, poiché i metodi dovrebbero essere leggeri seguendo le migliori pratiche;)
Fabio

@Fabio sono d'accordo. Ho incluso il termine come uno scherzo (probabilmente non terribilmente divertente e forse confuso) che funge da abbreviazione per la stesura di questa domanda. Penso che "leggero" sia sembrato appropriato qui perché le basi di codice in questione sono di lunga durata, eredità e un po 'disordinate, vale a dire che ci sono molti altri metodi (non di solito questi GUM ;-) che sono "pesi massimi". Se al momento avessi un budget maggiore (ehem, qualsiasi), prenderei sicuramente un approccio più aggressivo e mi rivolgerei a quest'ultimo.
William,

Risposte:


17

Penso che tu abbia due problemi strettamente collegati tra loro.

  • Le classi statiche contengono funzioni logicamente non correlate
  • I nomi delle classi statiche non forniscono informazioni utili sul significato / contesto delle funzioni contenute.

Questi problemi possono essere risolti combinando solo metodi logicamente correlati in una classe statica e spostando gli altri nella propria classe statica. In base al dominio del problema, tu e il tuo team dovreste decidere quali criteri userete per separare i metodi in classi statiche correlate.

Preoccupazioni e possibili soluzioni

I metodi sono per lo più non correlati tra loro - problema. Combina i metodi correlati in una classe statica e sposta i metodi non correlati nelle proprie classi statiche.

I metodi sono senza stato esplicito persistente tra le invocazioni - non è un problema. I metodi senza stato sono una caratteristica, non un bug. Lo stato statico è comunque difficile da testare e dovrebbe essere utilizzato solo se è necessario mantenere lo stato attraverso le invocazioni del metodo, ma esistono modi migliori per farlo come le macchine a stati e la yieldparola chiave.

I metodi sono piccoli - non è un problema. - I metodi dovrebbero essere piccoli.

I metodi sono ciascuno consumato da una varietà di tipi non correlati - non è un problema. Hai creato un metodo generale che può essere riutilizzato in molti luoghi non correlati.

Una classe statica viene utilizzata esclusivamente come spazio dei nomi , non un problema. Ecco come vengono utilizzati i metodi statici. Se questo stile di metodi di chiamata ti dà fastidio, prova a utilizzare i metodi di estensione o la using staticdichiarazione.

L'identificatore di classe statico è sostanzialmente insignificante - problema . Non ha senso perché gli sviluppatori stanno inserendo metodi non correlati. Inserisci i metodi non correlati nelle loro classi statiche e dai loro nomi specifici e comprensibili.

Quando viene aggiunto un nuovo GLUM, (a) questa classe "bag" viene toccata (tristezza SRP) - non è un problema se si segue l'idea che solo i metodi logicamente correlati dovrebbero essere in una classe statica. SRPnon è violato qui, perché il principio dovrebbe essere applicato per le classi o per i metodi. La singola responsabilità delle classi statiche significa contenere metodi diversi per una "idea" generale. Quell'idea può essere una funzione o una conversione, o iterazioni (LINQ), normalizzazione o convalida dei dati ...

o meno frequentemente (b) viene creata una nuova classe "bag" (più bag) - non è un problema. Classi / classi / funzioni statiche: sono i nostri strumenti (per sviluppatori) che utilizziamo per la progettazione di software. Il tuo team ha dei limiti per l'utilizzo delle lezioni? Quali sono i limiti massimi per "più borse"? La programmazione si basa sul contesto, se per la soluzione nel tuo particolare contesto si arriva a 200 classi statiche che comprensibilmente denominate, logicamente collocate in spazi dei nomi / cartelle con gerarchia logica - non è un problema.

La meta-nomenclatura è inevitabilmente terribile, non standard e di solito incoerente internamente, sia che si tratti di Helpers, Utilities o qualsiasi altra cosa - problema. Fornire nomi migliori che descrivono il comportamento previsto. Mantenere solo i metodi correlati in una classe, che forniscono assistenza per una migliore denominazione.


Non è una violazione di SRP, ma abbastanza chiaramente una violazione del principio aperto / chiuso. Detto questo, in genere ho riscontrato che Ocp ha più problemi di quanti ne valga la pena
Paul,

2
@Paul, il principio Open / Close non può essere applicato per le classi statiche. Quello era il mio punto: i principi SRP o OCP non possono essere applicati per le classi statiche. Puoi applicarlo per metodi statici.
Fabio,

Ok, c'era un po 'di confusione qui. Il primo elenco di voci della domanda non è un elenco di presunti problemi. È un elenco di caratteristiche comuni dei metodi specifici che sto esaminando. È contesto per aiutare le risposte a fornire soluzioni più applicabili. Modificherò per chiarire.
William,

1
Ri "classe statica come spazio dei nomi", il fatto che sia un modello comune non significa che non sia un anti-modello. Il metodo (che è fondamentalmente una "funzione libera" per prendere in prestito un termine da C / C ++) all'interno di questa particolare classe statica a bassa coesione è l'entità significativa in questo caso. In questo particolare contesto, è strutturalmente meglio avere uno spazio dei nomi secondario Acon 100 classi statiche a metodo singolo (in 100 file) rispetto a una classe statica Acon 100 metodi in un singolo file.
William,

1
Ho fatto una pulizia abbastanza importante sulla tua risposta, principalmente cosmetica. Non sono sicuro di cosa tratta il tuo ultimo paragrafo; nessuno si preoccupa di come stanno testando il codice che chiama la Mathclasse statica.
Robert Harvey,

5

Abbraccia la convenzione secondo cui le classi statiche vengono utilizzate come spazi dei nomi in situazioni come questa in C #. Gli spazi dei nomi ottengono buoni nomi, così dovrebbero essere queste classi. La using staticfunzione di C # 6 rende anche la vita un po 'più semplice.

Nomi di classi puzzolenti come Helperssegnalano che i metodi dovrebbero essere suddivisi in classi statiche meglio denominate, se possibile, ma uno dei tuoi presupposti enfatizzati è che i metodi con cui hai a che fare sono "non accoppiati", il che implica la suddivisione in una nuova classe statica per metodo esistente. Probabilmente va bene, se

  • ci sono buoni nomi per le nuove classi statiche e
  • lo giudichi per favorire effettivamente la manutenibilità del tuo progetto.

Come esempio inventato, Helpers.LoadImagepotrebbe diventare qualcosa di simile FileSystemInteractions.LoadImage.

Tuttavia, potresti finire con i metodi-bag di classe statica. Alcuni modi in cui ciò può accadere:

  • Forse solo alcuni (o nessuno) dei metodi originali hanno buoni nomi per le loro nuove classi.
  • Forse le nuove classi in seguito si evolveranno per includere altri metodi pertinenti.
  • Forse il nome della classe originale non era maleodorante e in realtà andava bene.

È importante ricordare che questi metodi-bag di classe statica non sono terribilmente rari in basi di codice C # reali. Probabilmente non è patologico averne alcuni piccoli .


Se vedi davvero un vantaggio nell'avere ogni metodo simile a una "funzione libera" nel suo file (il che va bene e vale la pena se giudichi onestamente che effettivamente benefici la manutenibilità del tuo progetto), potresti prendere in considerazione l'idea di rendere tale classe statica anziché statica classe parziale, impiegando il nome della classe statica simile a come si farebbe con uno spazio dei nomi e quindi utilizzandolo tramite using static. Per esempio:

struttura del progetto fittizio utilizzando questo modello

Program.cs

namespace ConsoleApp1
{
    using static Foo;
    using static Flob;

    class Program
    {
        static void Main(string[] args)
        {
            Bar();
            Baz();
            Wibble();
            Wobble();
        }
    }
}

Foo / Bar.cs

namespace ConsoleApp1
{
    static partial class Foo
    {
        public static void Bar() { }
    }
}

Foo / Baz.cs

namespace ConsoleApp1
{
    static partial class Foo
    {
        public static void Baz() { }
    }
}

Flob / Wibble.cs

namespace ConsoleApp1
{
    static partial class Flob
    {
        public static void Wibble() { }
    }
}

Flob / Wobble.cs

namespace ConsoleApp1
{
    static partial class Flob
    {
        public static void Wobble() { }
    }
}

-6

Generalmente non dovresti avere o richiedere alcun "metodo generale di utilità". Nella tua logica aziendale. Dai un'altra occhiata e mettili in un posto adatto.

Se stai scrivendo una libreria matematica o qualcosa del genere, suggerirei, anche se normalmente li odio, metodi di estensione.

È possibile utilizzare i metodi di estensione per aggiungere funzioni ai tipi di dati standard.

Ad esempio, diciamo che ho una funzione generale MySort () invece di Helpers.MySort o l'aggiunta di un nuovo ListOfMyThings.MySort posso aggiungerlo a IEnumerable


Il punto è non prendere ancora un altro "sguardo duro". Il punto è avere un modello riutilizzabile che può facilmente ripulire una "borsa" di utilità. So che i servizi pubblici sono odori. La domanda afferma questo. L'obiettivo è quello di isolare sensibilmente queste entità di dominio indipendenti (ma troppo vicine tra loro) per ora e passare il più facilmente possibile al budget del progetto.
William,

1
Se non hai "metodi di utilità generale", probabilmente non stai isolando abbastanza pezzi di logica dal resto della tua logica. Ad esempio, per quanto ne so, non esiste un percorso URL per scopi generali che unisce la funzione in .NET, quindi finisco spesso per scriverne uno. Quel tipo di cruft sarebbe meglio come parte della libreria standard, ma a ogni lib standard manca qualcosa . L'alternativa, lasciando la logica ripetuta direttamente all'interno dell'altro codice, la rende notevolmente meno leggibile.
jpmc26,


1
@Ewan che non è una funzione, è una firma delegata per scopi generici ...
TheCatWhisperer

1
@Ewan Quello era il punto centrale di TheCatWhisperer: le classi di utilità sono un kludge sul dover mettere metodi in una classe. Sono contento che tu sia d'accordo.
jpmc26,
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.