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:
- 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?
- 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.
fonte
Respostas:
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.
fonte
SystemDataHelperBuilder
especificamente.Seu colega de trabalho está correto (assumindo sistemas do tipo padrão).
Pense bem, as classes representam possíveis valores legais. Se a classe
A
tiver um campo de bytesF
, você pode pensar queA
tem 256 valores legais, mas essa intuição está incorreta.A
está restringindo "toda permutação de valores já" a "deve ter o campo entre 0 eF
255".Se você estender
A
comB
outro campo de bytesFF
, isso inclui restrições . Em vez de "valor ondeF
é byte", você tem "valor ondeF
é 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ê adiciona regras, você se especializa ainda mais em "poderia ser qualquer coisa".Ou pensar de outra forma, assumir que
A
tinha um monte de subtipos:B
,C
, eD
. Uma variável do tipoA
pode ser qualquer um desses subtipos, mas uma variável do tipoB
é mais específica. (Na maioria dos sistemas de tipos) não pode ser aC
ou aD
.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.
fonte
byte and byte
definitivamente tem mais valores do que apenas o tipobyte
; 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.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 deA
também é um elemento deB
" precisamente porque, uma vez que seus conjuntos se tornam infinitos, pode ser que eles tenham a mesma cardinalidade, mas tenham elementos diferentes.Não, não é um anti-padrão. Posso pensar em vários casos de uso prático para isso:
MySQLDao
e estáSqliteDao
em 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.AlgorithmConfiguration
+ProductClass
eAlgorithmInstance
. Em outras palavras, se você configurarFooAlgorithm
paraProductClass
no arquivo de propriedades, poderá obter apenas umFooAlgorithm
para essa classe de produto. A única maneira de obter doisFooAlgorithm
s para um dadoProductClass
é criar uma subclasseFooBarAlgorithm
. 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.Bottom line: Não há realmente nenhum mal em fazer isso. Um antipadrão é definido como:
Não há risco de ser altamente contraproducente aqui, por isso não é um anti-padrão.
fonte
Se algo é 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 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:
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).
fonte
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 deIOError
, e assim por diante, masReadError
não precisamos substituir nenhum método deIOError
.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
ReadError
sem capturar tudoIOError
, 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 substituirloggingname()
. 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 nomeSystemDataHelperBuilder
de 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 seImmutableSquare(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 seheight == width
, não cominstanceof
. No seu exemplo, há uma diferença entre aSystemDataHelperBuilder
e aDataHelperBuilder
criados 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.
fonte
ImmutableSquareMatrix:ImmutableMatrix
, desde que houvesse um meio eficiente de solicitar que umImmutableMatrix
renderizasse seu conteúdo comoImmutableSquareMatrix
possível. Mesmo queImmutableSquareMatrix
fosse basicamente umImmutableMatrix
construtor cujo requeresse que a altura e a largura correspondessem, e cujoAsSquareMatrix
método simplesmente retornasse (em vez de, por exemplo, construir umImmutableSquareMatrix
que 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 matrizesSystemDataHelperBuilder
, e não qualquerDataHelperBuilder
uma, 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 .height == width
, não cominstanceof
". Você pode preferir algo que possa afirmar estaticamente.foo=new ImmutableMatrix(someReadableMatrix)
é mais claro que o desomeReadableMatrix.AsImmutable()
ou mesmoImmutableMatrix.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.new
operador. 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.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.
fonte
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
interface
beneficiaria você, mas isso é verdade para todos os usos da herança.fonte
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.
fonte
Uso subclasses de construtor apenas regularmente para expressar conceitos diferentes.
Por exemplo, considere a seguinte classe:
Podemos então criar uma subclasse:
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.
fonte
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
DatabaseEngine
eConnectionString
foram reimplementadas pela subclasse, acessadaConfiguration
para 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.
fonte
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.
fonte