Como evitar “despachantes de tipo” ao trabalhar com o SOLID

5

Eu percebi que desde que eu fui mais e mais rigoroso sobre os princípios do SOLID, meu código tende a consistir de objetos de dados mais puros e um monte de classes de "operadores" que parecem ir contra os princípios opostos. Especificamente, geralmente acabo com "distribuidores de tipos" que mapeiam um tipo de objeto específico para uma classe de operadores adequada.

Deixe-me dar um exemplo:

No mundo antigo, eu teria uma classe base FinancialInstrument com um método .CalculatePrice () e .CalculateRisk (). Eu derivaria isso para OptionInstrument e EquityInstrument com implementações específicas de cálculos de risco e preço.

Mas devido à Responsabilidade Única (e já que há muitas métricas diferentes para calcular, não apenas preços e riscos), agora tenho uma classe RiskCalculator separada e uma classe PriceCalculator com implementações específicas derivadas como OptionPriceCalculator e EquityPriceCalculator etc.

Quando quero iterar os instrumentos e calcular os preços, preciso de algum tipo de classe ou fábrica de despachos de tipos que mapeie os instrumentos para corrigir a classe de cálculo de preço, o que parece errado e não muito orientado a objetos. Eu vi esse tipo de padrão emergir em meus projetos cada vez mais.

Estou fazendo algo errado ou há uma maneira melhor de evitar essas fábricas de "expedidores de tipos"?

    
por Bjorn 11.12.2014 / 13:21
fonte

2 respostas

1

Se suas turmas devem saber calcular um risco, um preço ou outras métricas, depende do que elas dependem.

Por exemplo para calcular um preço, se você precisar apenas de todas as propriedades de FinancialInstrument , ele deve ser implementado pela própria classe. Se, no entanto, você precisar de algumas dependências externas, como o imposto de um país específico, o cálculo (ou pelo menos essa parte específica do cálculo) deve ser fornecido por outra classe. Se você tentar explicar o que é um simples instrumento financeiro simples para uma terceira pessoa e não usar impostos como parte da explicação, isso é um bom sinal de que os impostos não devem fazer parte do classe.

Você também não precisa de dispatchers de tipo para fazer isso. porque as partes comuns devem ser implementadas nas próprias classes e as partes que são dependentes da classe devem ser implementadas em outra classe. Se você usa o tipo dispatchers, então há grandes chances de que suas classes tenham algo em comum que deve ser modelado por uma interface para que você possa usar uma classe adicional que possa lidar com essas duas classes da mesma maneira sem ter que usar algo como instanceof ou ambas as classes não são iguais, então você precisa de 2 classes adicionais (e semânticas diferentes).

Se você fornecer informações semânticas adicionais sobre as classes mencionadas, também poderei dar um exemplo concreto de como implementá-las. =)

    
por 11.12.2014 / 15:27
fonte
3

Isso parece que o padrão de visitantes pode ser útil. Ele não se livra do código de despacho, mas é uma maneira bastante elegante e padrão de implementar o despacho. Em OOP no estilo Java:

// an interface for anything calculatable
interface FinancialInstrument {
    // acceptVisitor makes the pattern obvious,
    // but you might want to pick a more domain-specific name.
    <T> T acceptVisitor(FinancialInstrumentVisitor<T> v);
}

class Option implements FinancialInstrument {
    <T> T acceptVisitor(FinancialInstrumentVisitor<T> v) {
        return v.visit(this);
    }
}

class Equity implements FinancialInstrument {
    <T> T acceptVisitor(FinancialInstrumentVisitor<T> v) {
        return v.visit(this);
    }
}

// an interface for all calculators
// basically, this consists of "visit" overloads that handle each type
interface FinancialInstrumentVisitor<T> {
    T of(FinancialInstrument fi); // convenience wrapper
    T visit(Option o);
    T visit(Equity e);
}

class PriceCalculator implements FinancialInstrumentVisitor<Price> {
    Price of(FinancialInstrument fi) {
        return fi.acceptVisitor(this);
    }
    Price visit(Option o) { ... }
    Price visit(Equity e) { ... }
}

class RiskCalculator implements FinancialInstrumentVisitor<Risk> {
    Price of(FinancialInstrument fi) {
        return fi.acceptVisitor(this);
    }
    Risk visit(Option o) { ... }
    Risk visit(Equity e) { ... }
}

Exemplo de uso:

PriceCalculator price = new PriceCalculator();
for (FinancialInstrument fi : portfolio) {
   doSomethingWith(price.of(fi));
   ...
}

O padrão de visitante é ótimo sempre que você deseja ser livre para adicionar facilmente novas operações, mas o conjunto de objetos em que essas operações trabalham é bastante fixo. Observe que adicionar um tipo de instrumento financeiro requer que uma sobrecarga seja adicionada à interface FinancialInstrumentCalculator , que não é compatível com versões anteriores - todos os consumidores dessa interface precisarão ser atualizados para suportar esse método. Este não é o caso se o novo instrumento financeiro for um subtipo de uma classe existente, como o manipulador existente pode ser usado.

Se novos instrumentos financeiros aparecem com mais frequência do que novas operações, usar o padrão de visitantes é provavelmente uma má ideia. Observe que qualquer estratégia usada para corresponder n operações a m instrumentos financeiros, você sempre (subtipagem ignorada por um momento) acabará com n·m métodos para codificar.

Se o seu idioma permitir isso, recomendo usar traços ou construções semelhantes para fornecer padrões na interface FinancialInstrumentCalculator . Por exemplo. quando eu tenho três tipos

class A implements FinancialInstrument
class B extends A
class C extends A

, os métodos de visitante para B e C podem ser implementados pelo método de visitante para A por padrão:

interface FinancialInstrumentVisitor<T> {
    default T of(FinancialInstrument fi) { return fi.acceptVisitor(this); }
    T visit(A a);
    default T visit(B b) { return visit((A) b); }
    default T visit(C c) { return visit((A) c); }
}
    
por 11.12.2014 / 15:27
fonte