Está criando subclasses para instâncias específicas uma prática ruim?

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 fossem suficientes? As classes não adicionam nada à instância.

    
por Ignacio Soler Garcia 29.09.2014 / 16:57
fonte

9 respostas

77

Não há necessidade de ter subclasses específicas para cada pessoa.

Você está certo, essas devem ser instâncias.

Meta de subclasses: para estender 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 pode Power() something e pode ter uma propriedade Voltage ,

  • 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 de RechargeableBattery class como um parâmetro para qualquer método que aceite Battery como argumento. Isso é chamado de Princípio da substituição de Liskov , um dos cinco princípios do SOLID. Da mesma forma, na vida real, se meu MP3 player aceitar duas baterias AA, posso substituí-las por duas pilhas AA recarregáveis.

Observe que às vezes é difícil determinar se você precisa 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, você criaria três subclasses ou usaria um enum? "Substituir subclasse por campos" em Refatoração por Martin Fowler, página 232, pode fornecer você algumas idéias e como passar de um para outro.

No seu exemplo, Karl e John não estendem nada, nem fornecem nenhum valor adicional: você pode ter exatamente a mesma funcionalidade usando a classe Person diretamente. Ter mais linhas de código sem valor adicional nunca é bom.

Um exemplo de um caso de negócios

O que poderia ser um caso de negócio em que faria sentido criar uma subclasse para uma pessoa específica?

Digamos que criemos um aplicativo que gerencie pessoas trabalhando em uma empresa. O aplicativo também gerencia permissões, portanto, Helen, o contador, 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 todo o sistema ou demitir uma pessoa. Você entendeu a ideia.

O modelo mais pobre para tal aplicação é ter classes como:

porqueaduplicaçãodecódigosurgirámuitorapidamente.Mesmonoexemplobásicodequatrofuncionários,vocêduplicaráocódigoentreasclassesThomaseMary.IssofarácomquevocêcrieumaclassepaicomumProgrammer.Comovocêtambémpodetervárioscontadores,provavelmentetambémcriaráAccountantclass.

Agora, você percebe que ter a classe Helen não é muito útil, bem como manter Thomas e Mary : a maior parte do seu código funciona no nível superior de qualquer maneira - 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:

"Mas eu posso manter Jimmy como está, já que sempre haveria apenas um CEO, um grande chefe - Jimmy", você pensa. Além disso, o Jimmy é muito usado em seu código, que na verdade 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 com essa abordagem é que Jimmy ainda pode ser atropelado por um ônibus, e haveria um novo CEO. Ou a diretoria pode decidir que Mary é tão boa que ela deveria ser uma nova CEO, e Jimmy seria rebaixado a cargo de vendedor, então agora você precisa percorrer todo seu código e mudar tudo.

    
por 29.09.2014 / 17:00
fonte
15

É bobagem usar esse tipo de estrutura de classe apenas para variar detalhes que devem obviamente ser campos configuráveis em uma instância. Mas isso é particular para o seu exemplo.

Fazer diferentes classes Person s é quase certamente uma má ideia - você teria que alterar seu programa e recompilar toda vez que uma nova pessoa entra em 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. As pessoas quase sempre são consideradas como indo e vindo, e quase todos os aplicativos sérios que lidam com usuários devem ser capazes de adicionar e remover usuários em tempo de execução. Mas se você modelar coisas como diferentes algoritmos de criptografia, apoiar um novo provavelmente é uma grande mudança, e faz sentido inventar uma nova classe cujo método myName() retorne uma string diferente (e cujo método perform() presumivelmente faça algo diferente ).

    
por 29.09.2014 / 17:03
fonte
12

Why would I create new classes when instances are enough?

Na maioria dos casos, você não faria isso. Seu exemplo é realmente um bom caso em que esse comportamento não agrega valor real.

Ele também viola o princípio Fechado Aberto , já que as subclasses basicamente não são uma extensão, mas modificam a Trabalhos internos. Além disso, o construtor público do pai agora está irritando as subclasses e a API tornou-se menos compreensível.

No entanto, às vezes, se você tiver apenas uma ou duas configurações especiais que são frequentemente usadas em todo o código, às vezes é mais conveniente e menos demorado simplesmente subclassificar um pai que tenha um construtor complexo. Em casos tão especiais, não vejo nada de errado com essa abordagem. Vamos chamá-lo construtor de curry j / k

    
por 29.09.2014 / 17:03
fonte
8

Se esta é a extensão da prática, então eu concordo que esta é uma prática ruim.

Se John e Karl tiverem comportamentos diferentes, as coisas mudam um pouco. Pode ser que person tenha um método para cleanRoom(Room room) , onde John é um ótimo colega de quarto e limpa com muita eficácia, mas Karl não é e não limpa muito a sala.

Nesse caso, faria sentido que eles fossem sua própria subclasse com o comportamento definido. Existem maneiras melhores de conseguir a mesma coisa (por exemplo, uma CleaningBehavior de classe), mas pelo menos dessa maneira não é uma violação terrível dos princípios OO.

    
por 29.09.2014 / 18:15
fonte
6

Em alguns casos, você sabe que haverá apenas um número definido de instâncias e, em seguida, seria correto (embora feio na minha opinião) usar essa abordagem. É melhor usar um enum se a linguagem permitir.

Exemplo de 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;
    }
}
    
por 29.09.2014 / 17:35
fonte
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 tinha uma classe Scale abstrata com várias subclasses: CMajor , DMinor , etc. Scale parecia algo assim:

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 instância específica de Scale para gerar música. O usuário selecionaria uma escala de uma lista para gerar música de.

Um dia, uma idéia legal veio à minha 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 às escalas disponíveis da lista.

Mas eu não consegui fazer isso. Isso porque todas as escalas já estão definidas em tempo de compilação - já que são expressas como classes. Então me bateu:

É geralmente intuitivo pensar em termos de 'superclasses e subclasses'. Quase tudo pode ser expresso através deste sistema: superclasse Person e subclasses John e Mary ; superclasse Car e subclasses Volvo e Mazda ; superclasse Missile e subclasses SpeedRocked , LandMine e TrippleExplodingThingy .

É muito natural pensar dessa maneira, especialmente para a pessoa relativamente nova no OO.

Mas devemos sempre lembrar que as classes são modelos e os objetos são enviados para esses modelos . Você pode derramar o conteúdo que quiser no modelo, criando inúmeras possibilidades.

Não é tarefa da subclasse preencher o modelo. É o trabalho do objeto. O trabalho da subclasse é adicionar a funcionalidade real ou expandir o modelo .

E é por isso que eu deveria ter criado uma classe Scale concreta, com um campo Note[] , e deixar que os objetos preencham esse modelo ; possivelmente através do construtor ou algo assim. E, finalmente, eu fiz.

Toda vez que você cria um modelo em uma classe (por exemplo, um membro Note[] vazio que precisa ser preenchido ou um campo String name que precisa receber um valor), lembre-se que é o trabalho dos objetos dessa classe preencher o modelo (ou possivelmente aqueles que criam esses objetos). As subclasses servem para adicionar funcionalidade, não para preencher modelos.

Você pode ser tentado a criar um tipo de sistema "superclasse Person , subclasses John e Mary ", como você fez, porque você gosta da formalidade que isso faz para você.

Dessa forma, você pode apenas dizer Person p = new Mary() , em vez de Person p = new Person("Mary", 57, Sex.FEMALE) . Isso 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, já que ela injeta o código em nada e limita você em termos de habilidades em tempo de execução.

Então, aqui está uma solução: usar uma fábrica básica, pode até ser uma fábrica estática. Assim:

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' como 'vem com o programa', assim: Person mary = PersonFactory.createMary() , mas também se reserva o direito de criar dinamicamente novas pessoas, 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 são destinadas a preencher os modelos definidos por suas superclasses. As subclasses servem para adicionar funcionalidade . Objeto serve para preencher os modelos, é para isso que eles servem.

Você não deveria criar uma nova classe para todas as combinações possíveis de dados. (Assim como eu não deveria ter criado uma nova subclasse Scale para todas as combinações possíveis de Note s).

Her'es uma diretriz: sempre que você criar uma nova subclasse, considere se ela adiciona alguma nova funcionalidade que não exista na superclasse. Se a resposta a essa pergunta for "não", do que você pode estar tentando "preencher o modelo" da superclasse, nesse caso apenas crie um objeto. (E possivelmente uma fábrica com 'presets' ', para facilitar a vida).

Espero que ajude.

    
por 01.10.2014 / 14:48
fonte
2

Esta é uma exceção ao 'deve haver instâncias' aqui - embora um pouco extrema.

Se você quiser que o compilador imponha permissões para acessar métodos com base em Karl ou John (neste exemplo), em vez de solicitar a implementação do método para verificar qual instância foi passada, você pode usar classes diferentes. Ao impor classes diferentes em vez de instanciar, você faz verificações de diferenciação entre 'Karl' e 'John' em tempo de compilação, em vez de tempo de execução e isso pode ser útil - particularmente em um contexto de segurança onde o compilador pode ser mais confiável do que verificações de código de tempo de execução de (por exemplo) bibliotecas externas.

    
por 29.09.2014 / 19:47
fonte
2

Considera-se uma prática ruim ter código duplicado .

Isso é pelo menos parcialmente porque linhas de códigos e portanto, o tamanho da base de código se correlaciona com custos de manutenção e defeitos .

Aqui está a lógica relevante do senso comum para mim neste tópico em particular:

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. Um que posso pensar em cima da minha cabeça é em alguns frameworks como o Grails antigo, onde a herança não necessariamente funcionava muito bem com alguns dos plugins do GORM. Poucos e distantes entre.

3) É mais limpo e mais 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, onde as classes separadas são realmente mais fáceis de manter (certos usos dos padrões de comando ou estratégia vêm à mente).

    
por 29.09.2014 / 22:46
fonte
0

Há um caso de uso: formando uma referência a uma função ou método como um objeto regular.

Você pode ver isso muito 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.

    
por 30.09.2014 / 11:19
fonte