web-dev-qa-db-ja.com

API設計で「パラメーターが多すぎる」問題を回避する方法

次のAPI関数があります。

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

好きじゃない。パラメータの順序が不必要に重要になるためです。新しいフィールドを追加するのが難しくなります。何が渡されているかを見るのは難しいです。メソッドをより小さな部分にリファクタリングすることは困難です。サブ関数ですべてのパラメーターを渡す別のオーバーヘッドが発生するためです。コードは読みにくいです。

最も明白なアイデアを思い付きました。各パラメーターを1つずつ渡す代わりに、データをカプセル化するオブジェクトを用意して、それを渡します。ここに私が思いついたものがあります:

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

これにより、API宣言が次のようになりました。

public ResultEnum DoSomeAction(DoSomeActionParameters parameters, out Guid code)

いいね非常に無害に見えますが、実際に大きな変更を導入しました。可変性を導入しました。私たちが以前やっていたことは、実際には、スタック上の関数パラメーターである匿名の不変オブジェクトを渡すことでした。ここで、非常に変更可能な新しいクラスを作成しました。 callerの状態を操作する機能を作成しました。ひどい。オブジェクトを不変にしたいのですが、どうすればよいですか?

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
    }
}

ご覧のように、実際に元の問題を再現しました:パラメーターが多すぎます。それが進むべき道ではないことは明らかです。私は何をするつもりですか?このような不変性を実現する最後のオプションは、次のような「読み取り専用」構造体を使用することです。

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;        
}

これにより、パラメーターが多すぎるコンストラクターを回避し、不変性を実現できます。実際には、すべての問題(パラメーターの順序付けなど)が修正されます。まだ:

そこで混乱し、この質問を書くことにしました:可変性を導入せずに「パラメーターが多すぎる」問題を回避するC#で最も簡単な方法は何ですかその目的のために読み取り専用の構造体を使用することはできますが、それでも悪いAPI設計はありませんか?

説明:

  • 単一の責任原則の違反はないと想定してください。私の元のケースでは、関数は与えられたパラメーターを単一のDBレコードに書き込むだけです。
  • 特定の機能に対する特定のソリューションを探しているわけではありません。このような問題に対する一般化されたアプローチを探しています。可変性やひどい設計を導入せずに「パラメーターが多すぎる」問題を解決することに特に興味があります。

[〜#〜] update [〜#〜]

ここで提供される回答には、さまざまな利点/欠点があります。したがって、これをコミュニティwikiに変換したいと思います。コードサンプルと長所/短所に関する各回答は、将来的に同様の問題の良いガイドになると思います。今、私はそれを行う方法を見つけようとしています。

154
Sedat Kapanoglu

ビルダーとドメイン固有言語スタイルのAPI--Fluent Interfaceの組み合わせを使用します。 APIはもう少し冗長ですが、インテリセンスを使用すると、入力が非常に速く、理解しやすくなります。

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

フレームワークに含まれるスタイルの1つは、通常、関連するパラメーターを関連するクラスにグループ化するようなものです(ただし、やはり可変性には問題があります)。

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

あなたが持っているものは、問題のクラスが Single Responsibility Principle に違反しているというかなり確実な兆候です。これらの依存関係を Facade Dependencies のクラスターにリファクタリングする方法を探します。

10
Mark Seemann

パラメータのデータ構造をclassからstructに変更するだけで準備完了です。

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) 

メソッドは、構造の独自のコピーを取得します。引数変数に加えられた変更はメソッドでは監視できず、メソッドが変数に加えた変更は呼び出し側では監視できません。分離は不変性なしで達成されます。

長所:

  • 実装が最も簡単
  • 基礎となる力学における挙動の最小変化

短所:

  • 不変性は明らかではないため、開発者の注意が必要です。
  • 不変性を維持するための不必要なコピー
  • スタックスペースを占有
10

データクラス内にビルダークラスを作成してください。データクラスにはすべてのセッターがプライベートとして設定され、ビルダーのみがそれらを設定できます。

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

不変を強制するインターフェイス(つまり、ゲッターのみ)を作成しないのはなぜですか?

これは本質的に最初のソリューションですが、関数にパラメーターをアクセスするためにインターフェイスを使用するように強制します。

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; }        
}

関数宣言は次のようになります。

public ResultEnum DoSomeAction(IDoSomeActionParameters parameters, out Guid code)

長所:

  • structソリューションのようなスタックスペースの問題はありません
  • 言語セマンティクスを使用した自然なソリューション
  • 不変性は明らかです
  • 柔軟(消費者は必要に応じて別のクラスを使用できます)

短所:

  • いくつかの反復作業(2つの異なるエンティティでの同じ宣言)
  • 開発者は、DoSomeActionParametersIDoSomeActionParametersにマッピングできるクラスであると推測する必要があります
6
trutheality

私はC#プログラマーではありませんが、C#は名前付き引数をサポートしていると思います:(F#はサポートしますが、C#はその種の機能と互換性があります)それは: http://msdn.Microsoft.com/en-us /library/dd264739.aspx#Y342

したがって、元のコードの呼び出しは次のようになります。

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

これは、オブジェクトの作成に必要なスペースや思考を必要とせず、すべての利点があります。これは、実際にシステムで起こっていることをまったく変更していないという事実です。引数に名前が付けられていることを示すために再コーディングする必要さえありません。

編集:ここで私はそれについて見つけたアーティカルです。 http://www.globalnerdy.com/2009/03/12/default-and-named-parameters-in-c-40-sith-lord-in-training/ C#4.0に言及する必要があります名前付き引数をサポートしていますが、3.0はサポートしていません

6
Lyndon White

これは古い質問であることは知っていますが、同じ問題を解決しなければならなかったので、私は自分の提案に手を出そうと思いました。さて、ユーザーがこのオブジェクトを自分で作成できないようにするという追加の要件があったため、私の問題はあなたのものとはわずかに異なっていました(データのすべての水和はデータベースから来たので、すべての建設を内部で投獄することができました)。これにより、プライベートコンストラクターと次のパターンを使用できました。

    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

ここはマイキーズとは少し異なりますが、私がやろうとしていることは、できる限り書くことをできるだけ少なくすることです

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);
        }
    }
}

DoSomeActionParametersは不変であり、デフォルトのコンストラクタはプライベートであるため、直接作成することはできません。

初期化子は不変ではなく、トランスポートのみです

使用法は、Initializerのイニシャライザーを利用します(ドリフトが発生する場合)。Initializerのデフォルトコンストラクターにデフォルトを設定できます。

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

パラメーターはここではオプションになります。一部のパラメーターが必要な場合は、Initializerのデフォルトコンストラクターに配置できます。

そして、検証は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);
    }
}

だから今のように見えます

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

まだ少し知ってるけど、とにかくやってみる

編集:createメソッドをparametersオブジェクトのstaticに移動し、イニシャライザーを渡すデリゲートを追加すると、呼び出しから一部の不自然さを取り除きます

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);
    }
}

呼び出しは次のようになります

DoSomeAction(
        DoSomeActionParameters.Create(
            i => {
                i.A = "Hello";
            })
        );
2

Builderスタイルのアプローチを使用することもできますが、DoSomeActionメソッドの複雑さにもよりますが、これは少々重いかもしれません。これらの線に沿って何か:

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

マンジ応答に加えて、1つの操作をいくつかの小さな操作に分割することもできます。比較する:

 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
 );

そして

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

POSIXを知らない人にとって、子の作成は次のように簡単にできます。

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

設計の選択はそれぞれ、実行できる操作のトレードオフを表しています。

PS。はい-それはビルダーに似ています-逆にのみ(つまり、呼び出し元ではなく呼び出し先側で)。この特定のケースでは、ビルダーよりも優れている場合とそうでない場合があります。

2

構造を使用しますが、パブリックフィールドの代わりにパブリックプロパティがあります。

•誰もが(FXCopとJon Skeetを含む)パブリックフィールドを公開することは悪いことに同意します。

JonとFXCopは、フィールドではなくプロパティを公開しているため、満足します。

Eric Lippertらは、不変性のために読み取り専用フィールドに依存するのは嘘だと言います。

エリックは満足します。プロパティを使用すると、値が一度だけ設定されることを保証できるからです。

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

1つの半不変オブジェクト(値は設定できますが、変更できません)。値型および参照型に機能します。

1
jmoreno

Samuel's answer のバリアントで、同じ問題が発生したときにプロジェクトで使用したものです。

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
    {
    }
}

そして使用するには:

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

私の場合、セッターメソッドはすべての可能な組み合わせを許可せず、それらの一般的な組み合わせを公開しただけなので、パラメーターは意図的に変更可能です。それは、私のパラメーターの一部が非常に複雑であり、考えられるすべてのケースのメソッドを記述するのが難しく、不必要だったからです(クレイジーな組み合わせはめったに使用されません)。

0
Vilx-