Esplosione combinatoria di interfacce: quante sono troppe?

2

Sono un neofita relativamente nuovo di OOP, e ho un po 'di problemi a creare buoni progetti quando si tratta di interfacce.

Considera una classe A con N metodi pubblici. Ci sono un certo numero di altre classi, B, C, ..., ognuna delle quali interagisce con A in un modo diverso, cioè accede ad alcuni sottoinsiemi (< = N) dei metodi di A.

Il massimo grado di incapsulamento si ottiene implementando un'interfaccia di A per ogni altra classe, cioè AInterfaceForB , AInterfaceForC , ecc.

Tuttavia, se B, C, ... ecc. interagiscono anche con A e l'uno con l'altro, allora ci sarà un'esplosione combinatoria di interfacce (un massimo di n (n-1), per essere precisi), e il vantaggio dell'incapsulamento diventa superato da un code-bloat.

Qual è la migliore pratica in questo scenario?

L'intera idea di limitare l'accesso alle funzioni pubbliche di una classe in modi diversi per altre classi è semplicemente sciocca? Si potrebbe immaginare un linguaggio che consenta esplicitamente questo tipo di incapsulamento (ad esempio, invece di dichiarare una funzione pubblica, si potrebbe specificare esattamente a quali classi è visibile); Dal momento che questa non è una caratteristica del C ++, forse è fuorviante provare a farlo attraverso la porta sul retro con le interazioni?

    
posta MGA 12.06.2014 - 22:54
fonte

4 risposte

7

Il punto delle interfacce è di permettere al pezzo di codice di esprimere con quale API funziona. E quindi consentire a questa API di essere implementata da più classi. Nel tuo caso, anziché AforB o AforC , rendilo Bneeds e Cneeds interface.

Ciò che stai dicendo suggerisce anche che in realtà esiste solo un'implementazione di tali interfacce in forma di A . Se c'è solo un'implementazione, allora c'è un difetto grave nella progettazione, perché il punto di interfaccia deve essere implementato da più classi. Se limiti di interfaccia con quale classe è implementata, allora è male interfaccia.

    
risposta data 12.06.2014 - 22:59
fonte
6

Is the whole idea of restricting access to a class's public functions in different ways for other different classes just silly altogether?

Sì e no. Portami con me.

Limitare l'accesso a una classe specifica è sciocco. Mentre è importante separare una classe dall'implementazione delle sue dipendenze, è probabilmente più importante separare una classe dai dettagli della classe che dipende da essa.

Per questo motivo, è anche sciocco chiamare le tue interfacce come se fossero un collegamento tra le due classi. Non lo sono.

Alla classe chiamante, come sottolinea Euforic, l'interfaccia è "Devo essere in grado di farlo ... Non mi interessa come è fatto". Il nome dovrebbe riflettere questo. vale a dire. Piuttosto che un'interfaccia tra ProductService e SqlProductRepository denominata SqlProductRepositoryForProductService, dovrebbe esserci un'interfaccia nota al ProductService denominato ProductRepository ("Devo essere in grado di salvare un prodotto e non mi interessa come"), che è implementato da SqlRepository.

Ma SqlRepository potrebbe anche essere in grado di memorizzare ordini. E in questo senso, ha senso limitare l'accesso FROM ProductService a SqlRepository in modo che possa solo memorizzare i Prodotti. OrderService può accedere a SqlRepository tramite una nuova interfaccia denominata OrderRepository.

Perché preoccuparsi, chiedi? Perché non accedere semplicemente alla classe o un'interfaccia che ti dà accesso all'intera classe?

Perché potrebbe essere che, una volta che SqlRepository è cresciuto oltre il proprio controllo, si desidera suddividerlo. Forse in un SqlMembershipRepository (che potrebbe gestire utenti, gruppi, ruoli, ecc.) E un SqlManufacturingRepository (che potrebbe gestire prodotti, componenti, ecc.) E un SqlSalesRepository (che potrebbe gestire ordini, clienti, ecc.). Se hai consentito a tutti gli accessi a tutto lo SqlRepository, potresti trovare che questo è un compito molto più impegnativo che semplicemente spezzare la classe in tre e implementare le stesse interfacce che i Servizi richiedevano sempre. Soprattutto se hai diverse classi che devono salvare un prodotto.

Oppure, e questo potrebbe essere il punto più importante, potresti voler cambiare il modo in cui gestisci solo le vendite. Per qualche motivo, potresti decidere di memorizzarlo in un file o in un database NoSql.

Se hai tenuto segregate le tue interfacce, devi solo implementare l'interfaccia ProductRepository sul nuovo NoSqlRepository. Se lo hai tenuto come un'unica interfaccia, dovresti implementare SaveOrder nel NoSqlRepository (anche se era un metodo vuoto, perché non è mai stato utilizzato) e lasciare l'implementazione di SaveProduct su SqlRepository.

All'improvviso la tua intera astrazione tra servizi e repository non avrebbe senso. Un altro sviluppatore può venire e scrivere un codice che memorizzerà un ordine. C'è una buona possibilità che chiamino accidentalmente il metodo SaveOrder vuoto sul NoSqlRepository, invece del metodo SaveOrder su SqlRepository. E potrebbero essere molto confusi quando ciò non funziona.

    
risposta data 12.06.2014 - 23:45
fonte
1

Se B e C hanno bisogno di cose diverse, probabilmente c'è bisogno di due interfacce. Se questi sono implementati dalla stessa classe, così sia.

Ciò che mi colpisce qui è la tua interdipendenza delle classi. Al fine di poter effettuare il refactoring del codice in un secondo momento, i tuoi oggetti non dovrebbero dipendere l'uno dall'altro. Anche se lo nascondi con le interfacce, non dovrebbero chiamarsi a vicenda, il che mi sembra il vero difetto qui. Forse quello di cui hai effettivamente bisogno è una specie di facciata, ma è una congettura sfrenata senza conoscere i dettagli.

Penso che dovresti ripensare A usando B e, allo stesso tempo, B usando A. Probabilmente è la causa principale del tuo problema.

    
risposta data 12.06.2014 - 23:35
fonte
0

Ciò mi indicherebbe che una classe è troppo grande e / o incoerente ( link ). Altrimenti dovresti essere guidato verso l'interfaccia dallo scopo della classe. Stai cercando di fare troppo in una singola classe?

Esprimere il più piccolo numero possibile di funzionalità tramite l'interfaccia pubblica effettiva della classe, ma mantienila flessibile. Ad esempio: supponiamo di disporre di una classe di archiviazione dati in grado di elaborare SQL. Non inserire metodi "helper" specifici per l'applicazione. Basta usare un generico get , put , update , delete metodi che accettano query SQL. Il resto (ad esempio std::vector<Record> getDepartmentRecords(const DeptId& dept) ) può essere gestito nelle classi wrapping.

L'ultima cosa è che non dovresti provare a progettare esplicitamente per un client della tua classe, basta mettere le operazioni nella classe che più ha senso per questo. Se alcune altre classi vogliono limitare le loro dipendenze alla tua classe, spetta a loro decidere se vogliono usare un'interfaccia intermedia. Personalmente ritengo che questa sia una buona pratica quando si usano le librerie di terze parti ma per le dipendenze interne non dovrebbe essere realmente necessario (al di là delle normali interfacce di gerarchia di classi, ovviamente!)

    
risposta data 13.06.2014 - 19:52
fonte

Leggi altre domande sui tag