web-dev-qa-db-fra.com

Quelle est la meilleure pratique - les méthodes d'assistance en tant qu'instance ou statique?

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

Quoi qu'il en soit, lorsque j'écris des méthodes d'assistance dans mes classes, j'ai tendance à les déclarer statiques et à passer les champs si la méthode d'assistance en a besoin. Par exemple, étant donné le code ci-dessous, je préfère utiliser Méthode Call # 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();
  }
}

Ma raison pour cela est qu'il indique clairement (à mes yeux au moins) 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 trouver des méthodes qui utilisent cette pratique et m'aider ainsi à déboguer les choses.

Que préférez-vous faire dans votre propre code et quelles sont vos raisons?

48
Ilian Pinzon

Cela dépend vraiment. Si les valeurs sur lesquelles vos assistants opèrent sont des primitives, les méthodes statiques sont un bon choix, comme l'a souligné Péter.

S'ils sont complexes, alors SOLIDE s'applique, plus précisément les S , les - 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 faire de makeEveryBodyAsHappyAsPossible une méthode statique, qui prendra 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, OurHouse n'a pas besoin de connaître les subtilités des règles de distribution des cookies. Il faut seulement maintenant un objet, qui implémente une règle. L'implémentation est résumée dans un objet, dont la seule responsabilité est d'appliquer la règle. Cet objet peut être testé isolément. OurHouse peut être testé en utilisant une simple maquette du 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 l'implémentation de CookieDistributor, où chaque classe remplit simplement une tâche minuscule, n'a pas vraiment de sens. Mon interprétation du SRP est que non seulement il dicte que chaque classe ne peut avoir qu'une seule responsabilité, mais aussi qu'une seule responsabilité doit être assumée par une seule 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 quelque chose), les classes d'assistance statiques ont beaucoup de sens. Si vous avez le choix, et que cela a vraiment du sens, alors vous pourriez réellement envisager d'ajouter une méthode à la classe représentant les données, par ex. il est judicieux pour une Point d'avoir une méthode add. Encore une fois, n'en faites pas trop.

Ainsi, selon votre problème, il existe différentes façons de le résoudre.

23
back2dos

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

Il y a cependant une sérieuse limitation à cette approche: de telles méthodes/classes ne peuvent pas être facilement moquées (bien que l'AFAIK au moins pour C # il existe des frameworks moqueurs qui peuvent y parvenir même, mais ils ne sont pas courants et au moins certains d’entre eux sont commerciaux). Donc si une méthode d'assistance a une dépendance externe (par exemple une base de données) qui rend difficile - à ses appelants - par conséquent un test unitaire, il est préférable de la déclarer non statique. Cela permet l'injection de dépendances, facilitant ainsi le test unitaire des appelants de la méthode.

Mise à jour

Clarification: ce qui précède parle des classes utilitaires, qui ne contiennent que des méthodes d'assistance de bas niveau et (généralement) aucun état. Les méthodes d'assistance dans les 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 avoir une meilleure place dans la classe B.Mais si je décide de la garder où elle est, je préfère l'option 1, car il est plus simple et plus facile à lire.

18
Péter Török

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émentation de l'instance static t_image OpenDefaultImage().

et quelles sont vos raisons de le faire?

l'interface publique et l'implémentation privée et les membres sont séparés. les programmes seraient beaucoup plus difficiles à lire si j'optais pour # 2. avec # 1, j'ai toujours les membres et l'instance disponibles. par rapport à de nombreuses implémentations basées sur OO, j'ai tendance à utiliser beaucoup d'encapsualtion 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 les arguments qu'il faudrait passer).

Le n ° 1 est beaucoup plus simple à lire, à entretenir et présente 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 courante (par exemple la valeur 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 assez d'encapsulation ou pas assez de réutilisation? le niveau d'abstraction est-il suffisamment élevé?

enfin, # 2 semble redondant si vous utilisez OO langues où la mutation est prise en charge (par exemple une méthode const).

4
justin