Criar subclasses para instâncias específicas é uma má prática?


37

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.


17
Infelizmente, você encontrará isso em muitos códigos de produção. Limpe quando puder.
Adam Zuckerman

14
Este é um grande projeto - se um desenvolvedor quer tornar-se indispensável para cada mudança nos dados de negócios, e não tem problemas para ser disponível 24/7 ;-)
Doc Brown

11
Aqui está um tipo de pergunta relacionada, em que o OP parece acreditar no oposto da sabedoria convencional. programmers.stackexchange.com/questions/253612/…
Dunk

11
Crie suas classes dependendo do comportamento, não dos dados. Para mim, as subclasses não adicionam novo comportamento à hierarquia.
Songo 30/09

2
Isso é amplamente usado com subclasses anônimas (como manipuladores de eventos) em Java antes dos lambdas chegarem ao Java 8 (que é a mesma coisa com sintaxe diferente).
marczellm

Respostas:


77

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 Batteryque pode Power()algo e pode ter uma Voltagepropriedade,

  • E uma subclasse RechargeableBattery, que pode herdar Power()e Voltage, mas também pode ser Recharge()d.

Observe que você pode passar uma instância da RechargeableBatteryclasse como parâmetro para qualquer método que aceite a Batterycomo 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, Karle Johnnão estenda nada, nem eles fornecem qualquer valor adicional: você pode ter exatamente a mesma funcionalidade usando a Personclasse 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:

          O diagrama de classes mostra as classes Helen, Thomas, Mary e Jimmy e seus pais comuns: a classe abstrata Person.

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á Accountantturma também.

          O diagrama de classes mostra uma Pessoa abstrata com três filhos: Contador abstrato, um Programador abstrato e Jimmy.  O contador tem uma subclasse Helen e o programador tem duas subclasses: Thomas e Mary.

Agora, você percebe que ter a classe Helennão é muito útil, além de manter Thomase Mary: 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.

if (person is Programmer)
{
    this.AccessGranted = true;
}

Então você acaba removendo as classes que não usa:

          O diagrama de classes contém as subclasses Accountant, Programmer e Jimmy com sua classe pai comum: abstract Person.

"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:

if (person is Jimmy)
{
    this.GiveUnrestrictedAccess(); // Because Jimmy should do whatever he wants.
}
else if (person is Programmer)
{
    this.AccessGranted = true;
}

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.


6
@DocBrown: Muitas vezes fico surpreso ao ver que respostas curtas que não estão erradas, mas que não são profundas o suficiente, têm uma pontuação muito maior do que as respostas que explicam em profundidade o assunto. Provavelmente muitas pessoas não gostam de ler muito, então respostas muito curtas parecem mais atraentes para elas. Ainda assim, editei minha resposta para fornecer pelo menos alguma explicação.
Arseni Mourzenko

3
Respostas curtas tendem a ser postadas primeiro. As publicações anteriores recebem mais votos.
Tim B

11
@ TimB: Geralmente começo com respostas de tamanho médio e as edito para serem muito completas (aqui está um exemplo , considerando que provavelmente havia cerca de quinze revisões, apenas quatro sendo mostradas). Percebi que quanto mais se torna a resposta ao longo do tempo, menos votos recebe; uma pergunta semelhante que permanece curta atrai mais votos.
Arseni Mourzenko

Concorde com @DocBrown. Por exemplo, uma boa resposta diria algo como "subclasse por comportamento , não por conteúdo" e se referir a alguma referência que não farei neste comentário.
user949300

2
Caso isso não tenha ficado claro no parágrafo anterior: Mesmo se você tiver apenas uma pessoa com acesso irrestrito, você ainda deve tornar essa pessoa uma instância de uma classe separada que implementa o acesso irrestrito. A verificação da própria pessoa resulta em interação código-dados e pode abrir uma lata inteira de worms. Por exemplo, e se o Conselho de Administração decidir nomear um triunvirato como CEO? Se você não tivesse uma classe "Irrestrita", não precisaria apenas atualizar o CEO existente, mas também adicionar mais 2 verificações para os outros membros. Se você o tiver, basta defini-los como "Irrestrito".
Nzall 30/09

15

É 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 Personclasses 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 cujo perform()método provavelmente faça algo diferente).


12

Por que eu criaria novas classes quando as instâncias são suficientes?

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


Não tenho certeza se concordo com o parágrafo do OCP. Se uma classe está expondo uma propriedade setter aos seus descendentes o então supondo que ele está seguindo um bom design principles- a propriedade deve não ser parte de seu funcionamento interno
Ben Aaronson

@BenAaronson Desde que o comportamento da propriedade em si não seja alterado, você não violará o OCP, mas aqui eles o fazem. Claro que você pode usar essa propriedade em seu funcionamento interno. Mas você não deve alterar o comportamento existente! Você só deve estender o comportamento, não alterá-lo com herança. Claro, existem exceções para todas as regras.
Falcon

11
Então, qualquer uso de membros virtuais é uma violação do OCP?
Ben Aaronson

3
@ Falcon Não há necessidade de derivar uma nova classe apenas para evitar chamadas repetitivas de construtores complexos. Basta criar fábricas para os casos comuns e parametrizar as pequenas variações.
Keen

@BenAaronson, eu diria que ter membros virtuais com uma implementação real é uma violação. Membros abstratos, ou digam métodos que não fazem nada, seriam pontos de extensão válidos. Basicamente, quando você olha para o código de uma classe base, sabe que ela será executada como está, em vez de todo método não final ser candidato a ser substituído por algo totalmente diferente. Ou, dito de outra maneira: as maneiras pelas quais uma classe herdada pode afetar o comportamento da classe base serão explícitas e restritas a serem "aditivas".
Millimoose #

8

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 persontenha um método para cleanRoom(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 CleaningBehaviorclasse), mas pelo menos dessa maneira não é uma violação terrível dos princípios de OO.


Nenhum comportamento diferente, apenas dados diferentes. Obrigado.
Ignacio Soler Garcia

6

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:

public enum Person
{
    KARL("Karl"),
    JOHN("John")

    private final String name;

    private Person(String name)
    {
        this.name = name;
    }

    public String getName()
    {
        return this.name;
    }
}

6

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 Scaleclasse com várias subclasses: CMajor, DMinor, etc. Scaleparecia algo como isso:

public abstract class Scale {
    protected Note[] notes;
    public Scale() {
        loadNotes();
    }
    // .. some other stuff ommited
    protected abstract void loadNotes(); /* subclasses put notes in the array
                                          in this method. */
}

Os geradores de música trabalharam com uma Scaleinstâ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 Persone subclasse Johne Mary; superclasse Care subclasse Volvoe Mazda; superclasse Missilee subclasses SpeedRocked, LandMinee TrippleExplodingThingy.

É 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 Scaleclasse concreta , com um Note[]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 um String namecampo 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, subclasse Johne Mary", como você, porque gosta da formalidade que isso leva a você.

Dessa forma, você pode apenas dizer Person p = new Mary(), em vez de Person 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:

public final class PersonFactory {
    private PersonFactory() { }

    public static Person createJohn(){
        return new Person("John", 40, Sex.MALE);
    }

    public static Person createMary(){
        return new Person("Mary", 57, Sex.FEMALE);
    }

    // ...
}

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:

// .. requesting the user for input ..
String name = // user input
int age = // user input
Sex sex = // user input, interpreted
Person newPerson = new Person(name, age, sex);

Ou melhor ainda: faça algo assim:

public final class PersonFactory {
    private PersonFactory() { }

    private static Map<String, Person> persons = new HashMap<>();
    private static Map<String, PersonData> personBlueprints = new HashMap<>();

    public static void addPerson(Person person){
        persons.put(person.getName(), person);
    }

    public static Person getPerson(String name){
        return persons.get(name);
    }

    public static Person createPerson(String blueprintName){
        PersonData data = personBlueprints.get(blueprintName);
        return new Person(data.name, data.age, data.sex);
    }

    // .. or, alternative to the last method
    public static Person createPerson(String personName){
        Person blueprint = persons.get(personName);
        return new Person(blueprint.getName(), blueprint.getAge(), blueprint.getSex());
    }

}

public class PersonData {
    public String name;
    public int age;
    public Sex sex;

    public PersonData(String name, int age, Sex sex){
        this.name = name;
        this.age = age;
        this.sex = sex;
    }
}

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 Scalesubclasse para todas as combinações possíveis de Notes).

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.


Este é um excelente exemplo, muito obrigado.
Ignacio Soler Garcia

@SoMoS Ainda bem que pude ajudar.
Aviv Cohn

2

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.


2

É 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).


11
se é ruim ou não, não está imutável. Existem, por exemplo, cenários em que a sobrecarga extra de criação de objeto e chamadas de método pode ser tão prejudicial para o desempenho que o aplicativo não atende mais às especificações.
jwenting

Veja o ponto 2. Claro que há razões pelas quais, ocasionalmente. É por isso que você precisa perguntar antes de eliminar o código de alguém.
precisa saber é o seguinte

0

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:

ActionListener a = new ActionListener() {
    public void actionPerformed(ActionEvent arg0) {
        /* ... */
    }
};

O compilador ajuda você aqui criando uma nova subclasse anônima.

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.