Quando ir Fluente em C #?

76

Em muitos aspectos eu realmente gosto da idéia de interfaces fluentes, mas com todos os recursos modernos do C # (inicializadores, lambdas, parâmetros nomeados) eu me pego pensando, "vale a pena?", e "É este o padrão certo para usar? Alguém poderia me dar, se não uma prática aceita, pelo menos sua própria experiência ou matriz de decisão para quando usar o padrão Fluente?

Conclusão:

Algumas boas regras das respostas até agora:

  • Interfaces fluentes ajudam muito quando você tem mais ações que setters, já que as chamadas se beneficiam mais da passagem de contexto.
  • Interfaces fluentes devem ser consideradas como uma camada por cima de uma API, não o único meio de uso.
  • Os recursos modernos, como lambdas, inicializadores e parâmetros nomeados, podem trabalhar juntos para tornar a interface fluente ainda mais amigável.

Aqui está um exemplo do que quero dizer com os recursos modernos, fazendo com que pareça menos necessário. Tomemos por exemplo um (talvez pobre exemplo) interface fluente que me permite criar um funcionário como:

Employees.CreateNew().WithFirstName("Peter")
                     .WithLastName("Gibbons")
                     .WithManager()
                          .WithFirstName("Bill")
                          .WithLastName("Lumbergh")
                          .WithTitle("Manager")
                          .WithDepartment("Y2K");

Poderia ser escrito facilmente com inicializadores como:

Employees.Add(new Employee()
              {
                  FirstName = "Peter",
                  LastName = "Gibbons",
                  Manager = new Employee()
                            {
                                 FirstName = "Bill",
                                 LastName = "Lumbergh",
                                 Title = "Manager",
                                 Department = "Y2K"
                            }
              });

Eu também poderia ter usado parâmetros nomeados nos construtores neste exemplo.

    
por Andrew Hanlon 19.04.2011 / 10:44
fonte

6 respostas

26

Escrevendo uma interface fluente (eu tenho misturado com ele) exige mais esforço, mas tem um retorno porque se você fizer certo, a intenção do código de usuário resultante é mais óbvio. É essencialmente uma forma de linguagem específica de domínio.

Em outras palavras, se seu código é lido muito mais do que está escrito (e qual código não é?), então você deve considerar criar uma interface fluente.

Interfaces fluentes são mais sobre contexto e são muito mais do que apenas maneiras de configurar objetos. Como você pode ver no link acima, usei uma API fluente para obter:

  1. Contexto (assim, quando você costuma executar muitas ações em uma sequência com a mesma coisa, pode encadear as ações sem precisar declarar seu contexto repetidamente).
  2. Detectabilidade (quando você vai para objectA. , então o intellisense lhe dá muitas dicas. No meu caso acima, plm.Led. te dá todas as opções para controlar o LED embutido, e plm.Network. te dá as coisas que você pode fazer com a interface de rede. plm.Network.X10. fornece o subconjunto de ações de rede para dispositivos X 10. Você não obterá isso com inicializadores de construtor (a menos que você queira construir um objeto para cada tipo diferente de ação, o que não é idiomática).
  3. Reflexão (não usada no exemplo acima) - a capacidade de usar a expressão LINQ e manipulá-la é uma ferramenta muito poderosa, particularmente em algumas APIs auxiliares criadas para testes de unidade. Eu posso passar uma expressão getter de propriedade, construir um monte de expressões úteis, compilar e executá-las, ou até mesmo usar o getter de propriedade para configurar meu contexto.

Uma coisa que normalmente faço é:

test.Property(t => t.SomeProperty)
    .InitializedTo(string.Empty)
    .CantBeNull() // tries to set to null and Asserts ArgumentNullException
    .YaddaYadda();

Eu não vejo como você pode fazer algo assim sem uma interface fluente.

Editar 2 : Você também pode fazer melhorias de legibilidade realmente interessantes, como:

test.ListProperty(t => t.MyList)
    .ShouldHave(18).Items()
    .AndThenAfter(t => testAddingItemToList(t))
    .ShouldHave(19).Items();
    
por 19.04.2011 / 15:30
fonte
23

Scott Hanselman fala sobre isso no Episódio 260 de seu podcast Hanselminutes com Jonathan Carter. Eles explicam que uma interface fluente é mais parecida com uma interface do usuário em uma API. Você não deve fornecer uma interface fluente como o único ponto de acesso, mas fornecer como uma espécie de interface de usuário de código no topo da "interface normal da API".

Jonathan Carter também fala um pouco sobre o design da API em seu blog .

    
por 19.04.2011 / 15:28
fonte
14

Estou um pouco atrasado, mas pensei em avaliar isso ...

Interfaces fluentes são recursos muito poderosos para fornecer dentro do contexto do seu código, quando usados com o raciocínio "correto".

Se o seu objetivo é simplesmente criar cadeias de código de uma linha como uma espécie de pseudo-caixa-preta, então provavelmente você está latindo na árvore errada. Se, por outro lado, você estiver usando isso para agregar valor à interface da API, fornecendo meios para encadear chamadas de método e melhorar a legibilidade do código, então, com muito planejamento e esforço, acho que o esforço vale a pena.

Eu evitaria seguir o que parece se tornar um "padrão" comum ao criar interfaces fluentes, onde você nomeia todos os seus métodos fluentes "com" algo, já que rouba uma interface de API potencialmente boa de seu contexto e, portanto seu valor intrínseco.

A chave é pensar na sintaxe fluente como uma implementação específica de uma linguagem específica do domínio. Como um bom exemplo do que estou falando, dê uma olhada no StoryQ, que emprega fluência como meio de expressar uma DSL de uma maneira muito valiosa e flexível.

    
por 24.11.2011 / 07:23
fonte
5

Nota inicial: Estou discordando de uma suposição na pergunta e extraio minhas conclusões específicas (no final deste post) sobre isso. Como isso provavelmente não contribui para uma resposta completa e abrangente, estou marcando isso como CW.

Employees.CreateNew().WithFirstName("Peter")…

Could easily be written with initializers like:

Employees.Add(new Employee() { FirstName = "Peter", … });

A meu ver, essas duas versões devem significar e fazer coisas diferentes.

  • Ao contrário da versão não fluente, a versão fluente oculta o fato de que o novo Employee também é Add ed para a coleção Employees - ele sugere apenas que um novo objeto é Create d .

  • O significado de ….WithX(…) é ambíguo, especialmente para pessoas que estão chegando de F #, que tem uma palavra-chave with para expressões de objetos : eles podem interpretar obj.WithX(x) como um objeto novo sendo derivado de obj que é idêntico a obj exceto sua propriedade X , cujo valor é x . Por outro lado, com a segunda versão, fica claro que nenhum objeto derivado é criado e que todas as propriedades são especificadas para o objeto original.

….WithManager().With…
  • Esse ….With… tem outro significado: alternar o "foco" da inicialização da propriedade para um objeto diferente. O fato de sua API fluente ter dois significados diferentes para With está dificultando a interpretação correta do que está acontecendo… e é por isso que você usou o recuo em seu exemplo para demonstrar o significado pretendido desse código. Seria mais claro assim:

    (employee).WithManager(Managers.CreateNew().WithFirstName("Bill").…)
    //                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    //                     value of the 'Manager' property appears inside the parentheses,
    //                     like with 'WithName', where the 'Name' is also inside the parentheses 
    

Conclusões: "Ocultar" um recurso de idioma simples, new T { X = x } , com uma API fluente ( Ts.CreateNew().WithX(x) ) pode obviamente ser feito, mas:

  1. É preciso ter cuidado para que os leitores do código fluente resultante ainda compreendam exatamente o que faz. Ou seja, a API fluente deve ser transparente e inequívoca. Projetar uma API desse tipo pode ser mais trabalhoso do que o previsto (pode ser necessário testá-la quanto à facilidade de uso e aceitação) e / ou…

  2. projetar pode ser mais trabalho do que o necessário: neste exemplo, a API fluente adiciona muito pouco "conforto do usuário" à API subjacente (um recurso de linguagem). Pode-se dizer que uma API fluente deve tornar o recurso de linguagem / API subjacente "mais fácil de usar"; isto é, deve poupar ao programador uma quantidade considerável de esforço. Se é apenas outra maneira de escrever a mesma coisa, provavelmente não vale a pena, porque não facilita a vida do programador, mas apenas dificulta o trabalho do designer (veja a conclusão 1 acima).

  3. Ambos os pontos acima assumem silenciosamente que a API fluente é uma camada sobre um recurso de idioma ou API existente. Essa suposição pode ser outra boa diretriz: uma API fluente pode ser uma maneira extra de fazer algo, não o único caminho. Ou seja, pode ser uma boa ideia oferecer uma API fluente como uma opção "opcional".

por 10.08.2013 / 19:25
fonte
2

Eu gosto do estilo fluente, ele expressa a intenção muito claramente. Com o exemplo de initaliser de objeto que você tem depois, você tem que ter setters de propriedade pública para usar essa sintaxe, você não faz com o estilo fluente. Dizendo isso, com o seu exemplo, você não ganha muito em relação aos setters públicos porque você está quase perdendo um método de estilo set / get em estilo java.

O que me leva ao segundo ponto, eu não tenho certeza se eu usaria o estilo fluente da maneira que você tem, com muitos setters de propriedade, eu provavelmente usaria a segunda versão para isso, eu acho melhor quando você tem muitos verbos para encadear juntos, ou pelo menos muitos feitos em vez de configurações.

    
por 19.04.2011 / 16:27
fonte
1

Eu não estava familiarizado com o termo interface fluente , mas isso me lembra algumas APIs que eu usado incluindo LINQ .

Pessoalmente, não vejo como os recursos modernos do C # impediriam a utilidade de tal abordagem. Eu prefiro dizer que eles andam de mãos dadas. Por exemplo. É ainda mais fácil alcançar essa interface usando métodos de extensão .

Talvez esclareça sua resposta com um exemplo concreto de como uma interface fluente pode ser substituída usando um dos recursos modernos que você mencionou.

    
por 19.04.2011 / 15:22
fonte

Tags