Quelle est la meilleure pratique - méthodes d'assistance comme instance ou statique?

48

Cette question est subjective mais j'étais simplement curieux de voir comment la plupart des programmeurs abordent cela. L'exemple ci-dessous est en pseudo-C #, mais cela devrait également s'appliquer à Java, C ++ et à d'autres langages POO.

Quoi qu'il en soit, lors de l'écriture de méthodes d'assistance dans mes classes, j'ai tendance à les déclarer statiques et à ne transmettre que les champs si la méthode d'assistance en a besoin. Par exemple, étant donné le code ci-dessous, je préfère utiliser l’ appel de méthode 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();
  }
}

Mon objectif est que cela indique clairement (du moins à mes yeux) que la méthode d'assistance a un effet secondaire possible sur d'autres objets, même sans lire son implémentation. Je trouve que je peux rapidement maîtriser les méthodes qui utilisent cette pratique et m'aident donc à déboguer des choses.

Ce qui vous préférez faire dans votre propre code et quelles sont vos raisons de le faire?

Ilian
la source
3
Je supprimerais le C ++ de cette question: en C ++, évidemment, vous en feriez une méthode libre, car il n’est pas utile qu’elle soit cachée arbitrairement dans un objet: elle désactive ADL.
Matthieu M.
1
@MatthieuM .: Méthode libre ou pas, peu importe. Le but de ma question était de demander s'il y avait un avantage à déclarer des méthodes auxiliaires comme étant des méthodes d'instance. Donc, en C ++, le choix pourrait être une méthode d'instance ou une méthode / fonction libre. Vous avez un bon point à propos de l'ADL. Alors, dites-vous que vous préférez utiliser des méthodes gratuites? Merci!
Ilian
2
pour C ++, les méthodes libres sont recommandées. Ils augmentent l’encapsulation (indirectement, car ils ne peuvent accéder qu’à l’interface publique) et restent agréables grâce à ADL (en particulier dans les modèles). Cependant, je ne sais pas si cela est applicable à Java / C #, le C ++ étant très différent de Java / C #, je m'attends donc à ce que les réponses diffèrent (et donc je ne pense pas que demander les 3 langages ensemble ait du sens).
Matthieu M.
1
Sans vouloir aggraver qui que ce soit, je n’ai jamais entendu une bonne raison de ne pas utiliser de méthodes statiques. Je les utilise partout où je peux car ils rendent plus évident le travail de la méthode.
Sridhar Sarnobat

Réponses:

23

Cela dépend vraiment. Si les valeurs que vos assistants exploitent sont des primitives, alors les méthodes statiques sont un bon choix, comme l'a souligné Péter.

Si elles sont complexes, puis SOLID applique, plus précisément le S , le I et le D .

Exemple:

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

Ce serait à propos de votre problème. Vous pouvez créer makeEveryBodyAsHappyAsPossibleune méthode statique prenant en compte les paramètres nécessaires. Une autre option est:

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

Maintenant, OurHouseje n'ai pas besoin de connaître la complexité des règles de distribution des cookies. Il doit seulement maintenant un objet, qui implémente une règle. L'implémentation est abstraite dans un objet dont la seule responsabilité est d'appliquer la règle. Cet objet peut être testé isolément. OurHousepeut être testé avec un simple simulacre de CookieDistributor. Et vous pouvez facilement décider de modifier les règles de distribution des cookies.

Cependant, veillez à ne pas en faire trop. Par exemple, avoir un système complexe de 30 classes agit comme une implémentation CookieDistributor, chaque classe ne remplissant qu'une tâche minuscule, cela n'a pas vraiment de sens. Mon interprétation du SRP est qu’il ne stipule pas seulement que chaque classe ne peut avoir qu’une responsabilité, mais qu’une seule responsabilité doit être assumée par une classe.

Dans le cas de primitives ou d'objets que vous utilisez comme des primitives (par exemple, des objets représentant des points dans l'espace, des matrices ou autre), les classes d'assistance statique ont beaucoup de sens. Si vous avez le choix, et que cela a vraiment du sens, vous pourriez alors envisager d'ajouter une méthode à la classe représentant les données, par exemple, il est judicieux pour a Pointd'avoir une addméthode. Encore une fois, n'en faites pas trop.

Donc, en fonction de votre problème, il y a différentes façons de s'y prendre.

back2dos
la source
18

Décrire des méthodes de classes d'utilitaires est un idiome bien connu static, de telles classes n'ont donc jamais besoin d'être instanciées. Suivre cet idiome rend votre code plus facile à comprendre, ce qui est une bonne chose.

Il existe cependant une limite sérieuse à cette approche: de telles méthodes / classes ne peuvent pas être facilement moquées (bien que, autant que je sache, du moins pour C #, il existe des frameworks moqueurs qui peuvent y parvenir, mais ils ne sont pas banals et au moins certains sont commercial). Ainsi, si une méthode auxiliaire a une dépendance externe (par exemple, une base de données) qui la rend difficile, ainsi que ses appelants, à effectuer des tests unitaires, il est préférable de la déclarer non statique . Cela permet l'injection de dépendance, facilitant ainsi le test unitaire des appelants de la méthode.

Mise à jour

Clarification: ce qui précède parle de classes d’utilitaires, qui ne contiennent que des méthodes d’aide de bas niveau et (généralement) aucun état. Les méthodes d'assistance à l'intérieur de classes non-utilitaires avec état sont un problème différent; excuses pour avoir mal interprété le PO.

Dans ce cas, je sens une odeur de code: une méthode de classe A qui fonctionne principalement sur une instance de classe B peut en fait occuper une meilleure place en classe B. Mais si je décide de le conserver où il se trouve, je préfère l’option 1, comme il est plus simple et plus facile à lire.

Péter Török
la source
Ne pouvez-vous pas simplement passer la dépendance à la méthode d'assistance statique?
Ilian
1
@JackOfAllTrades, si le seul but de la méthode est de traiter une ressource externe, il est pratiquement inutile d'injecter cette dépendance. Sinon, cela pourrait être une solution (bien que dans ce dernier cas, la méthode rompt probablement avec le principe de responsabilité unique et est donc candidate à la refactorisation).
Péter Török
1
les statiques se moquent facilement - Microsoft Moles ou Fakes (selon VS2010 ou VS2012 +) vous permet de remplacer une méthode statique par votre propre implémentation dans vos tests. Rend les vieux frameworks moqueurs obsolètes. (il fait aussi des moquettes traditionnelles, et peut se moquer des classes de béton). Il est libre de MS via le gestionnaire d'extensions.
Gbjbaanb
4

Que préférez-vous faire dans votre propre code?

Je préfère # 1 ( this->DoSomethingWithBarImpl();), à moins bien sûr que la méthode d'assistance n'ait pas besoin d'accéder aux données / implémentations de l'instance static t_image OpenDefaultImage().

et quelles sont vos raisons pour le faire?

interface publique et mise en œuvre privée et les membres sont séparés. les programmes seraient beaucoup plus difficiles à lire si je optais pour # 2. avec # 1, j'ai toujours les membres et l'instance disponibles. comparé à de nombreuses implémentations basées sur OO, j’ai tendance à utiliser beaucoup d’encapsulation et très peu de membres par classe. en ce qui concerne les effets secondaires - je suis d'accord avec cela, il y a souvent une validation d'état qui étend les dépendances (par exemple, des arguments à passer).

N ° 1 est beaucoup plus simple à lire, à maintenir et a de bonnes caractéristiques de performance. bien sûr, il y aura des cas où vous pourrez partager une implémentation:

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

Si # 2 était une bonne solution commune (par exemple la solution par défaut) dans une base de code, je serais préoccupé par la structure de la conception: les classes en font-elles trop? n'y a-t-il pas suffisamment d'encapsulation ou de réutilisation? le niveau d'abstraction est-il suffisamment élevé?

enfin, le n ° 2 semble redondant si vous utilisez des langages OO où la mutation est prise en charge (par exemple, une constméthode).

Justin
la source