Parfois, je rencontre des méthodes avec un nombre inconfortable de paramètres. Plus souvent qu'autrement, ils semblent être des constructeurs. Il semble qu'il devrait y avoir un meilleur moyen, mais je ne vois pas ce que c'est.
return new Shniz(foo, bar, baz, quux, fred, wilma, barney, dino, donkey)
J'ai pensé à utiliser des structures pour représenter la liste des paramètres, mais cela semble simplement déplacer le problème d'un endroit à un autre et créer un autre type dans le processus.
ShnizArgs args = new ShnizArgs(foo, bar, baz, quux, fred, wilma, barney, dino, donkey)
return new Shniz(args);
Cela ne semble donc pas être une amélioration. Quelle est donc la meilleure approche?
La meilleure façon serait de trouver des moyens de regrouper les arguments. Cela suppose, et ne fonctionne vraiment que si, vous vous retrouvez avec plusieurs "groupes" d'arguments.
Par exemple, si vous transmettez la spécification d'un rectangle, vous pouvez transmettre x, y, largeur et hauteur ou vous pouvez simplement passer un objet rectangle contenant x, y, largeur et hauteur.
Recherchez des éléments comme celui-ci lors de la refactorisation pour le nettoyer un peu. Si les arguments ne peuvent vraiment pas être combinés, commencez à chercher si vous avez une violation du principe de responsabilité unique.
Je vais supposer que vous voulez dire C # . Certaines de ces choses s'appliquent également à d'autres langues.
Vous avez plusieurs options:
passer du constructeur aux setters de propriétés . Cela peut rendre le code plus lisible, car il est évident pour le lecteur quelle valeur correspond à quels paramètres. La syntaxe de l'initialiseur d'objets rend cela agréable. Il est également simple à implémenter, car vous pouvez simplement utiliser des propriétés générées automatiquement et ignorer l'écriture des constructeurs.
class C
{
public string S { get; set; }
public int I { get; set; }
}
new C { S = "hi", I = 3 };
Toutefois, vous perdez l'immuabilité et vous perdez la possibilité de vous assurer que les valeurs requises sont définies avant d'utiliser l'objet au moment de la compilation.
Modèle de générateur .
Pensez à la relation entre string
et StringBuilder
. Vous pouvez l'obtenir pour vos propres cours. J'aime l'implémenter comme une classe imbriquée, donc la classe C
a une classe associée C.Builder
. J'aime aussi une interface fluide sur le constructeur. Bien fait, vous pouvez obtenir une syntaxe comme celle-ci:
C c = new C.Builder()
.SetX(4) // SetX is the fluent equivalent to a property setter
.SetY("hello")
.ToC(); // ToC is the builder pattern analog to ToString()
// Modify without breaking immutability
c = c.ToBuilder().SetX(2).ToC();
// Still useful to have a traditional ctor:
c = new C(1, "...");
// And object initializer syntax is still available:
c = new C.Builder { X = 4, Y = "boing" }.ToC();
J'ai un script PowerShell qui me permet de générer le code du générateur pour faire tout cela, où l'entrée ressemble à:
class C {
field I X
field string Y
}
Je peux donc générer au moment de la compilation. partial
classes me permettent d'étendre à la fois la classe principale et le générateur sans modifier le code généré.
Refactoring "Introduire un objet de paramètre" . Voir le Refactoring Catalogue . L'idée est que vous prenez certains des paramètres que vous transmettez et que vous les placez dans un nouveau type, puis passez une instance de ce type à la place. Si vous faites cela sans réfléchir, vous vous retrouverez là où vous avez commencé:
new C(a, b, c, d);
devient
new C(new D(a, b, c, d));
Cependant, cette approche a le plus grand potentiel pour avoir un impact positif sur votre code. Continuez donc en suivant ces étapes:
Recherchez sous-ensembles de paramètres qui ont du sens ensemble. Le fait de regrouper tous les paramètres d'une fonction sans réfléchir ne vous apporte pas grand-chose; l'objectif est d'avoir des regroupements qui ont du sens. Vous saurez que vous avez bien compris lorsque le nom du nouveau type est évident.
Recherchez d'autres endroits où ces valeurs sont utilisées ensemble et utilisez également le nouveau type. Il y a de fortes chances que, lorsque vous avez trouvé un bon nouveau type pour un ensemble de valeurs que vous utilisez déjà partout, ce nouveau type ait également un sens dans tous ces endroits.
Recherchez les fonctionnalités qui se trouvent dans le code existant, mais qui appartiennent au nouveau type.
Par exemple, vous voyez peut-être du code qui ressemble à ceci:
bool SpeedIsAcceptable(int minSpeed, int maxSpeed, int currentSpeed)
{
return currentSpeed >= minSpeed & currentSpeed < maxSpeed;
}
Vous pouvez prendre les paramètres minSpeed
et maxSpeed
et les mettre dans un nouveau type:
class SpeedRange
{
public int Min;
public int Max;
}
bool SpeedIsAcceptable(SpeedRange sr, int currentSpeed)
{
return currentSpeed >= sr.Min & currentSpeed < sr.Max;
}
C'est mieux, mais pour vraiment profiter du nouveau type, déplacez les comparaisons dans le nouveau type:
class SpeedRange
{
public int Min;
public int Max;
bool Contains(int speed)
{
return speed >= min & speed < Max;
}
}
bool SpeedIsAcceptable(SpeedRange sr, int currentSpeed)
{
return sr.Contains(currentSpeed);
}
Et maintenant nous arrivons quelque part: l'implémentation de SpeedIsAcceptable()
dit maintenant ce que vous voulez dire, et vous avez une classe utile et réutilisable. (La prochaine étape évidente consiste à faire de SpeedRange
dans Range<Speed>
.)
Comme vous pouvez le voir, Introduce Parameter Object était un bon début, mais sa vraie valeur était qu'il nous a aidés à découvrir un type utile qui manquait à notre modèle.
S'il s'agit d'un constructeur, en particulier s'il existe plusieurs variantes surchargées, vous devriez regarder le modèle Builder:
Foo foo = new Foo()
.configBar(anything)
.configBaz(something, somethingElse)
// and so on
S'il s'agit d'une méthode normale, vous devez penser aux relations entre les valeurs transmises et peut-être créer un objet de transfert.
Ceci est extrait du livre de Fowler et Beck: "Refactoring"
Liste de paramètres longue
Dans nos premiers jours de programmation, nous avons appris à passer en paramètres tout ce dont une routine avait besoin. Cela était compréhensible car l'alternative était des données globales, et les données globales sont mauvaises et généralement douloureuses. Les objets changent cette situation car si vous n'avez pas quelque chose dont vous avez besoin, vous pouvez toujours demander à un autre objet de vous l'obtenir. Ainsi, avec les objets, vous ne transmettez pas tout ce dont la méthode a besoin; au lieu de cela, vous passez suffisamment pour que la méthode puisse accéder à tout ce dont elle a besoin. Une grande partie de ce dont une méthode a besoin est disponible dans la classe Host de la méthode. Dans les programmes orientés objet, les listes de paramètres ont tendance à être beaucoup plus petites que dans les programmes traditionnels. C'est bien parce que les longues listes de paramètres sont difficiles à comprendre, parce qu'elles deviennent incohérentes et difficiles à utiliser, et parce que vous les modifiez à tout jamais car vous avez besoin de plus de données. La plupart des modifications sont supprimées en passant des objets, car il est plus probable que vous n'ayez besoin que de quelques demandes pour accéder à une nouvelle donnée. Utilisez Remplacer le paramètre par la méthode lorsque vous pouvez obtenir les données dans un paramètre en faisant la demande d'un objet que vous connaissez déjà. Cet objet peut être un champ ou un autre paramètre. Utilisez Conserver tout l'objet pour prendre un tas de données glanées d'un objet et le remplacer par l'objet lui-même. Si vous disposez de plusieurs éléments de données sans objet logique, utilisez Introduire un objet de paramètre. Il existe une exception importante à ces modifications. C'est lorsque vous ne souhaitez explicitement pas créer de dépendance de l'objet appelé vers l'objet plus grand. Dans ces cas, décompresser les données et les envoyer en tant que paramètres est raisonnable, mais faites attention à la douleur impliquée. Si la liste des paramètres est trop longue ou change trop souvent, vous devez repenser votre structure de dépendance.
La réponse classique à cela consiste à utiliser une classe pour encapsuler une partie ou la totalité des paramètres. En théorie, ça sonne bien, mais je suis le genre de gars qui crée des cours pour des concepts qui ont un sens dans le domaine, donc ce n'est pas toujours facile d'appliquer ce conseil.
Par exemple. au lieu de:
driver.connect(Host, user, pass)
Vous pourriez utiliser
config = new Configuration()
config.setHost(Host)
config.setUser(user)
config.setPass(pass)
driver.connect(config)
YMMV
Quand je vois de longues listes de paramètres, ma première question est de savoir si cette fonction ou cet objet en fait trop. Considérer:
EverythingInTheWorld earth=new EverythingInTheWorld(firstCustomerId,
lastCustomerId,
orderNumber, productCode, lastFileUpdateDate,
employeeOfTheMonthWinnerForLastMarch,
yearMyHometownWasIncorporated, greatGrandmothersBloodType,
planetName, planetSize, percentWater, ... etc ...);
Bien sûr, cet exemple est délibérément ridicule, mais j'ai vu de nombreux programmes réels avec des exemples légèrement moins ridicules, où une classe est utilisée pour contenir de nombreuses choses à peine liées ou non liées, apparemment simplement parce que le même programme appelant a besoin des deux ou parce que le le programmeur pensait aux deux en même temps. Parfois, la solution la plus simple est de simplement diviser la classe en plusieurs morceaux, chacun faisant sa propre chose.
Un peu plus compliqué est quand une classe a vraiment besoin de traiter plusieurs choses logiques, comme une commande client et des informations générales sur le client. Dans ces cas, créez une classe pour le client et une classe pour la commande, et laissez-les se parler au besoin. Donc au lieu de:
Order order=new Order(customerName, customerAddress, customerCity,
customerState, customerZip,
orderNumber, orderType, orderDate, deliveryDate);
Nous pourrions avoir:
Customer customer=new Customer(customerName, customerAddress,
customerCity, customerState, customerZip);
Order order=new Order(customer, orderNumber, orderType, orderDate, deliveryDate);
Bien que je préfère bien sûr les fonctions qui ne prennent que 1 ou 2 ou 3 paramètres, nous devons parfois accepter que, de façon réaliste, cette fonction prend un tas, et que le nombre de lui-même ne crée pas vraiment de complexité. Par exemple:
Employee employee=new Employee(employeeId, firstName, lastName,
socialSecurityNumber,
address, city, state, Zip);
Oui, c'est un tas de champs, mais probablement tout ce que nous allons faire avec eux est de les sauvegarder dans un enregistrement de base de données ou de les jeter sur un écran ou quelque chose du genre. Il n'y a pas vraiment beaucoup de traitement ici.
Lorsque mes listes de paramètres s'allongent, je préfère de loin donner aux champs différents types de données. Comme quand je vois une fonction comme:
void updateCustomer(String type, String status,
int lastOrderNumber, int pastDue, int deliveryCode, int birthYear,
int addressCode,
boolean newCustomer, boolean taxExempt, boolean creditWatch,
boolean foo, boolean bar);
Et puis je le vois appelé avec:
updateCustomer("A", "M", 42, 3, 1492, 1969, -7, true, false, false, true, false);
Je m'inquiète. En regardant l'appel, il n'est pas du tout clair ce que signifient tous ces chiffres cryptiques, codes et drapeaux. C'est juste demander des erreurs. Un programmeur peut facilement se tromper sur l'ordre des paramètres et en commuter accidentellement deux, et s'il s'agit du même type de données, le compilateur l'acceptera simplement. Je préfère de loin avoir une signature où toutes ces choses sont des énumérations, donc un appel passe dans des choses comme Type.ACTIVE au lieu de "A" et CreditWatch.NO au lieu de "faux", etc.
Je ne veux pas ressembler à un sage-crack, mais vous devez également vérifier que les données que vous transmettez vraiment doivent être transmises: Passer des choses à un constructeur (ou une méthode pour cela matière) sent un peu comme si l'on mettait peu l'accent sur le comportement d'un objet.
Ne vous méprenez pas: les méthodes et les constructeurs vont ont parfois beaucoup de paramètres. Mais une fois rencontré, essayez d'envisager d'encapsuler données avec comportement à la place.
Ce type d'odeur (puisque nous parlons de refactoring, cet horrible mot semble approprié ...) pourrait également être détecté pour des objets qui ont beaucoup de propriétés (lire: toutes) ou des getters/setters.
Si certains des paramètres du constructeur sont facultatifs, il est logique d'utiliser un générateur, qui obtiendrait les paramètres requis dans le constructeur, et aurait des méthodes pour les paramètres facultatifs, renvoyant le générateur, à utiliser comme ceci:
return new Shniz.Builder(foo, bar).baz(baz).quux(quux).build();
Les détails de ceci sont décrits dans Effective Java, 2nd Ed., P. 11. Pour les arguments de méthode, le même livre (p. 189) décrit trois approches pour raccourcir les listes de paramètres:
DinoDonkey
au lieu de dino
et donkey
Vous n'avez pas fourni suffisamment d'informations pour justifier une bonne réponse. Une longue liste de paramètres n'est pas intrinsèquement mauvaise.
Shniz (foo, bar, baz, quux, fred, wilma, barney, dino, âne)
pourrait être interprété comme:
void Shniz(int foo, int bar, int baz, int quux, int fred,
int wilma, int barney, int dino, int donkey) { ...
Dans ce cas, il vaut mieux créer une classe pour encapsuler les paramètres parce que vous donnez un sens aux différents paramètres d'une manière que le compilateur peut vérifier ainsi que rendre visuellement le code plus facile à lire. Il facilite également la lecture et la refonte plus tard.
// old way
Shniz(1,2,3,2,3,2,1,2);
Shniz(1,2,2,3,3,2,1,2);
//versus
ShnizParam p = new ShnizParam { Foo = 1, Bar = 2, Baz = 3 };
Shniz(p);
Alternativement si vous aviez:
void Shniz(Foo foo, Bar bar, Baz baz, Quux quux, Fred fred,
Wilma wilma, Barney barney, Dino dino, Donkey donkey) { ...
C'est un cas très différent car tous les objets sont différents (et ne risquent pas d'être embrouillés). Il est convenu que si tous les objets sont nécessaires et qu'ils sont tous différents, cela n'a pas de sens de créer une classe de paramètres.
De plus, certains paramètres sont-ils facultatifs? Existe-t-il des substitutions de méthode (même nom de méthode, mais signatures de méthodes différentes?) Ces sortes de détails ont tous une importance pour la réponse meilleure.
* Un sac de propriété peut également être utile, mais pas spécialement meilleur étant donné qu'il n'y a pas de contexte donné.
Comme vous pouvez le voir, il y a plus d'une réponse correcte à cette question. Faites votre choix.
J'utiliserais le constructeur par défaut et les évaluateurs de propriétés. C # 3.0 a une belle syntaxe pour le faire automatiquement.
return new Shniz { Foo = foo,
Bar = bar,
Baz = baz,
Quuz = quux,
Fred = fred,
Wilma = wilma,
Barney = barney,
Dino = dino,
Donkey = donkey
};
L'amélioration du code vient de simplifier le constructeur et de ne pas avoir à prendre en charge plusieurs méthodes pour prendre en charge diverses combinaisons. La syntaxe "appelante" est encore un peu "verbeuse", mais pas vraiment pire que d'appeler manuellement les setters de propriété.
Vous pouvez essayer de regrouper votre paramètre en plusieurs structures/classes significatives (si possible).
Je pencherais généralement vers l'approche structurée - sans doute la majorité de ces paramètres sont liés d'une certaine manière et représentent l'état d'un élément qui est pertinent pour votre méthode.
Si l'ensemble de paramètres ne peut pas être transformé en un objet significatif, c'est probablement un signe que Shniz
en fait trop, et le refactoring devrait impliquer de diviser la méthode en préoccupations distinctes.
Si votre langue le prend en charge, utilisez des paramètres nommés et rendez autant d'options (avec des valeurs par défaut raisonnables) que possible.
Si vous avez autant de paramètres, il est probable que la méthode en fasse trop, alors résolvez d'abord cela en divisant la méthode en plusieurs méthodes plus petites. Si vous avez encore trop de paramètres après cela, essayez de regrouper les arguments ou de transformer certains des paramètres en membres d'instance.
Préférez les petites classes/méthodes aux grandes. N'oubliez pas le principe de la responsabilité unique.
Je pense que la méthode que vous avez décrite est la voie à suivre. Lorsque je trouve une méthode avec beaucoup de paramètres et/ou qui aura probablement besoin de plus à l'avenir, je crée généralement un objet ShnizParams à traverser, comme vous le décrivez.
Que diriez-vous de ne pas le définir en une fois chez les constructeurs mais de le faire via propriétés/setters? J'ai vu quelques classes .NET qui utilisent cette approche telles que la classe Process
:
Process p = new Process();
p.StartInfo.UseShellExecute = false;
p.StartInfo.CreateNoWindow = true;
p.StartInfo.RedirectStandardOutput = true;
p.StartInfo.RedirectStandardError = true;
p.StartInfo.FileName = "cmd";
p.StartInfo.Arguments = "/c dir";
p.Start();
Vous pouvez échanger la complexité contre des lignes de code source. Si la méthode elle-même fait trop (couteau suisse), essayez de diviser par deux ses tâches en créant une autre méthode. Si la méthode est simple, elle nécessite trop de paramètres, alors les objets appelés paramètres sont la voie à suivre.
Les arguments nommés sont une bonne option (en supposant un langage qui les prend en charge) pour lever l'ambiguïté des listes de paramètres longues (ou même courtes!) Tout en permettant également (dans le cas des constructeurs) que les propriétés de la classe soient immuables sans imposer une condition pour lui permettre d'exister. dans un état partiellement construit.
L'autre option que je rechercherais en faisant ce type de refactoriser serait des groupes de paramètres liés qui pourraient être mieux traités comme un objet indépendant. En utilisant la classe Rectangle d'une réponse précédente comme exemple, le constructeur qui prend les paramètres pour x, y, hauteur et largeur pourrait factoriser x et y dans un objet Point, vous permettant de passer trois paramètres au constructeur du Rectangle. Ou allez un peu plus loin et faites-en deux paramètres (UpperLeftPoint, LowerRightPoint), mais ce serait une refactorisation plus radicale.
Je suis d'accord avec l'approche consistant à déplacer les paramètres dans un objet paramètre (struct). Plutôt que de simplement les coller tous dans un même objet, vérifiez si d'autres fonctions utilisent des groupes de paramètres similaires. Un objet paramater est plus utile s'il est utilisé avec plusieurs fonctions où vous vous attendez à ce que cet ensemble de paramètres change de manière cohérente entre ces fonctions. Il se peut que vous ne placiez que certains des paramètres dans le nouvel objet de paramètre.
Cela dépend du type d'arguments que vous avez, mais s'il y a beaucoup de valeurs/options booléennes, vous pourriez peut-être utiliser un Flag Enum?
Je pense que ce problème est profondément lié au domaine du problème que vous essayez de résoudre avec la classe.
Dans certains cas, un constructeur à 7 paramètres peut indiquer une mauvaise hiérarchie de classe: dans ce cas, la structure/classe d'aide suggérée ci-dessus est généralement une bonne approche, mais vous avez également tendance à vous retrouver avec des charges de structures qui ne sont que des sacs de propriétés et ne faites rien d'utile. Le constructeur à 8 arguments peut également indiquer que votre classe est trop générique/trop polyvalente, elle a donc besoin de beaucoup d'options pour être vraiment utile. Dans ce cas, vous pouvez soit refactoriser la classe, soit implémenter des constructeurs statiques qui masquent les vrais constructeurs complexes: par exemple. Shniz.NewBaz (foo, bar) pourrait en fait appeler le vrai constructeur en passant les bons paramètres.
Une considération est laquelle des valeurs serait en lecture seule une fois l'objet créé?
Des propriétés accessibles en écriture pourraient peut-être être attribuées après la construction.
D'où viennent finalement les valeurs? Peut-être que certaines valeurs sont vraiment externes alors que d'autres proviennent vraiment d'une configuration ou de données globales gérées par la bibliothèque.
Dans ce cas, vous pouvez cacher le constructeur à une utilisation externe et lui fournir une fonction Create. La fonction create prend les valeurs véritablement externes et construit l'objet, puis utilise des accesseurs disponibles uniquement pour la bibliothèque pour terminer la création de l'objet.
Il serait vraiment étrange d'avoir un objet qui nécessite 7 paramètres ou plus pour donner à l'objet un état complet et étant vraiment de nature externe.
Quand un clas a un constructeur qui prend trop d'arguments, c'est généralement un signe qu'il a trop de responsabilités. Il peut probablement être divisé en classes distinctes qui coopèrent pour donner les mêmes fonctionnalités.
Si vous avez vraiment besoin d'autant d'arguments pour un constructeur, le modèle Builder peut vous aider. Le but est de toujours transmettre tous les arguments au constructeur, ainsi son état est initialisé depuis le début et vous pouvez toujours rendre la classe immuable si nécessaire.
Voir ci-dessous :
public class Toto {
private final String state0;
private final String state1;
private final String state2;
private final String state3;
public Toto(String arg0, String arg1, String arg2, String arg3) {
this.state0 = arg0;
this.state1 = arg1;
this.state2 = arg2;
this.state3 = arg3;
}
public static class TotoBuilder {
private String arg0;
private String arg1;
private String arg2;
private String arg3;
public TotoBuilder addArg0(String arg) {
this.arg0 = arg;
return this;
}
public TotoBuilder addArg1(String arg) {
this.arg1 = arg;
return this;
}
public TotoBuilder addArg2(String arg) {
this.arg2 = arg;
return this;
}
public TotoBuilder addArg3(String arg) {
this.arg3 = arg;
return this;
}
public Toto newInstance() {
// maybe add some validation ...
return new Toto(this.arg0, this.arg1, this.arg2, this.arg3);
}
}
public static void main(String[] args) {
Toto toto = new TotoBuilder()
.addArg0("0")
.addArg1("1")
.addArg2("2")
.addArg3("3")
.newInstance();
}
}