Classe avec une seule méthode - meilleure approche?

173

Disons que j'ai une classe destinée à exécuter une seule fonction. Après avoir exécuté la fonction, il peut être détruit. Y a-t-il une raison de préférer l'une de ces approches?

// Initialize arguments in constructor
MyClass myObject = new MyClass(arg1, arg2, arg3);
myObject.myMethod();

// Pass arguments to method
MyClass myObject = new MyClass();
myObject.myMethod(arg1, arg2, arg3);

// Pass arguments to static method
MyClass.myMethod(arg1, arg2, arg3);

J'étais intentionnellement vague sur les détails, pour essayer d'obtenir des directives pour différentes situations. Mais je n'avais pas vraiment à l'esprit les fonctions de bibliothèque simples comme Math.random (). Je pense davantage aux classes qui exécutent une tâche spécifique et complexe, mais qui ne nécessitent qu'une seule méthode (publique) pour le faire.

JW.
la source

Réponses:

264

J'aimais les classes utilitaires remplies de méthodes statiques. Ils ont fait une grande consolidation des méthodes d'assistance qui, autrement, auraient causé la redondance et l'enfer de la maintenance. Ils sont très faciles à utiliser, aucune instanciation, aucune élimination, juste fire'n'forget. Je suppose que c'était ma première tentative involontaire de créer une architecture orientée services - beaucoup de services sans état qui faisaient juste leur travail et rien d'autre. Au fur et à mesure que le système se développe, les dragons arrivent.

Polymorphisme
Disons que nous avons la méthode UtilityClass.SomeMethod qui bourdonne joyeusement. Soudain, nous devons modifier légèrement la fonctionnalité. La plupart des fonctionnalités sont les mêmes, mais nous devons tout de même modifier quelques parties. S'il ne s'agissait pas d'une méthode statique, nous pourrions créer une classe dérivée et modifier le contenu de la méthode si nécessaire. Comme c'est une méthode statique, nous ne pouvons pas. Bien sûr, si nous avons juste besoin d'ajouter des fonctionnalités avant ou après l'ancienne méthode, nous pouvons créer une nouvelle classe et appeler l'ancienne à l'intérieur - mais c'est juste dégoûtant.

Problèmes d'interface
Les méthodes statiques ne peuvent pas être définies via des interfaces pour des raisons logiques. Et comme nous ne pouvons pas remplacer les méthodes statiques, les classes statiques sont inutiles lorsque nous devons les passer par leur interface. Cela nous rend incapables d'utiliser des classes statiques dans le cadre d'un modèle de stratégie. Nous pourrions corriger certains problèmes en passant des délégués au lieu d'interfaces .

Test
Cela va de pair avec les problèmes d'interface mentionnés ci-dessus. Comme notre capacité à interchanger les implémentations est très limitée, nous aurons également du mal à remplacer le code de production par du code de test. Encore une fois, nous pouvons les conclure, mais cela nous obligera à modifier de grandes parties de notre code juste pour pouvoir accepter des wrappers au lieu des objets réels.

Favorise les blobs
Comme les méthodes statiques sont généralement utilisées comme méthodes utilitaires et que les méthodes utilitaires ont généralement des objectifs différents, nous nous retrouverons rapidement avec une grande classe remplie de fonctionnalités non cohérentes - idéalement, chaque classe devrait avoir un seul objectif dans le système . Je préfère de loin avoir cinq fois les classes tant que leurs objectifs sont bien définis.

Fluage des paramètres
Pour commencer, cette petite méthode statique mignonne et innocente pourrait prendre un seul paramètre. Au fur et à mesure que la fonctionnalité augmente, quelques nouveaux paramètres sont ajoutés. Bientôt, d'autres paramètres sont ajoutés qui sont facultatifs, donc nous créons des surcharges de la méthode (ou ajoutons simplement des valeurs par défaut, dans les langues qui les prennent en charge). Avant longtemps, nous avons une méthode qui prend 10 paramètres. Seuls les trois premiers sont vraiment obligatoires, les paramètres 4-7 sont facultatifs. Mais si le paramètre 6 est spécifié, 7-9 doivent également être remplis ... Si nous avions créé une classe dans le seul but de faire ce que cette méthode statique a fait, nous pourrions résoudre ce problème en prenant les paramètres requis dans le constructeur, et permettant à l'utilisateur de définir des valeurs facultatives via des propriétés ou des méthodes pour définir plusieurs valeurs interdépendantes en même temps. De plus, si une méthode atteint ce niveau de complexité,

Exiger des consommateurs qu'ils créent une instance de classes sans raison
L'un des arguments les plus courants est: pourquoi exiger que les consommateurs de notre classe créent une instance pour invoquer cette méthode unique, tout en n'ayant aucune utilité pour l'instance par la suite? La création d'une instance d'une classe est une opération très bon marché dans la plupart des langages, donc la vitesse n'est pas un problème. L'ajout d'une ligne de code supplémentaire au consommateur est un faible coût pour jeter les bases d'une solution beaucoup plus maintenable à l'avenir. Et enfin, si vous voulez éviter de créer des instances, créez simplement un wrapper singleton de votre classe qui permet une réutilisation facile - bien que cela impose que votre classe soit sans état. S'il n'est pas sans état, vous pouvez toujours créer des méthodes de wrapper statiques qui gèrent tout, tout en vous offrant tous les avantages à long terme. Finalement,

Seul un Sith traite des absolus.
Bien sûr, il y a des exceptions à mon aversion pour les méthodes statiques. Les véritables classes d'utilité qui ne présentent aucun risque de gonflement sont d'excellents cas pour les méthodes statiques - System.Convert par exemple. Si votre projet est unique et ne nécessite aucune maintenance future, l'architecture globale n'est vraiment pas très importante - statique ou non statique, peu importe - la vitesse de développement, cependant.

Normes, normes, normes!
L'utilisation de méthodes d'instance ne vous empêche pas d'utiliser également des méthodes statiques, et vice versa. Tant qu'il y a un raisonnement derrière la différenciation et c'est standardisé. Il n'y a rien de pire que d'examiner une couche métier tentaculaire avec différentes méthodes de mise en œuvre.

Mark S. Rasmussen
la source
Comme Mark le dit, le polymorphisme, le fait de pouvoir passer les méthodes en tant que paramètres de «stratégie» et la possibilité de configurer / définir des options sont autant de raisons d'utiliser une instance . Par exemple: un ListDelimiter ou DelimiterParser pourrait être configuré pour savoir quels délimiteurs utiliser / accepter, s'il faut couper les espaces des jetons analysés, s'il faut mettre entre parenthèses la liste, comment traiter une liste vide / nulle ... etc.
Thomas W
4
"Au fur et à mesure qu'un système grandit ..." vous refactorisez?
Rodney Gitzel
3
@ user3667089 Arguments généraux nécessaires pour que la classe existe logiquement, ceux que je transmettrais au constructeur. Ceux-ci seraient généralement utilisés dans la plupart / toutes les méthodes. Arguments spécifiques à la méthode que je transmettrais dans cette méthode spécifique.
Mark S.Rasmussen
1
En réalité, ce que vous faites avec une classe statique revient au C brut, non orienté objet (bien que géré par la mémoire) - toutes les fonctions sont dans un espace global, il n'y en a pas this, vous devez gérer l'état global, etc. si vous aimez la programmation procédurale, faites-le. Mais sachez que vous perdez alors de nombreux avantages structurels de l'OO.
Ingénieur
1
La plupart de ces raisons sont liées à une mauvaise gestion du code et non à l'utilisation de méthodes statiques. En F # et dans d'autres langages fonctionnels, nous utilisons partout des méthodes essentiellement statiques (des fonctions sans état d'instance) et nous n'avons pas ces problèmes. Cela dit, F # fournit un meilleur paradigme pour l'utilisation des fonctions (les fonctions sont de première classe, les fonctions peuvent être curées et partiellement appliquées), ce qui les rend plus réalisables qu'en C #. Une raison plus importante d'utiliser des classes est que c'est pour cela que C # est construit. Toutes les constructions d'injection de dépendances dans .NET Core tournent autour de classes d'instance avec deps.
Justin J Stark
89

Je préfère la manière statique. Étant donné que la classe ne représente pas un objet, il n'a pas de sens d'en faire une instance.

Les classes qui n'existent que pour leurs méthodes doivent rester statiques.

jjnguy
la source
19
-1 "Les classes qui n'existent que pour leurs méthodes doivent rester statiques."
Rookian
8
@Rookian pourquoi n'êtes-vous pas d'accord avec ça?
jjnguy
6
un exemple serait le modèle de référentiel. La classe ne contient que des méthodes. Suivre votre approche ne permet pas d'utiliser des interfaces. => augmenter le couplage
Rookian
1
@Rook, en général, les gens ne devraient pas créer de classes utilisées uniquement pour les méthodes. Dans l'exemple que vous avez donné, les méthodes statiques ne sont pas une bonne idée. Mais pour les méthodes utilitaires simples, la méthode statique est la meilleure.
jjnguy
9
Absolument correct :) Avec votre condition "pour les méthodes utilitaires simples", je suis entièrement d'accord avec vous, mais vous avez fait une règle générale dans votre réponse qui est fausse imo.
Rookian
18

S'il n'y a aucune raison d'avoir une instance de la classe créée afin d'exécuter la fonction, utilisez l'implémentation statique. Pourquoi obliger les consommateurs de cette classe à créer une instance quand on n'en a pas besoin.

Ray Jezek
la source
16

Si vous n'avez pas besoin d'enregistrer l' état de l'objet, il n'est pas nécessaire de l'instancier en premier lieu. J'irais avec la méthode statique unique à laquelle vous passez des paramètres.

Je mettrais également en garde contre une classe d'Utils géante qui a des dizaines de méthodes statiques non liées. Cela peut devenir désorganisé et compliqué à la hâte. Il est préférable d'avoir plusieurs classes, chacune avec peu de méthodes connexes.

Bill le lézard
la source
6

Je dirais que le format de la méthode statique serait la meilleure option. Et je rendrais la classe statique aussi, de cette façon vous n'auriez pas à vous soucier de créer accidentellement une instance de la classe.

Nathen Silver
la source
5

Je ne sais vraiment pas quelle est la situation ici, mais j'essaierais de la mettre en tant que méthode dans l'une des classes auxquelles appartiennent arg1, arg2 ou arg3 - Si vous pouvez dire sémantiquement qu'une de ces classes posséderait le méthode.

Chris Cudmore
la source
4

Je suggérerais qu'il est difficile de répondre en fonction des informations fournies.

Mon instinct est que si vous avez juste une méthode, et que vous allez jeter la classe immédiatement, alors faites-en une classe statique qui prend tous les paramètres.

Bien sûr, il est difficile de dire exactement pourquoi vous devez créer une seule classe uniquement pour cette méthode. Est-ce la situation typique de «classe Utilities» comme le supposent la plupart? Ou implémentez-vous une sorte de classe de règles, dont il pourrait y en avoir plus à l'avenir.

Par exemple, faites en sorte que cette classe soit connectable. Ensuite, vous voudriez créer une interface pour votre méthode unique, puis vous voudriez que tous les paramètres soient passés dans l'interface, plutôt que dans le constructeur, mais vous ne voudriez pas qu'elle soit statique.

pseudo
la source
3

Votre classe peut-elle être rendue statique?

Si tel est le cas, j'en ferais une classe `` Utilitaires '' dans laquelle je placerais toutes mes classes à fonction unique.

Robert
la source
2
Je suis quelque peu en désaccord. Dans les projets dans lesquels je suis impliqué, votre classe Utilities peut contenir des centaines de méthodes non liées.
Paul Tomblin
3
@Paul: Généralement d'accord. Je déteste voir des classes avec des dizaines (ou oui, des centaines) de méthodes totalement indépendantes. Si vous devez adopter cette approche, divisez-les au moins en ensembles d'utilitaires connexes plus petits (EG, FooUtilities, BarUtilities, etc.).
John Rudy
9
une responsabilité de classe-une.
Andreas Petersson
3

Si cette méthode est sans état et que vous n'avez pas besoin de la transmettre, il est plus judicieux de la définir comme statique. Si vous avez besoin de transmettre la méthode, vous pouvez envisager d'utiliser un délégué plutôt que l'une de vos autres approches proposées.

Joel Wietelmann
la source
3

Pour les applications simples et les internalaides, j'utiliserais une méthode statique. Pour les applications avec des composants, j'adore le cadre d'extensibilité gérée . Voici un extrait d'un document que j'écris pour décrire les modèles que vous trouverez dans mes API.

  • Prestations de service
    • Défini par une I[ServiceName]Serviceinterface.
    • Exporté et importé par type d'interface.
    • L'implémentation unique est fournie par l'application hôte et consommée en interne et / ou par des extensions.
    • Les méthodes de l'interface de service sont thread-safe.

À titre d'exemple artificiel:

public interface ISettingsService
{
    string ReadSetting(string name);

    void WriteSetting(string name, string value);
}

[Export]
public class ObjectRequiringSettings
{
    [Import]
    private ISettingsService SettingsService
    {
        get;
        set;
    }

    private void Foo()
    {
        if (SettingsService.ReadSetting("PerformFooAction") == bool.TrueString)
        {
            // whatever
        }
    }
}
Sam Harwell
la source
2

Je ferais juste tout dans le constructeur. ainsi:

new MyClass(arg1, arg2, arg3);// the constructor does everything.

ou

MyClass my_object(arg1, arg2, arg3);
pravprab
la source
Et si vous avez besoin de renvoyer autre chose qu'un objet de type MyClass?
Bill the Lizard
13
Je considère comme une mauvaise pratique de mettre la logique d'exécution dans un constructeur. Un bon développement est une question de sémantique. Sémantiquement, un constructeur existe pour construire un objet, pas pour effectuer d'autres tâches.
mstrobl
bill, si vous avez besoin d'une valeur de retour, passez la référence à la valeur de retour: MyClass myObject (arg1, arg2, arg3, retvalue); mstrobl, si l'objet n'est pas nécessaire, pourquoi le créer? et cette astuce peut réellement vous aider dans certains cas.
Si un objet n'a pas d'état, c'est-à-dire pas de variables, l'instanciation ne produira aucune allocation - ni sur le tas ni sur la pile. Vous pouvez considérer les objets en C ++ comme de simples appels de fonction C avec un premier paramètre masqué pointant vers une structure.
mstrobl
mstrobl, il aime la fonction ac sur les stéroïdes car vous pouvez définir des fonctions privées que le ctor peut utiliser. vous pouvez également utiliser les variables membres de classe pour aider à transmettre des données aux fonctions privées.
0

Une autre question importante à considérer est de savoir si le système fonctionnerait dans un environnement multithread, et s'il serait thread-safe d'avoir une méthode statique ou des variables ...

Vous devez faire attention à l'état du système.

NZal
la source
0

Vous pourrez peut-être éviter la situation tous ensemble. Essayez de refactoriser pour obtenir arg1.myMethod1(arg2, arg3). Échangez arg1 avec arg2 ou arg3 si cela a plus de sens.

Si vous n'avez pas de contrôle sur la classe de arg1, décorez-la:

class Arg1Decorator
    private final T1 arg1;
    public Arg1Decorator(T1 arg1) {
        this.arg1 = arg1;
    }
    public T myMethod(T2 arg2, T3 arg3) {
        ...
    }
 }

 arg1d = new Arg1Decorator(arg1)
 arg1d.myMethod(arg2, arg3)

Le raisonnement est que, dans la POO, les données et les méthodes de traitement de ces données vont ensemble. De plus, vous bénéficiez de tous les avantages mentionnés par Mark.

Hugo Wood
la source
0

Je pense que si les propriétés de votre classe ou de l'instance de classe ne sont pas utilisées dans les constructeurs ou dans vos méthodes, il n'est pas suggéré que les méthodes soient conçues comme un modèle «statique». La méthode statique doit toujours être pensée de manière «aide».

user7545070
la source
0

Selon que vous souhaitiez seulement faire quelque chose ou faire et retourner quelque chose, vous pouvez le faire:

public abstract class DoSomethingClass<T>
{
    protected abstract void doSomething(T arg1, T arg2, T arg3);
}

public abstract class ReturnSomethingClass<T, V>
{
    public T value;
    protected abstract void returnSomething(V arg1, V arg2, V arg3);
}

public class DoSomethingInt extends DoSomethingClass<Integer>
{
    public DoSomethingInt(int arg1, int arg2, int arg3)
    {
        doSomething(arg1, arg2, arg3);
    }

    @Override
    protected void doSomething(Integer arg1, Integer arg2, Integer arg3)
    {
        // ...
    }
}

public class ReturnSomethingString extends ReturnSomethingClass<String, Integer>
{
    public ReturnSomethingString(int arg1, int arg2, int arg3)
    {
        returnSomething(arg1, arg2, arg3);
    }

    @Override
    protected void returnSomething(Integer arg1, Integer arg2, Integer arg3)
    {
        String retValue;
        // ...
        value = retValue;
    }
}

public class MainClass
{
    static void main(String[] args)
    {
        int a = 3, b = 4, c = 5;

        Object dummy = new DoSomethingInt(a,b,c);  // doSomething was called, dummy is still around though
        String myReturn = (new ReturnSomethingString(a,b,c)).value; // returnSomething was called and immediately destroyed
    }
}
Mark Walsh
la source