O padrão da estratégia pode ser implementado sem ramificação significativa?


14

O padrão de estratégia funciona bem para evitar enormes construções de if ... else e facilita a adição ou substituição de funcionalidades. No entanto, ainda deixa uma falha na minha opinião. Parece que em todas as implementações ainda precisa haver uma construção de ramificação. Pode ser uma fábrica ou um arquivo de dados. Como exemplo, considere um sistema de pedidos.

Fábrica:

// All of these classes implement OrderStrategy
switch (orderType) {
case NEW_ORDER: return new NewOrder();
case CANCELLATION: return new Cancellation();
case RETURN: return new Return();
}

O código depois disso não precisa se preocupar, e há apenas um lugar para adicionar um novo tipo de pedido agora, mas esta seção do código ainda não é extensível. Colocá-lo em um arquivo de dados ajuda a facilitar a leitura (discutível, eu sei):

<strategies>
   <order type="NEW_ORDER">com.company.NewOrder</order>
   <order type="CANCELLATION">com.company.Cancellation</order>
   <order type="RETURN">com.company.Return</order>
</strategies>

Mas isso ainda adiciona código padrão para processar o código de dados concedido, mais facilmente testável por unidade e relativamente estável, mas com complexidade adicional.

Além disso, esse tipo de construção não faz um teste de integração bem. Cada estratégia individual pode ser mais fácil de testar agora, mas cada nova estratégia adicionada é uma complexidade adicional a ser testada. É menos do que você faria se não tivesse usado o padrão, mas ainda está lá.

Existe uma maneira de implementar o padrão de estratégia que mitiga essa complexidade? Ou isso é tão simples quanto parece, e tentar ir além adicionaria outra camada de abstração com pouco ou nenhum benefício?


Hmmmmm .... pode ser possível simplificar coisas com coisas como eval... pode não funcionar em Java, mas talvez em outras linguagens?
FrustratedWithFormsDesigner

1
@FrustratedWithFormsDesigner reflexão é a palavra mágica em java
aberração catraca

2
Você ainda precisará dessas condições em algum lugar. Ao empurrá-los para uma fábrica, você está simplesmente cumprindo com o DRY, pois, caso contrário, a declaração if ou switch pode ter aparecido em vários lugares.
Daniel B

1
A falha que você está mencionando é muitas vezes chamado de violação do open-closed-priciple
k3b

resposta aceita em questão relacionada sugere / map dicionário como uma alternativa para if-else and switch
mosquito

Respostas:


16

Claro que não. Mesmo se você usar um contêiner de IoC, precisará ter condições em algum lugar, decidindo qual implementação concreta injetar. Essa é a natureza do padrão de estratégia.

Realmente não vejo por que as pessoas pensam que isso é um problema. Existem declarações em alguns livros, como a Refatoração de Fowler , de que se você vir uma opção / caixa ou uma cadeia de if / elses no meio de outro código, considere esse cheiro e procure movê-lo para seu próprio método. Se o código em cada caso for mais do que uma linha, talvez duas, considere tornar esse método um Método de Fábrica, retornando Estratégias.

Algumas pessoas entenderam que isso significa que uma troca / caso é ruim. Este não é o caso. Mas deve residir por conta própria, se possível.


1
Também não vejo isso como algo ruim. No entanto, estou sempre procurando maneiras de reduzir a quantidade de código mantendo o toque, o que levou à pergunta.
Michael K

As instruções de switch (e blocos longos se / else-se) são ruins e devem ser evitadas, se possível, para manter seu código sustentável. O dito "se possível" reconhece que há alguns casos em que a chave deve existir e, nesses casos, tente mantê-la em um único local e que torne a manutenção menos trabalhosa (é tão fácil acidentalmente, perca um dos 5 lugares no código que você precisava manter sincronizado quando não o isolasse adequadamente).
Shadow Man

10

O padrão da estratégia pode ser implementado sem ramificação significativa?

Sim , usando um hashmap / dicionário no qual todas as implementações da Estratégia são registradas. O método de fábrica se tornará algo como

Class strategyType = allStrategies[orderType];
return runtime.create(strategyType);

Toda implementação de estratégia deve registrar a fábrica com seu orderType e algumas informações sobre como criar uma classe.

factory.register(NEW_ORDER, NewOrder.class);

Você pode usar o construtor estático para registro, se o seu idioma suportar isso.

O método register nada mais é do que adicionar um novo valor ao hashmap:

void register(OrderType orderType, Class class)
{
   allStrategies[orderType] = class;
}

[update 2012-05-04]
Esta solução é muito mais complexa do que a "solução de switch" original, que eu preferiria na maioria das vezes.

No entanto, em um ambiente em que as estratégias mudam com frequência (ou seja, cálculo de preços dependendo do cliente, tempo, ....), essa solução de hashmap combinada com um IoC-Container pode ser uma boa solução.


3
Então agora você tem todo um Hashmap de estratégias para evitar uma troca / caso? Qual é exatamente as linhas de factory.register(NEW_ORDER, NewOrder.class);limpador, ou menos uma violação do OCP, do que as linhas de case NEW_ORDER: return new NewOrder();?
Pd

5
Não é mais limpo. É contra-intuitivo. Ele sacrifica o princípio de manter o stimple estúpido para melhorar o princípio aberto-fechado. O mesmo se aplica à inversão de controle e injeção de dependência: elas são mais difíceis de entender do que uma solução simples. A pergunta era "implementar ... sem ramificação significativa" e não "como criar um soltuion mais intuitivo"
k3b

1
Também não vejo que você tenha evitado a ramificação. Você acabou de alterar a sintaxe.
Pd

1
Não há problemas com esta solução em idiomas que não carregam classes até que sejam necessários? O código estático não será executado até que a classe seja carregada e a classe nunca será carregada porque nenhuma outra classe se refere a ela.
Kevin cline

2
Pessoalmente, gosto desse método, porque permite tratar suas estratégias como dados mutáveis , em vez de tratá-las como código imutável.
Tacroy

2

"Estratégia" é ter que escolher entre algoritmos alternativos pelo menos uma vez , não menos. Em algum lugar do seu programa, alguém precisa tomar uma decisão - talvez o usuário ou o seu programa. Se você usar IoC, reflexão, avaliador de arquivo de dados ou construção de switch / case para implementar isso, não mudará a situação.


Eu discordo de sua declaração de uma vez. Estratégias podem ser trocadas em tempo de execução. Por exemplo, após não processar uma solicitação usando uma ProcessingStrategy padrão, uma VerboseProcessingStrategy pode ser escolhida e o processamento executado novamente.
dmux 21/01

@ DMUX: claro, editado minha resposta em conformidade.
Doc Brown

1

O padrão de estratégia é usado quando você especifica comportamentos possíveis e é melhor usado ao designar um manipulador na inicialização. A especificação de qual instância da estratégia usar pode ser feita por meio de um mediador, seu contêiner de IoC padrão, alguma fábrica como você descreve ou simplesmente usando a correta com base no contexto (já que muitas vezes a estratégia é fornecida como parte de um uso mais amplo da classe que o contém).

Para esse tipo de comportamento em que diferentes métodos precisam ser chamados com base em dados, sugiro usar as construções de polimorfismo fornecidas pela linguagem em questão; não é o padrão de estratégia.


1

Ao conversar com outros desenvolvedores onde trabalho, encontrei outra solução interessante - específica para Java, mas tenho certeza de que a idéia funciona em outras linguagens. Construa uma enumeração (ou um mapa, não importa muito qual) usando as referências de classe e instanciar usando reflexão.

enum FactoryType {
   Type1(Type1.class),
   Type2(Type2.class);

   private Class<? extends Type> clazz;

   private FactoryType(Class<? extends Type> clazz) {
      this.clazz = clazz;
   }

   public Class<? extends Type> getTypeClass() {
      return clazz;
   }
}

Isso reduz drasticamente o código de fábrica:

public Type create(FactoryType type) throws Exception {
   return type.getTypeClass().newInstance();
}

(Por favor, ignore o tratamento incorreto de erros - código de exemplo :))

Não é o mais flexível, pois uma compilação ainda é necessária ... mas reduz a alteração de código para uma linha. Eu gosto que os dados sejam separados do código de fábrica. Você pode até fazer coisas como definir parâmetros usando classes / interfaces abstratas para fornecer um método comum ou criando anotações em tempo de compilação para forçar assinaturas de construtor específicas.


Não tenho certeza do que causou o retorno à página inicial, mas esta é uma boa resposta e, no Java 8, agora você pode criar referências do Constructor sem reflexão.
JimmyJames 21/02

1

A extensibilidade pode ser aprimorada fazendo com que cada classe defina seu próprio tipo de pedido. Em seguida, sua fábrica seleciona a que combina.

Por exemplo:

public interface IOrderStrategy
{
    OrderType OrderType { get; }
}

public class NewOrder : IOrderStrategy
{
    public OrderType OrderType { get; } = OrderType.NewOrder;
}

public class OrderFactory
{
    private IEnumerable<IOrderStrategy> _strategies;

    public OrderFactory(IEnumerable<IOrderStrategy> strategies) // Injected by IoC container
    {
        _strategies = strategies;
    }

    public IOrderStrategy Create(OrderType orderType)
    {
        IOrderStrategy strategy = _strategies.FirstOrDefault(s => s.OrderType == orderType);

        if (strategy == null)
            throw new ArgumentException("Invalid order type.", nameof(orderType));

        return strategy;
    }
}

1

Eu acho que deveria haver um limite em quantos ramos são aceitáveis.

Por exemplo, se eu tiver mais de oito casos dentro da minha instrução switch, reavaliar meu código e procurar o que posso re-fatorar. Muitas vezes, acho que certos casos podem ser agrupados em uma fábrica separada. Minha suposição aqui é que existe uma fábrica que constrói estratégias.

De qualquer forma, você não pode evitar isso e em algum lugar terá que verificar o estado ou o tipo do objeto com o qual está trabalhando. Você criará uma estratégia para isso. Boa e velha separação de preocupações.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.