c# - एपीआई डिज़ाइन में "बहुत सारे पैरामीटर" समस्या से कैसे बचें?




data-structures immutability (9)

मेरे पास यह एपीआई फ़ंक्शन है:

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

मुझे यह पसंद नहीं है। क्योंकि पैरामीटर आदेश अनावश्यक रूप से महत्वपूर्ण हो जाता है। नए क्षेत्रों को जोड़ना मुश्किल हो जाता है। यह देखना मुश्किल है कि क्या हो रहा है। छोटे भागों में विधि को पुन: सक्रिय करना कठिन होता है क्योंकि यह उप कार्यों में सभी मानकों को पारित करने का एक और उपर बनाता है। कोड पढ़ने के लिए मुश्किल है।

मैं सबसे स्पष्ट विचार के साथ आया: एक वस्तु को डेटा को encapsulating और प्रत्येक पैरामीटर को एक-एक करके गुजरने के बजाय इसे पास करें। यहां वह है जो मैंने जुटाया:

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

इससे हमें कई मापदंडों के साथ रचनाकारों से बचने और अपरिवर्तनीयता प्राप्त करने की अनुमति मिलती है। असल में यह सभी समस्याओं को हल करता है (पैरामीटर ऑर्डरिंग इत्यादि)। फिर भी:

जब मैं उलझन में आया और इस सवाल को लिखने का फैसला किया: उत्परिवर्तन शुरू किए बिना "बहुत सारे पैरामीटर" समस्या से बचने के लिए सी # में सबसे सरल तरीका क्या है? क्या उस उद्देश्य के लिए एक पठनीय संरचना का उपयोग करना संभव है और फिर भी एक खराब एपीआई डिज़ाइन नहीं है?

स्पष्टीकरण:

  • कृपया मान लें कि एकल प्रतिक्रियात्मक सिद्धांत का कोई उल्लंघन नहीं है। मेरे मूल मामले में फ़ंक्शन केवल एक ही डीबी रिकॉर्ड को दिए गए मानकों को लिखता है।
  • मैं दिए गए फ़ंक्शन के लिए एक विशिष्ट समाधान की तलाश नहीं कर रहा हूं। मैं ऐसी समस्याओं के लिए एक सामान्य दृष्टिकोण की तलाश में हूं। मैं विशेष रूप से उत्परिवर्तन या एक भयानक डिजाइन शुरू किए बिना "बहुत सारे पैरामीटर" समस्या को हल करने में रूचि रखता हूं।

अद्यतन करें

यहां दिए गए उत्तरों के अलग-अलग फायदे / नुकसान हैं। इसलिए मैं इसे एक समुदाय विकी में परिवर्तित करना चाहता हूं। मुझे लगता है कि कोड नमूना और पेशेवर / विपक्ष के साथ प्रत्येक उत्तर भविष्य में इसी तरह की समस्याओं के लिए एक अच्छी गाइड करेगा। अब मैं यह पता लगाने की कोशिश कर रहा हूं कि इसे कैसे किया जाए।


अपने डेटा वर्ग के अंदर एक बिल्डर वर्ग बनाने के बारे में कैसे। डेटा क्लास में सभी सेटर्स निजी होंगे और केवल निर्माता ही उन्हें सेट कर पाएंगे।

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

आप बिल्डर-स्टाइल दृष्टिकोण का उपयोग कर सकते हैं, हालांकि आपकी 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);

क्यों न केवल एक इंटरफेस बनाते हैं जो अपरिवर्तनीयता (यानी केवल गेटर्स) को लागू करता है?

यह अनिवार्य रूप से आपका पहला समाधान है, लेकिन आप पैरामीटर तक पहुंचने के लिए इंटरफ़ेस का उपयोग करने के लिए फ़ंक्शन को मजबूर करते हैं।

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 मैप किया जा सकता है

ढांचे में गले लगाए गए एक शैली आमतौर पर संबंधित वर्गों में संबंधित मानकों को समूहीकृत करने की तरह है (लेकिन फिर भी उत्परिवर्तन के साथ समस्याग्रस्त):

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

बिल्डर और डोमेन-विशिष्ट-भाषा शैली 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());

मांजी प्रतिक्रिया के अलावा - आप एक ऑपरेशन को कई छोटे लोगों में विभाजित करना भी चाह सकते हैं। की तुलना करें:

 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[])
 ...

उन लोगों के लिए जो पॉज़िक्स नहीं जानते हैं, बच्चे का निर्माण उतना आसान हो सकता है:

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

प्रत्येक डिज़ाइन विकल्प व्यापार-बंद का प्रतिनिधित्व करती है जो यह कर सकती है कि यह कौन सा संचालन कर सकता है।

पुनश्च। हां - यह बिल्डर के समान है - केवल विपरीत में (यानी कॉलर के बजाय कैली पक्ष पर)। यह इस विशिष्ट मामले में निर्माता के बाद बेहतर हो सकता है या नहीं भी हो सकता है।


मैं सी # प्रोग्रामर नहीं हूं लेकिन मेरा मानना ​​है कि सी # नामों का नाम है: (एफ # करता है और सी # काफी हद तक उस तरह की चीज़ के लिए अनुकूल है) यह करता है: 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/ मुझे सी # 4.0 नामित तर्कों का समर्थन करना चाहिए, 3.0 नहीं था


यहां माइकिस से थोड़ा अलग है लेकिन मैं जो करने की कोशिश कर रहा हूं वह पूरी चीज को जितना संभव हो उतना लिखने के लिए तैयार करता है

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

पैरामीटर यहां वैकल्पिक होंगे, अगर आप कुछ चाहते हैं तो आप उन्हें प्रारंभिक डिफॉल्ट कन्स्ट्रक्टर में डाल सकते हैं

और सत्यापन विधि में सत्यापन हो सकता है

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

अभी भी एक छोटी कुकी मुझे पता है, लेकिन वैसे भी कोशिश करने जा रहा है

संपादित करें: पैरामीटर ऑब्जेक्ट में एक स्थैतिक रूप से निर्माण विधि को स्थानांतरित करना और प्रारंभकर्ता को पास करने वाला एक प्रतिनिधि जो कॉल के बाहर कुछ कुकनेस लेता है

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

शमूएल के उत्तर का एक संस्करण जिसे मैंने अपनी परियोजना में इस्तेमाल किया जब मुझे एक ही समस्या थी:

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

मेरे मामले में पैरामीटर जानबूझकर संशोधित थे, क्योंकि सेटर विधियों ने सभी संभावित संयोजनों की अनुमति नहीं दी थी, और उनमें से केवल सामान्य संयोजनों का खुलासा किया था। ऐसा इसलिए है क्योंकि मेरे कुछ पैरामीटर बहुत जटिल थे और सभी संभावित मामलों के लिए लेखन विधियां कठिन और अनावश्यक थीं (पागल संयोजन शायद ही कभी उपयोग किए जाते हैं)।





immutability