Como evitar o problema de "muitos parâmetros" no design da API?

160

Eu tenho esta função de API:

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

Eu não gosto disso Porque a ordem dos parâmetros se torna desnecessariamente significativa. Torna-se mais difícil adicionar novos campos. É mais difícil ver o que está sendo repassado. É mais difícil refatorar o método em partes menores, porque isso cria outra sobrecarga na passagem de todos os parâmetros nas sub-funções. O código é mais difícil de ler.

Tive a idéia mais óbvia: ter um objeto encapsulando os dados e repassá-los ao invés de passar cada parâmetro um por um. Aqui está o que eu vim com:

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

Isso reduziu minha declaração da API para:

public ResultEnum DoSomeAction(DoSomeActionParameters parameters, out Guid code)

Agradável. Parece muito inocente, mas na verdade introduzimos uma grande mudança: introduzimos a mutabilidade. Porque o que estávamos fazendo anteriormente era realmente passar um objeto imutável anônimo: parâmetros de função na pilha. Agora criamos uma nova classe que é muito mutável. Criamos a capacidade de manipular o estado do chamador . Isso é péssimo. Agora eu quero meu objeto imutável, o que faço?

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

Como você pode ver, eu realmente recriei meu problema original: muitos parâmetros. É óbvio que esse não é o caminho a seguir. O que eu vou fazer? A última opção para alcançar essa imutabilidade é usar uma estrutura "somente leitura" como esta:

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

Isso nos permite evitar construtores com muitos parâmetros e obter imutabilidade. Na verdade, ele corrige todos os problemas (ordenação de parâmetros etc.). Ainda:

Foi quando fiquei confuso e decidi escrever esta pergunta: Qual é a maneira mais direta em C # de evitar o problema de "muitos parâmetros" sem introduzir mutabilidade? É possível usar uma estrutura somente leitura para esse fim e ainda não ter um design de API ruim?

ESCLARECIMENTOS:

  • Por favor, assuma que não há violação do princípio de responsabilidade única. No meu caso original, a função apenas grava parâmetros fornecidos em um único registro de banco de dados.
  • Não estou procurando uma solução específica para a função especificada. Estou buscando uma abordagem generalizada para esses problemas. Estou especificamente interessado em resolver o problema de "muitos parâmetros" sem introduzir mutabilidade ou um design terrível.

ATUALIZAR

As respostas fornecidas aqui têm diferentes vantagens / desvantagens. Portanto, eu gostaria de converter isso em um wiki da comunidade. Acho que cada resposta com amostra de código e Prós / Contras seria um bom guia para problemas semelhantes no futuro. Agora estou tentando descobrir como fazê-lo.

ssg
fonte
Clean Code: Um Manual de Agile Software Craftsmanship por Robert C. Martin e livro Refactoring do Martin Fowler cobre este um pouco
Ian Ringrose
1
Essa solução do Builder não é redundante se você usa C # 4 em que possui parâmetros opcionais ?
khellang
1
Eu posso ser estúpido, mas não vejo como isso é um problema, considerando que DoSomeActionParametersé um objeto descartável, que será descartado após a chamada do método.
usar o seguinte comando
6
Observe que não estou dizendo que você deve evitar campos somente leitura em uma estrutura; estruturas imutáveis ​​são uma prática recomendada e os campos somente leitura o ajudam a criar estruturas imutáveis ​​auto-documentadas. O que quero dizer é que você não deve confiar na observação de campos somente leitura para nunca mudar , porque isso não garante que um campo somente leitura lhe dê uma estrutura. Este é um caso específico dos conselhos mais gerais de que você não deve tratar os tipos de valor como se fossem tipos de referência; eles são um animal muito diferente.
Eric Lippert
7
@ssg: Eu adoraria isso também. Adicionamos recursos ao C # que promovem a imutabilidade (como LINQ) ao mesmo tempo que adicionamos recursos que promovem a mutabilidade (como inicializadores de objetos). Seria bom ter uma sintaxe melhor para promover tipos imutáveis. Estamos pensando muito sobre isso e temos algumas idéias interessantes, mas eu não esperaria isso na próxima versão.
Eric Lippert

Respostas:

22

Um estilo adotado nas estruturas é geralmente como agrupar parâmetros relacionados em classes relacionadas (mas mais uma vez problemático com mutabilidade):

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
fonte
Condensar todas as strings em uma matriz de strings só faz sentido se todas elas estiverem relacionadas. Da ordem dos argumentos, parece que eles não são todos completamente relacionados.
icktoofay
Além disso, concordou que isso só funcionaria se você tiver muitos dos mesmos tipos que o parâmetro.
Timo Willemsen
Como isso é diferente do meu primeiro exemplo na questão?
Sedat Kapanoglu
Bem, esse é o estilo adotado habitualmente, mesmo no .NET Framework, indicando que seu primeiro exemplo está certo no que está fazendo, mesmo que ele introduza pequenos problemas de mutabilidade (embora este exemplo seja de outra biblioteca, obviamente). Bela pergunta, a propósito.
Teoman Soygul
2
Eu tenho convergido para esse estilo mais ao longo do tempo. Aparentemente, uma quantidade significativa dos problemas de "muitos parâmetros" pode ser resolvida com bons grupos lógicos e abstrações. No final, torna o código mais legível e mais modularizado.
Sedat Kapanoglu
87

Use uma combinação da API do estilo do construtor e do idioma específico do domínio - Interface Fluente. A API é um pouco mais detalhada, mas com o intellisense é muito rápido digitar e fácil de entender.

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
fonte
+1 - esse tipo de abordagem (que eu costumava chamar de "interface fluente") é exatamente o que eu tinha em mente também.
Daniel Pryden
+1 - foi o que acabei na mesma situação. Exceto que havia apenas uma classe e DoSomeActionera um método dela.
Vilx-
+1 Eu também pensei nisso, mas como sou novo em interfaces fluentes, não sabia se era um ajuste perfeito aqui. Obrigado por validar minha própria intuição.
Matt
Eu nunca tinha ouvido falar do padrão Builder antes de fazer essa pergunta, portanto essa foi uma experiência interessante para mim. Eu me pergunto como é comum? Porque qualquer desenvolvedor que não tenha ouvido falar do padrão pode ter problemas para entender o uso sem uma documentação.
Sedat Kapanoglu
1
@ssg, é comum nesses cenários. O BCL possui classes do construtor de cadeias de conexão, por exemplo.
Samuel Neff 07/06
10

O que você tem lá é uma indicação bastante certa de que a classe em questão está violando o Princípio de Responsabilidade Única, porque possui muitas dependências. Procure maneiras de refatorar essas dependências em agrupamentos de dependências de fachada .

Mark Seemann
fonte
1
Eu ainda refatoraria a introdução de um ou mais objetos de parâmetro, mas obviamente, se você mover todos os argumentos para um único tipo imutável, não terá conseguido nada. O truque é procurar clusters de argumentos que estejam mais intimamente relacionados e refatorar cada um desses clusters para um objeto de parâmetro separado.
precisa
2
Você pode transformar cada grupo em um objeto imutável por si só. Cada objeto precisaria apenas de alguns parâmetros, portanto, enquanto o número real de argumentos permanecer o mesmo, o número de argumentos usados ​​em qualquer construtor será reduzido.
Mark Seemann
2
+1 @ssg: Toda vez que eu consegui me convencer de algo assim, eu me provei errado ao longo do tempo, tirando abstrações úteis de métodos grandes que exigem esse nível de parâmetros. O livro DDD de Evans pode lhe dar algumas idéias sobre como pensar sobre isso (embora seu sistema pareça ser um local potencialmente longe de ser relevante para a aplicação de tais padrões) - (e é apenas um ótimo livro de qualquer maneira).
Ruben Bartelink
3
@ Ruben: Nenhum livro sensato diria "em um bom projeto de OO, uma classe não deve ter mais do que 5 propriedades". As classes são agrupadas logicamente e esse tipo de contextualização não pode ser baseado em quantidades. No entanto, o suporte à imutabilidade do C # começa a criar problemas em um certo número de propriedades antes de começarmos a violar os bons princípios de design de OO.
Sedat Kapanoglu
3
@ Ruben: não julguei seu conhecimento, atitude ou temperamento. Eu espero que você faça o mesmo. Não estou dizendo que suas recomendações não são boas. Estou dizendo que meu problema pode aparecer mesmo no design mais perfeito e que parece ser um ponto esquecido aqui. Embora eu entenda por que pessoas experientes fazem perguntas fundamentais sobre os erros mais comuns, fica menos agradável esclarecer isso depois de algumas rodadas. Devo dizer novamente que é muito possível ter esse problema com um design perfeito. E obrigado pelo voto!
Sedat Kapanoglu
10

Basta alterar sua estrutura de dados de parâmetro de a classpara structe você estará pronto.

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) 

O método agora terá sua própria cópia da estrutura. As alterações feitas na variável de argumento não podem ser observadas pelo método, e as alterações feitas pelo método na variável não podem ser observadas pelo chamador. O isolamento é alcançado sem imutabilidade.

Prós:

  • Mais fácil de implementar
  • Menos mudança de comportamento na mecânica subjacente

Contras:

  • A imutabilidade não é óbvia, requer atenção do desenvolvedor.
  • Cópia desnecessária para manter a imutabilidade
  • Ocupa o espaço da pilha
Jeffrey L Whitledge
fonte
+1 Isso é verdade, mas não resolve a parte de "expor os campos diretamente é mau". Não tenho certeza de como as coisas piorariam se eu optar por usar campos em vez de propriedades nessa API.
Sedat Kapanoglu
@ssg - Torne-os propriedades públicas em vez de campos. Se você tratar isso como uma estrutura que nunca terá código, não fará muita diferença se você usa propriedades ou campos. Se você decidir fornecer um código (como validação ou algo assim), definitivamente desejará torná-los propriedades. Pelo menos nos campos públicos, ninguém jamais terá ilusões sobre os invariantes existentes nessa estrutura. Terá que ser validado pelo método exatamente como os parâmetros teriam sido.
Jeffrey L Whitledge
Oh, isso é verdade. Impressionante! Obrigado.
Sedat Kapanoglu
8
Penso que a regra de expor-campos-é-mau se aplica a objetos em um design orientado a objetos. A estrutura que estou propondo é apenas um contêiner de parâmetros simples. Como seus instintos eram imutáveis, acho que um recipiente tão básico pode ser apropriado para esta ocasião.
Jeffrey L Whitledge
1
@JeffreyLWhitledge: Eu realmente não gosto da idéia de que estruturas de campo exposto são más. Essa afirmação me parece equivalente a dizer que as chaves de fenda são más e as pessoas devem usar martelos, porque as pontas das chaves de fenda afiam as cabeças das unhas. Se for necessário acionar um prego, deve-se usar um martelo, mas se for necessário acionar um parafuso, deve-se usar uma chave de fenda. Existem muitas situações em que uma estrutura de campo exposto é precisamente a ferramenta certa para o trabalho. Aliás, há muito menos em que uma estrutura com propriedades get / set é realmente a ferramenta certa (na maioria dos casos em que ... #
3013
6

Que tal criar uma classe de construtor dentro de sua classe de dados. A classe de dados terá todos os configuradores como privados e somente o construtor poderá configurá-los.

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
fonte
1
+1 Essa é uma solução perfeitamente válida, mas acho que é demais o andaime para manter uma decisão de design tão simples. Especialmente quando a solução "readonly struct" está "muito" próxima do ideal.
Sedat Kapanoglu
2
Como isso resolve o problema de "muitos parâmetros"? A sintaxe pode ser diferente, mas o problema parece o mesmo. Isso não é uma crítica, estou apenas curioso porque não estou familiarizado com esse padrão.
alexD
1
@alexD isso resolve o problema de ter muitos parâmetros de função e manter o objeto imutável. Somente a classe do construtor pode definir as propriedades particulares e, depois de obter o objeto de parâmetros, não é possível alterá-lo. O problema é que exige muito código de andaime.
Marto
5
O objeto de parâmetro na sua solução não é imutável. Alguém que guarda os parâmetros construtor pode editar mesmo depois de construir
astef
1
Ao ponto do @ astef, como está atualmente escrito, uma DoSomeActionParameters.Builderinstância pode ser usada para criar e configurar exatamente uma DoSomeActionParametersinstância. Após chamar Build()as alterações subseqüentes nas Builderpropriedades da, continuará modificando as propriedades da instância original DoSomeActionParameters e as chamadas subseqüentes para Build()continuarão retornando a mesma DoSomeActionParametersinstância. Deveria mesmo ser public DoSomeActionParameters Build() { var oldObj = obj; obj = new DoSomeActionParameters(); return oldObj; }.
BACON
6

Eu não sou um programador de C #, mas acredito que o C # suporte argumentos nomeados: (o F # faz e o C # é amplamente compatível com esse tipo de coisa) Ele faz: http://msdn.microsoft.com/en-us/library/dd264739 .aspx # Y342

Assim, chamar seu código original se torna:

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

isso não ocupa mais espaço / pensamento de que o seu objeto é criado e possui todos os benefícios, do fato de você não ter alterado o que está acontecendo no sistema não-subjacente. Você nem precisa recodificar nada para indicar que os argumentos são nomeados

Edit: aqui está um artigo que eu encontrei sobre ele. http://www.globalnerdy.com/2009/03/12/default-and-named-parameters-in-c-40-sith-lord-in-training/ Eu devo mencionar que o C # 4.0 suporta argumentos nomeados, o 3.0 não

Lyndon White
fonte
Uma melhoria de fato. Mas isso apenas resolve a legibilidade do código e somente quando o desenvolvedor opta por usar parâmetros nomeados. É fácil esquecer de especificar nomes e divulgar os benefícios. Não ajuda ao escrever o código em si. por exemplo, ao refatorar a função para outras menores e passar os dados em um único pacote contido.
Sedat Kapanoglu
6

Por que não fazer apenas uma interface que imponha imutabilidade (ou seja, apenas getters)?

É essencialmente sua primeira solução, mas você força a função a usar a interface para acessar o parâmetro.

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 a declaração da função se torna:

public ResultEnum DoSomeAction(IDoSomeActionParameters parameters, out Guid code)

Prós:

  • Não tem problema de espaço na pilha como struct solução
  • Solução natural usando semântica de linguagem
  • Imutabilidade é óbvia
  • Flexível (o consumidor pode usar uma classe diferente, se quiser)

Contras:

  • Algum trabalho repetitivo (mesmas declarações em duas entidades diferentes)
  • O desenvolvedor precisa adivinhar que DoSomeActionParametersé uma classe que pode ser mapeada paraIDoSomeActionParameters
verdade
fonte
+1 Não sei por que não pensei nisso? :) Acho que pensei que o objeto ainda sofreria de um construtor com muitos parâmetros, mas esse não é o caso. Sim, essa é uma solução muito válida também. O único problema que consigo pensar é que não é realmente simples para o usuário da API encontrar o nome de classe certo que suporta a interface fornecida. Solução que requer menos documentação é melhor.
Sedat Kapanoglu
I como este, a duplicação e conhecimento do mapa são tratados com ReSharper, e eu posso fornecer valores padrão usando o construtor padrão da classe concreta
Anthony Johnston
2
Eu não gosto dessa abordagem. Alguém que tem uma referência a uma implementação arbitrária IDoSomeActionParameterse deseja capturar os valores nela não tem como saber se manter a referência será suficiente ou se deve copiar os valores para outro objeto. Interfaces legíveis são úteis em alguns contextos, mas não como um meio de tornar as coisas imutáveis.
Supercat 25/09/12
"Parâmetros IDoSomeActionParameters" podem ser convertidos para DoSomeActionParameters e alterados. O desenvolvedor não podem sequer perceber que eles estão a evadir uma tentativa de fazer parâmetros imutable
Joseph Simpson
3

Sei que essa é uma pergunta antiga, mas pensei em entrar na minha sugestão, pois tinha que resolver o mesmo problema. Agora, é certo que meu problema era um pouco diferente do seu, pois eu tinha o requisito adicional de não desejar que os usuários pudessem construir esse objeto eles mesmos (toda a hidratação dos dados veio do banco de dados, para que eu pudesse isolar toda a construção internamente). Isso me permitiu usar um construtor privado e o seguinte padrão;

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

Você pode usar uma abordagem no estilo Builder, embora, dependendo da complexidade do seu DoSomeActionmétodo, este possa ser um pouco pesado. Algo nesse sentido:

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
fonte
Ah, marto me venceu com a sugestão do construtor!
Chris White
2

Além da resposta do manji - você também pode dividir uma operação em várias menores. Comparar:

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

Para quem não conhece o POSIX, a criação do filho pode ser tão fácil quanto:

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

Cada opção de design representa uma troca sobre quais operações ela pode realizar.

PS. Sim - é semelhante ao construtor - somente no sentido inverso (ou seja, no lado chamado em vez de no chamador). Pode ou não ser melhor do que o construtor nesse caso específico.

Maciej Piechotka
fonte
É uma operação pequena, mas é uma boa recomendação para quem está violando o princípio de responsabilidade única. Minha pergunta ocorre logo após todas as outras melhorias de design.
Sedat Kapanoglu
Ups. Desculpe - preparei uma pergunta antes da sua edição e publiquei mais tarde - por isso não a notei.
Maciej Piechotka
2

aqui está um pouco diferente de Mikeys, mas o que estou tentando fazer é tornar a coisa toda o mínimo possível para escrever

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

O DoSomeActionParameters é imutável como pode e não pode ser criado diretamente, pois seu construtor padrão é privado

O inicializador não é imutável, mas apenas um transporte

O uso tira proveito do inicializador no Inicializador (se você entender meu desvio). E eu posso ter padrões no construtor padrão do Inicializador.

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

Os parâmetros serão opcionais aqui, se você quiser que alguns sejam necessários, você pode colocá-los no construtor padrão Initializer

E a validação pode ir no método 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);
    }
}

Então agora parece

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

Ainda um pouco maluco, eu sei, mas vou tentar mesmo assim

Editar: mover o método create para uma estática no objeto de parâmetros e adicionar um delegado que passa no inicializador retira parte da chamada da chamada

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

Portanto, a ligação agora se parece com isso

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

Use a estrutura, mas em vez de campos públicos, tenha propriedades públicas:

• Todos (incluindo FXCop e Jon Skeet) concordam que a exposição de campos públicos é ruim.

Jon e FXCop ficarão satisfeitos porque você está expondo propriedades, não campos.

• Eric Lippert e colaboradores dizem que confiar em campos somente leitura para imutabilidade é uma mentira.

Eric ficará satisfeito porque, ao usar propriedades, você pode garantir que o valor seja definido apenas uma vez.

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

Um objeto semi-imutável (o valor pode ser definido, mas não alterado). Funciona para tipos de valor e referência.

jmoreno
fonte
+1 Talvez campos privados somente de leitura e propriedades públicas somente para getter possam ser combinados em uma estrutura para permitir uma solução menos detalhada.
Sedat Kapanoglu
As propriedades públicas de leitura e gravação em estruturas que sofrem mutação thissão muito mais más que os campos públicos. Não sei por que você acha que apenas definir o valor uma vez torna as coisas "corretas"; se alguém começar com uma instância padrão de uma estrutura, os problemas associados às thispropriedades -mutating ainda existirão.
Supercat 25/09/12
0

Uma variante da resposta de Samuel que usei no meu projeto quando tive o mesmo 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 para usar:

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

No meu caso, os parâmetros foram intencionalmente modificáveis, porque os métodos setter não permitiram todas as combinações possíveis e apenas expuseram combinações comuns deles. Isso porque alguns dos meus parâmetros eram bastante complexos e os métodos de escrita para todos os casos possíveis teriam sido difíceis e desnecessários (combinações malucas raramente são usadas).

Vilx-
fonte
Esta é uma classe mutável, independentemente.
Sedat Kapanoglu 6/06/11
@ssg - Bem, sim, é. O DoMagiccontrato possui no entanto que ele não modificará o objeto. Mas acho que não protege contra modificações acidentais.
Vilx-