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
true
por 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 a
e d
fossem 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.