Comment éviter le problème de «trop de paramètres» dans la conception d'API?

160

J'ai cette fonction API:

public ResultEnum DoSomeAction(string a, string b, DateTime c, OtherEnum d, 
     string e, string f, out Guid code)

Je n'aime pas ça. Parce que l'ordre des paramètres devient inutilement significatif. Il devient plus difficile d'ajouter de nouveaux champs. Il est plus difficile de voir ce qui se passe. Il est plus difficile de refactoriser la méthode en parties plus petites car cela crée une autre surcharge de transmission de tous les paramètres dans les sous-fonctions. Le code est plus difficile à lire.

J'ai eu l'idée la plus évidente: avoir un objet encapsulant les données et le faire circuler au lieu de passer chaque paramètre un par un. Voici ce que j'ai trouvé:

public class DoSomeActionParameters
{
    public string A;
    public string B;
    public DateTime C;
    public OtherEnum D;
    public string E;
    public string F;        
}

Cela a réduit ma déclaration API à:

public ResultEnum DoSomeAction(DoSomeActionParameters parameters, out Guid code)

Agréable. Cela semble très innocent, mais nous avons en fait introduit un énorme changement: nous avons introduit la mutabilité. Parce que ce que nous faisions auparavant était en fait de passer un objet anonyme immuable: des paramètres de fonction sur la pile. Maintenant, nous avons créé une nouvelle classe qui est très modifiable. Nous avons créé la possibilité de manipuler l'état de l' appelant . Ça craint. Maintenant je veux que mon objet soit immuable, que dois-je faire?

public class DoSomeActionParameters
{
    public string A { get; private set; }
    public string B { get; private set; }
    public DateTime C { get; private set; }
    public OtherEnum D { get; private set; }
    public string E { get; private set; }
    public string F { get; private set; }        

    public DoSomeActionParameters(string a, string b, DateTime c, OtherEnum d, 
     string e, string f)
    {
        this.A = a;
        this.B = b;
        // ... tears erased the text here
    }
}

Comme vous pouvez le voir, j'ai recréé mon problème d'origine: trop de paramètres. Il est évident que ce n'est pas la voie à suivre. Qu'est ce que je vais faire? La dernière option pour obtenir une telle immuabilité est d'utiliser une structure "en lecture seule" comme celle-ci:

public struct DoSomeActionParameters
{
    public readonly string A;
    public readonly string B;
    public readonly DateTime C;
    public readonly OtherEnum D;
    public readonly string E;
    public readonly string F;        
}

Cela nous permet d'éviter les constructeurs avec trop de paramètres et d'atteindre l'immuabilité. En fait, il résout tous les problèmes (ordre des paramètres, etc.). Encore:

C'est à ce moment-là que j'ai été confus et j'ai décidé d'écrire cette question: Quelle est la manière la plus simple en C # d'éviter le problème de «trop de paramètres» sans introduire de mutabilité? Est-il possible d'utiliser une structure en lecture seule à cette fin sans avoir une mauvaise conception d'API?

CLARIFICATIONS:

  • Veuillez supposer qu'il n'y a pas de violation du principe de responsabilité unique. Dans mon cas d'origine, la fonction écrit simplement des paramètres donnés dans un seul enregistrement DB.
  • Je ne cherche pas une solution spécifique à la fonction donnée. Je recherche une approche généralisée de ces problèmes. Je suis spécifiquement intéressé à résoudre le problème de "trop ​​de paramètres" sans introduire de mutabilité ou une conception terrible.

METTRE À JOUR

Les réponses fournies ici présentent différents avantages / inconvénients. Par conséquent, j'aimerais convertir cela en un wiki communautaire. Je pense que chaque réponse avec un exemple de code et des avantages / inconvénients ferait un bon guide pour des problèmes similaires à l'avenir. J'essaie maintenant de savoir comment faire.

ssg
la source
Clean Code: A Handbook of Agile Software Craftsmanship par Robert C. Martin et Martin Fowler's Refactoring livre couvre un peu cela
Ian Ringrose
1
Cette solution Builder n'est-elle pas redondante si vous utilisez C # 4 avec des paramètres facultatifs ?
khellang le
1
Je suis peut-être stupide, mais je ne vois pas en quoi c'est un problème, étant donné que le DoSomeActionParametersest un objet jetable, qui sera jeté après l'appel de la méthode.
Vilx-
6
Notez que je ne dis pas que vous devriez éviter les champs en lecture seule dans une structure; Les structures immuables sont une bonne pratique et les champs en lecture seule vous aident à créer des structures immuables auto-documentées. Mon point est que vous ne devriez pas vous fier au fait que les champs en lecture seule ne changent jamais , car ce n'est pas une garantie qu'un champ en lecture seule vous donne dans une structure. Il s'agit d'un cas spécifique du conseil plus général selon lequel vous ne devez pas traiter les types valeur comme s'il s'agissait de types référence; c'est un animal très différent.
Eric Lippert
7
@ssg: J'adorerais ça aussi. Nous avons ajouté des fonctionnalités à C # qui favorisent l'immuabilité (comme LINQ) en même temps que nous avons ajouté des fonctionnalités qui favorisent la mutabilité (comme les initialiseurs d'objet.) Ce serait bien d'avoir une meilleure syntaxe pour promouvoir les types immuables. Nous y réfléchissons beaucoup et avons des idées intéressantes, mais je ne m'attendrais pas à une telle chose pour la prochaine version.
Eric Lippert

Réponses:

22

Un style adopté dans les frameworks est généralement comme le regroupement des paramètres liés dans des classes associées (mais encore une fois problématique avec la mutabilité):

var request = new HttpWebRequest(a, b);
var service = new RestService(request, c, d, e);
var client = new RestClient(service, f, g);
var resource = client.RequestRestResource(); // O params after 3 objects
Teoman Soygul
la source
La condensation de toutes les chaînes dans un tableau de chaînes n'a de sens que si elles sont toutes liées. D'après l'ordre des arguments, il semble qu'ils ne sont pas tous complètement liés.
icktoofay
D'ailleurs, cela ne fonctionnerait que si vous avez beaucoup des mêmes types en tant que paramètre.
Timo Willemsen le
En quoi est-ce différent de mon premier exemple dans la question?
Sedat Kapanoglu le
Eh bien, c'est le style habituel adopté même dans le .NET Framework, indiquant que votre premier exemple est tout à fait exact dans ce qu'il fait, même s'il présente des problèmes mineurs de mutabilité (bien que cet exemple provienne d'une autre bibliothèque évidemment). Belle question au fait.
Teoman Soygul le
2
J'ai davantage convergé vers ce style au fil du temps. Une quantité apparemment significative des problèmes de «trop de paramètres» peut être résolue avec de bons groupes logiques et abstractions. Au final, cela rend le code plus lisible et plus modulaire.
Sedat Kapanoglu
87

Utilisez une combinaison de générateur et d'API de style de langage spécifique au domaine - Interface Fluent. L'API est un peu plus verbeuse mais avec intellisense, elle est très rapide à taper et facile à comprendre.

public class Param
{
        public string A { get; private set; }
        public string B { get; private set; }
        public string C { get; private set; }


  public class Builder
  {
        private string a;
        private string b;
        private string c;

        public Builder WithA(string value)
        {
              a = value;
              return this;
        }

        public Builder WithB(string value)
        {
              b = value;
              return this;
        }

        public Builder WithC(string value)
        {
              c = value;
              return this;
        }

        public Param Build()
        {
              return new Param { A = a, B = b, C = c };
        }
  }


  DoSomeAction(new Param.Builder()
        .WithA("a")
        .WithB("b")
        .WithC("c")
        .Build());
Samuel Neff
la source
+1 - ce type d'approche (que j'ai souvent vu appelé une "interface fluide") est exactement ce que j'avais en tête aussi.
Daniel Pryden le
+1 - c'est ce que j'ai fini dans la même situation. Sauf qu'il n'y avait qu'une seule classe, et en DoSomeActionétait une méthode.
Vilx-
+1 J'y ai aussi pensé, mais comme je suis nouveau dans les interfaces fluides, je ne savais pas si c'était parfait ici. Merci d'avoir validé ma propre intuition.
Matt
Je n'avais pas entendu parler du modèle Builder avant de poser cette question, donc cela a été une expérience intéressante pour moi. Je me demande à quel point c'est courant? Parce que tout développeur qui n'a pas entendu parler du modèle peut avoir du mal à comprendre son utilisation sans documentation.
Sedat Kapanoglu
1
@ssg, c'est courant dans ces scénarios. Le BCL a des classes de générateur de chaîne de connexion, par exemple.
Samuel Neff
10

Ce que vous avez là est une indication assez sûre que la classe en question viole le principe de responsabilité unique parce qu'elle a trop de dépendances. Recherchez des moyens de refactoriser ces dépendances en clusters de dépendances de façade .

Mark Seemann
la source
1
Je refactoriserais toujours en introduisant un ou plusieurs objets paramètres, mais évidemment, si vous déplacez tous les arguments vers un seul type immuable, vous n'avez rien accompli. L'astuce consiste à rechercher des clusters d'arguments plus étroitement liés, puis à refactoriser chacun de ces clusters en un objet paramètre distinct.
Mark Seemann
2
Vous pouvez transformer chaque groupe en un objet immuable en soi. Chaque objet n'aurait besoin que de prendre quelques paramètres, donc bien que le nombre réel d'arguments reste le même, le nombre d'arguments utilisés dans un seul constructeur sera réduit.
Mark Seemann le
2
+1 @ssg: Chaque fois que j'ai réussi à me convaincre de quelque chose comme ça, je me suis trompé au fil du temps en chassant une abstraction utile de grandes méthodes nécessitant ce niveau de paramètres. Le livre DDD d'Evans peut vous donner quelques idées sur la façon d'y penser (bien que votre système semble être un endroit potentiellement loin d'être pertinent pour l'application de tels modèles) - (et c'est juste un excellent livre dans tous les cas).
Ruben Bartelink
3
@Ruben: Aucun livre sensé ne dirait "dans un bon design OO, une classe ne devrait pas avoir plus de 5 propriétés". Les classes sont regroupées logiquement et ce type de contextualisation ne peut pas être basé sur des quantités. Cependant, la prise en charge de l'immuabilité de C # commence à créer des problèmes à un certain nombre de propriétés avant de commencer à violer les bons principes de conception OO.
Sedat Kapanoglu
3
@Ruben: Je n'ai pas jugé vos connaissances, votre attitude ou votre tempérament. J'attends de vous que vous fassiez de même. Je ne dis pas que vos recommandations ne sont pas bonnes. Je dis que mon problème peut apparaître même sur le design le plus parfait et cela semble être un point négligé ici. Bien que je comprenne pourquoi les gens expérimentés posent des questions fondamentales sur les erreurs les plus courantes, il devient moins agréable de les clarifier après quelques tours. Je dois encore dire qu'il est très possible d'avoir ce problème avec un design parfait. Et merci pour le vote positif!
Sedat Kapanoglu
10

Changez simplement votre structure de données de paramètres de a classà a structet vous êtes prêt à partir.

public struct DoSomeActionParameters 
{
   public string A;
   public string B;
   public DateTime C;
   public OtherEnum D;
   public string E;
   public string F;
}

public ResultEnum DoSomeAction(DoSomeActionParameters parameters, out Guid code) 

La méthode obtiendra maintenant sa propre copie de la structure. Les modifications apportées à la variable argument ne peuvent pas être observées par la méthode et les modifications apportées par la méthode à la variable ne peuvent pas être observées par l'appelant. L'isolement est réalisé sans immuabilité.

Avantages:

  • Le plus simple à mettre en œuvre
  • Moins de changement de comportement dans la mécanique sous-jacente

Les inconvénients:

  • L'immuabilité n'est pas évidente, nécessite l'attention du développeur.
  • Copie inutile pour maintenir l'immuabilité
  • Occupe l'espace de la pile
Jeffrey L Whitledge
la source
+1 C'est vrai mais cela ne résout pas la partie sur "exposer directement les champs est mal". Je ne sais pas à quel point les choses pourraient empirer si j'opte pour l'utilisation de champs au lieu de propriétés sur cette API.
Sedat Kapanoglu
@ssg - Faites-en donc des propriétés publiques au lieu de champs. Si vous traitez cela comme une structure qui n'aura jamais de code, cela ne fait pas beaucoup de différence que vous utilisiez des propriétés ou des champs. Si vous décidez de lui donner du code (comme une validation ou quelque chose), vous voudrez certainement en faire des propriétés. Au moins avec les champs publics, personne ne se fera jamais d'illusions sur les invariants existant sur cette structure. Il devra être validé par la méthode exactement comme l'auraient été les paramètres.
Jeffrey L Whitledge
Oh c'est vrai. Impressionnant! Merci.
Sedat Kapanoglu
8
Je pense que la règle d'exposer les champs est le mal s'applique aux objets dans une conception orientée objet. La structure que je propose n'est qu'un simple conteneur de paramètres. Puisque votre instinct devait aller avec quelque chose d'immuable, je pense qu'un tel conteneur basique pourrait être approprié pour cette occasion.
Jeffrey L Whitledge
1
@JeffreyLWhitledge: Je n'aime vraiment pas l'idée que les structures de champ exposé sont mauvaises. Une telle affirmation me semble équivalente à dire que les tournevis sont mauvais et que les gens devraient utiliser des marteaux parce que les pointes des tournevis entaillent la tête des clous. Si l'on a besoin d'enfoncer un clou, il faut utiliser un marteau, mais si l'on a besoin d'enfoncer une vis, il faut utiliser un tournevis. Il existe de nombreuses situations où une structure à champ exposé est précisément le bon outil pour le travail. Incidemment, il y en a beaucoup moins où une structure avec des propriétés get / set est vraiment le bon outil (dans la plupart des cas où ...
supercat
6

Que diriez-vous de créer une classe de générateur dans votre classe de données. La classe de données aura tous les setters comme privés et seul le constructeur pourra les définir.

public class DoSomeActionParameters
    {
        public string A { get; private set; }
        public string B  { get; private set; }
        public DateTime C { get; private set; }
        public OtherEnum D  { get; private set; }
        public string E  { get; private set; }
        public string F  { get; private set; }

        public class Builder
        {
            DoSomeActionParameters obj = new DoSomeActionParameters();

            public string A
            {
                set { obj.A = value; }
            }
            public string B
            {
                set { obj.B = value; }
            }
            public DateTime C
            {
                set { obj.C = value; }
            }
            public OtherEnum D
            {
                set { obj.D = value; }
            }
            public string E
            {
                set { obj.E = value; }
            }
            public string F
            {
                set { obj.F = value; }
            }

            public DoSomeActionParameters Build()
            {
                return obj;
            }
        }
    }

    public class Example
    {

        private void DoSth()
        {
            var data = new DoSomeActionParameters.Builder()
            {
                A = "",
                B = "",
                C = DateTime.Now,
                D = testc,
                E = "",
                F = ""
            }.Build();
        }
    }
Marto
la source
1
+1 C'est une solution parfaitement valable mais je pense que c'est trop d'échafaudage pour maintenir une décision de conception aussi simple. Surtout quand la solution "readonly struct" est "très" proche de l'idéal.
Sedat Kapanoglu le
2
Comment cela résout-il le problème du «trop de paramètres»? La syntaxe peut être différente, mais le problème est le même. Ce n'est pas une critique, je suis juste curieux car je ne connais pas ce modèle.
alexD
1
@alexD cela résout le problème d'avoir trop de paramètres de fonction et de garder l'objet immuable. Seule la classe de générateur peut définir les propriétés privées et une fois que vous avez obtenu l'objet de paramètres, vous ne pouvez pas le modifier. Le problème est que cela nécessite beaucoup de code d'échafaudage.
marto le
5
L'objet de paramètre dans votre solution n'est pas immuable. Quelqu'un qui enregistre le constructeur peut modifier les paramètres même après la construction
astef
1
Au point de @ astef, tel qu'il est actuellement écrit, une DoSomeActionParameters.Builderinstance peut être utilisée pour créer et configurer exactement une DoSomeActionParametersinstance. Après avoir appelé Build()les modifications ultérieures Builderdes propriétés de, la modification des propriétés de l'instance d' origine continuera DoSomeActionParameterset les appels suivants à Build()continueront à renvoyer la même DoSomeActionParametersinstance. Ça devrait vraiment l'être public DoSomeActionParameters Build() { var oldObj = obj; obj = new DoSomeActionParameters(); return oldObj; }.
BACON
6

Je ne suis pas un programmeur C # mais je crois que C # prend en charge les arguments nommés: (F # le fait et C # est en grande partie compatible avec ce genre de chose) Il le fait: http://msdn.microsoft.com/en-us/library/dd264739 .aspx # Y342

Ainsi, appeler votre code d'origine devient:

public ResultEnum DoSomeAction( 
 e:"bar", 
 a: "foo", 
 c: today(), 
 b:"sad", 
 d: Red,
 f:"penguins")

cela ne prend plus de place / pense que votre création d'objet et a tous les avantages, du fait que vous n'avez pas changé du tout ce qui se passe dans le système sous-jacent. Vous n'avez même pas besoin de recoder quoi que ce soit pour indiquer que les arguments sont nommés

Edit: voici un artical que j'ai trouvé à ce sujet. http://www.globalnerdy.com/2009/03/12/default-and-named-parameters-in-c-40-sith-lord-in-training/ Je devrais mentionner que C # 4.0 prend en charge les arguments nommés, 3.0 ne l'a pas fait

Lyndon White
la source
Une amélioration en effet. Mais cela ne résout que la lisibilité du code et cela ne le fait que lorsque le développeur choisit d'utiliser des paramètres nommés. Il est facile pour quelqu'un d'oublier de spécifier les noms et de donner les avantages. Cela n'aide pas lors de l'écriture du code lui-même. par exemple, lors de la refactorisation de la fonction en des fonctions plus petites et de la transmission des données dans un seul paquet contenu.
Sedat Kapanoglu
6

Pourquoi ne pas simplement créer une interface qui renforce l'immuabilité (c'est-à-dire uniquement les getters)?

C'est essentiellement votre première solution, mais vous forcez la fonction à utiliser l'interface pour accéder au paramètre.

public interface IDoSomeActionParameters
{
    string A { get; }
    string B { get; }
    DateTime C { get; }
    OtherEnum D { get; }
    string E { get; }
    string F { get; }              
}

public class DoSomeActionParameters: IDoSomeActionParameters
{
    public string A { get; set; }
    public string B { get; set; }
    public DateTime C { get; set; }
    public OtherEnum D { get; set; }
    public string E { get; set; }
    public string F { get; set; }        
}

et la déclaration de fonction devient:

public ResultEnum DoSomeAction(IDoSomeActionParameters parameters, out Guid code)

Avantages:

  • N'a pas de problème d'espace de pile comme la structsolution
  • Solution naturelle utilisant la sémantique du langage
  • L'immuabilité est évidente
  • Flexible (le consommateur peut utiliser une classe différente s'il le souhaite)

Les inconvénients:

  • Quelques travaux répétitifs (mêmes déclarations dans deux entités différentes)
  • Le développeur doit deviner qu'il DoSomeActionParameterss'agit d'une classe qui pourrait être mappéeIDoSomeActionParameters
vérité
la source
+1 Je ne sais pas pourquoi je n'ai pas pensé à ça? :) Je suppose que je pensais que l'objet souffrirait encore d'un constructeur avec trop de paramètres mais ce n'est pas le cas. Oui, c'est aussi une solution très valable. Le seul problème auquel je peux penser est qu'il n'est pas vraiment simple pour l'utilisateur d'API de trouver le bon nom de classe prenant en charge l'interface donnée. La solution qui nécessite le moins de documentation est meilleure.
Sedat Kapanoglu
J'aime celui-ci, la duplication et la connaissance de la carte sont gérées avec resharper, et je peux fournir des valeurs par défaut en utilisant le constructeur par défaut de la classe concrète
Anthony Johnston
2
Je n'aime pas cette approche. Quelqu'un qui a une référence à une implémentation arbitraire de IDoSomeActionParameterset veut capturer les valeurs qui y figurent n'a aucun moyen de savoir si la détention de la référence sera suffisante, ou si elle doit copier les valeurs dans un autre objet. Les interfaces lisibles sont utiles dans certains contextes, mais pas pour rendre les choses immuables.
supercat du
Les «paramètres IDoSomeActionParameters» peuvent être convertis en DoSomeActionParameters et modifiés. Le développeur peut même ne pas se rendre compte qu'il contourne une tentative de rendre les paramètres imutables
Joseph Simpson
3

Je sais que c'est une vieille question, mais j'ai pensé que j'allais suivre ma suggestion car je devais simplement résoudre le même problème. Maintenant, je reconnais que mon problème était légèrement différent du vôtre car j'avais l'exigence supplémentaire de ne pas vouloir que les utilisateurs puissent construire cet objet eux-mêmes (toute l'hydratation des données provenait de la base de données, donc je pouvais emprisonner toute construction en interne). Cela m'a permis d'utiliser un constructeur privé et le modèle suivant;

    public class ExampleClass
    {
        //create properties like this...
        private readonly int _exampleProperty;
        public int ExampleProperty { get { return _exampleProperty; } }

        //Private constructor, prohibiting construction outside of this class
        private ExampleClass(ExampleClassParams parameters)
        {                
            _exampleProperty = parameters.ExampleProperty;
            //and so on... 
        }

        //The object returned from here will be immutable
        public ExampleClass GetFromDatabase(DBConnection conn, int id)
        {
            //do database stuff here (ommitted from example)
            ExampleClassParams parameters = new ExampleClassParams()
            {
                ExampleProperty = 1,
                ExampleProperty2 = 2
            };

            //Danger here as parameters object is mutable

            return new ExampleClass(parameters);    

            //Danger is now over ;)
        }

        //Private struct representing the parameters, nested within class that uses it.
        //This is mutable, but the fact that it is private means that all potential 
        //"damage" is limited to this class only.
        private struct ExampleClassParams
        {
            public int ExampleProperty { get; set; }
            public int AnotherExampleProperty { get; set; }
            public int ExampleProperty2 { get; set; }
            public int AnotherExampleProperty2 { get; set; }
            public int ExampleProperty3 { get; set; }
            public int AnotherExampleProperty3 { get; set; }
            public int ExampleProperty4 { get; set; }
            public int AnotherExampleProperty4 { get; set; } 
        }
    }
Mikey Hogarth
la source
2

Vous pouvez utiliser une approche de style Builder, bien que, selon la complexité de votre DoSomeActionméthode, cela puisse être un peu lourd. Quelque chose dans ce sens:

public class DoSomeActionParametersBuilder
{
    public string A { get; set; }
    public string B { get; set; }
    public DateTime C { get; set; }
    public OtherEnum D { get; set; }
    public string E { get; set; }
    public string F { get; set; }

    public DoSomeActionParameters Build()
    {
        return new DoSomeActionParameters(A, B, C, D, E, F);
    }
}

public class DoSomeActionParameters
{
    public string A { get; private set; }
    public string B { get; private set; }
    public DateTime C { get; private set; }
    public OtherEnum D { get; private set; }
    public string E { get; private set; }
    public string F { get; private set; }

    public DoSomeActionParameters(string a, string b, DateTime c, OtherEnum d, string e, string f)
    {
        A = a;
        // etc.
    }
}

// usage
var actionParams = new DoSomeActionParametersBuilder
{
    A = "value for A",
    C = DateTime.Now,
    F = "I don't care for B, D and E"
}.Build();

result = foo.DoSomeAction(actionParams, out code);
Chris White
la source
Ah, marto m'a battu à la suggestion du constructeur!
Chris White le
2

En plus de la réponse manji, vous pouvez également diviser une opération en plusieurs opérations plus petites. Comparer:

 BOOL WINAPI CreateProcess(
   __in_opt     LPCTSTR lpApplicationName,
   __inout_opt  LPTSTR lpCommandLine,
   __in_opt     LPSECURITY_ATTRIBUTES lpProcessAttributes,
   __in_opt     LPSECURITY_ATTRIBUTES lpThreadAttributes,
   __in         BOOL bInheritHandles,
   __in         DWORD dwCreationFlags,
   __in_opt     LPVOID lpEnvironment,
   __in_opt     LPCTSTR lpCurrentDirectory,
   __in         LPSTARTUPINFO lpStartupInfo,
   __out        LPPROCESS_INFORMATION lpProcessInformation
 );

et

 pid_t fork()
 int execvpe(const char *file, char *const argv[], char *const envp[])
 ...

Pour ceux qui ne connaissent pas POSIX la création d'enfant peut être aussi simple que:

pid_t child = fork();
if (child == 0) {
    execl("/bin/echo", "Hello world from child", NULL);
} else if (child != 0) {
    handle_error();
}

Chaque choix de conception représente un compromis sur les opérations qu'il peut effectuer.

PS. Oui - il est similaire au constructeur - uniquement en sens inverse (c'est-à-dire du côté de l'appelé au lieu de l'appelant). Il se peut que ce soit mieux ou non le constructeur dans ce cas précis.

Maciej Piechotka
la source
C'est une petite opération, mais c'est une bonne recommandation pour quiconque enfreint le principe de la responsabilité unique. Ma question a lieu juste après que toutes les autres améliorations de conception ont été apportées.
Sedat Kapanoglu
UPS. Désolé - j'ai préparé une question avant votre modification et je l'ai postée plus tard - donc je ne l'avais pas remarqué.
Maciej Piechotka
2

en voici un légèrement différent de Mikeys mais ce que j'essaie de faire, c'est de rendre le tout aussi peu à écrire que possible

public class DoSomeActionParameters
{
    readonly string _a;
    readonly int _b;

    public string A { get { return _a; } }

    public int B{ get { return _b; } }

    DoSomeActionParameters(Initializer data)
    {
        _a = data.A;
        _b = data.B;
    }

    public class Initializer
    {
        public Initializer()
        {
            A = "(unknown)";
            B = 88;
        }

        public string A { get; set; }
        public int B { get; set; }

        public DoSomeActionParameters Create()
        {
            return new DoSomeActionParameters(this);
        }
    }
}

Le DoSomeActionParameters est immuable comme il peut l'être et ne peut pas être créé directement car son constructeur par défaut est privé

L'initialiseur n'est pas immuable, mais seulement un transport

L'utilisation tire parti de l'initialiseur sur l'initialiseur (si vous obtenez ma dérive) Et je peux avoir des valeurs par défaut dans le constructeur par défaut de l'initialiseur

DoSomeAction(new DoSomeActionParameters.Initializer
            {
                A = "Hello",
                B = 42
            }
            .Create());

Les paramètres seront facultatifs ici, si vous voulez que certains soient requis, vous pouvez les mettre dans le constructeur par défaut de l'initialiseur

Et la validation pourrait aller dans la méthode Create

public class Initializer
{
    public Initializer(int b)
    {
        A = "(unknown)";
        B = b;
    }

    public string A { get; set; }
    public int B { get; private set; }

    public DoSomeActionParameters Create()
    {
        if (B < 50) throw new ArgumentOutOfRangeException("B");

        return new DoSomeActionParameters(this);
    }
}

Alors maintenant, il ressemble

DoSomeAction(new DoSomeActionParameters.Initializer
            (b: 42)
            {
                A = "Hello"
            }
            .Create());

Encore un peu kooki je sais, mais je vais quand même l'essayer

Edit: déplacer la méthode create vers une statique dans l'objet parameters et ajouter un délégué qui passe l'initialiseur enlève une partie de la kookieness de l'appel

public class DoSomeActionParameters
{
    readonly string _a;
    readonly int _b;

    public string A { get { return _a; } }
    public int B{ get { return _b; } }

    DoSomeActionParameters(Initializer data)
    {
        _a = data.A;
        _b = data.B;
    }

    public class Initializer
    {
        public Initializer()
        {
            A = "(unknown)";
            B = 88;
        }

        public string A { get; set; }
        public int B { get; set; }
    }

    public static DoSomeActionParameters Create(Action<Initializer> assign)
    {
        var i = new Initializer();
        assign(i)

        return new DoSomeActionParameters(i);
    }
}

Donc l'appel ressemble maintenant à ceci

DoSomeAction(
        DoSomeActionParameters.Create(
            i => {
                i.A = "Hello";
            })
        );
Anthony Johnston
la source
1

Utilisez la structure, mais au lieu des champs publics, ayez des propriétés publiques:

• Tout le monde (y compris FXCop et Jon Skeet) convient que l'exposition des champs publics est mauvaise.

Jon et FXCop seront satisfaits car vous exposez des propriétés et non des champs.

• Eric Lippert et al disent que se fier aux champs en lecture seule pour l'immuabilité est un mensonge.

Eric sera satisfait car en utilisant les propriétés, vous pouvez vous assurer que la valeur n'est définie qu'une seule fois.

    private bool propC_set=false;
    private date pC;
    public date C {
        get{
            return pC;
        }
        set{
            if (!propC_set) {
               pC = value;
            }
            propC_set = true;
        }
    }

Un objet semi-immuable (la valeur peut être définie mais pas modifiée). Fonctionne pour les types valeur et référence.

jmoreno
la source
+1 Peut-être que les champs privés en lecture seule et les propriétés publiques en lecture seule pourraient être combinés dans une structure pour permettre une solution moins verbeuse.
Sedat Kapanoglu
Les propriétés publiques en lecture-écriture sur les structures qui mutent thissont bien plus mauvaises que les champs publics. Je ne sais pas pourquoi vous pensez que le fait de ne régler la valeur qu'une seule fois rend les choses "correctes"; si on commence par une instance par défaut d'une structure, les problèmes associés aux thispropriétés -mutating continueront d'exister.
supercat
0

Une variante de la réponse de Samuel que j'ai utilisée dans mon projet lorsque j'ai eu le même problème:

class MagicPerformer
{
    public int Param1 { get; set; }
    public string Param2 { get; set; }
    public DateTime Param3 { get; set; }

    public MagicPerformer SetParam1(int value) { this.Param1 = value; return this; }
    public MagicPerformer SetParam2(string value) { this.Param2 = value; return this; }
    public MagicPerformer SetParam4(DateTime value) { this.Param3 = value; return this; }

    public void DoMagic() // Uses all the parameters and does the magic
    {
    }
}

Et à utiliser:

new MagicPerformer().SeParam1(10).SetParam2("Yo!").DoMagic();

Dans mon cas, les paramètres étaient intentionnellement modifiables, car les méthodes de setter ne permettaient pas toutes les combinaisons possibles, et exposaient simplement des combinaisons communes d'entre elles. C'est parce que certains de mes paramètres étaient assez complexes et que des méthodes d'écriture pour tous les cas possibles auraient été difficiles et inutiles (les combinaisons folles sont rarement utilisées).

Vilx-
la source
C'est une classe modifiable quoi qu'il en soit.
Sedat Kapanoglu
@ssg - Eh bien, oui, ça l'est. Le DoMagica cependant dans son contrat qu'il ne modifiera pas l'objet. Mais je suppose que cela ne protège pas des modifications accidentelles.
Vilx-