Qual è una pratica migliore: metodi di supporto come istanza o statici?

47

Questa domanda è soggettiva, ma ero solo curioso di sapere come la maggior parte dei programmatori si avvicina a questo. L'esempio sotto è in pseudo-C # ma questo dovrebbe applicarsi anche a Java, C ++ e altri linguaggi OOP.

Ad ogni modo, quando si scrivono i metodi di aiuto nelle mie classi, tendo a dichiararli come statici e basta passare i campi se il metodo helper ha bisogno di loro. Ad esempio, dato il codice qui sotto, preferisco utilizzare Metodo chiamata # 2 .

class Foo
{
  Bar _bar;

  public void DoSomethingWithBar()
  {
    // Method Call #1.
    DoSomethingWithBarImpl();

    // Method Call #2.
    DoSomethingWithBarImpl(_bar);
  }

  private void DoSomethingWithBarImpl()
  {
    _bar.DoSomething();
  }

  private static void DoSomethingWithBarImpl(Bar bar)
  {
    bar.DoSomething();
  }
}

Il motivo per cui lo faccio è che rende chiaro (almeno ai miei occhi) che il metodo helper ha un possibile effetto collaterale su altri oggetti, anche senza leggerne l'implementazione. Trovo che riesca a trovare rapidamente metodi che usano questa pratica e quindi mi aiutano nel debugging.

Quale preferisci fare nel tuo codice e quali sono i tuoi motivi per farlo?

    
posta Ilian 02.10.2011 - 15:40
fonte

3 risposte

23

Questo dipende davvero. Se i valori su cui i tuoi aiutanti operano sono primitivi, i metodi statici sono una buona scelta, come ha sottolineato Péter.

Se sono complessi, si applica SOLID , in particolare S , I e < a href="http://en.wikipedia.org/wiki/Dependency_inversion_principle"> D .

Esempio:

class CookieJar {
      function takeCookies(count:Int):Array<Cookie> { ... }
      function countCookies():Int { ... }
      function ressuplyCookies(cookies:Array<Cookie>
      ... // lot of stuff we don't care about now
}

class CookieFan {
      function getHunger():Float;
      function eatCookies(cookies:Array<Cookie>):Smile { ... }
}

class OurHouse {
      var jake:CookieFan;
      var jane:CookieFan;
      var cookies:CookieJar;
      function makeEveryBodyAsHappyAsPossible():Void {
           //perform a lot of operations on jake, jane and the cookies
      }
      public function cookieTime():Void {
           makeEveryBodyAsHappyAsPossible();
      }
}

Questo riguarderebbe il tuo problema. puoi rendere makeEveryBodyAsHappyAsPossible un metodo statico, che prenderà i parametri necessari. Un'altra opzione è:

interface CookieDistributor {
    function distributeCookies(to:Array<CookieFan>):Array<Smile>;
}
class HappynessMaximizingDistributor implements CookieDistributor {
    var jar:CookieJar;
    function distributeCookies(to:Array<CookieFan>):Array<Smile> {
         //put the logic of makeEveryBodyAsHappyAsPossible here
    }
}
//and make a change here
class OurHouse {
      var jake:CookieFan;
      var jane:CookieFan;
      var cookies:CookieDistributor;

      public function cookieTime():Void {
           cookies.distributeCookies([jake, jane]);
      }
}

Ora OurHouse non ha bisogno di conoscere la complessità delle regole di distribuzione dei cookie. Deve solo ora un oggetto, che implementa una regola. L'implementazione viene sottratta ad un oggetto, la cui unica responsabilità è quella di applicare la regola. Questo oggetto può essere testato separatamente. OurHouse può essere testato con l'utilizzo di un semplice mock di CookieDistributor . E puoi facilmente decidere di cambiare le regole di distribuzione dei cookie.

Tuttavia, fai attenzione a non esagerare. Ad esempio, avere un sistema complesso di 30 classi si comporta come l'implementazione di CookieDistributor , in cui ogni classe svolge semplicemente un piccolo compito, in realtà non ha senso. La mia interpretazione dell'SRP è che non solo stabilisce che ogni classe può avere una sola responsabilità, ma anche che una singola responsabilità dovrebbe essere svolta da una singola classe.

Nel caso di primitive o oggetti usati come primitive (per esempio oggetti che rappresentano punti nello spazio, matrici o qualcosa del genere), le classi di helper statico hanno molto senso. Se hai la scelta, e ha davvero senso, potresti effettivamente considerare di aggiungere un metodo alla classe che rappresenta i dati, ad es. è ragionevole che un Point abbia un metodo add . Ancora una volta, non esagerare.

Quindi, a seconda del problema, ci sono diversi modi per farlo.

    
risposta data 02.10.2011 - 16:22
fonte
18

È un idioma ben noto dichiarare metodi di classi di utilità static , quindi tali classi non devono mai essere istanziate. Seguire questo idioma rende il tuo codice più facile da capire, che è una buona cosa.

Però c'è un serio limite a questo approccio: tali metodi / classi non possono essere facilmente derisi (sebbene AFAIK almeno per C # ci siano strutture di derisione che possono raggiungere anche questo, ma non sono sono comuni e almeno alcuni di loro sono commerciali). Quindi se un metodo helper ha una dipendenza esterna (ad es. Un DB) che lo rende - quindi i suoi chiamanti - difficile testare l'unità, è meglio dichiararlo non statico . Ciò consente l'iniezione delle dipendenze, rendendo così i caller del metodo più facili da testare l'unità.

Aggiornamento

Chiarimento: il precedente parla di classi di utilità, che contengono solo metodi helper di basso livello e (in genere) nessuno stato. I metodi di supporto all'interno di classi di utilità non statiche sono un problema diverso; scuse per interpretare male l'OP.

In questo caso, percepisco un odore di codice: un metodo di classe A che opera principalmente su un'istanza di classe B potrebbe effettivamente avere un posto migliore nella classe B. Ma se decido di tenerlo dov'è, preferisco opzione 1, in quanto è più semplice e più facile da leggere.

    
risposta data 02.10.2011 - 15:45
fonte
4

Which do you prefer to do in your own code

Preferisco # 1 ( this->DoSomethingWithBarImpl(); ), a meno che, naturalmente, il metodo helper non abbia bisogno di accedere ai dati dell'istanza / all'implementazione static t_image OpenDefaultImage() .

and what are your reasons for doing so?

Interfaccia pubblica e implementazione privata e membri separati. i programmi sarebbero molto più difficili da leggere se optassi per # 2. con # 1, ho sempre i membri e l'istanza disponibili. rispetto a molte implementazioni basate su OO, tendo ad usare molte incapsulenze e pochissimi membri per classe. per quanto riguarda gli effetti collaterali - mi va bene, c'è spesso la convalida dello stato che estende le dipendenze (ad esempio argomenti che dovrebbero passare).

# 1 è molto più semplice da leggere, mantenere e ha buoni tratti prestazionali. naturalmente, ci saranno casi in cui è possibile condividere un'implementazione:

static void ValidateImage(const t_image& image);
void validateImages() const {
    ValidateImage(this->innerImage());
    ValidateImage(this->outerImage());
}

Se la # 2 fosse una buona soluzione comune (ad esempio l'impostazione predefinita) in una base di codice, sarei preoccupato per la struttura del progetto: le classi stanno facendo troppo? non c'è abbastanza incapsulamento o non abbastanza riuso? il livello di astrazione è abbastanza alto?

infine, # 2 sembra ridondante se si utilizzano lingue OO in cui è supportata la mutazione (ad esempio, un metodo const ).

    
risposta data 02.10.2011 - 16:08
fonte

Leggi altre domande sui tag