Esistono due modi in cui vengono utilizzate le classi di base astratte.
Stai specializzando il tuo oggetto astratto, ma tutti i client useranno la classe derivata attraverso la sua interfaccia di base.
Stai usando una classe base astratta per fattorizzare la duplicazione all'interno degli oggetti nel tuo progetto e i clienti usano le implementazioni concrete attraverso le proprie interfacce.!
Soluzione per 1 - Modello di strategia
Se hai la prima situazione, allora hai effettivamente un'interfaccia definita dai metodi virtuali nella classe astratta che le tue classi derivate stanno implementando.
Dovresti considerare di renderla una vera interfaccia, cambiare la tua classe astratta in concreta e prendere un'istanza di questa interfaccia nel suo costruttore. Le classi derivate diventano quindi implementazioni di questa nuova interfaccia.
Ciò significa che ora puoi testare la tua classe precedentemente astratta usando un'istanza finta della nuova interfaccia e ogni nuova implementazione attraverso l'interfaccia ora pubblica. Tutto è semplice e testabile.
Soluzione per 2
Se hai la seconda situazione, allora la tua classe astratta sta lavorando come classe di aiuto.
Dai un'occhiata alla funzionalità che contiene. Verifica se uno qualsiasi di essi può essere inserito negli oggetti che vengono manipolati per ridurre al minimo questa duplicazione. Se hai ancora qualcosa, cerca di renderlo una classe di supporto che la tua implementazione concreta prende nel loro costruttore e rimuove la loro classe di base.
Questo porta di nuovo a classi concrete che sono semplici e facilmente verificabili.
Come regola
Favorire una rete complessa di oggetti semplici su una semplice rete di oggetti complessi.
La chiave per un codice testabile estensibile sono i piccoli blocchi costitutivi e il cablaggio indipendente.
Aggiornato: come gestire le miscele di entrambi?
È possibile avere una classe base che svolge entrambi questi ruoli ... vale a dire: ha un'interfaccia pubblica e ha metodi di supporto protetti. In questo caso, puoi scomporre i metodi di supporto in una classe (scenario2) e convertire l'albero di ereditarietà in un modello di strategia.
Se scopri di avere alcuni metodi che la tua classe base implementa direttamente e altri sono virtuali, allora puoi comunque convertire l'albero ereditario in un modello di strategia, ma lo prenderei anche come un buon indicatore che le responsabilità non sono allineate correttamente, e potrebbe bisogno di refactoring.
Aggiornamento 2: Classi astratte come trampolino di lancio (2014/06/12)
L'altro giorno ho avuto una situazione in cui ho usato l'abstract, quindi mi piacerebbe esplorare il perché.
Abbiamo un formato standard per i nostri file di configurazione. Questo particolare strumento ha 3 file di configurazione tutti in quel formato. Volevo una classe fortemente tipizzata per ogni file di impostazione in modo che, tramite l'iniezione delle dipendenze, una classe potesse chiedere le impostazioni a cui teneva.
Ho implementato questo avendo una classe base astratta che sa come analizzare i formati dei file delle impostazioni e le classi derivate che esponevano quegli stessi metodi, ma incapsulava la posizione del file delle impostazioni.
Avrei potuto scrivere un "SettingsFileParser" che le 3 classi includevano e poi delegare alla classe base per esporre i metodi di accesso ai dati. Ho scelto di non farlo ancora perché porterebbe a 3 classi derivate con più codice di delega in esse di ogni altra cosa.
Tuttavia ... man mano che questo codice si evolve e i consumatori di ciascuna di queste classi di impostazioni diventano più chiari. Ogni impostazione gli utenti chiederanno alcune impostazioni e le trasformano in qualche modo (poiché le impostazioni sono di testo, possono essere avvolte in oggetti per convertirle in numeri, ecc.). In questo caso inizierò ad estrarre questa logica nei metodi di manipolazione dei dati e li riporterò sulle classi di impostazioni fortemente tipizzate. Ciò porterà a un'interfaccia di livello superiore per ogni set di impostazioni, che alla fine non è più consapevole di avere a che fare con "impostazioni".
A questo punto le classi di impostazioni fortemente tipizzate non avranno più bisogno dei metodi "getter" che espongono l'implementazione di "impostazioni" sottostante.
A quel punto non vorrei più che la loro interfaccia pubblica includesse i metodi di accesso alle impostazioni; quindi cambierò questa classe per incapsulare una classe parser di impostazioni invece di derivarne.
La classe astratta è quindi: un modo per me di evitare il codice di delega al momento e un indicatore nel codice per ricordarmi di cambiare il design in seguito. Non potrei mai raggiungerlo, quindi potrebbe vivere un bel po '... solo il codice può dirlo.
Trovo che questo sia vero con qualsiasi regola ... come "nessun metodo statico" o "nessun metodo privato". Indicano un odore nel codice ... e va bene. Ti tiene alla ricerca dell'astrazione che ti sei perso ... e ti consente di continuare a fornire valore ai tuoi clienti nel frattempo.
Immagino regole come questa che definiscono un paesaggio, in cui il codice gestibile vive nelle valli. Quando aggiungi un nuovo comportamento, è come l'atterraggio della pioggia sul tuo codice. Inizialmente lo metti ovunque atterra .. poi rifatti per consentire alle forze del buon design di spingere il comportamento fino a quando tutto finisce nelle valli.