Como manter um argumento baixo e ainda manter dependências de terceiros separadas?

13

Eu uso uma biblioteca de terceiros. Eles me passam um POJO que, para nossos propósitos e propósitos, provavelmente é implementado assim:

public class OurData {
  private String foo;
  private String bar;
  private String baz;
  private String quux;
  // A lot more than this

  // IMPORTANT: NOTE THAT THIS IS A PACKAGE PRIVATE CONSTRUCTOR
  OurData(/* I don't know what they do */) {
    // some stuff
  }

  public String getFoo() {
    return foo;
  }

  // etc.
}

Por muitas razões, incluindo, entre outras, encapsular sua API e facilitar o teste de unidade, desejo agrupar seus dados. Mas não quero que minhas classes principais dependam de seus dados (novamente, por razões de teste)! Então agora eu tenho algo parecido com isto:

public class DataTypeOne implements DataInterface {
  private String foo;
  private int bar;
  private double baz;

  public DataTypeOne(String foo, int bar, double baz) {
    this.foo = foo;
    this.bar = bar;
    this.baz = baz;
  }
}

public class DataTypeTwo implements DataInterface {
  private String foo;
  private int bar;
  private double baz;

  public DataTypeOne(String foo, int bar, double baz, String quux) {
    this.foo = foo;
    this.bar = bar;
    this.baz = baz;
    this.quux = quux;
  }
}

E então isso:

public class ThirdPartyAdapter {
  public static makeMyData(OurData data) {
    if(data.getQuux() == null) {
      return new DataTypeOne(
        data.getFoo(),
        Integer.parseInt(data.getBar()),
        Double.parseDouble(data.getBaz()),
      );
    } else {
      return new DataTypeTwo(
        data.getFoo(),
        Integer.parseInt(data.getBar()),
        Double.parseDouble(data.getBaz()),
        data.getQuux();
      );
  }
}

Essa classe de adaptador é acoplada às outras poucas classes que DEVEM saber sobre a API de terceiros, limitando sua abrangência pelo restante do meu sistema. No entanto ... esta solução é GROSS! No Código Limpo, página 40:

Mais de três argumentos (poládicos) exigem justificativa muito especial - e, portanto, não devem ser usados ​​de qualquer maneira.

Coisas que eu considerei:

  • Criando um objeto de fábrica em vez de um método auxiliar estático
    • Não resolve o problema de ter um bajilhão de argumentos
  • Criando uma subclasse de DataTypeOne e DataTypeTwo que possui um construtor dependente
    • Ainda possui um construtor protegido poládico
  • Crie implementações totalmente separadas que estejam em conformidade com a mesma interface
  • Múltiplas das idéias acima simultaneamente

Como essa situação deve ser tratada?


Observe que essa não é uma situação de camada anticorrupção . Não há nada errado com a API deles. Os problemas são:

  • Não quero que minhas estruturas de dados tenham import com.third.party.library.SomeDataStructure;
  • Não consigo construir suas estruturas de dados nos meus casos de teste
  • Minha solução atual resulta em contagens de argumentos muito muito altas. Eu quero manter a contagem de argumentos baixa, SEM passar em suas estruturas de dados.
  • Essa pergunta é "o que é uma camada anticorrupção?". Minha pergunta é " como posso usar um padrão, qualquer padrão, para resolver esse cenário?"

Também não estou pedindo código (caso contrário, essa pergunta estaria no SO), apenas pedindo uma resposta suficiente para que eu possa escrever o código efetivamente (o que essa pergunta não fornece).

durron597
fonte
Se houver vários POJOs de terceiros, pode valer a pena o esforço de escrever um código de teste personalizado que use um Mapa com algumas convenções (por exemplo, nomeie as chaves int_bar) como sua entrada de teste. Ou use JSON ou XML com algum código intermediário personalizado. Com efeito, uma espécie de DSL para testar com.thirdparty.
user949300
A citação completa do Clean Code:The ideal number of arguments for a function is zero (niladic). Next comes one (monadic), followed closely by two (dyadic). Three arguments (triadic) should be avoided where possible. More than three (polyadic) requires very special justification — and then shouldn’t be used anyway.
Lilienthal 29/01
11
A adesão cega a um padrão ou orientação de programação é seu próprio antipadrão .
Lilienthal 29/01
2
"encapsulando sua API e facilitando o teste de unidade" Parece que isso pode ser um caso de excesso de teste e / ou dano de projeto induzido por teste para mim (ou indicativo de que você pode projetar isso de forma diferente para começar). Pergunte a si mesmo: isso realmente torna seu código mais fácil de entender, alterar e reutilizar? Eu colocaria meu dinheiro em "não". Qual é a probabilidade realista de você trocar essa biblioteca? Provavelmente não muito. Se você trocá-lo, isso realmente facilita a instalação de um completamente diferente? Mais uma vez, eu apostaria em "não".
Jpmc26
1
@JamesAnderson Acabei de reproduzir a citação completa porque achei interessante, mas não estava claro para mim no trecho se referia a funções em geral ou a construtores especificamente. Não pretendia endossar a reivindicação e, como disse o jpmc26, meu próximo comentário deve lhe dar alguma indicação de que não estava fazendo isso. Não sei por que você sente a necessidade de atacar acadêmicos, mas o uso de polissílabos não torna alguém um elitista acadêmico empoleirado em sua torre de marfim acima das nuvens.
Lilienthal

Respostas:

10

A estratégia que usei quando existem vários parâmetros de inicialização é criar um tipo que apenas contenha os parâmetros para inicialização

public class DataTypeTwoParameters {
    public String foo;  // use getters/setters instead if it's appropriate
    public int bar;
    public double baz;
    public String quuz;
}

Em seguida, o construtor para DataTypeTwo pega um objeto DataTypeTwoParameters e DataTypeTwo é construído via:

DataTypeTwoParameters p = new DataTypeTwoParameters();
p.foo = "Hello";
p.bar = 4;
p.baz = 3;
p.quuz = "World";

DataTypeTwo dtt = new DataTypeTwo(p);

Isso oferece muitas oportunidades para deixar claro quais são todos os parâmetros do DataTypeTwo e o que eles significam. Você também pode fornecer padrões sensíveis no construtor DataTypeTwoParameters, para que apenas os valores que precisam ser configurados possam ser executados em qualquer ordem que o consumidor da API gostar.

Erik
fonte
Abordagem interessante. Onde você colocaria um relevante Integer.parseInt? Em um setter, ou fora da classe de parâmetros?
precisa saber é o seguinte
5
Fora da classe de parâmetros. A classe de parâmetros deve ser um objeto "burro" e não deve tentar fazer outra coisa senão expressar quais são as entradas necessárias e seus tipos. Análise deve ser feito em outros lugares, como: p.bar = Integer.parseInt("4").
Erik
7
isso soa como um padrão de objeto de parâmetro
gnat
9
... ou anti-padrão.
Telastyn
1
... ou você pode simplesmente mudar o nome DataTypeTwoParameterspara DataTypeTwo.
precisa saber é o seguinte
14

Você realmente tem duas preocupações separadas aqui: agrupar uma API e manter a contagem de argumentos baixa.

Ao agrupar uma API, a idéia é projetar a interface como se fosse do zero, sem conhecer nada além dos requisitos. Você diz que não há nada de errado com a API deles; em seguida, na mesma lista, várias coisas erradas com a API: testabilidade, construtibilidade, muitos parâmetros em um objeto, etc. Escreva a API que você deseja ter. Se isso exigir vários objetos em vez de um, faça isso. Se precisar envolver um nível mais alto, nos objetos que criam o POJO, faça isso.

Depois que você tiver a API desejada, a contagem de parâmetros poderá não ser mais um problema. Se for, há vários padrões comuns a serem considerados:

  • Um objeto de parâmetro, como na resposta de Erik .
  • O padrão do construtor , onde você cria um objeto construtor separado, chama um número de setters para configurar os parâmetros individualmente e, em seguida, cria seu objeto final.
  • O padrão de protótipo , no qual você clona subclasses do objeto desejado com os campos já configurados internamente.
  • Uma fábrica com a qual você já está familiarizado.
  • Alguma combinação dos itens acima.

Observe que esses padrões de criação geralmente acabam chamando um construtor poládico, que você deve considerar aceitável quando encapsulado. O problema com os construtores poliadicos não é chamá-los uma vez, é quando você é forçado a chamá-los toda vez que precisa construir um objeto.

Observe que geralmente é muito mais fácil e mais sustentável passar para a API subjacente armazenando uma referência ao OurDataobjeto e encaminhando as chamadas de método, em vez de tentar reimplementar seus componentes internos. Por exemplo:

public class DataTypeTwo implements DataInterface {
  private OurData data;

  public DataTypeOne(OurData data) {
    this.data = data;
  }

   public String getFoo() {
    return data.getFoo();
  }

  public int getBar() {
    return Integer.parseInt(data.getBar());
  }
  ...
}
Karl Bielefeldt
fonte
Primeira metade desta resposta: ótima, muito útil, +1. Segunda metade desta resposta: "passe para a API subjacente armazenando uma referência ao OurDataobjeto" - é isso que estou tentando evitar, pelo menos na classe base, para garantir que não haja dependência.
precisa saber é o seguinte
1
É por isso que você faz isso apenas em uma de suas implementações de DataInterface. Você cria outra implementação para seus objetos simulados.
Karl Bielefeldt
@ durron597: sim, mas você já sabe como resolver esse problema se realmente o incomoda.
Doc Brown
1

Eu acho que você pode estar interpretando estritamente a recomendação do tio Bob. Para classes normais, com lógica, métodos e construtores, um construtor poládico realmente se parece muito com o cheiro do código. Mas para algo que é estritamente um contêiner de dados que expõe campos e é gerado pelo que já é essencialmente um objeto Factory, não acho tão ruim assim.

Você pode usar o padrão Parameter Object, conforme sugerido em um comentário, pode agrupar esses parâmetros do construtor para você, qual é o seu wrapper de tipo de dados local é , essencialmente, um objeto Parameter. Tudo o que seu objeto Parameter fará é empacotar os parâmetros (como você o criará? Com ​​um construtor poládico?) E depois descompactá-los um segundo depois em um objeto quase idêntico.

Se você não deseja expor os setters para seus campos e chamá-los, acho que seguir um construtor poládico dentro de uma fábrica bem definida e encapsulada é bom.

Avner Shahar-Kashtan
fonte
O problema é que o número de campos na minha estrutura de dados mudou várias vezes e provavelmente mudará novamente. O que significa que eu preciso refatorar o construtor em todos os meus casos de teste. O padrão de parâmetro com padrões sensíveis parece ser o melhor caminho a percorrer; Ter uma versão mutável que é salva na forma imutável poderia facilitar minha vida de várias maneiras.
precisa saber é o seguinte