Come evitare il problema "troppi parametri" nella progettazione dell'API?


160

Ho questa funzione API:

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

Non mi piace Perché l'ordine dei parametri diventa inutilmente significativo. Diventa più difficile aggiungere nuovi campi. È più difficile vedere cosa succede. È più difficile refactoring del metodo in parti più piccole perché crea un altro sovraccarico di passaggio di tutti i parametri nelle funzioni secondarie. Il codice è più difficile da leggere.

Mi è venuta l'idea più ovvia: avere un oggetto che incapsula i dati e passarli invece di passare ogni parametro uno per uno. Ecco cosa mi è venuto in mente:

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

Ciò ha ridotto la mia dichiarazione API a:

public ResultEnum DoSomeAction(DoSomeActionParameters parameters, out Guid code)

Bello. Sembra molto innocente, ma in realtà abbiamo introdotto un enorme cambiamento: abbiamo introdotto la mutabilità. Perché quello che avevamo precedentemente fatto era in realtà passare un oggetto immutabile anonimo: parametri di funzione sullo stack. Ora abbiamo creato una nuova classe che è molto mutevole. Abbiamo creato la capacità di manipolare lo stato del chiamante . Che schifo Ora voglio il mio oggetto immutabile, cosa devo fare?

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

Come puoi vedere, ho ricreato il mio problema originale: troppi parametri. È ovvio che non è questa la strada da percorrere. Ciò che mi accingo a fare? L'ultima opzione per ottenere tale immutabilità è usare una struttura "di sola lettura" come questa:

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

Ciò ci consente di evitare costruttori con troppi parametri e raggiungere l'immutabilità. In realtà risolve tutti i problemi (ordinamento dei parametri ecc.). Ancora:

In quel momento mi sono confuso e ho deciso di scrivere questa domanda: qual è il modo più semplice in C # per evitare il problema "troppi parametri" senza introdurre la mutabilità? È possibile utilizzare una struttura di sola lettura a tale scopo e tuttavia non avere una cattiva progettazione API?

PRECISAZIONI:

  • Si prega di supporre che non vi sia violazione del principio di responsabilità singola. Nel mio caso originale, la funzione scrive solo i parametri dati in un singolo record DB.
  • Non sto cercando una soluzione specifica per la data funzione. Sto cercando un approccio generalizzato a tali problemi. Sono particolarmente interessato a risolvere il problema "troppi parametri" senza introdurre mutabilità o un design terribile.

AGGIORNARE

Le risposte fornite qui hanno diversi vantaggi / svantaggi. Pertanto, vorrei convertirlo in un wiki della comunità. Penso che ogni risposta con esempio di codice e pro / contro sarebbe una buona guida per problemi simili in futuro. Ora sto cercando di scoprire come farlo.


Clean Code: A Handbook of Agile Software Artigianato di Robert C. Martin e Martin Fowler Il libro Refactoring copre un po 'questo
argomento

1
Quella soluzione Builder non è ridondante se usi C # 4 dove hai parametri opzionali ?
Khellang,

1
Potrei essere stupido, ma non riesco a vedere come questo sia un problema, considerando che si DoSomeActionParameterstratta di un oggetto usa e getta, che verrà scartato dopo la chiamata del metodo.
Vilx-

6
Nota che non sto dicendo che dovresti evitare i campi di sola lettura in una struttura; le strutture immutabili sono una buona pratica e i campi di sola lettura ti aiutano a creare strutture immutabili autocompensanti. Il mio punto è che non dovresti fare affidamento sul fatto che i campi di sola lettura non cambiano mai , perché non è una garanzia che un campo di sola lettura ti dia in una struttura. Questo è un caso specifico del consiglio più generale di non trattare i tipi di valore come se fossero tipi di riferimento; sono un animale molto diverso.
Eric Lippert,

7
@ssg: anche a me piacerebbe. Abbiamo aggiunto funzionalità a C # che promuovono l'immutabilità (come LINQ) contemporaneamente a funzionalità che promuovono la mutabilità (come gli inizializzatori di oggetti). Sarebbe bello avere una sintassi migliore per promuovere tipi immutabili. Ci stiamo pensando intensamente e abbiamo alcune idee interessanti, ma non mi aspetto nulla del genere per la prossima versione.
Eric Lippert,

Risposte:


22

Uno stile abbracciato nei framework è di solito come raggruppare i parametri correlati in classi correlate (ma ancora una volta problematico con la mutabilità):

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

La condensazione di tutte le stringhe in un array di stringhe ha senso solo se sono tutte correlate. Dall'ordine degli argomenti, sembra che non siano tutti completamente correlati.
icktoofay,

D'accordo, inoltre, funzionerebbe solo se si hanno molti degli stessi tipi di parametro.
Timo Willemsen,

In che modo è diverso dal mio primo esempio nella domanda?
Sedat Kapanoglu,

Bene, questo è il solito stile abbracciato anche in .NET Framework, indicando che il tuo primo esempio è abbastanza giusto in quello che sta facendo anche introducendo piccoli problemi di mutabilità (sebbene questo esempio provenga ovviamente da un'altra libreria). Bella domanda a proposito.
Teoman Soygul,

2
Mi sono avvicinato a questo stile più nel tempo. Una quantità apparentemente significativa dei problemi "troppi parametri" può essere risolta con buoni gruppi logici e astrazioni. Alla fine rende il codice più leggibile e più modulare.
Sedat Kapanoglu,

87

Utilizzare una combinazione di builder e API stile linguaggio specifico del dominio - Fluent Interface. L'API è un po 'più dettagliata ma con intellisense è molto veloce da scrivere e facile da capire.

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

+1 - questo tipo di approccio (che ho visto comunemente chiamato "interfaccia fluente") è esattamente ciò che avevo in mente.
Daniel Pryden,

+1 - questo è quello che sono finito nella stessa situazione. Solo che c'era solo una classe, e ne DoSomeActionera un metodo.
Vilx-

+1 Anch'io ci ho pensato, ma dato che sono nuovo alle interfacce fluenti, non sapevo se fosse perfetto. Grazie per aver convalidato la mia intuizione.
Matt,

Non avevo mai sentito parlare del modello Builder prima di porre questa domanda, quindi questa è stata un'esperienza perspicace per me. Mi chiedo quanto sia comune? Perché qualsiasi sviluppatore che non ha sentito parlare del modello potrebbe avere difficoltà a comprendere l'utilizzo senza una documentazione.
Sedat Kapanoglu,

1
@ssg, è comune in questi scenari. Il BCL ha ad esempio classi di generatori di stringhe di connessione.
Samuel Neff,

10

Quello che hai è un'indicazione abbastanza sicura che la classe in questione stia violando il Principio di Responsabilità Singola perché ha troppe dipendenze. Cerca i modi per trasformare queste dipendenze in gruppi di dipendenze della facciata .


1
Rifletterei ancora introducendo uno o più oggetti parametro, ma ovviamente, se sposti tutti gli argomenti su un singolo tipo immutabile, non hai realizzato nulla. Il trucco è cercare cluster di argomenti che sono più strettamente correlati e quindi refactoring ciascuno di quei cluster in un oggetto parametro separato.
Mark Seemann,

2
Puoi trasformare ogni gruppo in un oggetto immutabile in se stesso. Ogni oggetto dovrebbe solo prendere alcuni parametri, quindi mentre il numero effettivo di argomenti rimane lo stesso, il numero di argomenti usati in ogni singolo costruttore sarà ridotto.
Mark Seemann,

2
+1 @ssg: ogni volta che sono mai riuscito a convincermi di qualcosa del genere, mi sono dimostrato nel tempo sbagliato sbagliando l'astrazione utile dai grandi metodi che richiedono questo livello di parametri. Il libro DDD di Evans potrebbe darti alcune idee su come pensare a questo (anche se il tuo sistema sembra potenzialmente un luogo tutt'altro che rilevante per l'applicazione di tali schemi) - (ed è solo un ottimo libro in entrambi i casi).
Ruben Bartelink,

3
@ Ruben: nessun libro sensato direbbe "in un buon progetto OO una classe non dovrebbe avere più di 5 proprietà". Le classi sono raggruppate logicamente e quel tipo di contestualizzazione non può essere basato su quantità. Tuttavia, il supporto per l'immutabilità di C # inizia a creare problemi in un determinato numero di proprietà prima di iniziare a violare i buoni principi di progettazione OO.
Sedat Kapanoglu,

3
@ Ruben: non ho giudicato la tua conoscenza, attitudine o carattere Mi aspetto che tu faccia lo stesso. Non sto dicendo che i tuoi consigli non sono buoni. Sto dicendo che il mio problema può apparire anche sul design più perfetto e che qui sembra essere un punto trascurato. Anche se capisco perché le persone esperte pongono domande fondamentali sugli errori più comuni, diventa meno piacevole chiarirlo dopo un paio di round. Devo ancora una volta dire che è molto possibile avere quel problema con un design perfetto. E grazie per l'upgrade!
Sedat Kapanoglu,

10

Basta cambiare la struttura dei dati dei parametri da a classa a structe sei a posto.

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) 

Il metodo ora otterrà la propria copia della struttura. Le modifiche apportate alla variabile argomento non possono essere osservate dal metodo e le modifiche apportate dal metodo alla variabile non possono essere osservate dal chiamante. L'isolamento si ottiene senza immutabilità.

Professionisti:

  • Più facile da implementare
  • Minimo cambio di comportamento nella meccanica di base

Contro:

  • L'immutabilità non è ovvia, richiede l'attenzione degli sviluppatori.
  • Copia non necessaria per mantenere l'immutabilità
  • Occupa lo spazio dello stack

+1 Questo è vero, ma non risolve la parte "esporre i campi direttamente è male". Non sono sicuro di come potrebbero peggiorare le cose se scelgo di utilizzare i campi anziché le proprietà su quell'API.
Sedat Kapanoglu,

@ssg - Quindi rendili proprietà pubbliche anziché campi. Se lo consideri come una struttura che non avrà mai un codice, allora non farà molta differenza se usi proprietà o campi. Se decidi di assegnargli un codice (come una convalida o qualcosa del genere), vorrai sicuramente renderli proprietà. Almeno nei settori pubblici, nessuno avrà mai illusioni sugli invarianti esistenti in questa struttura. Dovrà essere convalidato dal metodo esattamente come sarebbero stati i parametri.
Jeffrey L Whitledge,

Oh è vero Eccezionale! Grazie.
Sedat Kapanoglu,

8
Penso che la regola di esposizione-campi-è-male si applica agli oggetti in un design orientato agli oggetti. La struttura che sto proponendo è solo un contenitore di parametri in metallo nudo. Dato che il tuo istinto doveva accompagnare qualcosa di immutabile, penso che un contenitore così basilare potrebbe essere appropriato per questa occasione.
Jeffrey L Whitledge,

1
@JeffreyLWhitledge: Non mi piace l'idea che le strutture a campo esposto siano malvagie. Una simile affermazione mi sembra equivalente a dire che i cacciaviti sono cattivi e che le persone dovrebbero usare i martelli perché le punte dei cacciaviti ammaccano le teste dei chiodi. Se uno ha bisogno di guidare un chiodo, uno dovrebbe usare un martello, ma se uno ha bisogno di guidare una vite, si dovrebbe usare un cacciavite. Ci sono molte situazioni in cui una struttura a campo esposto è esattamente lo strumento giusto per il lavoro. Per inciso, ci sono molti meno casi in cui una struttura con proprietà get / set è davvero lo strumento giusto (nella maggior parte dei casi in cui ...
supercat

6

Che ne dici di creare una classe builder all'interno della tua classe di dati. La classe di dati avrà tutti i setter come privati ​​e solo il builder sarà in grado di impostarli.

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

1
+1 Questa è una soluzione perfettamente valida, ma penso che sia troppo ponteggi per mantenere una decisione di progetto così semplice. Soprattutto quando la soluzione "readonly struct" è "molto" vicina all'ideale.
Sedat Kapanoglu,

2
In che modo questo risolve il problema "troppi parametri"? La sintassi potrebbe essere diversa, ma il problema sembra lo stesso. Questa non è una critica, sono solo curioso perché non ho familiarità con questo schema.
alexD

1
@alexD questo risolve il problema con troppi parametri di funzione e mantenendo immutabile l'oggetto. Solo la classe builder può impostare le proprietà private e una volta ottenuto l'oggetto parametri non è possibile modificarlo. Il problema è che richiede molto codice di ponteggio.
marto

5
L'oggetto parametro nella soluzione non è immutabile. Qualcuno che salva il costruttore può modificare i parametri anche dopo la costruzione
astef

1
Al punto @ astef, poiché è attualmente scritta DoSomeActionParameters.Builderun'istanza può essere utilizzata per creare e configurare esattamente DoSomeActionParametersun'istanza. Dopo aver chiamato le Build()successive modifiche alle Builderproprietà dell ', continuerà a modificare le proprietà dell'istanza originale DoSomeActionParameters e le chiamate successive a Build()continueranno a restituire la stessa DoSomeActionParametersistanza. Dovrebbe essere davvero public DoSomeActionParameters Build() { var oldObj = obj; obj = new DoSomeActionParameters(); return oldObj; }.
BACON,

6

Non sono un programmatore C # ma credo che C # supporti argomenti chiamati: (F # funziona e C # è ampiamente compatibile con le funzionalità per quel tipo di cose) Lo fa: http://msdn.microsoft.com/en-us/library/dd264739 aspx # Y342

Quindi chiamare il tuo codice originale diventa:

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

questo non richiede più spazio / pensiero che la creazione del tuo oggetto e ha tutti i vantaggi, del fatto che non hai affatto cambiato ciò che sta accadendo nel sistema immeriante. Non è nemmeno necessario ricodificare nulla per indicare che gli argomenti sono denominati

Modifica: ecco un articolo artistico che ho trovato al riguardo. http://www.globalnerdy.com/2009/03/12/default-and-named-parameters-in-c-40-sith-lord-in-training/ Dovrei menzionare che C # 4.0 supporta argomenti denominati, 3.0 no


Un miglioramento davvero. Ma questo risolve solo la leggibilità del codice e lo fa solo quando lo sviluppatore opta per l'utilizzo di parametri denominati. È facile per uno dimenticare di specificare i nomi e dare via i benefici. Non aiuta a scrivere il codice stesso. ad es. quando si esegue il refactoring della funzione in più piccoli e si passa i dati in un singolo pacchetto contenuto.
Sedat Kapanoglu,

6

Perché non creare semplicemente un'interfaccia che imponga l'immutabilità (cioè solo i getter)?

È essenzialmente la tua prima soluzione, ma imponi alla funzione di utilizzare l'interfaccia per accedere al parametro.

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

e la dichiarazione di funzione diventa:

public ResultEnum DoSomeAction(IDoSomeActionParameters parameters, out Guid code)

Professionisti:

  • Non ha problemi di spazio nello stack come structsoluzione
  • Soluzione naturale usando la semantica del linguaggio
  • L'immutabilità è ovvia
  • Flessibile (il consumatore può utilizzare una classe diversa se lo desidera)

Contro:

  • Alcuni lavori ripetitivi (stesse dichiarazioni in due entità diverse)
  • Lo sviluppatore deve indovinare che DoSomeActionParametersè una classe a cui è possibile mappareIDoSomeActionParameters

+1 Non so perché non ci abbia pensato? :) Immagino di aver pensato che l'oggetto avrebbe ancora sofferto di un costruttore con troppi parametri, ma non è così. Sì, anche questa è una soluzione molto valida. L'unico problema che mi viene in mente è che non è davvero semplice per l'utente API trovare il nome della classe giusta che supporti l'interfaccia fornita. La soluzione che richiede la minima documentazione è migliore.
Sedat Kapanoglu,

Mi piace questo, la duplicazione e la conoscenza della mappa sono gestite con resharper e posso fornire valori predefiniti usando il costruttore predefinito della classe concreta
Anthony Johnston,

2
Non mi piace questo approccio. Qualcuno che ha un riferimento a un'implementazione arbitraria di IDoSomeActionParameterse vuole catturare i valori in essa contenuti non ha modo di sapere se mantenere il riferimento sarà sufficiente o se deve copiare i valori su qualche altro oggetto. Le interfacce leggibili sono utili in alcuni contesti, ma non come mezzo per rendere immutabili le cose.
supercat

"I parametri IDoSomeActionParameters" possono essere trasmessi a DoSomeActionParameters e modificati. Lo sviluppatore potrebbe anche non rendersi conto che stanno aggirando un tentativo di rendere i parametri imputabili
Joseph Simpson,

3

So che questa è una vecchia domanda, ma ho pensato di essere d'accordo con il mio suggerimento dato che ho dovuto risolvere lo stesso problema. Ora, ammettevo che il mio problema era leggermente diverso dal tuo in quanto avevo il requisito aggiuntivo di non volere che gli utenti fossero in grado di costruire questo oggetto da soli (tutta l'idratazione dei dati proveniva dal database, quindi potevo imprigionare tutta la costruzione internamente). Questo mi ha permesso di usare un costruttore privato e il seguente schema;

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

2

Potresti usare un approccio in stile Builder, anche se a seconda della complessità del tuo DoSomeActionmetodo, questo potrebbe essere un tocco pesante. Qualcosa del genere:

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

Ah, Marto mi ha battuto al suggerimento del costruttore!
Chris White,

2

Oltre alla risposta manji, potresti anche voler dividere un'operazione in più piccole. Confrontare:

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

e

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

Per chi non conosce POSIX la creazione di un bambino può essere semplice come:

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

Ogni scelta di design rappresenta un compromesso rispetto alle operazioni che può svolgere.

PS. Sì - è simile al costruttore - solo al contrario (cioè sul lato chiamante anziché sul chiamante). In questo caso specifico potrebbe essere o meno meglio del costruttore.


È una piccola operazione ma è una buona raccomandazione per chiunque stia violando il principio della responsabilità singola. La mia domanda si svolge subito dopo aver fatto tutti gli altri miglioramenti del design.
Sedat Kapanoglu,

UPS. Siamo spiacenti, ho preparato una domanda prima della modifica e l'ho pubblicata in un secondo momento, quindi non l'avevo notato.
Maciej Piechotka,

2

qui è leggermente diverso da Mikeys, ma quello che sto cercando di fare è rendere tutto il meno possibile da scrivere

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 è immutabile in quanto può essere e non può essere creato direttamente poiché il suo costruttore predefinito è privato

L'inizializzatore non è immutabile, ma solo un trasporto

L'utilizzo sfrutta l'inizializzatore sull'inizializzatore (se ottieni la mia deriva) E posso avere i valori predefiniti nel costruttore predefinito dell'inizializzatore

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

I parametri saranno facoltativi qui, se vuoi che alcuni siano richiesti potresti metterli nel costruttore predefinito di Initializer

E la validazione potrebbe andare nel metodo 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);
    }
}

Quindi ora sembra

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

Conosco ancora un po 'di kooki, ma lo proverò comunque

Modifica: spostando il metodo di creazione su un oggetto statico nell'oggetto parametri e l'aggiunta di un delegato che passa l'inizializzatore elimina un po 'di kookieness dalla chiamata

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

Quindi la chiamata ora appare così

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

1

Utilizzare la struttura, ma invece dei campi pubblici, hanno proprietà pubbliche:

• Tutti (compresi FXCop e Jon Skeet) concordano sul fatto che esporre i campi pubblici sia negativo.

Jon e FXCop saranno soddisfatti perché stai esponendo proprietà non campi.

• Eric Lippert e altri affermano che fare affidamento su campi di sola lettura per l'immutabilità è una bugia.

Eric sarà soddisfatto perché usando le proprietà, puoi assicurarti che il valore sia impostato una sola volta.

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

Un oggetto semi-immutabile (il valore può essere impostato ma non modificato). Funziona con valori e tipi di riferimento.


+1 Forse i campi di sola lettura privati ​​e le proprietà pubbliche solo di getter potrebbero essere combinati in una struttura per consentire una soluzione meno dettagliata.
Sedat Kapanoglu,

Le proprietà pubbliche di lettura / scrittura su strutture che mutano thissono molto più malvagie dei campi pubblici. Non sono sicuro del motivo per cui pensi che l'impostazione del valore una volta renda le cose "ok"; se si inizia con un'istanza predefinita di una struttura, i problemi associati alle thisproprietà mutanti continueranno a esistere.
supercat

0

Una variante della risposta di Samuel che ho usato nel mio progetto quando ho avuto lo stesso problema:

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

E usare:

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

Nel mio caso i parametri erano intenzionalmente modificabili, perché i metodi setter non consentivano tutte le possibili combinazioni, ma ne esponevano semplicemente combinazioni comuni. Questo perché alcuni dei miei parametri erano piuttosto complessi e la scrittura di metodi per tutti i possibili casi sarebbe stata difficile e superflua (le combinazioni folli sono usate raramente).


Questa è una classe mutabile a prescindere.
Sedat Kapanoglu,

@ssg - Beh, sì, lo è. Nel DoMagiccontratto ha tuttavia che non modificherà l'oggetto. Ma suppongo che non protegga da modifiche accidentali.
Vilx-
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.