Quando ir fluente em c #?

78

Em muitos aspectos, eu realmente gosto da idéia de interfaces Fluent, mas com todos os recursos modernos de C # (inicializadores, lambdas, parâmetros nomeados), eu me pego pensando: "vale a pena?" E "Esse é 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 sobre quando usar o padrão Fluente?

Conclusão:

Algumas boas regras práticas das respostas até agora:

  • As interfaces fluentes ajudam muito quando você tem mais ações do que setters, pois as chamadas se beneficiam mais com a passagem do contexto.
  • As interfaces fluentes devem ser pensadas como uma camada sobre uma API, não o único meio de uso.
  • Os recursos modernos, como lambdas, inicializadores e parâmetros nomeados, podem trabalhar lado a lado para tornar uma interface fluente ainda mais amigável.

Aqui está um exemplo do que quero dizer com os recursos modernos, tornando-o menos necessário. Tomemos, por exemplo, uma interface fluente (talvez um exemplo ruim) que me permita criar um Funcionário como:

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

Pode ser facilmente escrito 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.

Andrew Hanlon
fonte
1
Boa pergunta, mas eu acho que é mais uma questão wiki
Ivo
Sua pergunta está marcada com "fluente-inibidor". Então, você está tentando decidir se cria uma interface fluente ou se usa nhibernate fluente versus configuração XML?
Ilya Kogan
1
Votado para migrar para o programador.SE
Matt Ellen
@Ilya Kogan, acho que está realmente marcado como "interface fluente", que é uma tag genérica para o padrão de interface fluente. Esta questão não é em relação ao nhibernate, mas como você disse apenas se deve criar uma interface fluente. Obrigado.
1
Este post me inspirou a pensar em uma maneira de usar esse padrão em C. Minha tentativa pode ser encontrada no site irmão da Code Review .
Otto

Respostas:

28

Escrever uma interface fluente (eu já brinquei com ela) exige mais esforço, mas tem uma recompensa, porque se você fizer certo, a intenção do código de usuário resultante é mais óbvia. É essencialmente uma forma de idioma específico do domínio.

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

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

  1. Contexto (portanto, quando você normalmente executa muitas ações em uma sequência com a mesma coisa, pode encadear as ações sem precisar declarar seu contexto repetidamente).
  2. Descoberta (quando você vai para o objectA.intellisense, fornece várias dicas. No meu caso acima, plm.Led.fornece todas as opções para controlar o LED embutido e plm.Network.fornece o que você pode fazer com a interface de rede. plm.Network.X10.Fornece o subconjunto de ações de rede para dispositivos X10. Você não conseguirá isso com inicializadores de construtor (a menos que deseje construir um objeto para cada tipo de ação diferente, o que não é idiomático).
  3. Reflexão (não usada no exemplo acima) - a capacidade de obter uma expressão LINQ aprovada e manipulá-la é uma ferramenta muito poderosa, particularmente em algumas APIs auxiliares que eu construí para testes de unidade. Posso passar uma expressão getter de propriedades, criar um monte de expressões úteis, compilar e executá-las ou até mesmo usar o getter de propriedades 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();

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

Edit 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();
Scott Whitlock
fonte
Obrigado pela resposta, no entanto, estou ciente do motivo para usar o Fluent, mas estou procurando um motivo mais concreto para usá-lo sobre algo como o meu novo exemplo acima.
21711 Andrew Hanlon
Obrigado pela resposta estendida. Eu acho que você delineou duas boas regras práticas: 1) Use o Fluent quando tiver muitas chamadas que se beneficiam do "repasse" do contexto. 2) Pense no Fluent quando tiver mais chamadas do que setters.
21411 Andrew Hanlon
2
@ach, eu não vejo nada nesta resposta sobre "mais chamadas do que setters". Você está confuso com a afirmação dele sobre "o código [que] é lido muito mais do que está escrito"? Isso não é sobre getters / setters de propriedades, é sobre seres humanos lendo o código vs. seres humanos escrevendo o código - sobre tornar o código mais fácil para os humanos lerem, porque normalmente lemos uma determinada linha de código com muito mais frequência do que a modificamos.
Joe White
@ Joe White, talvez eu deva reformular meu termo 'chamada' para 'ação'. Mas a ideia ainda permanece. Obrigado.
Andrew Hanlon
Reflexão para testar é ruim!
Adronius 31/01
24

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

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

Kristof Claes
fonte
Muito obrigado pelos links de informações e a interface do usuário no topo da API é uma boa maneira de ver isso.
21711 Andrew Hanlon
14

Interfaces fluentes são recursos muito poderosos para fornecer dentro do contexto do seu código, quando você usa o raciocínio "certo".

Se seu objetivo é simplesmente criar enormes cadeias de códigos de uma linha como uma espécie de pseudo-caixa preta, provavelmente você está latindo na árvore errada. Se, por outro lado, você o estiver usando para agregar valor à sua interface de API, fornecendo um meio de encadear chamadas de método e melhorar a legibilidade do código, então, com muito bom planejamento e esforço, acho que 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, pois rouba uma interface 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 um meio de expressar uma DSL de uma maneira muito valiosa e flexível.

S.Robins
fonte
Obrigado pela resposta, nunca é tarde para dar uma resposta bem pensada.
Andrew Hanlon
Eu não me importo com o prefixo 'with' para métodos. Ele os diferencia de outros métodos que não retornam um objeto para encadeamento. Por exemplo, position.withX(5)versusposition.getDistanceToOrigin()
LegendLength
5

Nota inicial: Estou questionando uma suposição na pergunta e tiro minhas conclusões específicas (no final deste post). Como isso provavelmente não resulta em uma resposta abrangente e abrangente, estou marcando isso como CW.

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

Pode ser facilmente escrito com inicializadores como:

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

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

  • Diferentemente da versão não fluente, a versão fluente oculta o fato de que o novo Employeetambém é Addeditado para a coleção Employees- apenas sugere que um novo objeto é Created.

  • O significado de ….WithX(…)é ambíguo, especialmente para pessoas provenientes de F #, que possui uma withpalavra - chave para expressões de objeto : elas podem interpretar obj.WithX(x)como um novo objeto derivado objidêntico a, objexceto por sua Xpropriedade, 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
  • Isso ….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 Withdificulta a interpretação correta do que está acontecendo ... e talvez por isso você tenha usado o recuo no 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 linguagem simples o suficiente new T { X = x }, com uma API fluente ( Ts.CreateNew().WithX(x)) pode obviamente ser feito, mas:

  1. Deve-se tomar cuidado para que os leitores do código fluente resultante ainda entendam exatamente o que ele faz. Ou seja, a API fluente deve ter um significado transparente e inequívoco. Criar uma API desse tipo pode ser mais trabalhoso do que o esperado (pode ser necessário testá-lo para facilitar o uso e a aceitação) e / ou…

  2. projetar pode ser mais trabalhoso 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 API / idioma 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 nº 1 logo acima).

  3. Os dois pontos acima assumem silenciosamente que a API fluente é uma camada sobre uma API ou recurso de idioma existente. Essa suposição pode ser outra boa orientação: uma API fluente pode ser uma maneira extra de fazer algo, não a única. Ou seja, pode ser uma boa ideia oferecer uma API fluente como uma opção de "inclusão".

stakx
fonte
1
Obrigado por dedicar um tempo para adicionar à minha pergunta. Admito que meu exemplo escolhido foi mal pensado. Na época, eu estava realmente procurando usar uma interface fluida para uma API de consulta que estava desenvolvendo. Eu simplifiquei demais. Obrigado por apontar as falhas e pelos bons pontos de conclusão.
Andrew Hanlon
2

Eu gosto do estilo fluente, ele expressa intenção muito claramente. Com o exemplo de inicializador de objetos que você tem depois, é necessário ter criadores de propriedades públicas para usar essa sintaxe, não com o estilo fluente. Dizendo isso, com o seu exemplo, você não ganha muito com os setters públicos porque quase optou por um estilo set / get estilo java-esque.

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

Ian
fonte
Obrigado pela sua resposta, acho que você expressou uma boa regra geral: o Fluent é melhor com muitas chamadas em muitos setters.
21711 Andrew Hanlon
1

Eu não conhecia o termo interface fluente , mas ele me lembra algumas APIs que usei, incluindo LINQ .

Pessoalmente, não vejo como os recursos modernos do C # impediriam a utilidade dessa abordagem. Prefiro dizer que eles andam de mãos dadas. Por exemplo, é ainda mais fácil conseguir 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.

Steven Jeuris
fonte
1
Obrigado pela resposta - adicionei um exemplo básico para ajudar a esclarecer minha pergunta.
21711 Andrew Hanlon