Wie vermeide ich das Problem „zu viele Parameter“ beim API-Design?

160

Ich habe diese API-Funktion:

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

Ich mag es nicht Weil die Parameterreihenfolge unnötig wichtig wird. Es wird schwieriger, neue Felder hinzuzufügen. Es ist schwieriger zu sehen, was herumgereicht wird. Es ist schwieriger, die Methode in kleinere Teile umzugestalten, da dadurch ein weiterer Aufwand für die Übergabe aller Parameter in Unterfunktionen entsteht. Code ist schwerer zu lesen.

Ich hatte die naheliegendste Idee: Lassen Sie ein Objekt die Daten kapseln und weitergeben, anstatt jeden Parameter einzeln zu übergeben. Folgendes habe ich mir ausgedacht:

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

Das reduzierte meine API-Deklaration auf:

public ResultEnum DoSomeAction(DoSomeActionParameters parameters, out Guid code)

Nett. Sieht sehr unschuldig aus, aber wir haben tatsächlich eine große Veränderung eingeführt: Wir haben die Veränderlichkeit eingeführt. Denn was wir zuvor getan hatten, war tatsächlich, ein anonymes unveränderliches Objekt zu übergeben: Funktionsparameter auf dem Stapel. Jetzt haben wir eine neue Klasse erstellt, die sehr veränderlich ist. Wir haben die Möglichkeit geschaffen, den Status des Anrufers zu manipulieren . Das ist Scheiße. Jetzt möchte ich mein Objekt unveränderlich, was mache ich?

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

Wie Sie sehen, habe ich mein ursprüngliches Problem tatsächlich neu erstellt: zu viele Parameter. Es ist offensichtlich, dass dies nicht der richtige Weg ist. Was soll ich tun? Die letzte Möglichkeit, eine solche Unveränderlichkeit zu erreichen, besteht darin, eine "schreibgeschützte" Struktur wie diese zu verwenden:

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

Dadurch können wir Konstruktoren mit zu vielen Parametern vermeiden und Unveränderlichkeit erreichen. Eigentlich behebt es alle Probleme (Parameterreihenfolge usw.). Noch:

In diesem Moment war ich verwirrt und beschloss, diese Frage zu schreiben: Was ist der einfachste Weg in C #, um das Problem "zu vieler Parameter" zu vermeiden, ohne die Veränderlichkeit einzuführen? Ist es möglich, eine schreibgeschützte Struktur für diesen Zweck zu verwenden und dennoch kein schlechtes API-Design zu haben?

ERKLÄRUNGEN:

  • Bitte nehmen Sie an, dass kein Verstoß gegen das Prinzip der Einzelverantwortung vorliegt. In meinem ursprünglichen Fall schreibt die Funktion nur bestimmte Parameter in einen einzelnen DB-Datensatz.
  • Ich suche keine spezifische Lösung für die gegebene Funktion. Ich suche einen allgemeinen Ansatz für solche Probleme. Ich bin speziell daran interessiert, das Problem "zu viele Parameter" zu lösen, ohne die Veränderlichkeit oder ein schreckliches Design einzuführen.

AKTUALISIEREN

Die hier gegebenen Antworten haben unterschiedliche Vor- und Nachteile. Deshalb möchte ich dies in ein Community-Wiki konvertieren. Ich denke, jede Antwort mit Codebeispiel und Vor- / Nachteilen wäre ein guter Leitfaden für ähnliche Probleme in der Zukunft. Ich versuche jetzt herauszufinden, wie es geht.

ssg
quelle
Clean Code: Ein Handbuch für agiles Software-Handwerk von Robert C. Martin und Martin Fowlers Refactoring-Buch behandelt dies ein wenig
Ian Ringrose
1
Ist diese Builder-Lösung nicht redundant, wenn Sie C # 4 verwenden, für das Sie optionale Parameter haben ?
Khellang
1
Ich mag dumm sein, aber ich sehe nicht, wie dies ein Problem ist, wenn man bedenkt, dass DoSomeActionParameterses sich um ein Wegwerfobjekt handelt, das nach dem Methodenaufruf verworfen wird.
Vilx
6
Beachten Sie, dass ich nicht sage, dass Sie schreibgeschützte Felder in einer Struktur vermeiden sollten. Unveränderliche Strukturen sind eine bewährte Methode. Mit schreibgeschützten Feldern können Sie selbstdokumentierende unveränderliche Strukturen erstellen. Mein Punkt ist, dass Sie sich nicht darauf verlassen sollten, dass sich schreibgeschützte Felder niemals ändern , da dies keine Garantie dafür ist, dass Sie ein schreibgeschütztes Feld in einer Struktur erhalten. Dies ist ein spezieller Fall des allgemeineren Hinweises, dass Sie Werttypen nicht so behandeln sollten, als wären sie Referenztypen. Sie sind ein ganz anderes Tier.
Eric Lippert
7
@ssg: Das würde ich auch lieben. Wir haben C # Funktionen hinzugefügt, die die Unveränderlichkeit fördern (wie LINQ), und gleichzeitig Funktionen hinzugefügt, die die Veränderlichkeit fördern (wie Objektinitialisierer). Es wäre schön, eine bessere Syntax zu haben, um unveränderliche Typen zu fördern. Wir denken intensiv darüber nach und haben einige interessante Ideen, aber ich würde so etwas für die nächste Version nicht erwarten.
Eric Lippert

Antworten:

22

Ein Stil, der in den Frameworks enthalten ist, ist normalerweise das Gruppieren verwandter Parameter in verwandte Klassen (aber wiederum problematisch mit der Veränderbarkeit):

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
quelle
Das Zusammenfassen aller Zeichenfolgen zu einem Zeichenfolgenarray ist nur dann sinnvoll, wenn sie alle miteinander verbunden sind. Aus der Reihenfolge der Argumente geht hervor, dass sie nicht alle vollständig miteinander verbunden sind.
icktoofay
Einverstanden, dass dies nur funktionieren würde, wenn Sie viele der gleichen Typen wie Parameter haben.
Timo Willemsen
Wie unterscheidet sich das von meinem ersten Beispiel in der Frage?
Sedat Kapanoglu
Nun, dies ist der übliche Stil, der auch in .NET Framework üblich ist, was darauf hinweist, dass Ihr erstes Beispiel in seiner Funktionsweise völlig richtig ist, selbst wenn es geringfügige Veränderbarkeitsprobleme mit sich bringt (obwohl dieses Beispiel offensichtlich aus einer anderen Bibliothek stammt). Gute Frage übrigens.
Teoman Soygul
2
Ich habe mich im Laufe der Zeit mehr diesem Stil angenähert. Anscheinend kann eine erhebliche Menge der Probleme mit "zu vielen Parametern" mit guten logischen Gruppen und Abstraktionen gelöst werden. Am Ende wird der Code lesbarer und modularer.
Sedat Kapanoglu
87

Verwenden Sie eine Kombination aus Builder und domänenspezifischer API - Fluent Interface. Die API ist etwas ausführlicher, aber mit Intellisense ist sie sehr schnell zu tippen und leicht zu verstehen.

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
quelle
+1 - diese Art von Ansatz (den ich allgemein als "fließende Schnittstelle" bezeichnet habe) ist genau das, was ich auch im Sinn hatte.
Daniel Pryden
+1 - das ist, was ich in der gleichen Situation gelandet bin. Nur dass es nur eine Klasse gab und DoSomeActioneine Methode dafür war.
Vilx
+1 Auch ich habe darüber nachgedacht, aber da ich neu in fließenden Schnittstellen bin, wusste ich nicht, ob es hier perfekt passt. Vielen Dank für die Bestätigung meiner eigenen Intuition.
Matt
Ich hatte noch nie von Builder-Mustern gehört, bevor ich diese Frage gestellt habe, daher war dies eine aufschlussreiche Erfahrung für mich. Ich frage mich, wie häufig es ist? Denn jeder Entwickler, der noch nichts von dem Muster gehört hat, hat möglicherweise Probleme, die Verwendung ohne Dokumentation zu verstehen.
Sedat Kapanoglu
1
@ssg, es ist in diesen Szenarien üblich. Die BCL verfügt beispielsweise über Verbindungszeichenfolgen-Builder-Klassen.
Samuel Neff
10

Was Sie dort haben, ist ein ziemlich sicherer Hinweis darauf, dass die betreffende Klasse gegen das Prinzip der Einzelverantwortung verstößt, weil sie zu viele Abhängigkeiten aufweist. Suchen Sie nach Möglichkeiten, diese Abhängigkeiten in Cluster von Fassadenabhängigkeiten umzugestalten .

Mark Seemann
quelle
1
Ich würde immer noch umgestalten, indem ich ein oder mehrere Parameterobjekte einführe, aber wenn Sie alle Argumente in einen einzigen unveränderlichen Typ verschieben, haben Sie natürlich nichts erreicht. Der Trick besteht darin, nach Clustern von Argumenten zu suchen, die enger miteinander verbunden sind, und dann jeden dieser Cluster in ein separates Parameterobjekt umzugestalten.
Mark Seemann
2
Sie können jede Gruppe in ein unveränderliches Objekt für sich verwandeln. Jedes Objekt müsste nur wenige Parameter annehmen. Während die tatsächliche Anzahl der Argumente gleich bleibt, wird die Anzahl der in einem einzelnen Konstruktor verwendeten Argumente reduziert.
Mark Seemann
2
+1 @ssg: Jedes Mal, wenn ich es geschafft habe, mich von so etwas zu überzeugen, habe ich mich im Laufe der Zeit als falsch erwiesen, indem ich nützliche Abstraktionen aus großen Methoden herausgefahren habe, die diese Parameterebene erfordern. Das DDD-Buch von Evans gibt Ihnen möglicherweise einige Ideen, wie Sie darüber nachdenken können (obwohl Ihr System so klingt, als wäre es möglicherweise kein relevanter Ort für die Anwendung solcher Muster) - (und es ist in beiden Fällen einfach ein großartiges Buch).
Ruben Bartelink
3
@ Ruben: Kein vernünftiges Buch würde sagen "In einem guten OO-Design sollte eine Klasse nicht mehr als 5 Eigenschaften haben". Klassen werden logisch gruppiert und diese Art der Kontextualisierung kann nicht auf Mengen basieren. Die Unveränderlichkeitsunterstützung von C # führt jedoch bei einer bestimmten Anzahl von Eigenschaften zu Problemen, bevor wir gegen gute OO-Entwurfsprinzipien verstoßen.
Sedat Kapanoglu
3
@ Ruben: Ich habe dein Wissen, deine Einstellung oder dein Temperament nicht beurteilt. Ich erwarte, dass Sie dasselbe tun. Ich sage nicht, dass Ihre Empfehlungen nicht gut sind. Ich sage, dass mein Problem selbst bei perfektem Design auftreten kann, und das scheint hier ein übersehener Punkt zu sein. Obwohl ich verstehe, warum erfahrene Leute grundlegende Fragen zu den häufigsten Fehlern stellen, macht es weniger Spaß, sie nach ein paar Runden zu klären. Ich muss noch einmal sagen, dass es sehr gut möglich ist, dieses Problem mit einem perfekten Design zu haben. Und danke für die positive Bewertung!
Sedat Kapanoglu
10

Ändern Sie einfach Ihre Parameterdatenstruktur von a classnach a structund los geht's.

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) 

Die Methode erhält nun eine eigene Kopie der Struktur. Änderungen an der Argumentvariablen können von der Methode nicht beobachtet werden, und Änderungen, die die Methode an der Variablen vornimmt, können vom Aufrufer nicht beobachtet werden. Die Isolierung wird ohne Unveränderlichkeit erreicht.

Vorteile:

  • Am einfachsten zu implementieren
  • Geringste Verhaltensänderung in der zugrunde liegenden Mechanik

Nachteile:

  • Unveränderlichkeit ist nicht offensichtlich, erfordert die Aufmerksamkeit des Entwicklers.
  • Unnötiges Kopieren, um die Unveränderlichkeit zu erhalten
  • Besetzt Stapelplatz
Jeffrey L Whitledge
quelle
+1 Das stimmt, löst aber nicht den Teil über "Felder direkt freizulegen ist böse". Ich bin mir nicht sicher, wie schlimm es werden könnte, wenn ich Felder anstelle von Eigenschaften für diese API verwende.
Sedat Kapanoglu
@ssg - Machen Sie sie also zu öffentlichen Eigenschaften anstelle von Feldern. Wenn Sie dies als eine Struktur behandeln, die niemals Code enthält, spielt es keine große Rolle, ob Sie Eigenschaften oder Felder verwenden. Wenn Sie sich dafür entscheiden, ihm jemals Code zu geben (z. B. Validierung oder ähnliches), sollten Sie diese auf jeden Fall zu Eigenschaften machen. Zumindest auf öffentlichen Feldern wird niemand jemals Illusionen über Invarianten haben, die auf dieser Struktur existieren. Es muss von der Methode genau so validiert werden, wie es die Parameter gewesen wären.
Jeffrey L Whitledge
Oh das stimmt. Genial! Vielen Dank.
Sedat Kapanoglu
8
Ich denke, dass die Regel "Felder aussetzen ist böse" für Objekte in einem objektorientierten Design gilt. Die Struktur, die ich vorschlage, ist nur ein Bare-Metal-Container mit Parametern. Da Ihr Instinkt mit etwas Unveränderlichem einhergehen sollte, denke ich, dass ein solcher grundlegender Behälter für diesen Anlass geeignet sein könnte.
Jeffrey L Whitledge
1
@ JeffreyLWhitledge: Ich mag die Idee wirklich nicht, dass exponierte Feldstrukturen böse sind. Eine solche Behauptung scheint mir gleichbedeutend damit zu sein, dass Schraubendreher böse sind und die Leute Hämmer verwenden sollten, weil die Spitzen der Schraubendreher die Nagelköpfe verbeulen. Wenn man einen Nagel einschlagen muss, sollte man einen Hammer verwenden, aber wenn man eine Schraube einschlagen muss, sollte man einen Schraubendreher verwenden. Es gibt viele Situationen, in denen eine exponierte Feldstruktur genau das richtige Werkzeug für den Job ist. Übrigens gibt es weit weniger, wo eine Struktur mit get / set-Eigenschaften wirklich das richtige Werkzeug ist (in den meisten Fällen, wo ...
supercat
6

Wie wäre es, wenn Sie eine Builder-Klasse in Ihrer Datenklasse erstellen. Die Datenklasse hat alle Setter als privat und nur der Builder kann sie festlegen.

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
quelle
1
+1 Das ist eine absolut gültige Lösung, aber ich denke, es ist zu viel Gerüst, um eine so einfache Entwurfsentscheidung aufrechtzuerhalten. Besonders wenn die "readonly struct" -Lösung "sehr" nahezu ideal ist.
Sedat Kapanoglu
2
Wie löst dies das Problem "zu viele Parameter"? Die Syntax mag unterschiedlich sein, aber das Problem sieht gleich aus. Dies ist keine Kritik, ich bin nur neugierig, weil ich mit diesem Muster nicht vertraut bin.
AlexD
1
@alexD Dies löst das Problem, dass zu viele Funktionsparameter vorhanden sind und das Objekt unveränderlich bleibt. Nur die Builder-Klasse kann die privaten Eigenschaften festlegen. Sobald Sie das Parameterobjekt erhalten haben, können Sie es nicht mehr ändern. Das Problem ist, dass viel Gerüstcode erforderlich ist.
Marto
5
Das Parameterobjekt in Ihrer Lösung ist nicht unveränderlich. Jemand, der den Builder speichert, kann Parameter auch nach dem
Erstellen
1
In @ astefs Punkt, wie es derzeit geschrieben ist, kann eine DoSomeActionParameters.BuilderInstanz verwendet werden, um genau eine DoSomeActionParametersInstanz zu erstellen und zu konfigurieren . Nach dem Aufruf Build()nachfolgender Änderungen an den BuilderEigenschaften von 'werden die Eigenschaften der ursprünglichen DoSomeActionParameters Instanz weiter geändert , und nachfolgende Aufrufe von Build()werden weiterhin dieselbe DoSomeActionParametersInstanz zurückgeben. Es sollte wirklich so sein public DoSomeActionParameters Build() { var oldObj = obj; obj = new DoSomeActionParameters(); return oldObj; }.
Speck
6

Ich bin kein C # -Programmierer, aber ich glaube, C # unterstützt benannte Argumente: (F # funktioniert und C # ist weitgehend funktionskompatibel für solche Dinge) Es funktioniert: http://msdn.microsoft.com/en-us/library/dd264739 .aspx # Y342

Das Aufrufen Ihres Originalcodes wird also zu:

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

Dies nimmt keinen Platz mehr in Anspruch, da Ihre Objekterstellung alle Vorteile bietet, da Sie nicht geändert haben, was im unerliebten System überhaupt passiert. Sie müssen nicht einmal etwas neu codieren, um anzuzeigen, dass die Argumente benannt sind

Edit: hier ist ein Artikel, den ich darüber gefunden habe. http://www.globalnerdy.com/2009/03/12/default-and-named-parameters-in-c-40-sith-lord-in-training/ Ich sollte erwähnen, dass C # 4.0 benannte Argumente unterstützt, 3.0 nicht

Lyndon White
quelle
Eine Verbesserung in der Tat. Dies löst jedoch nur die Lesbarkeit des Codes und dies nur, wenn sich der Entwickler für die Verwendung benannter Parameter entscheidet. Es ist leicht zu vergessen, Namen anzugeben und die Vorteile zu verschenken. Es hilft nicht beim Schreiben des Codes selbst. B. wenn Sie die Funktion in kleinere umgestalten und die Daten in einem einzigen enthaltenen Paket weitergeben.
Sedat Kapanoglu
6

Warum nicht einfach eine Schnittstelle erstellen, die Unveränderlichkeit erzwingt (dh nur Getter)?

Es ist im Wesentlichen Ihre erste Lösung, aber Sie erzwingen, dass die Funktion die Schnittstelle verwendet, um auf den Parameter zuzugreifen.

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

und die Funktionsdeklaration wird:

public ResultEnum DoSomeAction(IDoSomeActionParameters parameters, out Guid code)

Vorteile:

  • Hat kein Stapelspeicherproblem wie eine structLösung
  • Natürliche Lösung mit Sprachsemantik
  • Unveränderlichkeit ist offensichtlich
  • Flexibel (Verbraucher können eine andere Klasse verwenden, wenn sie möchten)

Nachteile:

  • Einige sich wiederholende Arbeiten (gleiche Erklärungen in zwei verschiedenen Einheiten)
  • Entwickler müssen raten, dass dies DoSomeActionParameterseine Klasse ist, der zugeordnet werden könnteIDoSomeActionParameters
Wahrhaftigkeit
quelle
+1 Ich weiß nicht, warum ich nicht daran gedacht habe? :) Ich denke, ich dachte, das Objekt würde immer noch unter einem Konstruktor mit zu vielen Parametern leiden, aber das ist nicht der Fall. Ja, das ist auch eine sehr gültige Lösung. Das einzige Problem, an das ich denken kann, ist, dass es für API-Benutzer nicht einfach ist, den richtigen Klassennamen zu finden, der die angegebene Schnittstelle unterstützt. Eine Lösung, die am wenigsten Dokumentation erfordert, ist besser.
Sedat Kapanoglu
Ich mag dieses, die Vervielfältigung und das Wissen über die Karte werden mit Resharper behandelt, und ich kann Standardwerte mit dem Standardkonstruktor der konkreten Klasse
Anthony Johnston
2
Ich mag diesen Ansatz nicht. Jemand, der einen Verweis auf eine beliebige Implementierung von hat IDoSomeActionParametersund die darin enthaltenen Werte erfassen möchte, kann nicht wissen, ob das Halten des Verweises ausreicht oder ob er die Werte auf ein anderes Objekt kopieren muss. Lesbare Schnittstellen sind in einigen Kontexten nützlich, aber nicht, um Dinge unveränderlich zu machen.
Supercat
"IDoSomeActionParameters-Parameter" können in DoSomeActionParameters umgewandelt und geändert werden. Der Entwickler kann nicht einmal erkennen, dass sie einen Versuch umgehen, Parameter unveränderlich zu machen
Joseph Simpson
3

Ich weiß, dass dies eine alte Frage ist, aber ich dachte, ich würde mich meinem Vorschlag anschließen, da ich nur das gleiche Problem lösen musste. Zugegeben, mein Problem war etwas anders als das Ihre, da ich zusätzlich die Anforderung hatte, nicht zu wollen, dass Benutzer dieses Objekt selbst erstellen können (die gesamte Hydratation der Daten stammte aus der Datenbank, sodass ich alle Konstruktionen intern absperren konnte). Dies erlaubte mir, einen privaten Konstruktor und das folgende Muster zu verwenden;

    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
quelle
2

Sie können einen Builder-ähnlichen Ansatz verwenden. Abhängig von der Komplexität Ihrer DoSomeActionMethode kann dies jedoch ein Touch-Schwergewicht sein. Etwas in diese Richtung:

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
quelle
Ah, Marto hat mich auf den Vorschlag des Bauherren geschlagen!
Chris White
2

Zusätzlich zur Manji-Antwort möchten Sie möglicherweise auch eine Operation in mehrere kleinere aufteilen. Vergleichen Sie:

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

und

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

Für diejenigen, die POSIX nicht kennen, kann die Erstellung eines Kindes so einfach sein wie:

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

Jede Designauswahl stellt einen Kompromiss dar, welche Operationen sie ausführen kann.

PS. Ja - es ist ähnlich wie beim Builder - nur in umgekehrter Reihenfolge (dh auf der Angerufenen-Seite anstelle des Anrufers). In diesem speziellen Fall kann es besser sein als der Builder.

Maciej Piechotka
quelle
Es ist eine kleine Operation, aber das ist eine gute Empfehlung für alle, die gegen das Prinzip der Einzelverantwortung verstoßen. Meine Frage wird gestellt, nachdem alle anderen Designverbesserungen vorgenommen wurden.
Sedat Kapanoglu
UPS. Entschuldigung - ich habe eine Frage vor Ihrer Bearbeitung vorbereitet und später veröffentlicht - daher hatte ich sie nicht bemerkt.
Maciej Piechotka
2

Hier ist etwas anderes als bei Mikeys, aber ich versuche, das Ganze so wenig wie möglich zu schreiben

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

Die DoSomeActionParameters sind unveränderlich und können nicht direkt erstellt werden, da ihr Standardkonstruktor privat ist

Der Initialisierer ist nicht unveränderlich, sondern nur ein Transport

Die Verwendung nutzt den Initialisierer auf dem Initialisierer (wenn Sie meine Drift erhalten) und ich kann Standardeinstellungen im Standardkonstruktor des Initialisierers haben

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

Die Parameter sind hier optional. Wenn Sie möchten, dass einige erforderlich sind, können Sie sie in den Initializer-Standardkonstruktor einfügen

Die Validierung kann in der Create-Methode erfolgen

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

So sieht es jetzt aus

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

Ich weiß, dass es immer noch ein kleiner Kooki ist, aber ich werde es trotzdem versuchen

Bearbeiten: Wenn Sie die Methode create in eine statische Datei im Parameterobjekt verschieben und einen Delegaten hinzufügen, der den Initialisierer übergibt, wird der Aufruf etwas 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);
    }
}

Der Anruf sieht jetzt so aus

DoSomeAction(
        DoSomeActionParameters.Create(
            i => {
                i.A = "Hello";
            })
        );
Anthony Johnston
quelle
1

Verwenden Sie die Struktur, haben Sie jedoch anstelle von öffentlichen Feldern öffentliche Eigenschaften:

• Alle (einschließlich FXCop & Jon Skeet) sind sich einig, dass die Offenlegung öffentlicher Felder schlecht ist.

Jon und FXCop werden zufrieden sein, weil Sie Eigenschaften und keine Felder verfügbar machen.

• Eric Lippert et al. Sagen, dass es eine Lüge ist, sich für die Unveränderlichkeit auf schreibgeschützte Felder zu verlassen.

Eric wird zufrieden sein, da Sie mithilfe von Eigenschaften sicherstellen können, dass der Wert nur einmal festgelegt wird.

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

Ein halb unveränderliches Objekt (Wert kann gesetzt, aber nicht geändert werden). Funktioniert für Wert- und Referenztypen.

jmoreno
quelle
+1 Möglicherweise könnten private schreibgeschützte Felder und Nur-Getter-Eigenschaften in einer Struktur kombiniert werden, um eine weniger ausführliche Lösung zu ermöglichen.
Sedat Kapanoglu
Öffentliche Lese- / Schreibeigenschaften für mutierte Strukturen thissind weitaus böser als öffentliche Felder. Ich bin mir nicht sicher, warum Sie denken, dass das Festlegen des Werts die Dinge "in Ordnung" macht. Wenn man mit einer Standardinstanz einer Struktur beginnt, thisbleiben die Probleme, die mit -mutierenden Eigenschaften verbunden sind, bestehen.
Supercat
0

Eine Variante von Samuels Antwort , die ich in meinem Projekt verwendet habe, als ich das gleiche Problem hatte:

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

Und zu verwenden:

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

In meinem Fall waren die Parameter absichtlich veränderbar, da die Setter-Methoden nicht alle möglichen Kombinationen zuließen und nur gemeinsame Kombinationen davon enthüllten. Das liegt daran, dass einige meiner Parameter ziemlich komplex waren und Schreibmethoden für alle möglichen Fälle schwierig und unnötig gewesen wären (verrückte Kombinationen werden selten verwendet).

Vilx-
quelle
Dies ist unabhängig davon eine veränderbare Klasse.
Sedat Kapanoglu
@ssg - Nun ja, das ist es. Das DoMagichat in seinem Vertrag allerdings, dass es das Objekt nicht modifizieren wird. Aber ich denke, es schützt nicht vor versehentlichen Änderungen.
Vilx