Por que um tipo seria acoplado ao seu construtor?


20

Eu excluí recentemente uma resposta minha sobre na Code Review , que começou assim:

private Person(PersonBuilder builder) {

Pare. Bandeira vermelha. Um PersonBuilder criaria uma Pessoa; conhece uma pessoa. A classe Person não deve saber nada sobre um PersonBuilder - é apenas um tipo imutável. Você criou um acoplamento circular aqui, onde A depende de B, que depende de A.

A Pessoa deve apenas inserir seus parâmetros; um cliente que esteja disposto a criar uma Pessoa sem construí-la deve poder fazer isso.

Eu recebi um voto negativo e disse que (citando) bandeira vermelha, por quê? A implementação aqui tem a mesma forma que Joshua Bloch demonstrou em seu livro "Effective Java" (item # 2).

Portanto, parece que a maneira correta de implementar um padrão de construtor em Java é tornar o construtor um tipo aninhado (não é disso que se trata esta pergunta) e criar o produto (a classe do objeto que está sendo construído ) dependem do construtor , assim:

private StreetMap(Builder builder) {
    // Required parameters
    origin      = builder.origin;
    destination = builder.destination;

    // Optional parameters
    waterColor         = builder.waterColor;
    landColor          = builder.landColor;
    highTrafficColor   = builder.highTrafficColor;
    mediumTrafficColor = builder.mediumTrafficColor;
    lowTrafficColor    = builder.lowTrafficColor;
}

https://en.wikipedia.org/wiki/Builder_pattern#Java_example

A mesma página da Wikipedia para o mesmo padrão do Builder possui uma implementação totalmente diferente (e muito mais flexível) para C #:

//Represents a product created by the builder
public class Car
{
    public Car()
    {
    }

    public int Wheels { get; set; }

    public string Colour { get; set; }
}

Como você pode ver, o produto aqui não sabe nada sobre uma Builderclasse e, apesar de tudo, pode ser instanciado por uma chamada direta do construtor, uma fábrica abstrata, ... ou um construtor - tanto quanto eu entendo, o O produto de um padrão criacional nunca precisa saber nada sobre o que está sendo criado.

Recebi o contra-argumento (que aparentemente é explicitamente defendido no livro de Bloch) de que um padrão de construtor poderia ser usado para refazer um tipo que teria um construtor inchado com dezenas de argumentos opcionais. Então, em vez de seguir o que pensei que sabia, pesquisei um pouco neste site e descobri que, como suspeitava, esse argumento não retém a água .

Então, qual é o problema? Por que apresentar soluções superprojetadas para um problema que nem deveria existir em primeiro lugar? Se tirarmos Joshua Bloch de seu pedestal por um minuto, podemos encontrar uma única razão válida e boa para unir dois tipos de concreto e chamá-lo de melhor prática?

Isso tudo cheira a programação de cultos de carga para mim.


12
E este "pergunta" cheira a ser apenas um discurso ...
Philip Kendall

2
@PhilipKendall talvez um pouco. Mas estou realmente interessado em entender por que toda implementação de construtor Java tem esse acoplamento rígido.
Mathieu Guindon

19
@PhilipKendall Cheira a curiosidade para mim.
Mastro

8
@PhilipKendall Parece um discurso retórico, sim, mas em sua essência há uma pergunta válida sobre o tópico. Talvez o discurso retórico possa ser editado?
Andrés F.

Não estou impressionado com o argumento "muitos argumentos construtores". Mas estou ainda menos impressionado com esses dois exemplos de construtores. Talvez seja porque os exemplos são muito simples para demonstrar comportamento não trivial, mas o exemplo de Java parece uma interface fluida complicada , e o exemplo de C # é apenas uma maneira indireta de implementar propriedades. Nenhum exemplo faz nenhuma forma de validação de parâmetro; um construtor validaria pelo menos o tipo e o número de argumentos fornecidos, o que significa que o construtor com muitos argumentos vence.
Robert Harvey

Respostas:


22

Discordo da sua afirmação de que é uma bandeira vermelha. Também discordo de sua representação do contraponto, de que existe uma maneira correta de representar um padrão de construtor.

Para mim, há uma pergunta: o Builder é uma parte necessária da API do tipo? Aqui, o PersonBuilder é uma parte necessária da API da pessoa?

É inteiramente razoável que uma classe Person com verificação de validade, imutável e / ou fortemente encapsulada seja necessariamente criada por meio de um Construtor que ele fornece (independentemente de esse construtor estar aninhado ou adjacente). Ao fazer isso, a Pessoa pode manter todos os seus campos privados ou privados e finais do pacote, além de deixar a classe aberta para modificação, se for um trabalho em andamento. (Pode acabar fechado para modificação e aberto para extensão, mas isso não é importante no momento e é especialmente controverso para objetos de dados imutáveis.)

Se for o caso de uma Pessoa ser necessariamente criada por meio de um PersonBuilder fornecido como parte de um único design de API em nível de pacote, um acoplamento circular será ótimo e uma bandeira vermelha não é garantida aqui. Notavelmente, você o declarou como um fato incontestável e não como uma opinião ou escolha de design de API, de modo que isso pode ter contribuído para uma resposta ruim. Eu não teria votado contra você, mas não culpo outra pessoa por fazê-lo, e deixarei o resto da discussão sobre "o que merece voto negativo" para o centro de ajuda e a meta da Code Review . Os votos negativos acontecem; não é "tapa".

Obviamente, se você quiser deixar muitas maneiras de criar um objeto Person, o PersonBuilder poderá se tornar um consumidor ou utilitárioda classe Person, da qual a pessoa não deve depender diretamente. Essa escolha parece mais flexível - quem não gostaria de ter mais opções de criação de objetos? -, mas para garantir garantias (como imutabilidade ou serialização), de repente a Pessoa precisa expor um tipo diferente de técnica de criação pública, como um construtor muito longo. . Se o Construtor tiver a intenção de representar ou substituir um construtor por muitos campos opcionais ou um construtor aberto à modificação, o construtor longo da classe poderá ser um detalhe de implementação melhor deixado oculto. (Lembre-se também de que um Builder oficial e necessário não o impede de escrever seu próprio utilitário de construção, apenas para que ele consuma o Builder como uma API, como na minha pergunta original acima.)


ps: notei que o exemplo de revisão de código que você listou tem uma Pessoa imutável, mas o contra-exemplo da Wikipedia lista um carro mutável com getters e setters. Pode ser mais fácil ver o maquinário necessário omitido no carro se você manter o idioma e os invariantes consistentes entre eles.


Acho que vejo seu ponto de vista sobre o tratamento do construtor como um detalhe de implementação. Simplesmente não parece que o Java eficaz transmita corretamente essa mensagem (não li / não li esse livro), deixando uma tonelada de desenvolvedores júnior (?) De Java assumindo que "é assim que é feito", independentemente do senso comum . Eu concordo que os exemplos da Wikipedia são ruins para comparação .. mas ei, é Wikipedia .. e FWIW eu realmente não ligo para o voto negativo; a resposta excluída está com uma pontuação líquida positiva de qualquer maneira (e aí está minha chance de finalmente marcar um [distintivo: pressão dos colegas]).
Mathieu Guindon

1
No entanto, não consigo afastar a idéia de que um tipo com muitos argumentos de construtor é um problema de design (cheira a quebrar o SRP), que um padrão de construtor rapidamente enfia embaixo do tapete.
Mathieu Guindon

3
@ Mat'sMug Meu post acima considera que a classe precisa de muitos argumentos construtores . Da mesma forma, eu o trataria como um cheiro de design se estivéssemos falando sobre um componente arbitrário da lógica de negócios, mas para um objeto de dados, não é uma questão para um tipo ter dez ou vinte atributos diretos. Não é violação do SRP de um objeto representar um único registro fortemente tipado em um log , por exemplo.
Jeff Bowman apoia Monica

1
Justo. Ainda não entendi o String(StringBuilder)construtor, o que parece obedecer a esse "princípio", onde é normal que um tipo dependa do construtor. Fiquei espantado quando vi a sobrecarga do construtor. Não é como uma Stringtem 42 parâmetros do construtor ...
Mathieu Guindon

3
@Luaan Odd. Eu literalmente nunca o vi usado e não sabia que existia; toString()é universal.
chrylis -on strike-

7

Entendo como pode ser irritante quando o "apelo à autoridade" é usado em um debate. Os argumentos devem permanecer por conta própria da IMO e, embora não seja errado apontar o conselho de uma pessoa tão respeitada, ele realmente não pode ser considerado um argumento completo, já que sabemos que o Sol viaja pela Terra porque Aristóteles disse isso. .

Dito isto, acho que a premissa de seu argumento é a mesma. Nunca devemos acoplar dois tipos concretos e chamar isso de melhor prática, porque ... Alguém disse isso?

Para que o argumento de que o acoplamento é problemático seja válido, é necessário que haja problemas específicos que ele crie. Se essa abordagem não estiver sujeita a esses problemas, não será possível argumentar logicamente que essa forma de acoplamento é problemática. Portanto, se você quiser mostrar seu ponto aqui, acho que precisa mostrar como essa abordagem está sujeita a esses problemas e / ou como a dissociação do construtor melhorará o design.

De imediato, estou tendo dificuldades para ver como a abordagem acoplada criará problemas. As classes são completamente acopladas, com certeza, mas o construtor existe apenas para criar instâncias da classe. Outra maneira de ver isso seria como uma extensão do construtor.


Então ... maior acoplamento, menor coesão => final feliz? hmm ... vejo o ponto sobre o construtor "estendendo o construtor", mas, para citar outro exemplo, não consigo ver o que Stringestá fazendo com uma sobrecarga de construtor usando um StringBuilderparâmetro (embora seja discutível se StringBuilderum construtor é um construtor , mas isso é outra história).
Mathieu Guindon

4
@ Mat'sMug Para deixar claro, eu realmente não tenho um sentimento forte sobre isso de qualquer maneira. Eu não costumo usar o padrão do construtor porque realmente não crio classes que tenham muita entrada de estado. Geralmente decompunha essa classe por razões que você alude a outros lugares. Tudo o que estou dizendo é que, se você puder mostrar como essa abordagem leva a um problema, não importa se Deus desceu e entregou esse padrão construtor a Moisés em uma tábua de pedra. Você ganha'. Por outro lado, se você não pode ...
JimmyJames

Um uso legítimo do padrão de construtor que tenho em minha própria base de código é zombar da API VBIDE; basicamente, você fornece o conteúdo da sequência de um módulo de código e o construtor fornece uma VBEsimulação, completa com janelas, painel de código ativo, um projeto e um componente com um módulo que contém a sequência fornecida. Sem ele, não sei como seria capaz de escrever 80% dos testes em Rubberduck , pelo menos sem me esfaquear nos olhos toda vez que os olhava.
Mathieu Guindon

@ Mat'sMug Pode ser realmente útil para desorganizar dados em objetos imutáveis. Esses tipos de objetos tendem a ser sacos de propriedades, ou seja, não são realmente OO. Todo esse espaço de problemas é tão PITA que qualquer coisa que ajude a ser feita será usada, independentemente de seguirem boas práticas ou não.
JimmyJames 4/16

4

Para responder por que um tipo seria associado a um construtor, é necessário entender por que alguém usaria um construtor em primeiro lugar. Especificamente: Bloch recomenda usar um construtor quando você tiver um grande número de parâmetros do construtor. Esse não é o único motivo para usar um construtor, mas especulo que seja o mais comum. Em resumo, o construtor é um substituto para esses parâmetros do construtor e, muitas vezes, o construtor é declarado na mesma classe que está construindo. Portanto, a classe envolvente já tem conhecimento do construtor e passá-lo como argumento construtor não muda isso. É por isso que um tipo seria associado ao seu construtor.

Por que ter um grande número de parâmetros implica que você deve usar um construtor? Bem, ter um grande número de parâmetros não é incomum no mundo real, nem viola o princípio da responsabilidade única. Também é péssimo quando você tem dez parâmetros de String e está tentando lembrar quais são quais e se é a quinta ou a sexta String que deve ser nula ou não. Um construtor facilita o gerenciamento dos parâmetros e também pode fazer outras coisas legais sobre as quais você deve ler o livro para descobrir.

Quando você usa um construtor que não é associado ao seu tipo? Geralmente, quando os componentes de um objeto não estão disponíveis imediatamente e os objetos que contêm essas peças não precisam saber sobre o tipo que você está tentando construir ou está adicionando um construtor para uma classe sobre a qual você não tem controle. .


Estranho, as únicas vezes em que precisei de um construtor foi quando eu tinha um tipo que não tem uma forma definitiva, como este MockVbeBuilder , que cria uma simulação da API de um IDE; um teste pode precisar apenas de um módulo de código simples, outro teste pode precisar de dois formulários e um módulo de classe - IMO é para isso que serve o padrão de construtor do GoF. Dito isso, eu posso começar a usá-lo para outra classe com um construtor louco ... mas o acoplamento simplesmente não parece certo.
Mathieu Guindon

1
@ Mat's Mug A classe que você apresentou parece mais representativa do padrão de design do Builder (de Design Patterns de Gamma, et al.), Não necessariamente uma classe simples de construtor. Ambos constroem coisas, mas não são a mesma coisa. Além disso, você nunca "precisa" de um construtor, apenas facilita outras coisas.
Old Fat Ned

1

o produto de um padrão criacional nunca precisa saber nada sobre o que o está criando.

A Personturma não sabe o que está criando. Ele só tem um construtor com um parâmetro, e daí? O nome do tipo do parâmetro termina com ... Construtor que realmente não força nada. Afinal, é apenas um nome.

O construtor é um glorificado um objeto de configuração. E um objeto de configuração é realmente apenas agrupar propriedades que pertencem uma à outra. Se eles pertencem um ao outro com o objetivo de serem passados ​​para o construtor de uma classe, que assim seja! Ambos origine destinationdo StreetMapexemplo são do tipo Point. Seria possível passar as coordenadas individuais de cada ponto para o mapa? Claro, mas as propriedades de um Pointpertencem juntas, e você pode ter todos os tipos de métodos que ajudam na sua construção:

1 A diferença é que o construtor não é apenas um objeto de dados simples, mas permite essas chamadas de setter encadeadas. O que Buildermais preocupa é como se construir.

Agora vamos adicionar um pouco de padrão de comando à mistura, porque se você olhar de perto, o construtor é realmente um comando:

  1. Ele encapsula uma ação: chamando um método de outra classe
  2. Ele contém as informações necessárias para executar essa ação: os valores para os parâmetros do método

A parte especial para o construtor é que:

  1. Esse método a ser chamado é realmente o construtor de uma classe. Melhor chamá-lo de padrão criacional agora.
  2. O valor para o parâmetro é o próprio construtor

O que está sendo passado para o Personconstrutor pode construí-lo, mas não necessariamente. Pode ser um objeto de configuração antigo simples ou um construtor.


0

Não, eles estão completamente errados e você está absolutamente certo. Esse modelo de construtor é simplesmente bobo. Não é da conta da Pessoa de onde vêm os argumentos do construtor. O construtor é apenas uma conveniência para os usuários e nada mais.


4
Graças ... eu tenho certeza que essa resposta iria recolher mais votos se expandiu um pouco mais, parece que uma resposta inacabada =)
Mathieu Guindon

Sim, eu tinha um, uma abordagem alternativa para o construtor que fornece as mesmas garantias e garantias sem o acoplamento
Matt freake

3
Para um contraponto, veja a resposta aceita , que menciona casos em que PersonBuilderé de fato uma parte essencial da PersonAPI. Tal como está, esta resposta não discute seu caso.
Andrés F.
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.