Subclasses apenas de construtor: isso é um antipadrão?

37

Eu estava discutindo com um colega de trabalho e acabamos tendo intuições conflitantes sobre o propósito da subclasse. Minha intuição é que, se uma função primária de uma subclasse é expressar um intervalo limitado de valores possíveis de seu pai, provavelmente não deve ser uma subclasse. Ele argumentou pela intuição oposta: que a subclasse representa um objeto sendo mais "específico" e, portanto, um relacionamento com subclasse é mais apropriado.

Para colocar minha intuição de maneira mais concreta, acho que, se eu tenho uma subclasse que estende uma classe pai, mas o único código que a subclasse substitui é um construtor (sim, eu sei que os construtores geralmente não "substituem", aceite comigo), então o que era realmente necessário era um método auxiliar.

Por exemplo, considere esta classe da vida real:

public class DataHelperBuilder
{
    public string DatabaseEngine { get; set; }
    public string ConnectionString { get; set; }

    public DataHelperBuilder(string databaseEngine, string connectionString)
    {
        DatabaseEngine = databaseEngine;
        ConnectionString = connectionString;
    }

    // Other optional "DataHelper" configuration settings omitted

    public DataHelper CreateDataHelper()
    {
        Type dataHelperType = DatabaseEngineTypeHelper.GetType(DatabaseEngine);
        DataHelper dh = (DataHelper)Activator.CreateInstance(dataHelperType);
        dh.SetConnectionString(ConnectionString);

        // Omitted some code that applies decorators to the returned object
        // based on omitted configuration settings

        return dh;
    }
}

Ele afirma que seria totalmente apropriado ter uma subclasse como esta:

public class SystemDataHelperBuilder
{
    public SystemDataHelperBuilder()
        : base(Configuration.GetSystemDatabaseEngine(),
               Configuration.GetSystemConnectionString())
    {
    }
 }

Então, a pergunta:

  1. Entre as pessoas que falam sobre padrões de design, qual dessas intuições está correta? A subclasse, conforme descrito acima, é um anti-padrão?
  2. Se é um anti-padrão, qual é o seu nome?

Peço desculpas se isso tiver sido uma resposta fácil de mudar; minhas pesquisas no google geralmente retornavam informações sobre o antipadrão de construtor telescópico e não exatamente o que eu estava procurando.

HardlyKnowEm
fonte
2
Considero a palavra "Auxiliar" em um nome um antipadrão. É como ler um ensaio com referências constantes a uma coisinha e uma coisa. Diga o que é isso . É uma fábrica? Construtor? Para mim, parece um processo de pensamento preguiçoso e encoraja a violação do princípio da responsabilidade única, uma vez que um nome semântico adequado o direcionaria. Concordo com os outros eleitores e você deve aceitar a resposta de @ lxrec. Criar uma subclasse para um método de fábrica é completamente inútil, a menos que você não possa confiar nos usuários para transmitir argumentos corretos, conforme especificado na documentação. Nesse caso, obtenha novos usuários.
Aaron Hall
Concordo plenamente com a resposta de @ Ixrec. Afinal, a resposta dele diz que eu estava certa o tempo todo e meu colega de trabalho estava errado. ;-) Mas, falando sério, é exatamente por isso que me senti estranha ao aceitá-lo; Eu pensei que poderia ser acusado de preconceito. Mas eu sou novo nos programadores StackExchange; Eu estaria disposto a aceitá-lo se tivesse certeza de que não seria uma quebra de etiqueta.
precisa saber é o seguinte
11
Não, é esperado que você aceite a resposta que considera mais valiosa. O que a comunidade considera mais valioso até agora é o Ixrec de longe em mais de 3 para 1. Portanto, eles esperam que você o aceite. Nesta comunidade, há muito pouca frustração expressa em um interlocutor que aceita a resposta errada, mas há muita frustração expressa em questionadores que nunca aceitam uma resposta. Note-se que ninguém está reclamando sobre a resposta aceita aqui, ao invés disso eles se queixam da votação, que parece estar se corrigindo: stackoverflow.com/q/2052390/541136
Aaron Hall
Muito bem, eu aceito. Obrigado por dedicar um tempo para me informar sobre os padrões da comunidade aqui.
HardlyKnowEm

Respostas:

54

Se tudo o que você deseja fazer é criar a classe X com certos argumentos, a subclassificação é uma maneira estranha de expressar essa intenção, porque você não está usando nenhum dos recursos que as classes e a herança oferecem. Não é realmente um antipadrão, é apenas estranho e um pouco inútil (a menos que você tenha outras razões para isso). Uma maneira mais natural de expressar essa intenção seria um Método de Fábrica , que neste caso é um nome sofisticado para o seu "método auxiliar".

Em relação à intuição geral, tanto "mais específico" quanto "um alcance limitado" são maneiras potencialmente prejudiciais de pensar em subclasses, porque ambas implicam que fazer de Square uma subclasse de Retângulo é uma boa idéia. Sem depender de algo formal como o LSP, eu diria que uma intuição melhor é que uma subclasse fornece uma implementação da interface base ou estende a interface para adicionar novas funcionalidades.

Ixrec
fonte
2
Os Métodos de Fábrica, no entanto, não permitem impor determinados comportamentos (governados por argumentos) em sua base de código. E às vezes é útil anotar explicitamente que essa classe / método / campo usa SystemDataHelperBuilderespecificamente.
Telastyn
3
Ah, acho que o argumento quadrado x retângulo era exatamente o que eu estava procurando. Obrigado!
precisa saber é o seguinte
7
É claro que ter quadrado como subclasse de retângulo pode ser bom se eles forem imutáveis. Você realmente tem que olhar para o LSP.
David Conrad
10
O problema do "problema" do quadrado / retângulo é que as formas geométricas são imutáveis. Você sempre pode usar um quadrado sempre que um retângulo fizer sentido, mas redimensionar não é algo que quadrados ou retângulos fazem.
Doval
3
@nha LSP = Princípio da substituição de Liskov
Ixrec 27/02
16

Qual dessas intuições está correta?

Seu colega de trabalho está correto (assumindo sistemas do tipo padrão).

Pense bem, as classes representam possíveis valores legais. Se a classe Ativer um campo de bytes F, você pode pensar que Atem 256 valores legais, mas essa intuição está incorreta. Aestá restringindo "toda permutação de valores já" a "deve ter o campo entre 0 e F255".

Se você estender Acom Boutro campo de bytes FF, isso inclui restrições . Em vez de "valor onde Fé byte", você tem "valor onde Fé byte && FFtambém é byte". Como você mantém as regras antigas, tudo o que funcionou para o tipo de base ainda funciona para o seu subtipo. Mas como você adiciona regras, você se especializa ainda mais em "poderia ser qualquer coisa".

Ou pensar de outra forma, assumir que Atinha um monte de subtipos: B, C, e D. Uma variável do tipo Apode ser qualquer um desses subtipos, mas uma variável do tipo Bé mais específica. (Na maioria dos sistemas de tipos) não pode ser a Cou a D.

Isso é um anti-padrão?

Enh? Ter objetos especializados é útil e não é claramente prejudicial ao código. Alcançar esse resultado usando a subtipagem talvez seja um pouco questionável, mas depende de quais ferramentas você tem à sua disposição. Mesmo com ferramentas melhores, essa implementação é simples, direta e robusta.

Eu não consideraria isso um antipadrão, uma vez que não está claramente errado nem ruim em nenhuma situação.

Telastyn
fonte
5
"Mais regras significam menos valores possíveis, significam mais específicas." Eu realmente não sigo isso. O tipo byte and bytedefinitivamente tem mais valores do que apenas o tipo byte; 256 vezes mais. Além disso, não acho que você possa combinar regras com número de elementos (ou cardinalidade, se quiser ser preciso). Ele divide quando você considera conjuntos infinitos. Existem tantos números pares quanto números inteiros, mas saber que um número é par ainda é uma restrição / me dá mais informações do que apenas saber que é um número inteiro! O mesmo poderia ser dito para os números naturais.
D
6
@Telastyn Estamos ficando fora do tópico, mas é provável que a cardinalidade (livremente, "tamanho") do conjunto de números pares seja igual ao conjunto de todos os números inteiros ou mesmo de todos os números racionais. É uma coisa muito legal. Veja math.grinnell.edu/~miletijo/museum/infinite.html
HardlyKnowEm
2
A teoria dos conjuntos não é intuitiva. Você pode colocar os números inteiros e pares na correspondência individual (por exemplo, usando a função n -> 2n). Isso nem sempre é possível; você não pode mapear os números inteiros para os reais; portanto, o conjunto de reais é "maior" que os números inteiros. Mas você pode mapear o intervalo (real) [0, 1] para o conjunto de todos os reais, o que os torna do mesmo "tamanho". Os subconjuntos são definidos como "todo elemento de Atambém é um elemento de B" precisamente porque, uma vez que seus conjuntos se tornam infinitos, pode ser que eles tenham a mesma cardinalidade, mas tenham elementos diferentes.
D
11
Mais relacionado ao tópico em questão, o exemplo que você dá é uma restrição que, em termos de programação orientada a objetos, na verdade estende a funcionalidade em termos de um campo adicional. Estou falando de subclasses que não estendem a funcionalidade - como o problema da elipse circular.
HardlyKnowEm
11
@Doval: Há mais de um significado possível de "menos" - ou seja, mais de uma relação de pedidos em conjuntos. A relação "é um subconjunto de" fornece uma ordem (parcial) bem definida em conjuntos. Além disso, "mais regras" podem ser entendidas como "todos os elementos do conjunto satisfazem mais (isto é, um superconjunto de) proposições (de um determinado conjunto)". Com essas definições, "mais regras" realmente significa "menos valores". Essa é, grosso modo, a abordagem adotada por vários modelos semânticos de programas de computador, por exemplo, domínios de Scott.
Psmears
12

Não, não é um anti-padrão. Posso pensar em vários casos de uso prático para isso:

  1. Se você deseja usar a verificação do tempo de compilação para garantir que as coleções de objetos estejam em conformidade apenas com uma subclasse específica. Por exemplo, se você possui MySQLDaoe está SqliteDaoem seu sistema, mas, por algum motivo, deseja garantir que uma coleção contenha apenas dados de uma fonte, se você usar subclasses como descreve, poderá fazer com que o compilador verifique essa correção específica. Por outro lado, se você armazenar a fonte de dados como um campo, isso se tornaria uma verificação em tempo de execução.
  2. Meu aplicativo atual tem um relacionamento individual entre AlgorithmConfiguration+ ProductClasse AlgorithmInstance. Em outras palavras, se você configurar FooAlgorithmpara ProductClassno arquivo de propriedades, poderá obter apenas um FooAlgorithmpara essa classe de produto. A única maneira de obter dois FooAlgorithms para um dado ProductClassé criar uma subclasse FooBarAlgorithm. Tudo bem, porque facilita o uso do arquivo de propriedades (não há necessidade de sintaxe para muitas configurações diferentes em uma seção do arquivo de propriedades) e é muito raro haver mais de uma instância para uma determinada classe de produto.
  3. A resposta de @ Telastyn dá outro bom exemplo.

Bottom line: Não há realmente nenhum mal em fazer isso. Um antipadrão é definido como:

Um antipadrão (ou antipadrão) é uma resposta comum a um problema recorrente que geralmente é ineficaz e corre o risco de ser altamente contraproducente.

Não há risco de ser altamente contraproducente aqui, por isso não é um anti-padrão.

durron597
fonte
2
Sim, acho que se eu tivesse que formular a pergunta novamente, diria "cheiro de código". Não é ruim o suficiente garantir um aviso à próxima geração de jovens desenvolvedores para evitá-lo, mas é algo que, se você o vir , pode indicar que há um problema no design abrangente, mas também pode estar correto.
HardlyKnowEm
O ponto 1 está bem no alvo. Eu realmente não entendo o ponto 2, mas eu tenho certeza que é tão bom ... :)
Phil
9

Se algo é um padrão ou um antipadrão depende significativamente de qual idioma e ambiente você está escrevendo. Por exemplo, forloops são um padrão em assembly, C e idiomas semelhantes e um antipadrão no lisp.

Deixe-me contar uma história de algum código que escrevi há muito tempo ...

Estava em uma linguagem chamada LPC e implementou uma estrutura para lançar feitiços em um jogo. Você tinha uma superclasse de feitiços, que possuía uma subclasse de feitiços de combate que controlava alguns testes e, em seguida, uma subclasse para feitiços de dano direto de alvo único, que eram subclassificados para os feitiços individuais - míssil mágico, raio e similares.

A natureza de como o LPC funcionava era que você tinha um objeto mestre que era um Singleton. antes de você ir 'eww Singleton' - era e não era "eww". Não se fez cópias dos feitiços, mas invocou os métodos estáticos (efetivamente) nos feitiços. O código para o míssil mágico parecia:

inherit "spells/direct";

void init() {
  ::init();
  damage = 10;
  cost = 3;
  delay = 1.0;
  caster_message = "You cast a magic missile at ${target}";
  target_message = "You are hit with a magic missile cast by ${caster}";
}

E você vê isso? É uma classe apenas construtora. Ele definiu alguns valores que estavam em sua classe abstrata pai (não, não deveria usar setters - é imutável) e foi isso. Funcionou bem . Era fácil de codificar, fácil de estender e fácil de entender.

Esse tipo de padrão se traduz em outras situações em que você tem uma subclasse que estende uma classe abstrata e define alguns valores próprios, mas toda a funcionalidade está na classe abstrata que herda. Dê uma olhada no StringBuilder em Java para um exemplo disso - não apenas o construtor, mas estou pressionado a encontrar muita lógica nos próprios métodos (tudo está no AbstractStringBuilder).

Isso não é uma coisa ruim e certamente não é um antipadrão (e em alguns idiomas pode ser um padrão em si).

Comunidade
fonte
2

O uso mais comum dessas subclasses em que consigo pensar são as hierarquias de exceção, embora esse seja um caso degenerado em que normalmente não definiríamos nada na classe, desde que a linguagem nos permita herdar construtores. Usamos herança para expressar que ReadErroré um caso especial de IOError, e assim por diante, mas ReadErrornão precisamos substituir nenhum método de IOError.

Fazemos isso porque as exceções são detectadas verificando seu tipo. Portanto, precisamos codificar a especialização no tipo para que alguém possa capturar apenas ReadErrorsem capturar tudo IOError, caso deseje.

Normalmente, verificar os tipos de coisas é muito ruim e tentamos evitá-lo. Se conseguirmos evitá-lo, não haverá necessidade essencial de expressar especializações no sistema de tipos.

Ainda pode ser útil, por exemplo, se o nome do tipo aparecer na forma de string registrada do objeto: esta é a linguagem e possivelmente a estrutura específica. Você poderia, em princípio, substituir o nome do tipo em qualquer sistema com uma chamada para um método da classe, caso em que iria substituir mais do que apenas o construtor em SystemDataHelperBuilder, nós também iria substituir loggingname(). Portanto, se existe algum registro em seu sistema, você pode imaginar que, embora sua classe literalmente substitua apenas o construtor, "moralmente" ela também substitua o nome da classe e, por motivos práticos, não é uma subclasse apenas para construtores. Tem um comportamento diferente desejável, é '

Então, eu diria que o código dele pode ser uma boa idéia, se houver algum código em algum outro lugar que, de alguma forma, verifique instâncias de SystemDataHelperBuilder, seja explicitamente com uma ramificação (como a exceção que captura ramificações com base no tipo) ou talvez porque haja algum código genérico que acabará usando o nome SystemDataHelperBuilderde uma maneira útil (mesmo que seja apenas log).

No entanto, o uso de tipos para casos especiais leva a alguma confusão, pois se ImmutableRectangle(1,1)e se ImmutableSquare(1)comportam de maneira diferente de alguma maneira , eventualmente alguém vai se perguntar por que e talvez deseje que não. Diferentemente das hierarquias de exceção, você verifica se um retângulo é um quadrado, verificando se height == width, não com instanceof. No seu exemplo, há uma diferença entre a SystemDataHelperBuildere a DataHelperBuildercriados com os mesmos argumentos, e isso pode causar problemas. Portanto, uma função para retornar um objeto criado com os argumentos "certos" normalmente me parece a coisa certa a fazer: a subclasse resulta em duas representações diferentes da "mesma coisa" e só ajuda se você estiver fazendo algo que você está fazendo. normalmente tentaria não fazer.

Note que estou não falar em termos de padrões de projeto e anti-padrões, porque eu não acredito que é possível fornecer uma taxonomia de todas as boas e más idéias que um programador já pensado. Portanto, nem toda idéia é um padrão ou antipadrão, apenas quando alguém identifica que ocorre repetidamente em diferentes contextos e o nomeia ;-) Não conheço nenhum nome específico para esse tipo de subclasse.

Steve Jessop
fonte
Eu pude ver usos para ImmutableSquareMatrix:ImmutableMatrix, desde que houvesse um meio eficiente de solicitar que um ImmutableMatrixrenderizasse seu conteúdo como ImmutableSquareMatrixpossível. Mesmo que ImmutableSquareMatrixfosse basicamente um ImmutableMatrixconstrutor cujo requeresse que a altura e a largura correspondessem, e cujo AsSquareMatrixmétodo simplesmente retornasse (em vez de, por exemplo, construir um ImmutableSquareMatrixque envolva a mesma matriz de apoio), esse design permitiria a validação de parâmetros em tempo de compilação para métodos que requerem quadrados matrizes
supercat 27/02
@ supercat: bom argumento, e no exemplo do interlocutor, se existem funções que devem ser passadas a SystemDataHelperBuilder, e não qualquer DataHelperBuilderuma, então faz sentido definir um tipo para isso (e uma interface, assumindo que você lida com DI dessa maneira) . No seu exemplo, existem algumas operações que uma matriz quadrada poderia ter que não fosse uma quadrada, como determinante, mas seu argumento é bom, mesmo que o sistema não precise daquelas que você ainda deseja verificar por tipo .
Steve Jessop
... então suponho que não seja inteiramente verdade o que eu disse, que "você verifica se um retângulo é um quadrado verificando se height == width, não com instanceof". Você pode preferir algo que possa afirmar estaticamente.
Steve Jessop
Eu gostaria que houvesse um mecanismo que permitisse algo como a sintaxe do construtor invocar métodos estáticos de fábrica. O tipo de expressão foo=new ImmutableMatrix(someReadableMatrix)é mais claro que o de someReadableMatrix.AsImmutable()ou mesmo ImmutableMatrix.Create(someReadableMatrix), mas as últimas formas oferecem possibilidades semânticas úteis que a primeira carece; se o construtor puder dizer que a matriz é quadrada, conseguir refletir seu tipo que poderia ser mais limpo do que exigir código a jusante que precise de uma matriz quadrada para criar uma nova instância de objeto.
Supercat 27/02
@ supercat: Uma coisa pequena sobre Python que eu gosto é a ausência de um newoperador. Uma classe é apenas uma chamada, que, quando chamada, retorna (por padrão) uma instância de si mesma. Portanto, se você deseja preservar a consistência da interface, é perfeitamente possível escrever uma função de fábrica cujo nome comece com uma letra maiúscula; portanto, ela se parece com uma chamada de construtor. Não que eu faça isso rotineiramente com funções de fábrica, mas está lá quando você precisa.
Steve Jessop
2

A definição mais estrita de um relacionamento de subclasse Orientada a Objetos é conhecida como «is-a». Usando o exemplo tradicional de um quadrado e um retângulo, um retângulo quadrado «é-um».

Por isso, você deve usar a subclasse para qualquer situação em que sinta que o «é-a» é um relacionamento significativo para o seu código.

No seu caso específico de uma subclasse com nada além de um construtor, você deve analisá-la da perspectiva «é-a». Está claro que um SystemDataHelperBuilder «é um» DataHelperBuilder; portanto, a primeira passagem sugere que é um uso válido.

A pergunta que você deve responder é se algum dos outros códigos utiliza esse relacionamento «is-a». Algum outro código tenta distinguir SystemDataHelperBuilders da população geral de DataHelperBuilders? Nesse caso, o relacionamento é bastante razoável e você deve manter a subclasse.

No entanto, se nenhum outro código fizer isso, o que você tem é um relacionamentohp que pode ser um relacionamento «é-a» ou pode ser implementado com uma fábrica. Nesse caso, você poderia ir de qualquer maneira, mas eu recomendaria uma fábrica em vez de um relacionamento «é-a». Ao analisar o código Orientado a Objetos, escrito por outro indivíduo, a hierarquia de classes é uma importante fonte de informações. Ele fornece os relacionamentos «is-a» de que eles precisam para entender o código. Assim, colocar esse comportamento em uma subclasse promove o comportamento de "algo que alguém encontrará se aderir aos métodos da superclasse" a "algo que todos examinarão antes mesmo de começar a mergulhar no código". Citando Guido van Rossum, "o código é lido com mais frequência do que está escrito". portanto, diminuir a legibilidade do seu código para os desenvolvedores futuros é um preço alto a pagar. Eu optaria por não pagar, se pudesse usar uma fábrica.

Cort Ammon
fonte
1

Um subtipo nunca está "errado", desde que você sempre possa substituir uma instância do supertipo por uma instância do subtipo e fazer com que tudo ainda funcione corretamente. Isso ocorre desde que a subclasse nunca tente enfraquecer nenhuma das garantias que o supertipo faz. Ele pode oferecer garantias mais fortes (mais específicas), portanto, nesse sentido, a intuição de seu colega de trabalho está correta.

Dado que, a subclasse que você criou provavelmente não está errada (já que eu não sei exatamente o que eles fazem, não posso dizer com certeza.) Você precisa ter cuidado com o frágil problema da classe base e também precisa considere se um interfacebeneficiaria você, mas isso é verdade para todos os usos da herança.

Doval
fonte
1

Eu acho que a chave para responder a essa pergunta é olhar para esse cenário de uso específico.

A herança é usada para impor opções de configuração do banco de dados, como a cadeia de conexão.

Esse é um antipadrão, porque a classe está violando o Principal Responsável Único --- Ele está se configurando com uma fonte específica dessa configuração e não "Fazendo uma coisa e fazendo bem". A configuração de uma classe não deve ser inserida na própria classe. Para definir cadeias de conexão de banco de dados, na maioria das vezes eu vi isso acontecer no nível do aplicativo durante algum tipo de evento que ocorre quando o aplicativo é iniciado.

Greg Burghardt
fonte
1

Uso subclasses de construtor apenas regularmente para expressar conceitos diferentes.

Por exemplo, considere a seguinte classe:

public class Outputter
{
    private readonly IOutputMethod outputMethod;
    private readonly IDataMassager dataMassager;

    public Outputter(IOutputMethod outputMethod, IDataMassager dataMassager)
    {
        this.outputMethod = outputMethod;
        this.dataMassager = dataMassager;
    }

    public MassageDataAndOutput(string data)
    {
        this.outputMethod.Output(this.dataMassager.Massage(data));
    }
}

Podemos então criar uma subclasse:

public class CapitalizeAndPrintOutputter : Outputter
{
    public CapitalizeAndPrintOutputter()
        : base(new PrintOutputMethod(), new Capitalizer())
    {
    }
}

Você pode usar um CapitalizeAndPrintOutputter em qualquer lugar do seu código e saber exatamente que tipo de saída é esse. Além disso, esse padrão facilita muito o uso de um contêiner de DI para criar essa classe. Se o contêiner de DI conhecer o PrintOutputMethod e o Capitalizer, ele poderá criar automaticamente o CapitalizeAndPrintOutputter automaticamente para você.

O uso desse padrão é realmente bastante flexível e útil. Sim, você pode usar métodos de fábrica para fazer a mesma coisa, mas (pelo menos em C #) você perde parte do poder do sistema de tipos e, certamente, o uso de contêineres DI fica mais complicado.

Stephen
fonte
1

Quero observar primeiro que, à medida que o código é escrito, a subclasse não é mais específica que o pai, pois os dois campos inicializados são configuráveis ​​pelo código do cliente.

Uma instância da subclasse só pode ser distinguida usando um downcast.

Agora, se você tiver um design no qual as propriedades DatabaseEnginee ConnectionStringforam reimplementadas pela subclasse, acessada Configurationpara isso, terá uma restrição que faz mais sentido em ter a subclasse.

Dito isto, "anti-padrão" é forte demais para o meu gosto; não há nada realmente errado aqui.

Keith
fonte
0

No exemplo que você deu, seu parceiro está certo. Os dois construtores auxiliares de dados são estratégias. Um é mais específico que o outro, mas ambos são intercambiáveis ​​uma vez inicializados.

Basicamente, se um cliente do datahelperbuilder recebesse o systemdatahelperbuilder, nada seria interrompido. Outra opção seria fazer com que o systemdatahelperbuilder fosse uma instância estática da classe datahelperbuilder.

Michael Brown
fonte