Re-architecting un classico design di ereditarietà

7

Ho l'opportunità di riscrivere un pezzo principale di un progetto (C #) che è pesantemente legato all'eredità e si sente sempre più restrittivo nel modo in cui è progettato. Lo scenario è piuttosto semplice, immagina un'applicazione creata per gestire varie piccole attività. Ogni attività è unica nella sua funzionalità, quindi implementa un'interfaccia ITask con alcuni semplici metodi che vengono implementati da ogni classe di attività (ad esempio Init (), Start (), Stop (), ecc.)

Oltre all'interfaccia ITask c'è anche una classe BaseTask astratta da cui ereditano tutte le attività. Questa classe BaseTask è una sorta di approccio "tutto tranne che in cucina" che contiene utili funzionalità che le attività richiedono spesso. Ad esempio, tutte le attività registrano il loro stato e procedono allo stesso modo, quindi questa logica è definita nel BaseTask e disponibile per tutti i bambini attraverso un metodo protetto. O per dare un altro esempio, alcune attività devono inviare notifiche e-mail quando vengono soddisfatte determinate condizioni, quindi ancora una volta, il BaseTask fornisce l'accesso alle funzionalità core smtp dell'app per questo scopo. Esistono molti esempi di questo tipo di metodi / proprietà "helper"

Il problema, naturalmente, è che questa classe base è già diventata ingombrante e ingombra di proprietà e metodi che non sono universali per tutte le attività e sta iniziando a introdurre più problemi di quanti ne risolva. Ad esempio, ogni volta che viene richiesta una modifica alla classe base, ti trovi in una posizione in cui potresti finire per interrompere alcune (o tutte) delle attività. Sembra anche il suo incapsulamento irrinunciabile e il principio di responsabilità singola. È anche abbastanza chiaro come ciò sia successo in primo luogo (e anche un evento comune). Inizialmente si trattava di un'applicazione molto piccola e specializzata, ma è esplosa molto rapidamente in termini di portata e dimensioni e ciò che inizialmente non era probabilmente una decisione errata architettonicamente si è rivelato inflessibile e scomodo.

Dove inizieresti con qualcosa di simile? Come affronteresti il problema e quali principi o schemi di progettazione sceglieresti? Grazie mille

    
posta bruiseruser 17.05.2013 - 20:03
fonte

6 risposte

11

Passaggio 1: imposta ciascuno di questi metodi in BaseTask statico e passa tutti i membri privati richiesti come argomenti. Ora possono vivere e respirare comodamente fuori dalla classe.

Passaggio 2: implementa un contenitore IOC . Risolvi i tuoi compiti da lì.

Passaggio 3: prendi tutti quei metodi protetti e trasferiscili in un servizio, rendili pubblici. Rendili non statici. Non usarli ancora.

Passaggio 4: trovare un gruppo correlato di metodi, eliminarli e creare un'interfaccia che esponga tutti questi metodi. Implementa quell'interfaccia sul tuo servizio (dovrebbe essere facile, dato che stai eliminando i metodi esistenti qui) e il costruttore - inseriscili solo nelle attività che non compilano più.

Passaggio 5: Ripeti il passaggio 4 finché tutti i metodi non sono passati da BaseTask.

Passaggio 6: rompere il servizio in servizi più piccoli che implementano solo un'interfaccia; fai sapere alla tua registrazione IOC.

Passaggio 7: Refactor fino allo sfinimento.

    
risposta data 17.05.2013 - 20:14
fonte
5

Questo sembra un candidato ideale per i principi SOLID .

S: principio di responsabilità singola

Separa tutte le funzionalità nella classe base in classi separate, ognuna facendo una sola cosa. Hai bisogno di inviare e-mail attraverso un server SMTP? Costruisci una singola classe che faccia questo. È necessario inviare e-mail attraverso un server Exchange (senza utilizzare la sua interfaccia SMTP)? Implementa una classe diversa che faccia questo. Se necessario, crea l'ereditarietà con ciò che è comune.

Ad esempio, l'invio di e-mail è praticamente universale nei requisiti, quindi creare una classe base con gli elementi comuni e classi derivate che implementano la logica di invio effettiva, ad es. parlando con diversi tipi di server.

O: Princple aperto / chiuso

Invece di creare un compito in grado di segnalare tramite e-mail, renderlo in grado di segnalare attraverso un'interfaccia comune che può prendere il tipo di rapporto che produce. Se riporta solo testo stupido, è quello che dovrebbe essere l'interfaccia. Non rendere l'interfaccia denominata o collegata in modo pertinente all'email. In questo modo è possibile imbullonare nuove classi in un secondo momento che possono prendere quei report e inviarli tramite IM, tramite SMS, scaricarli su un sistema di registrazione o simili.

Se l'attività ha bisogno di input, ad esempio da un file, rendila invece un'interfaccia simile a una "sorgente di input" che richiede l'elaborazione dell'input. In questo modo l'attività potrebbe richiedere qualsiasi tipo di sorgente come input, purché la sorgente possa produrre l'input nel formato richiesto dall'attività. Oggi puoi elaborare i file, domani puoi elaborare i record da un database o le pagine scaricate tramite HTTP.

In questo modo puoi finalizzare la logica dell'attività (chiuderla), mentre continua a tenerla aperta per nuove funzionalità in seguito (nuovi tipi che implementano le interfacce richieste).

L: Principio di sostituzione di Liskov

(non sono sicuro di come si applicherebbe)

I: Principio di segregazione dell'interfaccia

Sposta tutto fuori dalla classe base, eccetto quelle cose che logicamente appartengono a questa, cioè. quelle cose che tutte richiedono / richiedono. Tutto il resto dovrebbe essere in classi piccole che possono essere facilmente testate da sole, riutilizzate in altri luoghi, ecc.

D: Principio di inversione delle dipendenze

Invece di aggiungere funzionalità SMTP a un'attività che deve segnalare lo stato di avanzamento, fartelo prendere in un oggetto che implementa un'interfaccia generica (non in .NET / C #, ma a scopo generale) che riguarda il reporting e il tipo di attività usa quell'oggetto per i suoi scopi di reporting. In questo modo, oggi hai solo rapporti via e-mail tramite SMTP, ma le conoscenze su come inviare quel rapporto non sono né incorporate nella classe, né cablate nell'ambiente della classe. In futuro il compito può essere chiesto di segnalare qualsiasi tipo di supporto che possa essere scelto da un tipo che implementa la stessa interfaccia.

    
risposta data 17.05.2013 - 20:09
fonte
3

Ho visto la stessa complessità nei controlli Infragistics che possono fare tutto ciò che vuoi ma forniscono un'API ingombrante che è quasi impossibile da navigare senza esperienza.

Con le API di grandi dimensioni è opportuno raggruppare logicamente i metodi in suite funzionali che hanno tutte una sola area di responsabilità. Per questo potrei creare decoratori . Fare ciò è molto semplice e diretto, quindi ridurrà di molto le possibilità di introdurre bug. Ogni decoratore viene istanziato secondo necessità per accedere alle funzionalità che fornisce. Il tuo decoratore non farebbe altro che racchiudere l'oggetto principale (un'attività) con proprietà / metodi aggiuntivi, riposizionando quelli esistenti dall'oggetto principale all'oggetto decoratore.

    
risposta data 17.05.2013 - 20:28
fonte
1

Mi piace suggerire @pdr, è una buona soluzione. Per evitare l'interfaccia inferno menzionata in un commento, prendere in considerazione l'uso del pattern Observer. In questo modo, puoi aggiungere plugins con nuove funzionalità in qualsiasi momento senza modificare alcun codice (attività) esistente.

    
risposta data 18.05.2013 - 02:14
fonte
0

Penso che questa enorme classe base sia un sintomo di qualcosa di più profondo.

Nel tuo caso vedo due problemi, che dovresti concentrarti sulla risoluzione prima di iniziare il refactoring.

  1. Gli sviluppatori non hanno esperienza nel design OOP. Questo è un problema abbastanza comune. Invece di creare una struttura OOP adeguata, trovano semplicemente un posto globale e mettono il loro codice qui. Quindi, anche se hai qualche parvenza di OOP all'esterno, degenera in codice procedurale all'interno. Prova a insegnare ai tuoi sviluppatori il design OOP appropriato e motivali a praticarlo.
  2. L'astrazione del compito ha smesso di essere abbastanza. Grazie a requisiti molto complessi, sembra che tu debba fare un'astrazione molto più fine. Quindi, invece di concentrarti sull'implementazione di un'attività specifica, ti concentri su componenti separati e poi componi l'attività finale da quei componenti. Se avessi un design OOP adeguato e avessi eseguito il refactoring mentre andavi, probabilmente vedresti questo problema molto prima.

Se non risolvi questi problemi prima di iniziare il refactoring, il problema potrebbe riapparire. Ma questa volta, sarà ancora peggio grazie alla maggiore complessità.

Inoltre, ti consiglio di fare pratica di Test Driven Development o almeno di fare test unitari per i casi d'uso più comuni. Questo ti aiuterà a capire come dovrebbe apparire il tuo progetto dal punto di vista di un estraneo.

    
risposta data 17.05.2013 - 20:46
fonte
-1

Potresti volere alcune classi intermedie. Dì che le classi W e X vogliono fare qualcosa in un modo, e le classi Y e Z vogliono farne un altro. Al momento hai qualche metodo di confusione nella classe base A che funziona in entrambi i modi con molti se . Crea sottoclassi di A, ad esempio B e C, ciascuna con la propria copia di questo metodo che contiene solo il codice pertinente. Ora fai in modo che W e X ereditino da B e Y e Z ereditino da C.

Questo diventa complicato con l'aggiunta di metodi, ma con un po 'di fortuna e un po' di brillantezza hai la possibilità di finire con un codice ordinato, facile da capire, semplice, ASCIUTTO. Potrebbero esserci molte classi e diversi livelli di ereditarietà, ma ogni classe dovrebbe essere facile da capire.

    
risposta data 17.05.2013 - 21:08
fonte

Leggi altre domande sui tag