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

37

Eu estava tendo uma discussão com um colega de trabalho e acabamos tendo intuições conflitantes sobre o propósito da subclassificação. 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 ela não deveria ser uma subclasse. Ele argumentou pela intuição oposta: que a subclasse representa um objeto sendo mais "específico" e, portanto, uma relação de subclasse é mais apropriada.

Para colocar minha intuição mais concretamente, acho que se eu tiver uma subclasse que estenda uma classe pai, mas o único código que a subclasse substitui é um construtor (sim, eu sei que os construtores geralmente não "sobrescrevem", suportem ), então o que era realmente necessário era um método auxiliar.

Por exemplo, considere esta classe um tanto 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;
    }
}

Sua afirmação é de que seria inteiramente 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, quais dessas intuições estão corretas? A subclassificação é descrita acima como um antipadrão?
  2. Se for um antipadrão, qual é o nome dele?

Peço desculpas se isso for uma resposta fácil de usar; minhas buscas no google principalmente retornaram informações sobre o antipadrão do construtor telescópico e não exatamente o que eu estava procurando.

    
por HardlyKnowEm 26.02.2015 / 20:27
fonte

11 respostas

54

Se tudo o que você quer fazer é criar classe X com certos argumentos, a subclasse é uma maneira estranha de expressar essa intenção, porque você não está usando nenhum dos recursos que as classes e a herança fornecem. Não é realmente um anti-padrão, é apenas estranho e um pouco inútil (a menos que você tenha algumas outras razões para isso). Uma maneira mais natural de expressar essa intenção seria um Método de Fábrica , que nesse 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 formas potencialmente perigosas de pensar sobre subclasses, porque ambas implicam que fazer da Square uma subclasse de Rectangle é uma boa ideia. Sem depender de algo formal como o LSP, eu diria que uma melhor intuição é que uma subclasse fornece uma implementação da interface base, ou estende a interface para adicionar alguma nova funcionalidade .

    
por 26.02.2015 / 20:45
fonte
16

Which of these intuitions is correct?

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

Pense nisso, as classes representam possíveis valores legais. Se a classe A tiver um campo de byte F , você pode estar inclinado a pensar que A tem 256 valores legais, mas essa intuição está incorreta. A está restringindo "toda permutação de valores" a "deve ter o campo F be 0-255".

Se você estender A com B , que tem outro campo de byte FF , isto é, adicionando restrições . Em vez de "valor em que F é byte", você tem "valor em que F é byte & & FF també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ê está adicionando regras, você está se especializando mais longe de "poderia ser qualquer coisa".

Ou para pensar de outra forma, assuma que A tinha vários subtipos: B , C e D . Uma variável do tipo A poderia ser qualquer um desses subtipos, mas uma variável do tipo B é mais específica. Ele (na maioria dos sistemas de tipos) não pode ser um C ou um D .

Is this an anti-pattern?

Enh? Ter objetos especializados é útil e não prejudica claramente o código. Conseguir esse resultado usando 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 anti-padrão, uma vez que não é claramente errado e ruim em qualquer situação.

    
por 26.02.2015 / 20:38
fonte
12

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

  1. Se você quiser usar a verificação de tempo de compilação para garantir que as coleções de objetos estejam em conformidade com apenas uma subclasse específica. Por exemplo, se você tem MySQLDao e SqliteDao em seu sistema, mas por algum motivo você quer ter certeza de que uma coleção contém somente dados de uma fonte, se você usar subclasses como você descreve, você pode fazer com que o compilador verifique correção. Por outro lado, se você armazenar a fonte de dados como um campo, isso se tornará uma verificação em tempo de execução.
  2. Meu aplicativo atual tem um relacionamento de um para um entre AlgorithmConfiguration + ProductClass e AlgorithmInstance . Em outras palavras, se você configurar FooAlgorithm para ProductClass no arquivo de propriedades, só poderá obter um FooAlgorithm para essa classe de produto. A única maneira de obter dois FooAlgorithm s para um determinado ProductClass é criar uma subclasse FooBarAlgorithm . Tudo bem, porque torna o arquivo de propriedades fácil de usar (não há necessidade de sintaxe para muitas configurações diferentes em uma seção no arquivo de propriedades), e é muito raro haver mais de uma instância para uma determinada classe de produto.
  3. @selastyn's answer dá outro bom exemplo.

Linha de fundo: Não há realmente nenhum dano em fazer isso. Um Anti-padrão é definido como:

An anti-pattern (or antipattern) is a common response to a recurring problem that is usually ineffective and risks being highly counterproductive.

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

    
por 26.02.2015 / 20:44
fonte
9

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

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

Ele estava em um idioma chamado LPC e implementou uma estrutura para lançar feitiços em um jogo. Você tinha uma superclasse de feitiços, que tinha uma subclasse de feitiços de combate que controlava algumas verificações, e então uma subclasse de feitiços de dano direto de alvo único, que era então subclassificada para as magias individuais - míssil mágico, relâmpago e afins. >

A natureza de como o LPC funcionava era que você tinha um objeto mestre que era um Singleton. antes de ir 'eww Singleton' - foi e não foi "eww" em tudo. Um não fez cópias das magias, mas sim invocou os métodos (efetivamente) estáticos nas magias. O código do 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 única classe de construtor. Ele definiu alguns valores que estavam em sua classe abstrata pai (não, ele não deveria usar setters - é um imutável) e foi isso. Funcionou direito . Foi fácil de codificar, fácil de ampliar 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 ela herda. Dê uma olhada no StringBuilder em Java para um exemplo disso - não apenas construtor, mas estou pressionado para 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).

    
por 26.02.2015 / 23:35
fonte
2

O uso mais comum de tais subclasses que eu posso imaginar são as hierarquias de exceção, embora seja um caso degenerado em que normalmente não definimos 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 ReadError não precisa substituir nenhum método de IOError .

Fazemos isso porque exceções são capturadas ao verificar o tipo delas. Assim, precisamos codificar a especialização no tipo para que alguém possa capturar somente ReadError sem capturar todos os IOError , caso deseje.

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

Ele ainda pode ser útil, por exemplo, se o nome do tipo aparecer no formulário de string registrada do objeto: isso é linguagem e possivelmente 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 substituiríamos mais do que apenas o construtor em SystemDataHelperBuilder , também substitui loggingname() . Então, se existe algum registro em seu sistema, você pode imaginar que, embora sua classe literalmente apenas substitua o construtor, "moralmente" também está substituindo o nome da classe e, para fins práticos, não é uma subclasse apenas de construtor. Ele tem um comportamento diferente desejável, é apenas que você obtém esse comportamento de graça, sem escrever nenhum método, graças a um uso inteligente da reflexão em outro lugar.

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

No entanto, usar tipos para casos especiais gera alguma confusão, pois se ImmutableRectangle(1,1) e ImmutableSquare(1) se comportarem de maneira diferente de alguma forma , eventualmente alguém se perguntará por que e talvez deseje t. Ao contrário 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 um SystemDataHelperBuilder e um DataHelperBuilder criado com os mesmos argumentos exatos e que tem potencial para causar problemas. Portanto, uma função para retornar um objeto criado com os argumentos "certos" parece-me normalmente 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ê normalmente tentaria não fazer.

Note que eu não falo em termos de padrões de design e antipadrões, porque não acredito que seja possível fornecer uma taxonomia de todas as boas e más idéias que um programador tem já pensou. Assim, nem toda idéia é um padrão ou antipadrão, só se torna quando alguém identifica que ela ocorre repetidamente em contextos diferentes e a nomeia ;-) Eu não tenho conhecimento de nenhum nome particular para esse tipo de subclasse.

    
por 27.02.2015 / 13:55
fonte
2

A mais rígida definição orientada a objetos de uma relação de subclasse é conhecida como «é-a». Usando o exemplo tradicional de um quadrado e um retângulo, um quadrado é um retângulo.

Com isso, você deve usar a subclasse para qualquer situação em que achar que um "is-a" é um relacionamento significativo para seu código.

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

A pergunta que você deve responder é se algum outro código aproveita essa relação «é-um». 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, então o que você tem é um relacionamento que poderia ser um relacionamento "é-um" ou poderia ser implementado com uma fábrica. Nesse caso, você poderia ir de qualquer forma, mas eu recomendaria uma relação Fábrica, em vez de uma relação "é-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 "é-um" que eles precisarão para entender o código. Assim, colocar esse comportamento em uma subclasse promove o comportamento de "algo que alguém encontrará se investigar os métodos da superclasse" para "algo que todo mundo procurará 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", por isso a legibilidade decrescente do código para desenvolvedores futuros é um preço alto a pagar. Eu preferiria não pagar, se eu pudesse usar uma fábrica.

    
por 27.02.2015 / 18:17
fonte
1

Um subtipo nunca é "errado", desde que você sempre possa substituir uma instância de seu supertipo por uma instância do subtipo e fazer com que tudo funcione corretamente. Isto verifica enquanto a subclasse nunca tenta enfraquecer qualquer das garantias que o supertipo faz. Ele pode fazer garantias mais strongs (mais específicas), portanto, nesse sentido, a intuição de seu colega de trabalho está correta.

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

    
por 26.02.2015 / 21:49
fonte
1

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 string de conexão.

Este é um anti-padrão porque a classe está violando o Principal Responsabilidade Única --- 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 incluída na própria classe. Para definir strings de conexão de banco de dados, na maioria das vezes, vi isso acontecer no nível do aplicativo durante algum tipo de evento que ocorre quando o aplicativo é inicializado.

    
por 26.02.2015 / 22:05
fonte
1

Eu uso subclasses de construtores 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 então usar um CapitalizeAndPrintOutputter em qualquer lugar em seu código e você sabe exatamente que tipo de outputter é. Além disso, esse padrão realmente torna muito fácil usar um contêiner DI para criar essa classe. Se o container DI conhecer o PrintOutputMethod e o Capitalizer, ele pode até mesmo criar automaticamente o CapitalizeAndPrintOutputter para você.

Usar esse padrão é realmente bastante flexível e útil. Sim, você pode usar métodos de fábrica para fazer a mesma coisa, mas então (pelo menos em C #) você perde um pouco do poder do sistema de tipos e certamente usar contêineres DI torna-se mais trabalhoso.

    
por 27.02.2015 / 00:58
fonte
1

Deixe-me primeiro observar que, como o código está escrito, a subclasse não é mais específica do que o pai, já que os dois campos inicializados são ambos 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 DatabaseEngine e ConnectionString foram reimplementadas pela subclasse, que acessou Configuration para fazer isso, você terá uma restrição que fará mais sentido de ter a subclasse.

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

    
por 27.02.2015 / 04:15
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 depois de inicializados.

Basicamente, se um cliente do datahelperbuilder recebesse o systemdatahelperbuilder, nada iria quebrar. Outra opção seria ter o systemdatahelperbuilder como uma instância estática da classe datahelperbuilder.

    
por 27.02.2015 / 16:55
fonte