Classes especificas para crianças vazias

5

As aulas para crianças estão vazias, apenas para especificar uma prática ruim?

Vamos supor que eu tenha uma classe de produto genérica. Existem produtos elétricos, eletrônicos e mecânicos. Eu preciso representar todos eles e deve ser capaz de diferenciar cada um em algumas operações. Eu tenho duas opções:

  • adicione um membro chamado "type" na classe Product para especificar se é um produto elétrico, eletrônico ou mecânico.

  • crie classes de produtos para crianças, uma para cada tipo de produto: ElectricalProduct, ElectronicProduct e MechanicalProduct. Como essas classes não diferem umas das outras em métodos ou membros - já definidas na classe pai - elas estariam todas vazias.

Eu considero a segunda opção uma abordagem melhor, mas é o caminho certo? Eu nunca vi tal coisa em qualquer aplicativo ou biblioteca.

O que você acha? Alguma sugestão?

Editar:

Aqui está um exemplo de como fazer isso usando a segunda opção: link

Eu não vejo uma maneira melhor de fazer isso, mas não sei se é uma boa prática ter aulas vazias.

    
por Tiago.SR 30.11.2014 / 23:00
fonte

5 respostas

1

Agora que você explicou mais detalhadamente, parece que realmente não há lógica específica para subclasses. Nesse caso, a herança não é a escolha certa. Se realmente existem apenas maneiras pequenas em que outras partes do código tratam os produtos, colocar o código nas classes de produto seria errado - uma violação da separação de interesses que sobrecarregaria as classes de produto com o conhecimento de outras partes do sistema. O exemplo de classificação deixa isso claro. Os produtos não devem saber como são classificados, armazenados ou apresentados; se o código que classifica ou apresenta objetos for alterado, a classe do produto não precisará ser reescrita.

Peço desculpas por duvidar de você sobre isso. Vemos tantas pessoas aqui apresentando perguntas baseadas em conhecimentos inadequados de OO, e presumi que esse era o caso quando eu deveria ter solicitado mais informações primeiro.

Na verdade ...

A opção de sobrecarga de método, conforme mostrado em seu código de exemplo, está realmente indo contra princípios como a separação de interesses e DRY. Qual é a coisa comum sobre o método add_product ? Adiciona um produto à coleção apropriada. Mas no código de exemplo, essa única tarefa fundamentalmente idêntica é duplicada em três locais diferentes. Isso realmente não é muito diferente de uma cadeia de lógica if..then..else .

  • A lógica é duplicada e quaisquer alterações no comportamento comum terão que ser duplicadas em cada lugar. Os erros são prováveis e não serão detectados.
  • O código de adição de produtos não precisa ser para saber quais tipos de produtos estão disponíveis. Tudo o que precisa saber é que há uma variedade de produtos, cada um dos quais deve ser adicionado a uma coleção apropriada. Torná-lo ciente significa que você precisa alterar esta seção do código sempre que adicionar um novo produto.

Uma maneira melhor de fazer isso, se você realmente precisa de uma coleta separada para cada tipo de produto, seria ter um mapa de coleções de produtos. A chave seria o tipo de produto. Em seguida, você pode ter um único método product_add , que consulta esse mapa e adiciona o produto ao local correto. Mesmo se você descobrir que existe alguma necessidade de diferentes subclasses de produtos, esse método não precisa ser conhecido. A classe de objeto em si pode ser a chave para o mapa, por exemplo.

Como é provável que mais de um método deseje atualizar ou ler essas coleções, provavelmente é melhor ter um objeto Produtos que armazena os objetos. Se ele contém um mapa de N coleções diferentes ou uma coleção que ele sabe filtrar / pesquisar por tipo é um detalhe de implementação que não precisa se preocupar com outras partes do seu código.

Em resumo

Como realmente parece não haver código específico do produto, classes extras vazias não são apenas inúteis, elas complicam seu código e o tornam mais frágil. Um membro "tipo" é melhor.

  • A maioria do seu código nem precisa saber que existe tal membro.
  • Em cada domínio (as seções claramente separadas de seu aplicativo), identifique os poucos bits que realmente precisam diferenciar entre os tipos de produto.
  • Isole-os em classes auxiliares especiais ou funções que retornarão o produto apropriado (ou faça a coisa apropriada que corresponda ao tipo)
  • Os mapas são muito úteis (podem conter não apenas coleções, mas outros objetos auxiliares / funções para retornar a algum outro código para fazer a coisa apropriada com este objeto), mas devem estar ocultos de qualquer coisa, exceto do código privilegiado que realmente precisa ter conhecimento do tipo de produto.

Se você fizer isso, não apenas seu código será mais limpo e mais elegante (livre de feias if..then..else cadeias, por uma coisa), se você alguma vez encontrar um real necessidade de subclasses de produtos, quase nenhum dos seus códigos existentes terá que mudar. E até mesmo eles precisam apenas alterar sua implementação interna, porque o código deles ainda não é uma preocupação da classe de produto ou de qualquer uma de suas subclasses.

Ainda há muita diversão com a adição de novas classes nos respectivos domínios para encapsular qualquer "Eu sei que tipo de produtos existem". Um desafio é diferenciar entre os bits daqueles códigos que são específicos do domínio (por exemplo, classificação em uma coleção que é relevante somente em um domínio) daqueles que são realmente gerais. A lista de todos os tipos de produto válidos, por exemplo, deve ser um enum que pode ser buscado por qualquer código específico do domínio com reconhecimento de produto. Qualquer mapa deve usar esse enum como o tipo para sua chave. Qualquer função que tenha um "tipo" de produto como um parâmetro deve ter um parâmetro apenas desse tipo de enum e assim por diante. Agora, onde no seu código você vai definir esse enum?

    
por 04.12.2014 / 16:09
fonte
4

Acho que o motivo pelo qual você está tendo problemas com qual caminho seguir é porque há uma falha fundamental no design aqui. Você tem classes anêmicas aqui, que é em si um anti-padrão OO. A lógica externa que está ligando o tipo precisa ser trazida para a família de classes Product .

O cheiro do código para isso é que, não importa qual caminho você vá, você precisará verificar um membro de tipo ou instanceof em outros lugares em seu código. O outro cheiro é que você estará constantemente consultando um objeto Product para dados e tomando decisões com base nele (veja dizer-não -ask ). Isso significa que será difícil modificar ou adicionar comportamento, pois você precisará fazer alterações em muitos locais diferentes, em vez de em uma classe apropriada para manter esse comportamento para cada tipo.

Editar:

A adição dos detalhes do código mostra que há um pouco mais de nuance na sua pergunta. Vamos começar olhando para o que estamos falando por "tipo" de produto.

Tradicionalmente, sempre que ouvimos a palavra "type", saltamos para o conceito de objeto Type (o qual eu aproveito para diferenciar). Esse tipo é associado ao comportamento polimórfico, herança, encapsulamento e todos os outros conceitos orientados a objetos. Aqui, porém, não estamos necessariamente descrevendo um tipo. Estamos falando sobre o que parece ser um atributo de dados chamado tipo. O comportamento não muda realmente com base no tipo, apenas no valor .

Como um exemplo para comparar, podemos olhar para números inteiros. Temos int s 1 e 2 . Os comportamentos entre eles são exatamente os mesmos; apenas os valores são diferentes. Isso significa que não devemos ter um tipo abstrato para int que é implementado como int1 tipo concreto e int2 tipo concreto, etc. Precisamos de um tipo concreto para todos os int s, e um simples maneira de interagir entre eles (como comparação para classificação). Sua ideia de um tipo de produto é a mesma.

Se os seus produtos têm ou terão comportamentos distintos e polimórficos, a herança é uma solução válida. Aqui, no entanto, seu tipo é apenas um atributo de dados, como o preço e a marca, sem diferença de comportamento. Portanto, você deve ter um atributo de tipo, como se você tivesse um atributo de preço e um atributo de marca.

O ideal é que você queira tornar seu código mais aberto-fechado, portanto, deve haver uma maneira de fazer isso da maneira mais elegante possível. Podemos criar um enum contendo os diferentes tipos de produtos existentes, ordenados na ordem em que eles precisam ser produzidos (usando um std :: set estático ou similar em product pode ser mais fácil de trabalhar, se você preferir) . O atributo type do product apontará para um valor enum. Na sua classe product_manager , em vez de três vetores nomeados, você deve ter uma estrutura de tipo de mapa / dicionário para mapear do tipo enum para o respectivo vetor, gerado a partir dos elementos no enum. Dessa forma, essa estrutura adicionará automaticamente vetores para cada tipo na instanciação. Quando você adiciona um produto, selecione o vetor para adicioná-lo com base no tipo de produto. Em seguida, você pode percorrer o mapa e, em seguida, sobre cada vetor para imprimir os produtos, agrupados por tipo.

Pseudocódigo:

enum product_type { electrical, electronic, mechanical }

class product {
    //...

    get_type() {
        return this.type;
    }

    //...
}

class product_manager {
    map<product_type, vector<product>> products;

    product_manager() {
        foreach(p_type in product_type) {
            products.insert(p_type, new vector<product>);
        }
    }

    //...

    add_product(product p) {
        products.at(p.get_type()).append(p);
    }

    //...

    print_products() {
        foreach(p_type in product_type) {
            foreach(product in products.at(p_type)) {
                print_product(product);
            }
        }
    }

    //...
}

Se você nunca quiser usar o tipo em outro lugar, pode torná-lo privado e tornar o product_manager um amigo em product .

Eu também quero fazer um breve comentário sobre o código de exemplo que você postou.

//in real application we don't know instantiation proccess, so don't know what type of product comes to us.
test::electrical_product *ep = new test::electrical_product;
test::electronic_product *etp = new test::electronic_product;
test::mechanical_product *mp = new test::mechanical_product;

// set products' members values

pm.add_product(ep);
pm.add_product(etp);
pm.add_product(mp);

pm.print_electrical_products();
pm.print_electronic_products();
pm.print_mechanical_products();

Você usa sobrecarga de método com diferentes tipos de parâmetro para determinar qual implementação chamar. O problema é que, na sua aplicação real, todos os produtos seriam do tipo abstract_product * . O compilador roteia as chamadas de método com base no tipo de tempo de compilação do objeto, significando que quaisquer produtos sendo adicionados serão enviados para um método product_manager.add_product(abstract_product *) em vez dos métodos sobrecarregados, o que significa que ele não faria o que você pretende.

    
por 01.12.2014 / 04:46
fonte
1

Minha opinião é que adicionar um membro do tipo é uma solução melhor. É mais simples de definir, permite tratar o tipo como um valor de primeira classe, permite codificar o Produto como uma estrutura de dados antiga simples (que é), funciona sem RTTI e você pode usar um switch em vez de vários if / else para ramificar eficientemente com base no tipo. Usando herança, seu código pode quebrar se alguém introduzir uma nova subclasse, já que você está esperando apenas um número finito de tipos de produtos.

    
por 02.12.2014 / 17:46
fonte
-1

Acho melhor ter uma classe Product com um campo de tipo, por alguns motivos:

  • Se você precisar serializar / desserializar de uma maneira que a linguagem não suporta nativamente, existe apenas um construtor; você não precisa ativar o tipo e usar um dos vários construtores.
  • Há um pequeno benefício para a legibilidade - se os leitores virem ElectricalProduct , talvez se perguntem se ela contém uma lógica especial e precisam abri-la para ver se não.
  • Você pode fazer Product final (Java) ou selado (C #), o que é bom para eficiência - certas chamadas de método não precisarão passar por uma vtable.

Dito isso, se você acha que pode adicionar alguma lógica específica a determinados tipos de produto, as classes separadas podem ser melhores para que você possa fazê-lo sem um grande refatorador.

    
por 30.11.2014 / 23:23
fonte
-1

Se realmente você "deve ser capaz de diferenciar cada uma em algumas operações" como você diz, introduzir três classes intermediárias é perfeitamente idéia sensata.

Veja a ideia da Classe abstrata (por exemplo, Java ). Uma classe abstrata não pode ser instanciada em si (a maioria das linguagens OO tem uma construção respectiva que tecnicamente proibir instanciação), mas fornece variáveis e métodos aos seus filhos - e cada classe infantil ainda precisa adicionar um pouco para fazer uma aula completa, instanciável.

No seu caso, ElectricalProduct, ElectronicProduct e MechanicalProduct seriam classes abstratas.

    
por 02.12.2014 / 17:24
fonte