Categorie specifiche per bambini vuoti

5

Avere classi di bambini vuoti, solo per specificare, una cattiva pratica?

Supponiamo di avere una classe di prodotto generica. Ci sono prodotti elettrici, elettronici e meccanici. Devo rappresentare tutti loro e devo essere in grado di differire ciascuno in alcune operazioni. Ho due opzioni:

  • aggiungi un membro chiamato "type" nella classe Product per specificare se si tratta di un prodotto elettrico, elettronico o meccanico.

  • crea classi di prodotti per bambini, una per ogni tipo di prodotto: ElectricalProduct, ElectronicProduct e MechanicalProduct. Poiché queste classi non differiscono l'una dall'altra in metodi o membri - già definiti nella classe genitore - sarebbero tutti vuoti.

Considero la seconda opzione un approccio migliore, ma è la strada giusta? Non ho mai visto nulla del genere su alcuna applicazione o libreria.

Che cosa fai? Qualche suggerimento?

Modifica:

Ecco un esempio di come farlo utilizzando la seconda opzione: link

Non riesco a vedere un modo migliore di farlo, ma non so se è una buona pratica avere quelle classi vuote.

    
posta Tiago.SR 30.11.2014 - 23:00
fonte

5 risposte

1

Ora che hai spiegato in modo più completo, sembra che non ci sia davvero una logica specifica per sottoclassi. In tal caso, l'ereditarietà è non la scelta giusta. Se ci sono solo modi minimi in cui altre parti del codice trattano i prodotti, mettere il codice nelle classi di prodotto sarebbe sbagliato - una violazione della separazione delle preoccupazioni che potrebbe ingombrare le classi di prodotto con la conoscenza di altre parti del sistema. L'esempio di classificazione lo rende chiaro. I prodotti non dovrebbero sapere come sono ordinati, memorizzati o presentati; se il codice che ordina o presenta gli oggetti è cambiato, la classe del prodotto non dovrebbe essere riscritta.

Mi scuso per averti dubitato di questo. Vediamo così tante persone qui che presentano domande basate su una scarsa conoscenza OO e ho pensato che fosse il caso in cui avrei dovuto chiedere prima ulteriori informazioni.

In realtà ...

La scelta dell'overload del metodo, come mostrato nel codice di esempio, in realtà sta andando contro i principi come la separazione delle preoccupazioni e l'ASCIUTO. Qual è la cosa comune del metodo add_product ? Aggiunge un prodotto alla raccolta appropriata. Ma nel codice di esempio, quel singolo compito fondamentalmente identico è duplicato in tre diversi posti. Questo è davvero poco diverso da una catena di if..then..else logic.

  • La logica è duplicata e qualsiasi modifica al comportamento comune dovrà essere duplicata in ogni luogo. Gli errori sono probabili e passeranno inosservati.
  • Il codice di aggiunta del prodotto non dovrebbe essere necessario per sapere quali tipi di prodotti sono disponibili. Tutto ciò che deve sapere è che ci sono una varietà di prodotti, ognuno dei quali dovrebbe essere aggiunto a una raccolta appropriata. Rendilo consapevole significa che devi modificare questa sezione del codice ogni volta che aggiungi un nuovo prodotto.

Un modo migliore per farlo, se hai davvero bisogno di una collezione separata per ogni tipo di prodotto, è di avere una mappa delle collezioni di prodotti. La chiave sarebbe il tipo di prodotto. Quindi potresti avere un solo metodo product_add , che consulta quella mappa e aggiunge il prodotto al posto giusto. Anche se hai scoperto che è necessario per diverse sottoclassi di prodotti, questo metodo non ha bisogno di sapere. La classe dell'oggetto stessa potrebbe essere la chiave per la mappa, ad esempio.

Poiché è probabile che più di un metodo desideri aggiornare o leggere da tali raccolte, è probabilmente meglio avere un oggetto Prodotti che memorizza gli oggetti. Indipendentemente dal fatto che contenga una mappa di N raccolte diverse o una raccolta che sa filtrare / cercare per tipo è un dettaglio di implementazione che non deve riguardare altre parti del tuo codice.

In breve

Dal momento che sembra non esserci alcun codice specifico del prodotto, le classi extra vuote non sono solo inutili, hanno complicato il codice e reso più fragile. Un membro di "tipo" è migliore.

  • La maggior parte del tuo codice non ha nemmeno bisogno di sapere che esiste un membro del genere.
  • In ogni dominio (le sezioni chiaramente distinte della tua applicazione), identifica quei pochi bit che veramente necessitano di differenziare tra i tipi di prodotto.
  • Isolali in classi o funzioni di helper speciali che restituiscono il prodotto appropriato (o fanno la cosa appropriata che corrisponde al tipo)
  • Le mappe sono molto utili (possono contenere non solo collezioni ma anche altri oggetti / funzioni di supporto da restituire a un altro codice per fare la cosa appropriata con questo oggetto) ma dovrebbero essere nascoste da qualsiasi cosa tranne il codice privilegiato che ha veramente bisogno di essere consapevole del tipo di prodotto.

Se lo fai, non solo il tuo codice sarà più pulito e più elegante (privo di brutte catene if..then..else , per una cosa), se mai trovi un vero bisogno di sottoclassi di prodotti, quasi nessuno del codice esistente dovrà cambiare. E anche loro hanno solo bisogno di cambiare la loro implementazione interna, perché il loro codice non non dovrebbe interessare la classe del prodotto o alcuna delle sue sottoclassi.

C'è ancora molto divertimento con l'aggiunta di nuove classi nei rispettivi domini per incapsulare qualsiasi "So che tipo di prodotti ci sono". Una sfida è la differenziazione tra i bit di quei codici che sono specifici del dominio (ad esempio l'ordinamento in una raccolta che è rilevante solo in un dominio) da quelli che sono in realtà generali. L'elenco di tutti i tipi di prodotto validi, ad esempio, dovrebbe essere un enum che può essere recuperato da qualsiasi codice specifico del dominio sensibile al prodotto. Qualsiasi mappa dovrebbe usare quell'enumerazione come tipo per la sua chiave. Qualsiasi funzione che prende un "tipo" di prodotto come parametro dovrebbe prendere un parametro solo di quel tipo di enum e così via. Ora, dove nel tuo codice definirai quell'enum?

    
risposta data 04.12.2014 - 16:09
fonte
4

Penso che la ragione per cui hai problemi con quale percorso prendere è perché qui c'è un difetto di progettazione fondamentale. Hai classi anemiche qui, che è di per sé un anti-pattern OO. La logica esterna che sta attivando il tipo deve essere inserita nella famiglia di classi Product .

L'odore del codice per questo è che, non importa in quale percorso si va, sarà necessario controllare un membro del tipo o instanceof in altri punti del codice. L'altro odore è che dovrai costantemente interrogare un oggetto Product per i dati e prendere decisioni basate su di esso (vedi tell-don't -Chiedere ). Ciò significa che sarà difficile modificare o aggiungere un comportamento perché sarà necessario apportare modifiche in molti e diversi luoghi piuttosto che in una classe appropriata per mantenere tale comportamento per ciascun tipo.

Modifica

L'aggiunta dei dettagli del codice mostra che c'è un po 'più di sfumatura nella tua domanda. Iniziamo osservando di cosa stiamo parlando per "tipo" di prodotto.

Tradizionalmente, ogni volta che ascoltiamo la parola "tipo", saltiamo sul concetto di oggetto di Tipo (che capitalizzerò per differenziare). Questo tipo è associato a comportamento polimorfico, ereditarietà, incapsulamento e tutti gli altri concetti orientati agli oggetti. Qui, però, non stiamo necessariamente descrivendo un tipo. Stiamo parlando di quello che sembra essere un attributo di dati chiamato tipo. Il comportamento non cambia in base al tipo, solo al valore .

Come esempio da confrontare, possiamo guardare interi. Abbiamo int s 1 e 2 . I comportamenti tra loro sono esattamente gli stessi; solo i valori differiscono. Ciò significa che non dovremmo avere un tipo astratto per int implementato come tipo di calcestruzzo int1 e un tipo di calcestruzzo int2 , ecc. Abbiamo bisogno di un tipo concreto per tutti int s, e un semplice modo di interagire tra loro (come il confronto per l'ordinamento). La tua idea di un tipo di prodotto è la stessa.

Se i tuoi prodotti hanno o avranno comportamenti distinti e polimorfici, l'ereditarietà è una soluzione valida. Qui, però, il tuo tipo è solo un attributo di dati, come il prezzo e il marchio, senza alcuna differenza di comportamento. Pertanto, dovresti avere un attributo type come hai un attributo price e un attributo brand.

Idealmente, vorrai rendere il tuo codice più aperto-chiuso, quindi dovrebbe esserci un modo per farlo nel modo più elegante possibile. Possiamo creare un enum contenente i diversi tipi di prodotti esistenti, ordinati nell'ordine in cui devono essere emessi (utilizzando uno statico std :: set o simile su product potrebbe essere più facile lavorare con, se preferisci) . L'attributo type di product punterà a un valore enum. Nella tua classe product_manager , invece di tre vettori nominati, dovresti avere una struttura di tipo map / dictionary per mappare da enum di tipo al rispettivo vettore, generato dagli elementi nell'enum. In questo modo, quella struttura aggiungerà automaticamente i vettori per ciascun tipo all'istanziazione. Quando aggiungi un prodotto, seleziona il vettore per aggiungerlo in base al tipo di prodotto. Quindi puoi scorrere sulla mappa, quindi su ciascun vettore per stampare i prodotti, raggruppati per tipo.

Pseudocodice:

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 non vuoi utilizzare il tipo in nessun altro luogo, puoi renderlo privato e rendere product_manager un amico a product .

Voglio anche fare un rapido punto sul codice di esempio che hai postato.

//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();

Si utilizza l'overloading del metodo con diversi tipi di parametri per determinare quale implementazione chiamare. Il problema è che, nella tua vera applicazione, tutti i prodotti sarebbero di tipo abstract_product * . Il compilatore indirizza le chiamate al metodo in base al tipo tempo di compilazione dell'oggetto, il che significa che qualsiasi prodotto aggiunto verrà inviato a un metodo product_manager.add_product(abstract_product *) invece dei metodi sovraccaricati, il che significa che non lo farebbe cosa intendi.

    
risposta data 01.12.2014 - 04:46
fonte
1

La mia opinione è che l'aggiunta di un membro del tipo è una soluzione migliore. È più semplice da definire, ti permette di trattare il tipo come un valore di prima classe, ti consente di codificare il prodotto come una semplice vecchia struttura dati (che è), funziona senza RTTI e puoi utilizzare un switch invece di più if / else per diramazione efficiente in base al tipo. Usando l'ereditarietà il tuo codice può rompersi se qualcuno introduce una nuova sottoclasse, dal momento che ti aspetti solo un numero finito di tipi di prodotti.

    
risposta data 02.12.2014 - 17:46
fonte
-1

Penso che sia meglio avere una classe Product con un campo tipo, per alcuni motivi:

  • Se hai mai bisogno di serializzare / deserializzare in un modo che il linguaggio non supporta nativamente, c'è solo un costruttore; non è necessario attivare il tipo e utilizzare uno dei vari costruttori.
  • C'è un leggero vantaggio per la leggibilità - se i lettori vedono ElectricalProduct potrebbero chiedersi se contiene una logica speciale e devono aprirlo per vedere che non lo fa.
  • Puoi creare Product final (Java) o sealed (C #), il che è positivo per l'efficienza: alcune invocazioni di metodo non devono passare attraverso un vtable.

Detto questo, se pensi che in seguito potresti aggiungere una logica specifica a determinati tipi di prodotto, le classi separate potrebbero essere migliori, in modo che tu possa farlo senza un refactoring principale.

    
risposta data 30.11.2014 - 23:23
fonte
-1

Se davvero "deve essere in grado di differire ciascuna in alcune operazioni" come dici tu, introdurre tre classi intermedie di questo tipo è perfettamente idea sensata.

Dai un'occhiata all'idea di Classe astratta (ad esempio in Java). Una classe astratta non può essere istanziata stessa (la maggior parte delle lingue OO avere un costrutto rispettivo che tecnicamente proibisci l'istanziazione) ma fornisce variabili e metodi ai suoi figli - e ogni classe di bambino deve ancora aggiungere qualcosa per creare una classe completa e istantanea.

Nel tuo caso, ElectricalProduct, ElectronicProduct e MechanicalProduct sarebbero classi astratte.

    
risposta data 02.12.2014 - 17:24
fonte

Leggi altre domande sui tag