Considere o seguinte design
public class Person
{
public virtual string Name { get; }
public Person (string name)
{
this.Name = name;
}
}
public class Karl : Person
{
public override string Name
{
get
{
return "Karl";
}
}
}
public class John : Person
{
public override string Name
{
get
{
return "John";
}
}
}
Você acha que há algo errado aqui? Para mim, as classes Karl e John devem ser apenas instâncias, em vez de classes, pois são exatamente iguais a:
Person karl = new Person("Karl");
Person john = new Person("John");
Por que eu criaria novas classes quando as instâncias são suficientes? As classes não adicionam nada à instância.
programming-practices
inheritance
class-design
anti-patterns
Ignacio Soler Garcia
fonte
fonte
Respostas:
Não há necessidade de ter subclasses específicas para cada pessoa.
Você está certo, essas devem ser instâncias.
Objetivo das subclasses: estender as classes pai
As subclasses são usadas para estender a funcionalidade fornecida pela classe pai. Por exemplo, você pode ter:
Uma classe pai
Battery
que podePower()
algo e pode ter umaVoltage
propriedade,E uma subclasse
RechargeableBattery
, que pode herdarPower()
eVoltage
, mas também pode serRecharge()
d.Observe que você pode passar uma instância da
RechargeableBattery
classe como parâmetro para qualquer método que aceite aBattery
como argumento. Isso é chamado de princípio de substituição de Liskov , um dos cinco princípios do SOLID. Da mesma forma, na vida real, se meu MP3 player aceitar duas pilhas AA, eu posso substituí-las por duas pilhas AA recarregáveis.Observe que às vezes é difícil determinar se é necessário usar um campo ou uma subclasse para representar uma diferença entre algo. Por exemplo, se você tiver que lidar com pilhas AA, AAA e 9 volts, criaria três subclasses ou usaria uma enumeração? “Substituir subclasse por campos” em Refatoração de Martin Fowler, página 232, pode fornecer algumas idéias e como passar de uma para outra.
No seu exemplo,
Karl
eJohn
não estenda nada, nem eles fornecem qualquer valor adicional: você pode ter exatamente a mesma funcionalidade usando aPerson
classe diretamente. Ter mais linhas de código sem valor adicional nunca é bom.Um exemplo de caso de negócios
O que poderia ser um caso de negócios em que realmente faria sentido criar uma subclasse para uma pessoa específica?
Digamos que criamos um aplicativo que gerencia pessoas que trabalham em uma empresa. O aplicativo também gerencia permissões, de modo que Helen, a contadora, não pode acessar o repositório SVN, mas Thomas e Mary, dois programadores, não podem acessar documentos relacionados à contabilidade.
Jimmy, o grande chefe (fundador e CEO da empresa) tem privilégios muito específicos que ninguém tem. Ele pode, por exemplo, desligar o sistema inteiro ou demitir uma pessoa. Você entendeu a ideia.
O modelo mais pobre para esse aplicativo é ter classes como:
porque a duplicação de código surgirá muito rapidamente. Mesmo no exemplo muito básico de quatro funcionários, você duplicará o código entre as classes Thomas e Mary. Isso fará com que você crie uma classe pai comum
Programmer
. Como você pode ter vários contadores também, provavelmente criaráAccountant
turma também.Agora, você percebe que ter a classe
Helen
não é muito útil, além de manterThomas
eMary
: a maior parte do seu código funciona de qualquer maneira no nível superior - no nível de contadores, programadores e Jimmy. O servidor SVN não se importa se é Thomas ou Mary quem precisa acessar o log - ele só precisa saber se é um programador ou um contador.Então você acaba removendo as classes que não usa:
"Mas posso manter o Jimmy como está, pois sempre haveria apenas um CEO, um grande chefe - Jimmy", você pensa. Além disso, Jimmy é muito usado em seu código, que se parece mais com isso, e não como no exemplo anterior:
O problema dessa abordagem é que Jimmy ainda pode ser atropelado por um ônibus e haveria um novo CEO. Ou o conselho de administração pode decidir que Mary é tão boa que deveria ser uma nova CEO, e Jimmy seria rebaixado para uma posição de vendedor. Agora, você precisa percorrer todo o seu código e mudar tudo.
fonte
É tolice usar esse tipo de estrutura de classe apenas para variar detalhes que obviamente devem ser campos configuráveis em uma instância. Mas isso é particular no seu exemplo.
Fazer diferentes
Person
classes diferentes é quase certamente uma má idéia - você teria que mudar seu programa e recompilar toda vez que uma nova Pessoa entrar no seu domínio. No entanto, isso não significa que herdar de uma classe existente para que uma string diferente seja retornada em algum lugar às vezes não é útil.A diferença é como você espera que as entidades representadas por essa classe variem. Presume-se que as pessoas quase sempre venham e vão, e espera-se que quase todos os aplicativos sérios que lidam com usuários possam adicionar e remover usuários em tempo de execução. Mas se você modelar coisas como algoritmos de criptografia diferentes, oferecer suporte a um novo provavelmente será uma grande mudança, e faz sentido inventar uma nova classe cujo
myName()
método retorne uma string diferente (e cujoperform()
método provavelmente faça algo diferente).fonte
Na maioria dos casos, você não faria isso. Seu exemplo é realmente um bom caso em que esse comportamento não agrega valor real.
Também viola o princípio Aberto Fechado , pois as subclasses basicamente não são uma extensão, mas modificam o funcionamento interno dos pais. Além disso, o construtor público do pai agora é irritante nas subclasses e a API tornou-se menos compreensível.
No entanto, algumas vezes, se você tiver apenas uma ou duas configurações especiais usadas com frequência em todo o código, às vezes é mais conveniente e menos demorado apenas subclassificar um pai que possui um construtor complexo. Nesses casos especiais, não vejo nada de errado com essa abordagem. Vamos chamá-lo de construtor curry j / k
fonte
Se essa é a extensão da prática, concordo que é uma má prática.
Se John e Karl têm comportamentos diferentes, as coisas mudam um pouco. Pode ser que ele
person
tenha um método paracleanRoom(Room room)
descobrir que John é um ótimo companheiro de quarto e limpa muito efetivamente, mas Karl não é e não limpa muito o quarto.Nesse caso, faria sentido que elas fossem sua própria subclasse com o comportamento definido. Existem maneiras melhores de conseguir a mesma coisa (por exemplo, uma
CleaningBehavior
classe), mas pelo menos dessa maneira não é uma violação terrível dos princípios de OO.fonte
Em alguns casos, você sabe que haverá apenas um número definido de instâncias e seria bom (embora feio na minha opinião) usar essa abordagem. É melhor usar um enum se o idioma permitir.
Exemplo Java:
fonte
Muitas pessoas já responderam. Pensei em dar minha própria perspectiva pessoal.
Era uma vez eu trabalhei em um aplicativo (e ainda faço) que cria música.
O aplicativo teve um resumo
Scale
classe com várias subclasses:CMajor
,DMinor
, etc.Scale
parecia algo como isso:Os geradores de música trabalharam com uma
Scale
instância específica para gerar música. O usuário selecionaria uma escala de uma lista para gerar música.Um dia, uma ideia interessante me veio à mente: por que não permitir que o usuário crie suas próprias escalas? O usuário selecionaria notas de uma lista, pressionaria um botão e uma nova escala seria adicionada à lista de escalas disponíveis.
Mas não fui capaz de fazer isso. Isso ocorreu porque todas as escalas já estão definidas em tempo de compilação - uma vez que são expressas como classes. Então me ocorreu:
Muitas vezes, é intuitivo pensar em termos de 'superclasses e subclasses'. Quase tudo pode ser expresso através deste sistema: superclasse
Person
e subclasseJohn
eMary
; superclasseCar
e subclasseVolvo
eMazda
; superclasseMissile
e subclassesSpeedRocked
,LandMine
eTrippleExplodingThingy
.É muito natural pensar dessa maneira, especialmente para a pessoa relativamente nova no OO.
Mas devemos sempre lembrar que classes são modelos e objetos são conteúdo derramado nesses modelos . Você pode derramar qualquer conteúdo que desejar no modelo, criando inúmeras possibilidades.
Não é tarefa da subclasse preencher o modelo. É o trabalho do objeto. O trabalho da subclasse é adicionar funcionalidade real ou expandir o modelo .
E é por isso que eu deveria ter criado uma
Scale
classe concreta , com umNote[]
campo, e deixar objetos preencherem esse modelo ; possivelmente através do construtor ou algo assim. E, eventualmente, eu fiz.Toda vez que você cria um modelo em uma classe (por exemplo, um
Note[]
membro vazio que precisa ser preenchido ou umString name
campo que precisa receber um valor), lembre-se de que é tarefa dos objetos dessa classe preencher o modelo ( ou possivelmente aqueles que criam esses objetos). As subclasses são destinadas a adicionar funcionalidade, não a preencher modelos.Você pode ser tentado a criar um tipo de sistema de "superclasse
Person
, subclasseJohn
eMary
", como você, porque gosta da formalidade que isso leva a você.Dessa forma, você pode apenas dizer
Person p = new Mary()
, em vez dePerson p = new Person("Mary", 57, Sex.FEMALE)
. Torna as coisas mais organizadas e mais estruturadas. Mas, como dissemos, criar uma nova classe para cada combinação de dados não é uma boa abordagem, pois sobrecarrega o código e limita você em termos de habilidades de tempo de execução.Então, aqui está uma solução: use uma fábrica básica, pode até ser estática. Igual a:
Dessa forma, você pode facilmente usar as 'predefinições' e 'vem com o programa', assim:,
Person mary = PersonFactory.createMary()
mas também se reserva o direito de projetar novas pessoas dinamicamente, por exemplo, no caso em que você deseja permitir que o usuário faça isso . Por exemplo:Ou melhor ainda: faça algo assim:
Eu me empolguei. Eu acho que você entendeu a ideia.
As subclasses não devem preencher os modelos definidos por suas superclasses. As subclasses são destinadas a adicionar funcionalidade . Objetos devem preencher os modelos, é para isso que eles servem.
Você não deve criar uma nova classe para todas as combinações possíveis de dados. (Assim como eu não deveria ter criado uma nova
Scale
subclasse para todas as combinações possíveis deNote
s).Aqui está uma diretriz: sempre que você criar uma nova subclasse, considere se ela inclui alguma nova funcionalidade que não exista na superclasse. Se a resposta a essa pergunta for "não", você pode estar tentando 'preencher o modelo' da superclasse, nesse caso, basta criar um objeto. (E possivelmente uma fábrica com 'presets', para facilitar a vida).
Espero que ajude.
fonte
Esta é uma exceção às 'deveria haver instâncias' aqui - embora um pouco extremas.
Se você deseja que o compilador imponha permissões para acessar métodos com base em Karl ou John (neste exemplo), em vez de solicitar à implementação do método que verifique qual instância foi passada, você poderá usar diferentes classes. Ao impor classes diferentes em vez de instanciação, você possibilita verificações de diferenciação entre 'Karl' e 'John' no tempo de compilação, em vez do tempo de execução, e isso pode ser útil - particularmente em um contexto de segurança em que o compilador pode ser mais confiável que o tempo de execução verificações de código de (por exemplo) bibliotecas externas.
fonte
É uma prática ruim ter código duplicado .
Isso ocorre pelo menos parcialmente porque as linhas de códigos e, portanto, o tamanho da base de código se correlacionam com os custos e defeitos de manutenção .
Aqui está a lógica relevante do senso comum para mim neste tópico específico:
1) Se eu precisasse mudar isso, quantos lugares precisaria visitar? Menos é melhor aqui.
2) Existe uma razão pela qual isso é feito dessa maneira? No seu exemplo - não poderia haver -, mas em alguns exemplos, pode haver algum tipo de razão para fazer isso. Uma coisa que eu posso pensar é que, em alguns frameworks, como no início do Grails, onde a herança não necessariamente funcionava bem com alguns dos plugins do GORM. Poucos e distantes entre si.
3) É mais limpo e fácil de entender dessa maneira? Novamente, não está no seu exemplo - mas existem alguns padrões de herança que podem ser mais extensos em que classes separadas são realmente mais fáceis de manter (certos usos dos padrões de comando ou estratégia vêm à mente).
fonte
Há um caso de uso: formar uma referência a uma função ou método como um objeto regular.
Você pode ver isso no código da GUI Java:
O compilador ajuda você aqui criando uma nova subclasse anônima.
fonte