development

API 디자인에서 "너무 많은 매개 변수"문제를 피하는 방법은 무엇입니까?

big-blog 2020. 6. 8. 07:56
반응형

API 디자인에서 "너무 많은 매개 변수"문제를 피하는 방법은 무엇입니까?


이 API 기능이 있습니다.

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

나는 그것을 좋아하지 않는다. 매개 변수 순서가 불필요하게 중요해지기 때문입니다. 새 필드를 추가하기가 더 어려워집니다. 전달되는 내용을 확인하기가 더 어렵습니다. 하위 함수의 모든 매개 변수를 전달하는 또 다른 오버 헤드를 생성하기 때문에 메소드를 더 작은 부분으로 리팩토링하기가 더 어렵습니다. 코드를 읽기가 더 어렵습니다.

가장 명확한 아이디어를 생각해 냈습니다. 데이터를 캡슐화하는 객체를 가지고 각 매개 변수를 하나씩 전달하는 대신 전달하십시오. 다음은 내가 생각해 낸 것입니다.

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)

좋은. 무고 해 보이지만 실제로는 큰 변화가있었습니다. 이전에 수행 한 작업은 실제로 익명의 불변 개체를 스택에 전달하는 것이 었습니다. 이제 우리는 매우 변경 가능한 새로운 클래스를 만들었습니다. 우리는 호출자 의 상태를 조작하는 기능을 만들었습니다 . 짜증나 이제 객체를 변경할 수 없게하려면 어떻게해야합니까?

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 레코드에 씁니다.
  • 주어진 기능에 대한 특정 솔루션을 찾고 있지 않습니다. 이러한 문제에 대한 일반적인 접근 방식을 찾고 있습니다. 나는 가변성이나 끔찍한 디자인을 도입하지 않고 "너무 많은 매개 변수"문제를 해결하는 데 특히 관심이 있습니다.

최신 정보

여기에 제공된 답변은 서로 다른 장점 / 단점이 있습니다. 따라서 이것을 커뮤니티 위키로 변환하고 싶습니다. 코드 샘플과 찬반 양론으로 각 대답은 앞으로 비슷한 문제에 대한 좋은 가이드를 만들 것이라고 생각합니다. 나는 지금 그것을하는 방법을 찾으려고 노력하고있다.


빌더 및 도메인 별 언어 스타일 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());

프레임 워크에 포함 된 한 스타일은 일반적으로 관련 매개 변수를 관련 클래스로 그룹화하는 것과 같습니다 (그러나 다시 가변성에 문제가 있음).

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

당신이 가지고있는 것은 문제의 클래스 가 너무 많은 의존성을 가지고 있기 때문에 단일 책임 원칙을 위반하고 있음을 분명히 나타냅니다 . 이러한 종속성을 Facade Dependency의 클러스터로 리팩토링하는 방법을 찾으십시오 .


매개 변수 데이터 구조를 a에서 a 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) 

메소드는 이제 자체 구조 사본을 얻습니다. 인수 변수에 대한 변경 사항은 메소드에 의해 관찰 될 수 없으며, 변수에 대한 메소드 변경 사항은 호출자가 관찰 할 수 없습니다. 불변성없이 격리가 달성됩니다.

장점 :

  • 가장 쉬운 구현
  • 기초 역학에서의 행동의 최소 변화

단점 :

  • 불변성은 분명하지 않으며 개발자의주의가 필요합니다.
  • 불변성을 유지하기 위해 불필요한 복사
  • 점유 공간

데이터 클래스 내에 빌더 클래스를 작성하는 것은 어떻습니까. 데이터 클래스에는 모든 세터가 개인용으로 있으며 빌더 만 설정할 수 있습니다.

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

나는 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은 그렇지 않습니다.


왜 불변성을 강제하는 인터페이스를 만드는가 (즉, 게터 만)?

본질적으로 첫 번째 솔루션이지만 인터페이스를 사용하여 매개 변수에 액세스하도록 함수를 강제합니다.

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솔루션 과 같은 스택 공간 문제가 없습니다.
  • 언어 시맨틱을 사용한 자연스러운 솔루션
  • 불변성은 명백하다
  • 융통성 (소비자가 원하면 다른 클래스를 사용할 수 있음)

단점 :

  • 일부 반복적 인 작업 (두 개의 다른 엔티티에서 동일한 선언)
  • 개발자는 DoSomeActionParameters그것이 매핑 될 수있는 클래스라고 추측해야 합니다.IDoSomeActionParameters

나는 이것이 오래된 질문이라는 것을 알고 있지만 방금 동일한 문제를 해결해야했기 때문에 내 제안에 빠져 들었다고 생각했다. 자, 사용자가이 객체를 스스로 구성 할 수 없도록하는 추가 요구 사항이 있었기 때문에 내 문제는 약간 다릅니다. (데이터의 모든 수화는 데이터베이스에서 왔기 때문에 내부에서 모든 구성을 탈옥 할 수있었습니다). 이를 통해 개인 생성자와 다음 패턴을 사용할 수있었습니다.

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

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

manji 응답 외에도 하나의 작업을 여러 개의 작은 작업으로 분할 할 수도 있습니다. 비교:

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

각 디자인 선택은 수행 할 수있는 작업과의 균형을 나타냅니다.

추신. 예-빌더와 비슷합니다 (반대자 (발신자 대신 수신자 측)). 이 특정 경우 빌더보다 더 좋을 수도 있고 그렇지 않을 수도 있습니다.


여기에 Mikeys와 약간 다른 것이 있지만 내가하려고하는 것은 가능한 한 모든 것을 최대한 작게 작성하는 것입니다.

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는 기본 생성자가 개인이므로 직접 작성하거나 작성할 수 없으므로 변경할 수 없습니다.

이니셜 라이저는 변경할 수 없지만 전송 만 가능합니다

사용법은 이니셜 라이저에서 이니셜 라이저를 활용합니다 (드리프트가있는 경우). 이니셜 라이저 기본 생성자에서 기본값을 사용할 수 있습니다

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 메소드를 정적으로 이동하고 초기화자를 전달하는 대리자를 추가하면 호출에서 일부 kookieness가 발생합니다.

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

공용 필드 대신 구조를 사용하지만 공용 속성이 있습니다.

• FXCop 및 Jon Skeet을 포함한 모든 사람은 공공 장소 노출이 나쁘다는 데 동의합니다.

Jon과 FXCop은 필드가 아닌 적절한 항목을 노출하므로 만족할 것입니다.

• 에릭 리퍼 (Eric Lippert) 등은 불변성을 위해 읽기 전용 필드에 의존하는 것이 거짓말이라고 말합니다.

속성을 사용하면 Eric이 만족할 것입니다. 값을 한 번만 설정할 수 있습니다.

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

반 불변 개체 1 개 (값은 설정할 수 있지만 변경할 수는 없음) 값 및 참조 유형에 적용됩니다.


같은 문제가 발생했을 때 프로젝트에서 사용한 Samuel의 대답 변형 :

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

필자의 경우 세터 메소드가 가능한 모든 조합을 허용하지 않고 공통 조합을 노출했기 때문에 매개 변수를 의도적으로 수정할 수있었습니다. 내 매개 변수 중 일부는 매우 복잡하고 가능한 모든 경우에 대한 작성 방법이 어렵고 불필요했기 때문입니다 (미친 조합은 거의 사용되지 않음).

참고 URL : https://stackoverflow.com/questions/6239373/how-to-avoid-too-many-parameters-problem-in-api-design

반응형