web-dev-qa-db-fra.com

Comment éviter le problème "trop ​​de paramètres" dans la conception de l'API?

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 passer 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 les transmettre 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 d'API à:

public ResultEnum DoSomeAction(DoSomeActionParameters parameters, out Guid code)

Agréable. 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 immuable anonyme: les paramètres de fonction sur la pile. Maintenant, nous avons créé une nouvelle classe qui est très mutable. 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 atteindre 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 je suis devenu confus et j'ai décidé d'écrire cette question: Quelle est la manière la plus simple en C # pour éviter un problème de "trop de paramètres" sans introduire de mutabilité? Est-il possible d'utiliser une structure en lecture seule à cet effet 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 les paramètres donnés dans un seul enregistrement DB.
  • Je ne cherche pas une solution spécifique à la fonction donnée. Je cherche une approche généralisée de ces problèmes. Je suis spécifiquement intéressé à résoudre le problème "trop ​​de paramètres" sans introduire de mutabilité ou une conception terrible.

[~ # ~] mise à jour [~ # ~]

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

154
Sedat Kapanoglu

Utilisez une combinaison de générateur et d'API de style 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());
81
Samuel Neff

Un style adopté dans les cadres est généralement comme le regroupement des paramètres liés dans des classes lié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
21
Teoman Soygul

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 grappes de Facade Dependencies .

10
Mark Seemann

Changez simplement la structure de vos données de paramètres de class en struct et 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 d'argument ne peuvent pas être observées par la méthode, et les modifications que la méthode apporte à la variable ne peuvent pas être observées par l'appelant. L'isolement est réalisé sans immuabilité.

Avantages:

  • 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 pile
10

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();
        }
    }
6
marto

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 struct solution
  • 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 que DoSomeActionParameters est une classe qui pourrait être mappée sur IDoSomeActionParameters
6
trutheality

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 largement compatible avec ce genre de chose) Il le fait: http://msdn.Microsoft.com/en-us /library/dd264739.aspx#Y342

Donc, 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 d'espace/de pensée que la création de votre 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 C # 4.0 prend en charge les arguments nommés, 3.0 n'a pas

6
Lyndon White

Je sais que c'est une vieille question mais je pensais que j'allais entrer dans ma suggestion car je viens de résoudre le même problème. Maintenant, admettons 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; } 
        }
    }
3
Mikey Hogarth

voici un peu différent de Mikeys mais ce que j'essaie de faire est de rendre le tout aussi petit que possible à écrire

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 car il peut ê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 profite 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, ça ressemble

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

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

Modifier: déplacer la méthode create vers un objet statique dans l'objet paramètres et ajouter un délégué qui passe l'initialiseur supprime 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";
            })
        );
2
Anthony Johnston

Vous pouvez utiliser une approche de type Builder, mais en fonction de la complexité de votre méthode DoSomeAction, cela peut être un poids 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);
2
Chris White

En plus de la réponse du manji, vous pouvez également diviser une opération en plusieurs 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'un 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 peut ou non être préférable au constructeur dans ce cas spécifique.

2
Maciej Piechotka

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 s'appuyer sur des 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.

1
jmoreno

Une variante de réponse de Samuel que j'ai utilisée dans mon projet quand 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 pour utiliser:

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

Dans mon cas, les paramètres étaient intentionnellement modifiables, car les méthodes de définition ne permettaient pas toutes les combinaisons possibles et en exposaient simplement les combinaisons courantes. En effet, certains de mes paramètres étaient assez complexes et les méthodes d'écriture pour tous les cas possibles auraient été difficiles et inutiles (des combinaisons folles sont rarement utilisées).

0
Vilx-