Estou trabalhando em um projeto que processa solicitações e há dois componentes para a solicitação: o comando e os parâmetros. O manipulador para cada comando é muito simples (<10 linhas, geralmente <5). Existem pelo menos 20 comandos e provavelmente terá mais de 50.
Eu vim com algumas soluções:
- um grande switch / if-else nos comandos
- mapa de comandos para funções
- mapa de comandos para classes estáticas / singletons
Cada comando faz uma pequena verificação de erro, e o único bit que pode ser extraído é a verificação do número de parâmetros, definidos para cada comando.
Qual seria a melhor solução para esse problema e por quê? Também estou aberto a quaisquer padrões de design que eu possa ter perdido.
Eu vim com a seguinte lista pro / con para cada um:
interruptor
- profissionais
- mantém todos os comandos em uma função; por serem simples, isso torna uma tabela de pesquisa visual
- não precisa desordenar a fonte com toneladas de pequenas funções / classes que só serão usadas em um só lugar
- contras
- muito longo
- difícil adicionar comandos programaticamente (é necessário encadear usando o caso padrão)
comandos de mapa -> função
- profissionais
- pequenos pedaços pequenos
- pode adicionar / remover comandos programaticamente
- contras
- se feito em linha, o mesmo visualmente que o switch
- se não for feito em linha, muitas funções são usadas apenas em um só lugar
comandos de mapa -> classe estática / singleton
- profissionais
- pode usar polimorfismo para lidar com a verificação de erros simples (apenas três linhas, mas ainda)
- benefícios semelhantes ao map - - função function
- contras
- muitas classes muito pequenas irão desorganizar o projeto
- implementação nem tudo no mesmo lugar, por isso não é tão fácil verificar implementações
Notas extras:
Estou escrevendo isso no Go, mas não acho que a solução seja específica do idioma. Estou procurando uma solução mais geral, porque talvez eu precise fazer algo muito semelhante em outros idiomas.
Um comando é uma string, mas posso mapeá-lo facilmente para um número, se for conveniente. A assinatura da função é algo como:
Reply Command(List<String> params)
O Go tem funções de nível superior, e outras plataformas que estou considerando também têm funções de nível superior, daí a diferença entre a segunda e a terceira opções.
fonte
Respostas:
Este é um ótimo ajuste para um mapa (segunda ou terceira solução proposta). Eu usei dezenas de vezes, e é simples e eficaz. Eu realmente não faço distinção entre essas soluções; o ponto importante é que existe um mapa com nomes de funções como as teclas.
A principal vantagem da abordagem de mapa, na minha opinião, é que a tabela é de dados. Isso significa que ele pode ser repassado, aumentado ou modificado em tempo de execução; também é fácil codificar funções adicionais que interpretam o mapa de maneiras novas e interessantes. Isso não seria possível com a solução case / switch.
Eu realmente não experimentei os contras mencionados, mas gostaria de mencionar uma desvantagem adicional: o envio é fácil se apenas o nome da string for importante, mas se você precisar levar informações adicionais em conta para decidir qual função executar , é muito menos limpo.
Talvez eu nunca tenha tido um problema difícil o suficiente, mas vejo pouco valor no padrão de comando e na codificação do despacho como uma hierarquia de classes, como outros mencionaram. O núcleo do problema é mapear solicitações para funções; um mapa é simples, óbvio e fácil de testar. Uma hierarquia de classes requer mais código e design, aumenta o acoplamento entre esse código e pode forçá-lo a tomar mais decisões antecipadas que, mais tarde, provavelmente precisará alterar. Eu não acho que o padrão de comando se aplique.
fonte
Seu problema empresta muito bem ao padrão de design do comando . Então, basicamente, você terá uma
Command
interface base e haverá váriasCommandImpl
classes que implementariam essa interface. A interface precisa essencialmente ter apenas um método únicodoCommand(Args args)
. Você pode ter os argumentos passados através de uma instância daArgs
classe. Dessa forma, você aproveita o poder do polimorfismo em vez de declarações if / else desajeitadas. Além disso, este design é facilmente extensível.fonte
Sempre que me pergunto se devo usar uma instrução switch ou um polimorfismo no estilo OO, refiro-me ao Problema da Expressão . Basicamente, se você tem "casos" diferentes para seus dados e oferece suporte a diferentes "ações" (onde cada ação faz algo diferente para cada caso), é realmente difícil criar um sistema que naturalmente permita adicionar novos casos e novas ações no futuro.
Se você usar instruções switch (ou o padrão Visitor), é fácil adicionar novas ações (porque você organiza tudo em uma única função), mas é difícil adicionar novos casos (porque você precisa voltar e editar as funções antigas)
Por outro lado, se você usar o polimorfismo no estilo OO, é fácil adicionar novos casos (basta criar uma nova classe), mas é difícil adicionar métodos a uma interface (porque você precisará voltar e editar várias classes)
No seu caso, você possui apenas um método que precisa suportar (processar uma solicitação), mas muitos casos possíveis (cada comando diferente). Como facilitar a adição de novos casos é mais importante do que adicionar novos métodos, basta criar uma classe separada para cada comando diferente.
A propósito, do ponto de vista de como as coisas são extensíveis, não faz grande diferença se você usa classes ou funções. Se estamos comparando com uma instrução switch, o que importa é como as coisas são "despachadas" e as classes e funções são despachadas da mesma maneira. Portanto, basta usar o que for mais conveniente em seu idioma (e como o Go tem escopo e fechamento lexicais, a distinção entre classes e funções é realmente muito pequena).
Por exemplo, você geralmente pode usar a delegação para fazer a parte da verificação de erros em vez de confiar na herança (meu exemplo está em Javascript porque não sei a sintaxe Go, espero que não se importe)
Obviamente, este exemplo pressupõe que existe uma maneira sensata de ter a função wrapper ou a classe pai verificar argumentos para todos os casos. Dependendo de como as coisas vão, pode ser mais simples colocar a chamada para check_arguments dentro dos próprios comandos (já que cada comando pode precisar chamar a função de verificação com argumentos diferentes, devido a um número diferente de argumentos, diferentes tipos de comando, etc.)
tl; dr: Não há melhor maneira de resolver todos os problemas. Do ponto de vista de "fazer as coisas funcionarem", concentre-se em criar suas abstrações de maneira a impor invariantes importantes e protegê-lo contra erros. De uma perspectiva "à prova de futuro", lembre-se de que partes do código têm maior probabilidade de serem estendidas.
fonte
Eu nunca usei go, como programador ac # eu provavelmente iria para a seguinte linha, espero que essa arquitetura caiba no que você está fazendo.
Eu criaria uma pequena classe / objeto para cada um com a função principal a ser executada, cada um deve conhecer sua representação de string. Isso fornece plugabilidade que parece que você deseja, à medida que o número de funções aumenta. Observe que eu não usaria estática, a menos que você realmente precise, elas não dão muita vantagem.
Eu teria então uma fábrica que sabe como descobrir essas classes em tempo de execução, alterando-as para serem carregadas de diferentes montagens etc. Isso significa que você não precisa delas no mesmo projeto.
Assim, também o torna mais modular para teste e deve tornar o código agradável e pequeno, o que é mais fácil de manter posteriormente.
fonte
Como você seleciona seus comandos / funções?
Se houver uma maneira "inteligente" de selecionar a função correta, esse é o caminho - significa que você pode adicionar novo suporte (talvez em uma biblioteca externa) sem modificar a lógica principal.
Além disso, testar funções individuais é mais fácil isoladamente do que uma enorme declaração de chave.
Por fim, apenas sendo usado em um único local - você pode descobrir que, quando chegar aos 50, diferentes bits de diferentes funções podem ser reutilizados?
fonte
Não tenho idéia de como o Go funciona, mas uma arquitetura que usei no ActionScript é ter uma lista duplamente vinculada que atua como uma cadeia de responsabilidade. Cada link possui uma função determineResponsibility (que eu implementei como retorno de chamada, mas você pode escrever cada link separadamente se isso funcionar melhor para o que você está tentando fazer). Se um link determinasse que tinha responsabilidade, chamaria meetResponsibility (novamente, um retorno de chamada) e isso encerraria a cadeia. Se não tivesse responsabilidade, passaria a solicitação para o próximo elo da cadeia.
Diferentes casos de uso podem ser facilmente adicionados e removidos simplesmente adicionando um novo link entre os links (ou no final da) cadeia existente.
Isso é semelhante à, mas sutilmente diferente da sua idéia de um mapa de funções. O bom é que você apenas passa a solicitação e ela faz o seu trabalho sem que você precise fazer mais nada. O lado negativo é que não funciona bem se você precisar retornar um valor.
fonte