Quante sono troppe interfacce in una classe? [chiuso]


28

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?


3
Bene, è discutibile in entrambi i modi. Se la classe ha 23 metodi che hanno tutti una sola responsabilità (non tutte le classi hanno bisogno di così tanti metodi, ma non è impossibile - specialmente se la maggior parte sono wrapper di convenienza a tre righe attorno ad altri metodi), hai solo interfacce troppo dettagliate; )

La classe in questione è un tipo di entità e le interfacce sono principalmente di proprietà. Ce ne sono 113 e solo quattro metodi.
Jonas Elfström,

6
Pensavo che le interfacce fossero essenzialmente utilizzate per la tipizzazione duck in linguaggi compilati staticamente. Qual è il problema? :)
ashes999,

2
solo una domanda: tutte le 113 proprietà sono popolate per ogni istanza di questa entità? O è una sorta di "entità comune" utilizzata in contesti diversi con solo alcune proprietà riempite?
David,

1
Torniamo alla comprensione concettuale di ciò che costituisce una "classe". Una classe è una cosa sola con una responsabilità e un ruolo chiaramente definiti. Puoi definire il tuo oggetto con 23 interfacce in questo modo? Puoi pubblicare una sola frase (non Joycean) che sintetizzi adeguatamente la tua classe? In tal caso, allora 23 interfacce vanno bene. Altrimenti, è troppo grande, troppo complicato.
Stephen Gross,

Risposte:



23

Sottolineo che questo anti-pattern si chiami Jack of All Trades, o forse Too Many Hats.


2
Che ne dici di The Swiss Army Knife?
FrustratedWithFormsDesigner,

Sembra che questo termine sia già utilizzato per interfacce con troppi metodi :-) Anche se avrebbe senso anche qui.
Jalayn,

1
@FrustratedWithFormsDesigner Un coltellino svizzero non avrebbe mai il cattivo gusto di avere un'interfaccia chiamata IValue che contiene 30 proprietà di cui 20 precedute dalla nuova parola chiave modificatore.
Jonas Elfström,

3
+1 per Troppi cappelli ... per nascondere correttamente la divinità a più teste
Newtopian,

1
Un coltellino svizzero è un modello di riutilizzo. cioè un singolo "blade" può eseguire metodi may quindi è probabilmente una cattiva analogia!
James Anderson,

17

Se dovessi dargli un nome, penso che lo chiamerei l' Idra :

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.


16

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.


Non sono sicuro che questo sia l'anti-modello di God Object, poiché Dio non è vincolato da nessuna interfaccia. Essere vincolati da così tante interfacce potrebbe ridurre l'onnipotenza di Dio. ;) Forse questo è un oggetto Chimera?
FrustratedWithFormsDesigner,

Dio ha molte facce e può essere molte cose per molte persone. Se la funzionalità delle interfacce combinate rende l'oggetto "onnisciente" non sta veramente vincolando Dio, vero?
KeithS,

1
A meno che ciò che ha creato le interfacce non le modifichi. Quindi potrebbe essere necessario reimplementare Dio per adeguarsi ai cambiamenti dell'interfaccia.
FrustratedWithFormsDesigner,

3
Fa male che tu pensi che sono stato io a creare la classe.
Jonas Elfström,

L'uso di "tu" è più il plurale che fai, in riferimento al tuo team di sviluppo. QUALCUNO in quella squadra, passata o presente, ha creato questa classe. Modificherò, però.
KeithS

8

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à.

  1. 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.

  2. 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.

  3. 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!

  4. (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.


La classe in questione ha 23 interfacce e queste contengono, tutto sommato, 113 proprietà pubbliche e quattro metodi. Solo alcuni di essi sono di sola lettura. C # ha proprietà implementate automaticamente, ecco perché differisco tra metodi e proprietà msdn.microsoft.com/en-us/library/bb384054.aspx
Jonas Elfström

7

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 extendsparola 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.


Ho pensato che questa domanda fosse un linguaggio agnostico. Il codice effettivo è in C #.
Jonas Elfström,

Non ho fatto alcun C #, ma sembra supportare una forma simile di eredità dell'interfaccia.
Blrfl,

Sì, un'interfaccia può "ereditare" altre interfacce e così via.
Jonas Elfström,

Trovo interessante che molte discussioni si concentrino su ciò che le lezioni dovrebbero fare, piuttosto che su quali istanze dovrebbero fare. Un robot con una varietà di sensori di imaging e audio collegati a uno chassis comune dovrebbe essere modellato come un'entità che ha una posizione e un orientamento, che può vedere, ascoltare e associare immagini e suoni. Tutta la vera logica alla base di tali funzioni potrebbe essere in altre classi, ma il robot stesso dovrebbe supportare metodi come "vedere se ci sono oggetti in vista", "ascoltare eventuali rumori nell'area" o "vedere se l'oggetto in anticipo sembra che sia generare i suoni rilevati ".
supercat,

Può darsi che un determinato robot debba suddividere la sua funzionalità in sottosistemi in un modo particolare, ma nella misura in cui il codice pratico che utilizza il robot non dovrebbe conoscere o preoccuparsi di come sia suddivisa la funzionalità all'interno di un determinato robot. Se gli "occhi" e le "orecchie" fossero esposti al mondo esterno come oggetti separati, ma i sensori corrispondenti fossero su un braccio di estensione condiviso, il codice esterno potrebbe chiedere agli "orecchi" di ascoltare i suoni a livello del suolo contemporaneamente a chiese agli "occhi" di guardare sopra un ostacolo.
supercat,

1

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.


È in C # e ha proprietà implementate automaticamente. Queste 23 interfacce, nel complesso, contengono 113 di tali proprietà.
Jonas Elfström,

1

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.


0

"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".


0

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 .

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.