Comment décomposer un constructeur?

21

Disons que j'ai une classe Enemy, et le constructeur ressemblerait à quelque chose comme:

public Enemy(String name, float width, float height, Vector2 position, 
             float speed, int maxHp, int attackDamage, int defense... etc.){}

Cela semble mauvais parce que le constructeur a tellement de paramètres, mais lorsque je crée une instance Enemy, je dois spécifier toutes ces choses. Je veux également ces attributs dans la classe Enemy, afin de pouvoir parcourir une liste d'entre eux et obtenir / définir ces paramètres. Je pensais peut-être sous-classer l'ennemi en EnemyB, EnemyA, tout en codant en dur leur maxHp et d'autres attributs spécifiques, mais je perdrais ensuite l'accès à leurs attributs codés en dur si je voulais parcourir une liste d'ennemis (composée d'EnemyA, d'EnemyB et EnemyC).

J'essaie juste d'apprendre à coder proprement. Si cela fait une différence, je travaille en Java / C ++ / C #. Tout point dans la bonne direction est apprécié.

Travis
la source
5
Il n'y a rien de mal à avoir un constructeur qui lie tous les attributs. En fait, dans certains environnements de persistance, il est requis. Rien ne dit que vous ne pouvez pas avoir plusieurs constructeurs, peut-être avec la méthode de vérification de validité à appeler après avoir fait la construction par morceaux.
BobDalgleish
1
Je devrais me demander si vous avez l'intention de construire des objets ennemis dans du code à l'aide de littéraux. Si vous ne le faites pas, et je ne vois pas pourquoi vous le feriez, alors construisez des constructeurs qui tirent les données d'une interface de base de données, ou d'une chaîne de sérialisation, ou ...
Zan Lynx

Réponses:

58

La solution consiste à regrouper les paramètres en types composites. La largeur et la hauteur sont liées conceptuellement - elles spécifient les dimensions de l'ennemi et seront généralement nécessaires ensemble. Ils pourraient être remplacés par un Dimensionstype, ou peut-être un Rectangletype qui inclut également la position. D'un autre côté, il peut être plus judicieux de regrouper positionet de créer speedun MovementDatatype, surtout si l'accélération entre plus tard dans l'image. De contexte , je suppose maxHp, attackDamage, defense, etc appartiennent également ensemble dans un Statsgenre. Ainsi, une signature révisée pourrait ressembler à ceci:

public Enemy(String name, Dimensions dimensions, MovementData movementData, Stats stats)

Les détails précis de l'endroit où tracer les lignes dépendront du reste de votre code et des données couramment utilisées ensemble.

Doval
la source
21
J'ajouterais également qu'avoir autant de valeurs pourrait indiquer une violation du principe de responsabilité unique. Et le regroupement des valeurs en objets spécifiques est la première étape pour séparer ces responsabilités.
Euphoric
2
Je ne pense pas que la liste de valeurs soit un problème SRP; la plupart d'entre eux sont probablement destinés aux constructeurs de classes de base. Chaque classe de la hiérarchie peut avoir une seule responsabilité. Enemyest juste la classe qui cible Player, mais leur classe de base commune a Combatantbesoin des statistiques de combat.
MSalters
@MSalters Cela n'indique pas nécessairement un problème SRP, mais cela pourrait. S'il a besoin de faire suffisamment de calculs, ces fonctions pourraient trouver leur place dans la classe Enemy alors qu'elles devraient être des fonctions statiques / libres (s'il utilise Dimensions/ MovementDatacomme de vieux conteneurs de données) ou des méthodes (s'il les transforme en données abstraites types / objets). Par exemple, s'il n'avait pas déjà créé un Vector2type, il aurait peut-être fini par faire des mathématiques vectorielles dans Enemy.
Doval
24

Vous voudrez peut-être jeter un œil au modèle Builder . À partir du lien (avec des exemples du modèle par rapport aux alternatives):

[Le] modèle Builder est un bon choix lors de la conception de classes dont les constructeurs ou les usines statiques auraient plus d'une poignée de paramètres, surtout si la plupart de ces paramètres sont facultatifs. Le code client est beaucoup plus facile à lire et à écrire avec les constructeurs qu'avec le modèle de constructeur télescopique traditionnel, et les constructeurs sont beaucoup plus sûrs que JavaBeans.

Rory Hunter
la source
4
Un extrait de code court serait utile. C'est un excellent modèle pour construire des objets ou des structures complexes avec diverses entrées. Vous pouvez également spécialiser les générateurs, comme EnemyABuilder, EnemyBBuilder, etc. qui encapsulent les différentes propriétés partagées. C'est en quelque sorte le revers du modèle Factory (comme indiqué ci-dessous), mais ma préférence personnelle est pour Builder.
Rob
1
Merci, le modèle Builder et les modèles Factory semblent bien fonctionner avec ce que j'essaie de faire dans l'ensemble. Je pense qu'une combinaison de Builder / Factory et de la suggestion de Doval pourrait être ce que je recherche. Edit: Je suppose que je ne peux marquer qu'une seule réponse; Je vais le donner à Doval car il répond à la question du sujet, mais les autres sont tout aussi utiles à mon problème spécifique. Merci à tous.
Travis
Je pense qu'il vaut la peine de noter que si votre langage prend en charge les types fantômes, vous pouvez écrire un modèle de générateur qui impose que certaines / toutes les fonctions SetX soient appelées. Cela permet également de s'assurer qu'ils ne sont appelés qu'une seule fois (si vous le souhaitez).
Thomas Eding
1
@ Mark16 Comme mentionné dans le lien, > le modèle Builder simule les paramètres facultatifs nommés tels que trouvés dans Ada et Python. Vous avez mentionné que vous utilisez également C # dans la question, et que le langage prend en charge les arguments nommés / facultatifs (à partir de C # 4.0), ce qui peut être une autre option.
Bob
5

L'utilisation de sous-classes pour prédéfinir certaines valeurs n'est pas souhaitable. Sous-classe uniquement lorsqu'un nouveau type d'ennemi a un comportement différent ou de nouveaux attributs.

Le modèle d'usine est généralement utilisé pour résumer la classe exacte utilisée, mais il peut également être utilisé pour fournir des modèles pour la création d'objets:

class EnemyFactory {

    // each of these methods is essentially a template for a kind of enemy

    Enemy enemyA(String name, ...) {
        return new Enemy(name, ..., presetValue, ...);
    }

    Enemy enemyB(String name, ...) {
        return new Enemy(name, ..., otherValue, ...);
    }

    Enemy enemyC(String name, ...) {
        return new EnemySubclass(name, ..., otherValue, ...);
    }

    ...
}

EnemyFactory factory = new EnemyFactory();
Enemy a = factory.enemyA("fred", ...);
Enemy b = factory.enemyB("willy", ...);
amon
la source
0

Je réserverais le sous-classement aux classes qui représentent des objets que vous pourriez utiliser indépendamment, par exemple une classe de caractères où tous les personnages, pas seulement les ennemis ont un nom, une vitesse, maxHp, ou une classe pour représenter des sprites qui ont une présence à l'écran avec une largeur, hauteur, position.

Je ne vois rien de mal en soi avec un constructeur avec beaucoup de paramètres d'entrée mais si vous voulez le diviser un peu, vous pouvez avoir un constructeur qui configure la plupart des paramètres et un autre constructeur (surchargé) qui peut être utilisé pour en définir des spécifiques et en définir d'autres sur des valeurs par défaut.

Selon la langue que vous choisissez d'utiliser, certains peuvent définir des valeurs par défaut pour les paramètres d'entrée de votre constructeur comme:

Enemy(float height = 42, float width = 42);
Encaitar
la source
0

Un exemple de code à ajouter à la réponse de Rory Hunter (en Java):

public class Enemy{
   private String name;
   private float width;
   ...

   public static class Builder{
       private Enemy instance;

       public Builder(){
           this.instance = new Enemy();
       }


       public Builder withName(String name){
           instance.name = name;
           return this;
       }

       ...

       public Enemy build(){
           return instance;
       }
   }
}

Maintenant, vous pouvez créer de nouvelles instances d'Ennemi comme ceci:

Enemy myEnemy = new Enemy.Builder().withName("John").withX(x).build();
Toon Borgers
la source
1
Les programmeurs sont censés visiter les questions conceptuelles et les réponses sont censées expliquer les choses . Lancer des vidages de code au lieu d'explications revient à copier du code de l'IDE vers le tableau blanc: cela peut sembler familier et même parfois compréhensible, mais cela semble bizarre ... juste bizarre. Le tableau blanc n'a pas de compilateur
moucher