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


48

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

Ad ogni modo, quando scrivo i metodi helper nelle mie classi, tendo a dichiararli come statici e passare i campi se il metodo helper ne ha bisogno. Ad esempio, dato il codice riportato di seguito, preferisco utilizzare la chiamata di metodo n . 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();
  }
}

La mia ragione per farlo è che chiarisce (almeno ai miei occhi) che il metodo helper ha un possibile effetto collaterale su altri oggetti, anche senza leggerne l'implementazione. Trovo che posso rapidamente individuare metodi che usano questa pratica e quindi aiutarmi nel debug delle cose.

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


3
Vorrei rimuovere C ++ da questa domanda: in C ++, ovviamente, lo renderesti un metodo gratuito, perché non ha senso che venga arbitrariamente nascosto in un oggetto: disabilita ADL.
Matthieu M.,

1
@MatthieuM .: Metodo gratuito o no, non importa. L'intento della mia domanda era di chiedere se ci fossero dei vantaggi nel dichiarare i metodi di supporto come di esempio. Quindi in C ++ la scelta potrebbe essere metodo / istanza vs metodo / funzione liberi. Hai un buon punto su ADL. Quindi stai dicendo che preferiresti usare metodi gratuiti? Grazie!
Ilian,

2
per C ++, sono consigliati metodi gratuiti. Aumentano l'incapsulamento (indirettamente, in quanto sono in grado di accedere solo all'interfaccia pubblica) e giocano comunque bene a causa dell'ADL (specialmente nei modelli). Tuttavia non so se questo sia applicabile a Java / C #, C ++ differisce notevolmente da Java / C #, quindi mi aspetto che le risposte differiscano (e quindi non credo che chiedere insieme le 3 lingue abbia senso).
Matthieu M.


1
Senza cercare di aggravare nessuno, non ho mai sentito una buona ragione per non usare metodi statici. Li uso dove posso poiché rendono più ovvio il metodo.
Sridhar Sarnobat,

Risposte:


23

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

Se complessa, quindi SOLIDO applica, più specificamente la S , la I e 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. È possibile effettuare makeEveryBodyAsHappyAsPossibleun metodo statico, che si terrà nei 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 OurHousenon è necessario conoscere la complessità delle regole di distribuzione dei cookie. Deve solo ora un oggetto, che implementa una regola. L'implementazione viene sottratta a un oggetto, la cui unica responsabilità è applicare la regola. Questo oggetto può essere testato in isolamento. OurHousepuò essere testato con un semplice mocking di CookieDistributor. E puoi facilmente decidere di modificare le regole di distribuzione dei cookie.

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

Nel caso di primitivi o oggetti che usi come primitivi (ad esempio oggetti che rappresentano punti nello spazio, matrici o altro), le classi di supporto statiche hanno molto senso. Se hai la scelta, e ha davvero senso, allora potresti effettivamente prendere in considerazione l'aggiunta di un metodo alla classe che rappresenta i dati, ad esempio è ragionevole per un Pointavere un addmetodo. Ancora una volta, non esagerare.

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


18

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

Esiste una seria limitazione a questo approccio: tali metodi / classi non possono essere facilmente derisi (anche se AFAIK almeno per C # ci sono framework beffardi che possono raggiungere anche questo, ma non sono all'ordine del giorno e almeno alcuni di loro sono commerciale). Quindi, se un metodo di supporto ha una dipendenza esterna (ad esempio un DB) che lo rende - quindi i suoi chiamanti - difficile da testare, è meglio dichiararlo non statico . Ciò consente l'iniezione di dipendenza, rendendo così più semplice per i test di unità i chiamanti del metodo.

Aggiornare

Chiarimento: quanto sopra parla di classi di utilità, che contengono solo metodi di supporto di basso livello e (in genere) nessuno stato. I metodi di supporto all'interno delle classi stateful non utility rappresentano un problema diverso; scusa per l'interpretazione errata del PO.

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


Non puoi semplicemente passare la dipendenza al metodo dell'helper statico?
Ilian,

1
@JackOfAllTrades, se l'unico scopo del metodo è gestire una risorsa esterna, non ha praticamente senso iniettare quella dipendenza. Altrimenti, potrebbe essere una soluzione (anche se in quest'ultimo caso, il metodo probabilmente rompe il principio di responsabilità singola, quindi è un candidato per il refactoring).
Péter Török,

1
le statistiche possono essere facilmente "derise": Microsoft Moles or Fakes (a seconda di VS2010 o VS2012 +) consente di sostituire un metodo statico con la propria implementazione nei test. Rende obsoleti i vecchi quadri beffardi. (fa anche beffe tradizionali e può anche deridere classi concrete). È libero da MS tramite il gestore estensioni.
gbjbaanb,

4

Cosa preferisci fare nel tuo codice

Io preferisco # 1 ( this->DoSomethingWithBarImpl();), a meno che, naturalmente, il metodo di supporto non ha bisogno di accedere ai dati dell'istanza / implementazione static t_image OpenDefaultImage().

e quali sono le tue ragioni per farlo?

l'interfaccia pubblica e l'implementazione privata e i membri sono separati. i programmi sarebbero molto più difficili da leggere se avessi optato per il n. 2. con il numero 1, ho sempre a disposizione i membri e l'istanza. rispetto a molte implementazioni basate su OO, tendo a usare molta incapsulamento e pochissimi membri per classe. per quanto riguarda gli effetti collaterali, sono d'accordo con questo, spesso esiste una convalida dello stato che estende le dipendenze (ad es. argomenti che bisognerebbe passare).

Il numero 1 è molto più semplice da leggere, mantenere e presenta buoni tratti prestazionali. ovviamente, 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 il n. 2 fosse una buona soluzione comune (ad esempio, il valore predefinito) in una base di codice, sarei preoccupato per la struttura del progetto: le classi stanno facendo troppo? non c'è abbastanza incapsulamento o non è abbastanza riutilizzo? il livello di astrazione è abbastanza alto?

infine, il numero 2 sembra ridondante se si utilizzano linguaggi OO in cui è supportata la mutazione (ad esempio un constmetodo).

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.