Construtor com toneladas de parâmetros versus padrão do construtor


21

É sabido que, se sua classe tiver um construtor com muitos parâmetros, digamos mais de 4, provavelmente é um cheiro de código . Você precisa reconsiderar se a classe atende ao SRP .

Mas e se construirmos e objetarmos que depende de 10 ou mais parâmetros e, eventualmente, terminarmos com a configuração de todos esses parâmetros através do padrão Builder? Imagine, você constrói um Personobjeto de texto com suas informações pessoais, informações de trabalho, informações de amigos, informações de interesses, informações sobre educação e assim por diante. Isso já é bom, mas você de alguma forma define os mesmos mais de 4 parâmetros, certo? Por que esses dois casos não são considerados iguais?

Respostas:


25

O Padrão do Construtor não resolve o "problema" de muitos argumentos. Mas por que muitos argumentos são problemáticos?

  • Eles indicam que sua turma pode estar fazendo muito . No entanto, existem muitos tipos que contêm legitimamente muitos membros que não podem ser sensivelmente agrupados.
  • Testar e entender uma função com muitas entradas fica exponencialmente mais complicado - literalmente!
  • Quando o idioma não oferece parâmetros nomeados, uma chamada de função não é auto-documentada . Ler uma chamada de função com muitos argumentos é bastante difícil, porque você não tem idéia do que o sétimo parâmetro deve fazer. Você nem notaria se o quinto e o sexto argumento foram trocados acidentalmente, especialmente se você estiver em um idioma digitado dinamicamente ou se tudo for uma string, ou quando o último parâmetro for truepor algum motivo.

Falsos parâmetros nomeados

O Padrão do Construtor aborda apenas um desses problemas, ou seja, as preocupações de manutenção de chamadas de função com muitos argumentos . Então, uma chamada de função como

MyClass o = new MyClass(a, b, c, d, e, f, g);

pode se tornar

MyClass o = MyClass.builder()
  .a(a).b(b).c(c).d(d).e(e).f(f).g(g)
  .build();

Pattern O padrão Builder foi originalmente concebido como uma abordagem independente da representação para montar objetos compostos, o que é uma aspiração muito maior do que apenas argumentos nomeados para parâmetros. Em particular, o padrão do construtor não requer uma interface fluente.

Isso oferece um pouco de segurança extra, já que explodirá se você chamar um método do construtor que não existe, mas, caso contrário, não trará nada que um comentário na chamada do construtor não teria. Além disso, a criação manual de um construtor requer código, e mais código sempre pode conter mais erros.

Nas linguagens em que é fácil definir um novo tipo de valor, descobri que é muito melhor usar microtipagem / tipos minúsculos para simular argumentos nomeados. É nomeado assim porque os tipos são realmente pequenos, mas você acaba digitando muito mais ;-)

MyClass o = new MyClass(
  new MyClass.A(a), new MyClass.B(b), new MyClass.C(c),
  new MyClass.D(d), new MyClass.E(e), new MyClass.F(f),
  new MyClass.G(g));

Obviamente, os nomes de tipo A, B, C, ... deve ser nomes de auto-documentado que ilustram o significado do parâmetro, muitas vezes o mesmo nome que você daria a variável de parâmetro. Comparada com o idioma do construtor para argumentos nomeados, a implementação necessária é muito mais simples e, portanto, menos provável que contenha erros. Por exemplo (com sintaxe Java-ish):

class MyClass {
  ...
  public static class A {
    public final int value;
    public A(int a) { value = a; }
  }
  ...
}

O compilador ajuda a garantir que todos os argumentos foram fornecidos; com um construtor, você teria que verificar manualmente se há argumentos ausentes ou codificar uma máquina de estado no sistema de tipo de idioma do host - ambos provavelmente conteriam erros.

Há outra abordagem comum para simular argumentos nomeados: um único objeto de parâmetro abstrato que usa uma sintaxe de classe embutida para inicializar todos os campos. Em Java:

MyClass o = new MyClass(new MyClass.Arguments(){{ argA = a; argB = b; argC = c; ... }});

class MyClass {
  ...
  public static abstract class Arguments {
    public int argA;
    public String ArgB;
    ...
  }
}

No entanto, é possível esquecer os campos, e essa é uma solução bastante específica para o idioma (já vi usos em JavaScript, C # e C).

Felizmente, o construtor ainda pode validar todos os argumentos, o que não acontece quando seus objetos são criados em um estado parcialmente construído, e exige que o usuário forneça argumentos adicionais por meio de setters ou de um init()método - esses exigem o mínimo de esforço de codificação, mas fazem mais difícil escrever programas corretos .

Portanto, embora existam muitas abordagens para abordar os “muitos parâmetros sem nome tornam o código difícil de manter o problema”, outros problemas permanecem.

Abordando o problema raiz

Por exemplo, o problema da testabilidade. Quando escrevo testes de unidade, preciso injetar dados de teste e fornecer implementações de teste para simular dependências e operações com efeitos colaterais externos. Não posso fazer isso quando você instancia alguma classe dentro do seu construtor. A menos que a responsabilidade de sua classe seja a criação de outros objetos, ela não deve instanciar nenhuma classe não trivial. Isso anda de mãos dadas com o problema de responsabilidade única. Quanto mais focada a responsabilidade de uma classe, mais fácil é testar (e geralmente mais fácil de usar).

A abordagem mais fácil e geralmente a melhor é que o construtor tome dependências totalmente construídas como parâmetro , embora isso empurre a responsabilidade de gerenciar dependências para o chamador - também não é o ideal, a menos que as dependências sejam entidades independentes no seu modelo de domínio.

Às vezes, fábricas (abstratas) ou estruturas de injeção de dependência total são usadas, embora elas possam ser um exagero na maioria dos casos de uso. Em particular, eles apenas reduzem o número de argumentos se muitos desses argumentos são objetos quase globais ou valores de configuração que não mudam entre a instanciação do objeto. Por exemplo, se parâmetros ae dfossem global-ish, teríamos

Dependencies deps = new Dependencies(a, d);
...
MyClass o = deps.newMyClass(b, c, e, f, g);

class MyClass {
  MyClass(Dependencies deps, B b, C c, E e, F f, G g) {
    this.depA = deps.newDepA(b, c);
    this.depB = deps.newDepB(e, f);
    this.g = g;
  }
  ...
}

class Dependencies {
  private A a;
  private D d;
  public Dependencies(A a, D d) { this.a = a; this.d = d; }
  public DepA newDepA(B b, C c) { return new DepA(a, b, c); }
  public DepB newDepB(E e, F f) { return new DepB(d, e, f); }
  public MyClass newMyClass(B b, C c, E e, F f, G g) {
    return new MyClass(deps, b, c, e, f, g);
  }
}

Dependendo do aplicativo, isso pode mudar o jogo, onde os métodos de fábrica acabam praticamente sem argumentos, porque todos podem ser fornecidos pelo gerenciador de dependências ou pode ser uma grande quantidade de código que complica a instanciação sem benefício aparente. Tais fábricas são muito mais úteis para mapear interfaces para tipos concretos do que para gerenciar parâmetros. No entanto, essa abordagem tenta solucionar o problema raiz de muitos parâmetros, em vez de apenas ocultá-lo com uma interface bastante fluente.


Eu realmente argumentaria contra a parte de auto-documentação. Se você tem um IDE decente + comentários decentes, na maioria das vezes o construtor e a definição de parâmetro ficam a um toque de tecla.
JavaScript é necessário para o funcionamento completo desta página

No Android Studio 3.0, o nome do parâmetro no construtor é mostrado adjacente ao valor que está sendo passado em uma chamada do construtor. por exemplo: novo A (operando 1: 34, operando 2: 56); operando1 e operando2 são os nomes dos parâmetros no construtor. Eles são mostrados pelo IDE para tornar o código mais legível. Portanto, não há necessidade de ir para a definição para descobrir qual é o parâmetro.
granada

9

O padrão do construtor não resolve nada para você e não corrige falhas de design.

Se você tem uma classe que precisa de 10 parâmetros para serem construídos, criar um construtor para construí-lo não melhorará repentinamente o seu design. Você deve optar por refatorar a turma em questão.

Por outro lado, se você tiver uma classe, talvez um DTO simples, em que certos atributos da classe são opcionais, o padrão do construtor poderia facilitar a construção do referido objeto.


1

Cada parte, por exemplo, informações pessoais, informações de trabalho (cada "emprego"), cada amigo, etc., deve ser convertida em seus próprios objetos.

Considere como você implementaria uma funcionalidade de atualização. O usuário deseja adicionar um novo histórico de trabalho. O usuário não deseja alterar nenhuma outra parte da informação nem o histórico de trabalho anterior - apenas mantenha-os iguais.

Quando há muita informação, é inevitável construir uma informação de cada vez. Você pode agrupar essas partes logicamente - com base na intuição humana (o que facilita o uso de um programador) ou com base no padrão de uso (que informações são normalmente atualizadas ao mesmo tempo).

O padrão do construtor é apenas uma maneira de capturar uma lista (ordenada ou não ordenada) de argumentos digitados, que podem ser passados ​​para o construtor real (ou o construtor pode ler os argumentos capturados do construtor).

A orientação de 4 é apenas uma regra de ouro. Existem situações que exigem mais de 4 argumentos e que não há uma maneira lógica ou sã de agrupá-las. Nesses casos, pode fazer sentido criar um structque possa ser preenchido diretamente ou com configuradores de propriedades. Observe que, structneste caso, o construtor e o servem a um propósito muito semelhante.

No seu exemplo, no entanto, você já descreveu o que seria uma maneira lógica de agrupá-los. Se você está enfrentando uma situação em que isso não é verdade, talvez você possa ilustrar isso com um exemplo diferente.

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.