Jaka jest lepsza praktyka - metody pomocnicze jako instancja lub statyczne?

48

To pytanie jest subiektywne, ale byłem ciekawy, jak podchodzi do tego większość programistów. Poniższy przykład jest w pseudo-C #, ale powinno to również dotyczyć języków Java, C ++ i innych języków OOP.

W każdym razie, pisząc metody pomocnicze w moich klasach, mam tendencję do deklarowania ich jako statycznych i po prostu przekazuję pola, jeśli metoda pomocnicza ich potrzebuje. Na przykład, biorąc pod uwagę poniższy kod, wolę używać metody wywołania nr 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();
  }
}

Moim powodem jest to, że wyjaśnia (przynajmniej moim oczom), że metoda pomocnicza ma możliwy efekt uboczny na inne obiekty - nawet bez czytania jej implementacji. Uważam, że mogę szybko sprawdzać metody wykorzystujące tę praktykę, a tym samym pomagać mi w debugowaniu rzeczy.

Co wolisz robić we własnym kodzie i jakie są tego powody?

Ilian
źródło
3
Chciałbym usunąć C ++ z tego pytania: w C ++, oczywiście, uczyniłbyś to darmową metodą, ponieważ nie ma sensu, aby była arbitralnie ukryta w obiekcie: wyłącza ADL.
Matthieu M.
1
@MatthieuM .: Darmowa metoda czy nie, to nie ma znaczenia. Celem mojego pytania było pytanie, czy istnieją jakieś zalety deklarowania metod pomocniczych jako instancji. Tak więc w C ++ można wybrać metodę instancji zamiast metody / funkcji wolnej. Masz dobre zdanie na temat ADL. Więc mówisz, że wolisz korzystać z bezpłatnych metod? Dzięki!
Ilian
2
dla C ++ zalecane są bezpłatne metody. Zwiększają enkapsulację (pośrednio, ponieważ są w stanie uzyskać dostęp tylko do interfejsu publicznego) i nadal grają dobrze dzięki ADL (szczególnie w szablonach). Jednak nie wiem, czy dotyczy to Java / C #, C ++ znacznie różni się od Java / C #, więc spodziewam się, że odpowiedzi będą się różnić (i dlatego nie sądzę, aby pytanie o 3 języki razem miało sens).
Matthieu M.,
1
Nie próbując nikogo denerwować, nigdy nie słyszałem dobrego powodu, aby nie używać metod statycznych. Używam ich, gdzie tylko mogę, ponieważ dzięki temu stają się bardziej oczywiste, co robi metoda.
Sridhar Sarnobat,

Odpowiedzi:

23

To naprawdę zależy. Jeśli wartości, na których operują pomocnicy, są prymitywne, dobrym wyborem są metody statyczne, jak zauważył Péter.

Jeśli są one skomplikowane, a następnie STAŁY dotyczy, bardziej szczegółowo z S , z I i D .

Przykład:

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();
      }
}

To dotyczy twojego problemu. Państwo może uczynić makeEveryBodyAsHappyAsPossiblemetodę statyczną, która będzie miała w niezbędnych parametrów. Inną opcją jest:

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]);
      }
}

Teraz OurHousenie musisz wiedzieć o zawiłościach zasad dystrybucji plików cookie. Musi tylko teraz obiekt, który implementuje regułę. Implementacja jest abstrahowana od obiektu, którego wyłączną odpowiedzialnością jest stosowanie reguły. Ten obiekt można przetestować w izolacji. OurHousemożna przetestować za pomocą zwykłej makiety CookieDistributor. Możesz łatwo zmienić zasady dystrybucji plików cookie.

Uważaj jednak, aby tego nie przesadzić. Na przykład posiadanie złożonego systemu 30 klas działa jako implementacja CookieDistributor, w której każda klasa po prostu spełnia małe zadanie, naprawdę nie ma sensu. Moja interpretacja SRP jest taka, że ​​nie tylko dyktuje, że każda klasa może mieć tylko jedną odpowiedzialność, ale także, że jedna klasa powinna być wypełniona przez jedną klasę.

W przypadku prymitywów lub obiektów, których używasz jak prymitywów (na przykład obiektów reprezentujących punkty w przestrzeni, macierzy lub czegoś takiego), statyczne klasy pomocnicze mają wiele sensu. Jeśli masz wybór, a to naprawdę ma sens, możesz rozważyć dodanie metody do klasy reprezentującej dane, np. Rozsądne jest, Pointaby mieć addmetodę. Ponownie, nie przesadzaj.

Zatem w zależności od problemu istnieją różne sposoby rozwiązania tego problemu.

back2dos
źródło
18

Powszechnie znanym idiomem jest deklarowanie metod klas użyteczności static, więc takich klas nigdy nie trzeba tworzyć. Przestrzeganie tego idiomu ułatwia zrozumienie kodu, co jest dobrą rzeczą.

Istnieje jednak poważne ograniczenie tego podejścia: takich metod / klas nie można łatwo wyśmiewać (chociaż AFAIK przynajmniej dla C # istnieją frameworki, które potrafią to osiągnąć, ale nie są one powszechne i przynajmniej niektóre z nich są Reklama w telewizji). Więc jeśli metoda pomocnicza ma jakąkolwiek zewnętrzną zależność (np. DB), która sprawia, że ​​- a więc jej wywołujący - jest trudny do testowania jednostkowego, lepiej jest zadeklarować ją jako niestatyczną . Umożliwia to wstrzykiwanie zależności, dzięki czemu osoby wywołujące metodę są łatwiejsze do testowania jednostkowego.

Aktualizacja

Wyjaśnienie: powyższe mówi o klasach użyteczności, które zawierają tylko metody pomocnicze niskiego poziomu i (zazwyczaj) nie ma stanu. Inną kwestią są metody pomocnicze w stanowych klasach nieprzydatnych do użycia; przeprosiny za błędną interpretację PO.

W tym przypadku wyczuwam zapach kodu: metoda klasy A, która działa przede wszystkim na instancji klasy B, może faktycznie mieć lepsze miejsce w klasie B. Ale jeśli zdecyduję się zachować go tam, gdzie jest, wolę opcję 1, ponieważ jest prostszy i łatwiejszy do odczytania.

Péter Török
źródło
Czy nie możesz po prostu przekazać zależności statycznej metodzie pomocnika?
Ilian
1
@JackOfAllTrades, jeśli jedynym celem metody jest radzenie sobie z zasobem zewnętrznym, nie ma sensu wprowadzać tej zależności. W przeciwnym razie może to być rozwiązanie (chociaż w tym drugim przypadku metoda prawdopodobnie łamie zasadę pojedynczej odpowiedzialności, a zatem jest kandydatem do refaktoryzacji).
Péter Török
1
statyki można łatwo „wyśmiewać” - Mole lub podróbki Microsoft (w zależności od VS2010 lub VS2012 +) pozwalają na zastąpienie metody statycznej własną implementacją w testach. Sprawia, że ​​stare szydercze ramy stają się przestarzałe. (robi to również tradycyjne kpiny, a także może kpić z konkretnych klas). Jest wolny od MS poprzez menedżera rozszerzeń.
gbjbaanb
4

Co wolisz robić we własnym kodzie?

Wolę # 1 ( this->DoSomethingWithBarImpl();), chyba że metoda pomocnicza nie potrzebuje dostępu do danych / implementacji instancji static t_image OpenDefaultImage().

i jakie są tego powody?

interfejs publiczny i prywatna implementacja oraz członkowie są osobni. programy byłyby o wiele trudniejsze do odczytania, gdybym wybrał # 2. z numerem 1 zawsze mam członków i instancję dostępne. w porównaniu do wielu implementacji opartych na OO, zwykle używam dużej ilości enkapsualacji i bardzo niewielu członków na klasę. jeśli chodzi o skutki uboczne - nie mam nic przeciwko, często istnieje walidacja stanu, która rozszerza zależności (np. argumenty, które należałoby przekazać).

# 1 jest znacznie prostszy w czytaniu, utrzymaniu i ma dobre cechy wydajnościowe. oczywiście będą przypadki, w których można udostępnić implementację:

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

Gdyby # 2 było dobrym wspólnym rozwiązaniem (np. Domyślnym) w bazie kodu, martwiłbym się strukturą projektu: czy klasy robią za dużo? czy nie ma wystarczającej ilości kapsułkowania lub ponownego użycia? czy poziom abstrakcji jest wystarczająco wysoki?

wreszcie # 2 wydaje się zbędny, jeśli używasz języków OO, w których obsługiwana jest mutacja (np. constmetoda).

justin
źródło