Probabilmente considererei un odore di codice o anche un anti-pattern avere una classe che implementa 23 interfacce. Se fosse davvero un anti-schema, come lo chiameresti? O semplicemente non segue il principio della responsabilità unica?
Probabilmente considererei un odore di codice o anche un anti-pattern avere una classe che implementa 23 interfacce. Se fosse davvero un anti-schema, come lo chiameresti? O semplicemente non segue il principio della responsabilità unica?
Risposte:
Famiglia reale: non fanno nulla di particolarmente folle ma hanno un miliardo di titoli e sono in qualche modo legati alla maggior parte degli altri reali.
somehow
Sottolineo che questo anti-pattern si chiami Jack of All Trades, o forse Too Many Hats.
Se dovessi dargli un nome, penso che lo chiamerei l' Idra :
Nella mitologia greca, l'idra di Lernaean (in greco: Λερναία Ὕδρα) era un'antica bestia acquatica simile a un serpente senza nome, con tratti rettiliani (come dimostra il suo nome) che possedeva molte teste - i poeti menzionano più teste di quante ne potessero i pittori di vasi dipingeva, e per ogni testa tagliata ne cresceva altre due - e un respiro velenoso così virulento che persino le sue tracce erano mortali.
Di particolare rilevanza è il fatto che non solo ha molte teste, ma continua a crescere sempre di più ed è impossibile ucciderlo a causa di ciò. Questa è stata la mia esperienza con questo tipo di design; gli sviluppatori continuano a disturbare sempre più interfacce fino a quando non ha troppe per adattarsi allo schermo, e da allora è diventato così profondamente radicato nella progettazione del programma e ipotesi che dividerlo è una prospettiva senza speranza (se ci provi, in realtà spesso finiscono per avere bisogno di più interfacce per colmare il divario).
Un primo segno di imminente rovina a causa di una "Hydra" è il lancio frequente di un'interfaccia verso un'altra, senza troppi controlli di sanità mentale, e spesso verso un bersaglio che non ha senso, come in:
public void CreateWidget(IPartLocator locator, int widgetTypeId)
{
var partsNeeded = locator.GetPartsForWidget(widgetTypeId);
return ((IAssembler)locator).BuildWidget(partsNeeded);
}
È ovvio quando tolto dal contesto che c'è qualcosa di sospetto in questo codice, ma la probabilità che ciò accada aumenta con il numero di "teste" che un oggetto cresce, perché gli sviluppatori sanno intuitivamente che hanno sempre a che fare con la stessa creatura.
Buona fortuna mai fare qualsiasi manutenzione su un oggetto che implementa 23 interfacce. Le probabilità che tu non provochi danni collaterali durante il processo sono scarse.
L' oggetto Dio viene in mente; un singolo oggetto che sa fare TUTTO. Ciò deriva dalla bassa aderenza ai requisiti di "coesione" delle due principali metodologie di progettazione; se hai un oggetto con 23 interfacce, hai un oggetto che sa come 23 cose diverse per i suoi utenti e quelle 23 cose diverse probabilmente non sono sulla stessa linea di una singola attività o area del sistema.
In SOLID, mentre il creatore di questo oggetto ha ovviamente provato a seguire il principio di segregazione dell'interfaccia, hanno violato una regola precedente; Principio unico di responsabilità. C'è una ragione per cui è "SOLID"; S viene sempre prima di tutto quando si pensa al design e seguono tutte le altre regole.
In GRASP, il creatore di tale classe ha trascurato la regola "Alta coesione"; GRASP, a differenza di SOLID, insegna che un oggetto non DEVE avere un'unica responsabilità, ma al massimo dovrebbe avere due o tre responsabilità strettamente correlate.
23 è solo un numero! Nello scenario più probabile, è abbastanza alto da allarmare. Tuttavia, se chiediamo, qual è il numero più alto di metodi / interfacce prima che possa ottenere un tag da chiamare come "anti-pattern"? Sono 5 o 10 o 25? Ti rendi conto che il numero non è davvero una risposta perché se 10 è buono, anche 11 può essere - e quindi qualsiasi numero intero dopo quello.
La vera domanda riguarda la complessità. E dovremmo sottolineare che il lungo codice, il numero di metodi o le grandi dimensioni della classe da parte di qualsiasi misura NON sono in realtà la definizione di complessità. Sì, un codice sempre più grande (un numero maggiore di metodi) rende difficile leggere e comprendere un nuovo principiante. Gestisce anche funzionalità potenzialmente varie, un gran numero di eccezioni e algoritmi abbastanza evoluti per vari scenari. Ciò non significa che sia complesso - è solo difficile da digerire.
D'altra parte, un codice di dimensioni relativamente piccole che si può sperare di leggere tra qualche ora dentro e fuori può essere ancora complesso. Ecco quando penso che il codice sia (inutilmente) complesso.
Ogni parte di saggezza del design orientato agli oggetti può essere messa qui per definire "complesso", ma vorrei limitarmi qui per mostrare quando "così tanti metodi" è un'indicazione di complessità.
Conoscenza reciproca. (aka accoppiamento) Molte volte quando le cose sono scritte come classi, tutti pensiamo che sia un codice "bello" orientato agli oggetti. Ma l'ipotesi sull'altra classe essenzialmente interrompe l'incapsulamento realmente necessario. Quando hai metodi che "perdono" dettagli profondi sullo stato interno degli algoritmi - e l'applicazione è costruita con presupposti chiave sullo stato interno della classe di servizio.
Troppe ripetizioni (irruzione) Quando i metodi che hanno nomi simili ma funzionano in modo contraddittorio - o contraddiscono i nomi con funzionalità simili. Molte volte i codici si evolvono per supportare interfacce leggermente diverse per diverse applicazioni.
Troppi ruoli Quando la classe continua ad aggiungere funzioni secondarie e continua ad espandersi man mano che piace alla gente, solo per sapere che la classe ora è effettivamente una classe due. Sorprendentemente, tutto ciò inizia con autenticorequisiti e nessun'altra classe esiste per fare questo. Considera questo, esiste una Transazione di classe, che indica i dettagli della transazione. Sembra buono finora, ora qualcuno richiede la conversione del formato sul "tempo della transazione" (tra UTC e simili), in seguito, le persone aggiungono regole per verificare se certe cose erano in determinate date per convalidare le transazioni non valide. - Non scriverò l'intera storia ma alla fine, la classe di transazione costruisce l'intero calendario con al suo interno e quindi le persone iniziano a usare "solo il calendario" parte di esso! Questo è molto complesso (da immaginare) perché istanzerò la "classe di transazione" per avere funzionalità che un "calendario" mi fornirebbe!
(In) coerenza dell'API Quando faccio book_a_ticket () - prenoto un biglietto! È molto semplice, indipendentemente da quanti controlli e processi ci vogliono per farlo accadere. Ora diventa complesso quando il flusso di tempo inizia a fare questo. In genere si consentirebbe la "ricerca" e l'eventuale stato disponibile / non disponibile, quindi per ridurre il back-n-forward si inizia a salvare alcuni puntatori di contesto all'interno del ticket e quindi si fa riferimento a prenotare il ticket. La ricerca non è l'unica funzionalità: le cose peggiorano dopo molte di queste "funzionalità secondarie". Nel processo il significato di book_a_ticket () implica book_that_ticket ()! e che può essere inimmaginabilmente complesso.
Probabilmente ci sono molte situazioni del genere che vedresti in un codice molto evoluto e sono sicuro che molte persone possono aggiungere scenari, in cui "così tanti metodi" semplicemente non hanno senso o non fanno ciò che pensi ovviamente. Questo è l'anti-pattern.
La mia esperienza personale è che quando progetti che iniziano ragionevolmente dal basso verso l'alto, molte cose per le quali avrebbero dovuto esserci delle classi autentiche seppellite o rimaste seppellite rimangono ancora divise tra classi diverse e aumenti di accoppiamento. Molto spesso ciò che avrebbe meritato 10 classi, ma ne ha solo 4, è probabile che molti di loro abbiano confusione multiuso e un gran numero di metodi. Chiamalo DIO e DRAGO, questo è ciò che è MALE.
MA ti imbatti in classi MOLTO GRANDI che sono perfettamente coerenti, hanno 30 metodi e sono ancora molto PULITE. Possono essere bravi.
Le classi dovrebbero avere esattamente il giusto numero di interfacce; Ne più ne meno.
Dire "troppi" sarebbe impossibile senza vedere se tutte quelle interfacce servono o meno a uno scopo utile in quella classe. Dichiarare che un oggetto implementa un'interfaccia significa che è necessario implementare i suoi metodi se si prevede che la classe venga compilata. (Presumo che la classe che stai guardando faccia.) Se tutto nell'interfaccia è implementato e quelle implementazioni fanno qualcosa in relazione alle viscere della classe, sarebbe difficile dire che l'implementazione non dovrebbe essere lì. L'unico caso in cui riesco a pensare a dove un'interfaccia che soddisfi questi criteri non appartenga è esterna: quando nessuno la usa.
Alcune lingue consentono di "sottoclassare" le interfacce usando un meccanismo come la extends
parola chiave Java , e chiunque lo abbia scritto potrebbe non saperlo. Potrebbe anche essere che tutti e 23 siano sufficientemente distanti da non aver senso aggregarli.
Sembra che abbiano una coppia "getter" / "setter" per ogni proprietà, come è normale per un "bean" tipico e ha promosso tutti questi metodi sull'interfaccia. Che ne dici di chiamarlo " ha un fagiolo ". O più "flatulenza" rabelaisiana dopo gli affetti ben noti di troppi fagioli.
Il numero di interfacce aumenta spesso quando un oggetto generico viene utilizzato in ambienti diversi. In C # le varianti di IComparable, IEqualityComparer e IComparer consentono l'ordinamento in configurazioni distinte, quindi potresti finire per implementarle tutte, alcune forse più di una volta poiché puoi implementare le versioni generiche fortemente tipizzate e le versioni non generiche , inoltre puoi implementare più di uno dei generici.
Facciamo uno scenario di esempio, diciamo un negozio online in cui è possibile acquistare crediti con i quali è possibile acquistare qualcos'altro (i siti di foto stock spesso utilizzano questo schema). Potresti avere una classe "Valuta" e una classe "Crediti" che ereditano dalla stessa base. Valuta presenta alcuni inutili sovraccarichi di operatore e routine di confronto che consentono di eseguire calcoli senza preoccuparsi della proprietà "Valuta" (ad esempio aggiungendo sterline ai dollari). I crediti sono molto più semplici ma hanno altri comportamenti distinti. Volendo essere in grado di confrontarli tra loro, potresti finire per implementare IComparable e IComparable e le altre varianti di interfacce di confronto su entrambi (anche se usano un'implementazione comune sia nella classe base che altrove).
Quando si implementa la serializzazione, vengono implementati ISerializable, IDeserializationCallback. Quindi implementando gli stack di annullamento: viene aggiunto IClonable. Funzionalità IsDirty: IObservable, INotifyPropertyChanged. Consentire agli utenti di modificare i valori utilizzando le stringhe: IConvertable ... L'elenco può continuare all'infinito ...
Nei linguaggi moderni vediamo una tendenza diversa che aiuta a separare questi aspetti e metterli nelle proprie classi, al di fuori della classe principale. La classe o l'aspetto esterno viene quindi associato alla classe di destinazione utilizzando l'annotazione (attributi). Spesso è possibile rendere le classi di aspetto esterne più o meno generiche.
L'uso degli attributi (annotazione) è soggetto a riflessione. Uno svantaggio è la perdita di prestazioni (iniziale) minore. Uno svantaggio (spesso emotivo) è che i principi come l'incapsulamento devono essere rilassati.
Ci sono sempre altre soluzioni, ma per ogni soluzione elegante c'è un compromesso o un trucco. Ad esempio, l'utilizzo di una soluzione ORM potrebbe richiedere che tutte le proprietà siano dichiarate virtuali. Le soluzioni di serializzazione potrebbero richiedere costruttori predefiniti per le tue classi. Se si utilizza l'iniezione di dipendenza, si potrebbe finire con l'implementazione di 23 interfacce su una singola classe.
Ai miei occhi, 23 interfacce non devono essere cattive per definizione. Potrebbe esserci uno schema ben ponderato dietro di esso, o qualche determinazione di principio come evitare l'uso della riflessione o convinzioni di incapsulamento estremo.
Ogni volta che si cambia lavoro o si deve basare su un'architettura esistente. Il mio consiglio è di prima conoscere a fondo, non cercare di refacturing tutto troppo rapidamente. Ascolta lo sviluppatore originale (se è ancora lì) e prova a capire i pensieri e le idee dietro ciò che vedi. Quando fai domande, fallo non per il gusto di romperlo, ma per imparare ... Sì, ognuno ha i suoi martelli d'oro, ma più martelli puoi raccogliere, più facile andrai d'accordo con i tuoi colleghi.
"Troppi" è soggettivo: stile di programmazione? prestazione? conformità agli standard? precedenti? solo semplice senso di benessere / fiducia?
Fintanto che il codice si comporterà correttamente e non presenterà problemi di manutenibilità, 23 potrebbe persino essere la nuova norma. Un giorno potrei quindi dire: "Puoi fare un ottimo lavoro con 23 interfacce, vedi: Jonas Elfström".
Direi che il limite dovrebbe essere un numero inferiore a 23 - più simile a 5 o 7,
tuttavia, ciò non include alcun numero di interfacce ereditate da tali interfacce o un numero qualsiasi di interfacce implementate dalle classi di base.
(Quindi nel complesso, N + qualsiasi numero di interfacce ereditate, dove N <= 7.)
Se la tua classe implementa troppe interfacce, probabilmente è una classe divina .