Jak uniknąć problemu „zbyt wielu parametrów” w projektowaniu API?

160

Mam tę funkcję API:

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

Nie podoba mi się to. Ponieważ kolejność parametrów staje się niepotrzebnie znacząca. Trudniej jest dodawać nowe pola. Trudniej jest zobaczyć, co się dzieje. Trudniej jest dokonać refaktoryzacji metody na mniejsze części, ponieważ powoduje to dodatkowe obciążenie związane z przekazywaniem wszystkich parametrów w podfunkcjach. Kod jest trudniejszy do odczytania.

Wpadłem na najbardziej oczywisty pomysł: mieć obiekt hermetyzujący dane i przekazywać go zamiast przekazywania każdego parametru jeden po drugim. Oto co wymyśliłem:

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

To zredukowało moją deklarację API do:

public ResultEnum DoSomeAction(DoSomeActionParameters parameters, out Guid code)

Miły. Wygląda bardzo niewinnie, ale tak naprawdę wprowadziliśmy ogromną zmianę: wprowadziliśmy zmienność. Ponieważ to, co wcześniej robiliśmy, polegało na przekazaniu anonimowego niezmiennego obiektu: parametrów funkcji na stosie. Teraz stworzyliśmy nową klasę, która jest bardzo zmienna. Stworzyliśmy możliwość manipulowania stanem rozmówcy . To jest do bani. Teraz chcę, aby mój obiekt był niezmienny, co mam zrobić?

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

Jak widać, odtworzyłem mój pierwotny problem: zbyt wiele parametrów. To oczywiste, że to nie jest droga. Co ja mam zrobić? Ostatnią opcją osiągnięcia takiej niezmienności jest użycie struktury „tylko do odczytu”, takiej jak ta:

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

Pozwala to uniknąć konstruktorów ze zbyt dużą liczbą parametrów i uzyskać niezmienność. Właściwie to rozwiązuje wszystkie problemy (porządkowanie parametrów itp.). Jeszcze:

Wtedy zdezorientowałem się i postanowiłem napisać to pytanie: Jaki jest najprostszy sposób w C # na uniknięcie problemu „zbyt wielu parametrów” bez wprowadzania zmienności? Czy można użyć do tego celu struktury tylko do odczytu, a jednocześnie nie mieć złego projektu API?

OBJAŚNIENIA:

  • Proszę założyć, że nie doszło do naruszenia zasady odpowiedzialności pojedynczej. W moim oryginalnym przypadku funkcja po prostu zapisuje podane parametry do pojedynczego rekordu DB.
  • Nie szukam konkretnego rozwiązania dla danej funkcji. Szukam uogólnionego podejścia do takich problemów. Szczególnie interesuje mnie rozwiązanie problemu „zbyt wielu parametrów” bez wprowadzania zmienności lub okropnego projektu.

AKTUALIZACJA

Podane tutaj odpowiedzi mają różne zalety / wady. Dlatego chciałbym przekształcić to w wiki społeczności. Myślę, że każda odpowiedź z próbką kodu i zaletami / wadami byłaby dobrym przewodnikiem po podobnych problemach w przyszłości. Teraz próbuję dowiedzieć się, jak to zrobić.

ssg
źródło
Clean Code: A Handbook of Agile Software Craftsmanship autorstwa Roberta C. Martina i Martina Fowlera w książce Refactoring trochę to omawia
Ian Ringrose
1
Czy to rozwiązanie Builder nie jest zbędne, jeśli używasz języka C # 4, w którym masz opcjonalne parametry ?
khellang
1
Mogę być głupi, ale nie rozumiem, jak to jest problem, biorąc pod uwagę, że DoSomeActionParametersjest to obiekt jednorazowy, który zostanie odrzucony po wywołaniu metody.
Vilx
6
Zauważ, że nie mówię, że powinieneś unikać pól tylko do odczytu w strukturze; niezmienne struktury są najlepszą praktyką, a pola tylko do odczytu pomagają tworzyć samodokumentujące się niezmienne struktury. Chodzi mi o to, że nie powinieneś polegać na obserwowaniu pól readonly, które nigdy się nie zmieniają , ponieważ nie jest to gwarancja, że ​​pole readonly daje ci w struct. Jest to szczególny przypadek bardziej ogólnej porady, że nie należy traktować typów wartości jako typów referencyjnych; to zupełnie inne zwierzę.
Eric Lippert
7
@ssg: Też bym chciał. Dodaliśmy funkcje do języka C #, które promują niezmienność (takie jak LINQ) w tym samym czasie, gdy dodaliśmy funkcje promujące zmienność (takie jak inicjatory obiektów). Byłoby miło mieć lepszą składnię do promowania niezmiennych typów. Ciężko o tym myślimy i mamy kilka ciekawych pomysłów, ale nie spodziewałbym się czegoś takiego w następnej wersji.
Eric Lippert

Odpowiedzi:

22

Jeden styl przyjęty we frameworkach jest zwykle podobny do grupowania powiązanych parametrów w powiązane klasy (ale po raz kolejny problematyczny jest zmienność):

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
źródło
Kondensowanie wszystkich ciągów w tablicy ciągów ma sens tylko wtedy, gdy wszystkie są powiązane. Z kolejności argumentów wygląda na to, że nie wszystkie są całkowicie powiązane.
icktoofay,
Uzgodniono poza tym, że zadziała to tylko wtedy, gdy masz wiele typów tego samego parametru.
Timo Willemsen
Czym się to różni od mojego pierwszego przykładu w pytaniu?
Sedat Kapanoglu
Cóż, jest to zwykle przyjęty styl nawet w .NET Framework, co wskazuje, że Twój pierwszy przykład jest całkiem poprawny w tym, co robi, nawet jeśli wprowadza drobne problemy ze zmiennością (chociaż ten przykład pochodzi oczywiście z innej biblioteki). Przy okazji, fajne pytanie.
Teoman Soygul
2
Z czasem coraz bardziej zbliżam się do tego stylu. Najwyraźniej znaczna część problemów związanych ze „zbyt dużą liczbą parametrów” może zostać rozwiązana za pomocą dobrych grup logicznych i abstrakcji. W końcu sprawia, że ​​kod jest bardziej czytelny i bardziej zmodularyzowany.
Sedat Kapanoglu
87

Użyj połączenia konstruktora i interfejsu API w stylu języka specyficznego dla domeny - Fluent Interface. Interfejs API jest nieco bardziej szczegółowy, ale dzięki funkcji Intellisense bardzo szybko się pisze i jest łatwy do zrozumienia.

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
źródło
+1 - tego rodzaju podejście (które powszechnie widziałem nazywane „płynnym interfejsem”) jest dokładnie tym, o czym myślałem.
Daniel Pryden
+1 - oto co znalazłem w takiej samej sytuacji. Tyle że była tylko jedna klasa i DoSomeActionbyła to metoda.
Vilx
+1 Ja też o tym myślałem, ale ponieważ jestem nowy w płynnych interfejsach, nie wiedziałem, czy pasuje tutaj idealnie. Dziękuję za potwierdzenie mojej własnej intuicji.
Matt
Nie słyszałem o wzorcu Builder, zanim zadałem to pytanie, więc było to dla mnie wnikliwe doświadczenie. Zastanawiam się, jak często to jest? Ponieważ każdy programista, który nie słyszał o wzorcu, może mieć problemy ze zrozumieniem użycia bez dokumentacji.
Sedat Kapanoglu
1
@ssg, jest to typowe w tych scenariuszach. Na przykład BCL zawiera klasy konstruktora parametrów połączenia.
Samuel Neff
10

To, co masz, jest całkiem pewnym wskazaniem, że dana klasa narusza zasadę pojedynczej odpowiedzialności, ponieważ ma zbyt wiele zależności. Poszukaj sposobów na refaktoryzację tych zależności w klastry zależności fasady .

Mark Seemann
źródło
1
Nadal bym refaktoryzował, wprowadzając jeden lub więcej obiektów parametrów, ale oczywiście, jeśli przeniesiesz wszystkie argumenty do jednego niezmiennego typu, nic nie osiągniesz. Sztuczka polega na wyszukaniu klastrów argumentów, które są bliżej spokrewnione, a następnie refaktoryzacji każdego z tych klastrów do osobnego obiektu parametru.
Mark Seemann
2
Możesz zmienić każdą grupę w niezmienny obiekt sam w sobie. Każdy obiekt musiałby przyjąć tylko kilka parametrów, więc chociaż rzeczywista liczba argumentów pozostaje taka sama, liczba argumentów używanych w dowolnym konstruktorze zostanie zmniejszona.
Mark Seemann
2
+1 @ssg: Za każdym razem, gdy udało mi się przekonać siebie do czegoś takiego, z biegiem czasu udowodniłem, że się mylę, wypierając użyteczną abstrakcję z dużych metod wymagających tego poziomu parametrów. Książka DDD autorstwa Evansa może dać ci kilka pomysłów, jak o tym pomyśleć (chociaż twój system brzmi tak, jakby był potencjalnie daleko od odpowiedniego miejsca do zastosowania takich wzorców) - (i tak czy inaczej jest to po prostu świetna książka).
Ruben Bartelink
3
@Ruben: Żadna rozsądna książka nie powie "w dobrym projekcie OO klasa nie powinna mieć więcej niż 5 właściwości". Klasy są pogrupowane logicznie i tego rodzaju kontekstualizacja nie może opierać się na ilościach. Jednak obsługa niezmienności języka C # zaczyna stwarzać problemy w pewnej liczbie właściwości, zanim zaczniemy naruszać dobre zasady projektowania obiektów obiektowych.
Sedat Kapanoglu
3
@Ruben: Nie oceniałem Twojej wiedzy, nastawienia ani temperamentu. Oczekuję, że zrobisz to samo. Nie mówię, że twoje rekomendacje nie są dobre. Mówię, że mój problem może pojawić się nawet przy najdoskonalszym projekcie i wydaje się, że jest to przeoczony punkt. Chociaż rozumiem, dlaczego doświadczeni ludzie zadają podstawowe pytania dotyczące najczęstszych błędów, wyjaśnienie ich po kilku rundach staje się mniej przyjemne. Muszę jeszcze raz powiedzieć, że bardzo możliwy jest ten problem z doskonałym projektem. Dziękuję za pozytywne głosy!
Sedat Kapanoglu
10

Po prostu zmień strukturę danych parametrów z a classna a structi gotowe.

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) 

Metoda otrzyma teraz własną kopię struktury. Zmiany dokonane w zmiennej argumentu nie mogą być obserwowane przez metodę, a zmiany wprowadzone przez metodę w zmiennej nie mogą być obserwowane przez obiekt wywołujący. Izolację osiąga się bez niezmienności.

Plusy:

  • Najłatwiejszy do wdrożenia
  • Najmniejsza zmiana zachowania w podstawowej mechanice

Cons:

  • Niezmienność nie jest oczywista, wymaga uwagi programisty.
  • Niepotrzebne kopiowanie w celu zachowania niezmienności
  • Zajmuje miejsce na stosie
Jeffrey L Whitledge
źródło
+1 To prawda, ale nie rozwiązuje części dotyczącej „bezpośredniego ujawniania pól jest złem”. Nie jestem pewien, jak złe rzeczy mogą się pogorszyć, jeśli zdecyduję się na użycie pól zamiast właściwości w tym API.
Sedat Kapanoglu
@ssg - Więc uczyń je właściwościami publicznymi zamiast pól. Jeśli potraktujesz to jako strukturę, która nigdy nie będzie miała kodu, nie ma większego znaczenia, czy używasz właściwości, czy pól. Jeśli zdecydujesz się kiedykolwiek nadać mu kod (taki jak walidacja lub coś w tym rodzaju), na pewno będziesz chciał nadać im właściwości. Przynajmniej w przypadku pól publicznych nikt nigdy nie będzie miał złudzeń co do niezmienników istniejących w tej strukturze. Będzie musiał zostać zweryfikowany metodą dokładnie tak, jak byłyby to parametry.
Jeffrey L Whitledge
Och, to prawda. Niesamowite! Dzięki.
Sedat Kapanoglu
8
Myślę, że reguła eksponowania pól jest złem dotyczy obiektów w projektowaniu obiektowym. Konstrukcja, którą proponuję, to po prostu nieosłonięty pojemnik z parametrami. Ponieważ Twój instynkt miał iść z czymś niezmiennym, myślę, że taki podstawowy pojemnik może być odpowiedni na tę okazję.
Jeffrey L Whitledge
1
@JeffreyLWhitledge: Naprawdę nie podoba mi się pomysł, że struktury odsłoniętych pól są złe. Takie stwierdzenie wydaje mi się równoznaczne ze stwierdzeniem, że śrubokręty są złe i ludzie powinni używać młotków, ponieważ groty śrubokrętów wgryzają się w łby gwoździ. Do wbicia gwoździa należy użyć młotka, natomiast w przypadku wkręcenia śruby należy użyć śrubokręta. Istnieje wiele sytuacji, w których konstrukcja z odsłoniętym polem jest właśnie odpowiednim narzędziem do pracy. Nawiasem mówiąc, jest znacznie mniej przypadków, w których struktura z właściwościami get / set jest naprawdę odpowiednim narzędziem (w większości przypadków, gdy ...
supercat
6

Co powiesz na utworzenie klasy konstruktora w klasie danych. Klasa danych będzie miała wszystkie ustawiacze jako prywatne i tylko twórca będzie mógł je ustawić.

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
źródło
1
+1 To doskonale uzasadnione rozwiązanie, ale myślę, że to za dużo rusztowań, aby utrzymać tak prostą decyzję projektową. Zwłaszcza, gdy rozwiązanie „readonly struct” jest „bardzo” bliskie ideału.
Sedat Kapanoglu
2
Jak to rozwiązuje problem „zbyt wielu parametrów”? Składnia może być inna, ale problem wygląda tak samo. To nie jest krytyka, jestem po prostu ciekawy, ponieważ nie znam tego schematu.
alexD
1
@alexD rozwiązuje to problem ze zbyt dużą liczbą parametrów funkcji i zachowaniem niezmienności obiektu. Tylko klasa budująca może ustawić prywatne właściwości, a po otrzymaniu obiektu parametrów nie można go zmienić. Problem polega na tym, że wymaga dużo kodu rusztowania.
marto
5
Obiekt parametru w rozwiązaniu nie jest niezmienny. Ktoś, kto zapisuje konstruktora, może edytować parametry nawet po zbudowaniu
astef
1
Zgodnie z @ astef, ponieważ jest obecnie napisane, DoSomeActionParameters.Builderinstancja może zostać użyta do utworzenia i skonfigurowania dokładnie jednej DoSomeActionParametersinstancji. Po wywołaniu Build()kolejnych zmian Builderwłaściwości będzie kontynuował modyfikowanie właściwości oryginalnej DoSomeActionParameters instancji, a kolejne wywołania Build()będą nadal zwracały tę samą DoSomeActionParametersinstancję. Naprawdę powinno public DoSomeActionParameters Build() { var oldObj = obj; obj = new DoSomeActionParameters(); return oldObj; }.
BACON,
6

Nie jestem programistą C #, ale uważam, że C # obsługuje nazwane argumenty: (F # to robi, a C # jest w dużej mierze kompatybilny z funkcjami tego rodzaju). http://msdn.microsoft.com/en-us/library/dd264739 .aspx # Y342

Więc wywołanie oryginalnego kodu staje się:

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

nie zajmuje to więcej miejsca / myśli, że tworzenie obiektu jest tworzone i ma wszystkie zalety, z faktu, że w ogóle nie zmieniłeś tego, co dzieje się w nieokreślonym systemie. Nie musisz nawet niczego przekodowywać, aby wskazać, że argumenty są nazwane

Edycja: oto artykuł, który znalazłem na ten temat. http://www.globalnerdy.com/2009/03/12/default-and-named-parameters-in-c-40-sith-lord-in-training/ Powinienem wspomnieć, że C # 4.0 obsługuje nazwane argumenty, 3.0 nie

Lyndon White
źródło
Rzeczywiście poprawa. Ale to rozwiązuje tylko czytelność kodu i robi to tylko wtedy, gdy programista wyrazi zgodę na używanie nazwanych parametrów. Łatwo zapomnieć o określeniu nazw i przekazaniu korzyści. Nie pomaga przy pisaniu samego kodu. np. podczas refaktoryzacji funkcji na mniejsze i przekazywania danych w jednym zawartym pakiecie.
Sedat Kapanoglu,
6

Dlaczego po prostu nie stworzyć interfejsu, który wymusza niezmienność (tj. Tylko pobierające)?

Zasadniczo jest to pierwsze rozwiązanie, ale wymuszasz na funkcji użycie interfejsu w celu uzyskania dostępu do parametru.

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

a deklaracja funkcji staje się:

public ResultEnum DoSomeAction(IDoSomeActionParameters parameters, out Guid code)

Plusy:

  • Nie ma problemu ze stosem, jak struct rozwiązanie
  • Naturalne rozwiązanie wykorzystujące semantykę języka
  • Niezmienność jest oczywista
  • Elastyczny (konsument może użyć innej klasy, jeśli chce)

Cons:

  • Pewna powtarzalna praca (te same deklaracje w dwóch różnych jednostkach)
  • Deweloper musi zgadnąć, że DoSomeActionParametersjest to klasa, na którą można zmapowaćIDoSomeActionParameters
prawdomówność
źródło
+1 Nie wiem, dlaczego o tym nie pomyślałem? :) Myślę, że myślałem, że obiekt nadal będzie cierpieć z powodu konstruktora ze zbyt dużą liczbą parametrów, ale tak nie jest. Tak, to też bardzo ważne rozwiązanie. Jedynym problemem, jaki przychodzi mi do głowy, jest to, że nie jest to łatwe dla użytkownika API, aby znaleźć właściwą nazwę klasy, która obsługuje dany interfejs. Lepsze jest rozwiązanie wymagające najmniejszej dokumentacji.
Sedat Kapanoglu,
Podoba mi się ten, powielanie i znajomość mapy są obsługiwane za pomocą resharpera i mogę podać domyślne wartości za pomocą domyślnego konstruktora konkretnej klasy
Anthony Johnston
2
Nie podoba mi się to podejście. Ktoś, kto ma odniesienie do dowolnej implementacji IDoSomeActionParametersi chce uchwycić zawarte w niej wartości, nie ma możliwości dowiedzenia się, czy trzymanie odwołania będzie wystarczające, czy też musi skopiować wartości do innego obiektu. Czytelne interfejsy są przydatne w niektórych kontekstach, ale nie jako sposób na uczynienie rzeczy niezmiennymi.
supercat
„Parametry IDoSomeActionParameters” można rzutować na DoSomeActionParameters i zmieniać. Deweloper może nawet nie zdawać sobie sprawy, że obchodzi próbę uczynienia parametrów niezmiennymi
Joseph Simpson
3

Wiem, że to stare pytanie, ale pomyślałem, że zgodzę się z moją sugestią, ponieważ właśnie musiałem rozwiązać ten sam problem. Wprawdzie mój problem był nieco inny niż twój, ponieważ miałem dodatkowe wymaganie, aby nie chcieć, aby użytkownicy mogli samodzielnie skonstruować ten obiekt (całe nawodnienie danych pochodziło z bazy danych, więc mogłem uwięzić całą konstrukcję wewnętrznie). To pozwoliło mi użyć prywatnego konstruktora i następującego wzorca;

    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
źródło
2

Możesz zastosować podejście w stylu Konstruktora, choć w zależności od złożoności DoSomeActionmetody może to być dotkliwe. Coś w tym stylu:

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
źródło
Ach, marto pokonał mnie zgodnie z sugestią budowniczego!
Chris White
2

Oprócz odpowiedzi manji - możesz też chcieć podzielić jedną operację na kilka mniejszych. Porównać:

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

i

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

Dla tych, którzy nie znają POSIX, stworzenie dziecka może być tak proste, jak:

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

Każdy wybór projektu stanowi kompromis dotyczący tego, jakie operacje może wykonać.

PS. Tak - jest podobny do budowniczego - tylko odwrotnie (tj. Po stronie odbiorcy zamiast dzwoniącego). W tym konkretnym przypadku może być lepsze niż builder.

Maciej Piechotka
źródło
To niewielka operacja, ale to dobra rekomendacja dla każdego, kto narusza zasadę pojedynczej odpowiedzialności. Moje pytanie pojawia się zaraz po wprowadzeniu wszystkich innych ulepszeń projektu.
Sedat Kapanoglu,
UPS. Przepraszam - przygotowałem pytanie przed twoją edycją i opublikowałem je później - stąd nie zauważyłem tego.
Maciej Piechotka
2

tutaj jest trochę inny niż Mikeys, ale staram się, aby całość była jak najmniejsza do napisania

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

Obiekt DoSomeActionParameters jest niezmienny, ponieważ może być i nie można go utworzyć bezpośrednio, ponieważ jego domyślny konstruktor jest prywatny

Inicjator nie jest niezmienny, a jedynie transport

Użycie korzysta z inicjalizatora w inicjatorze (jeśli dostaniesz mój dryf) I mogę mieć wartości domyślne w domyślnym konstruktorze inicjatora

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

Parametry będą tutaj opcjonalne, jeśli chcesz, aby niektóre były wymagane, możesz je umieścić w domyślnym konstruktorze inicjatora

A walidacja może przejść w metodzie 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);
    }
}

Więc teraz wygląda na to

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

Wciąż trochę kooki, wiem, ale i tak spróbuję

Edycja: przeniesienie metody create do statycznej w obiekcie parametrów i dodanie delegata, który przekazuje inicjalizator, usuwa część funkcjonalności z wywołania

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

Więc teraz rozmowa wygląda tak

DoSomeAction(
        DoSomeActionParameters.Create(
            i => {
                i.A = "Hello";
            })
        );
Anthony Johnston
źródło
1

Użyj struktury, ale zamiast pól publicznych, mają właściwości publiczne:

• Wszyscy (w tym FXCop i Jon Skeet) zgadzają się, że ujawnianie pól publicznych jest złe.

Jon i FXCop będą usatysfakcjonowani, ponieważ ujawniasz właściwości, a nie pola.

• Eric Lippert i wsp. Twierdzą, że poleganie na polach tylko do odczytu w celu uzyskania niezmienności jest kłamstwem.

Eric będzie usatysfakcjonowany, ponieważ korzystając z właściwości, możesz upewnić się, że wartość jest ustawiona tylko raz.

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

Jeden pół-niezmienny obiekt (wartość można ustawić, ale nie można jej zmienić). Działa dla typów wartości i referencji.

jmoreno
źródło
+1 Może prywatne pola tylko do odczytu i publiczne właściwości tylko do pobierania można połączyć w strukturze, aby umożliwić mniej rozwlekłe rozwiązanie.
Sedat Kapanoglu,
Publiczne właściwości odczytu i zapisu struktur, które thisulegają mutacji, są znacznie gorsze niż pola publiczne. Nie jestem pewien, dlaczego uważasz, że ustawienie wartości tylko raz sprawia, że ​​wszystko jest „w porządku”; jeśli thiszaczniemy od domyślnego wystąpienia struktury, problemy związane z -mutating properties będą nadal istnieć.
supercat
0

Wariant odpowiedzi Samuela , którego użyłem w swoim projekcie, gdy miałem ten sam problem:

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

I do użycia:

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

W moim przypadku parametry były celowo modyfikowalne, ponieważ metody ustawiające nie pozwalały na wszystkie możliwe kombinacje, a jedynie ujawniały ich wspólne kombinacje. To dlatego, że niektóre z moich parametrów były dość złożone, a pisanie metod dla wszystkich możliwych przypadków byłoby trudne i niepotrzebne (rzadko używane są szalone kombinacje).

Vilx-
źródło
Niezależnie od tego jest to klasa zmienna.
Sedat Kapanoglu
@ssg - Cóż, tak. DoMagicPosiada w swojej umowie jednak, że nie będzie modyfikować obiekt. Ale chyba nie chroni to przed przypadkowymi modyfikacjami.
Vilx-