Come applicare il principio di segregazione dell'interfaccia in C?


15

Ho un modulo, ad esempio "M", che ha alcuni client, ad esempio "C1", "C2", "C3". Voglio dividere lo spazio dei nomi del modulo M, ovvero le dichiarazioni delle API e dei dati che espone, in file di intestazione in modo tale che -

  1. per qualsiasi client sono visibili solo i dati e le API richiesti; il resto dello spazio dei nomi del modulo è nascosto dal client, ovvero aderisce al principio di segregazione dell'interfaccia .
  2. una dichiarazione non viene ripetuta in più file di intestazione, ovvero non viola DRY .
  3. il modulo M non ha dipendenze dai suoi client.
  4. un client non è influenzato dalle modifiche apportate in alcune parti del modulo M che non è utilizzato da esso.
  5. i client esistenti non sono interessati dall'aggiunta (o eliminazione) di più client.

Attualmente mi occupo di questo dividendo lo spazio dei nomi del modulo in base alle esigenze dei suoi clienti. Ad esempio, nell'immagine sotto sono mostrate le diverse parti dello spazio dei nomi del modulo richiesto dai suoi 3 client. I requisiti del cliente si sovrappongono. Lo spazio dei nomi del modulo è diviso in 4 file di intestazione separati: 'a', '1', '2' e '3' .

Partizionamento dello spazio dei nomi del modulo

Tuttavia, ciò viola alcuni dei requisiti di cui sopra, ad esempio R3 e R5. Il requisito 3 viene violato perché questo partizionamento dipende dalla natura dei client; anche con l'aggiunta di un nuovo client questo partizionamento modifica e viola il requisito 5. Come si può vedere nella parte destra dell'immagine sopra, con l'aggiunta di un nuovo client lo spazio dei nomi del modulo è ora diviso in 7 file di intestazione - 'a ',' b ',' c ',' 1 ',' 2 * ',' 3 * 'e' 4 ' . I file di intestazione significano per 2 delle modifiche dei client più vecchi, innescando così la loro ricostruzione.

C'è un modo per ottenere la segregazione delle interfacce in C in modo non forzato?
Se sì, come tratteresti l'esempio sopra?

Una soluzione ipotetica irreale che immagino sarebbe:
il modulo ha un grosso file di intestazione che copre l'intero spazio dei nomi. Questo file di intestazione è diviso in sezioni indirizzabili e sottosezioni come una pagina di Wikipedia. Ogni client ha quindi un file di intestazione specifico su misura per esso. I file di intestazione specifici del client sono solo un elenco di collegamenti ipertestuali alle sezioni / sottosezioni del file di intestazione fat. E il sistema di compilazione deve riconoscere un file di intestazione specifico del client come 'modificato' se una delle sezioni a cui punta nell'intestazione del modulo viene modificata.


1
Perché questo problema è specifico di C? È perché C non ha eredità?
Robert Harvey,

Inoltre, la violazione dell'ISP rende migliore la progettazione?
Robert Harvey,

2
C in realtà non supporta intrinsecamente concetti OOP (come interfacce o ereditarietà). Ci accontentiamo di hack grezzi (ma creativi). Alla ricerca di un trucco per simulare le interfacce. In genere, l'intero file di intestazione è l'interfaccia di un modulo.
work.bin,

1
structè quello che usi in C quando vuoi un'interfaccia. Certo, i metodi sono un po 'difficili. Potresti trovare questo interessante: cs.rit.edu/~ats/books/ooc.pdf
Robert Harvey,

Non sono riuscito a trovare un'interfaccia equivalente usando structe function pointers.
work.bin,

Risposte:


5

La segregazione dell'interfaccia, in generale, non dovrebbe essere basata sui requisiti del cliente. Dovresti cambiare l'intero approccio per raggiungerlo. Direi, modularizzare l'interfaccia raggruppando le caratteristiche in coerenti gruppi . Il raggruppamento si basa sulla coerenza delle funzionalità stesse, non sui requisiti del cliente. In tal caso, avrai una serie di interfacce, I1, I2, ecc. Il client C1 può usare solo I2. Il client C2 può utilizzare I1 e I5 ecc. Si noti che, se un client utilizza più di un Ii, non è un problema. Se hai scomposto l'interfaccia in moduli coerenti, è qui che si trova il nocciolo della questione.

Ancora una volta, l'ISP non è basato sul client. Si tratta di scomporre l'interfaccia in moduli più piccoli. Se ciò viene eseguito correttamente, garantirà anche che i client siano esposti a tutte le funzioni di cui hanno bisogno.

Con questo approccio, i tuoi clienti possono aumentare a qualsiasi numero ma tu M non è interessato. Ogni client utilizzerà una o alcune combinazioni delle interfacce in base alle proprie esigenze. Ci saranno casi in cui un client, C, deve includere dire I1 e I3, ma non utilizzare tutte le funzionalità di queste interfacce? Sì, non è un problema. Utilizza solo il minor numero di interfacce.


Sicuramente intendevi gruppi disgiunti o non sovrapposti , suppongo?
Doc Brown

Sì, disgiunto e non sovrapposto.
Nazar Merza,

3

Il principio di segregazione dell'interfaccia afferma:

Nessun client dovrebbe essere costretto a dipendere da metodi che non utilizza. L'ISP suddivide le interfacce che sono molto grandi in quelle più piccole e più specifiche in modo che i clienti debbano solo conoscere i metodi che li interessano.

Ci sono alcune domande senza risposta qui. Uno è:

Quanto piccolo?

Tu dici:

Attualmente mi occupo di questo dividendo lo spazio dei nomi del modulo in base alle esigenze dei suoi clienti.

Chiamo questo manuale digitando anatra . Costruisci interfacce che espongono solo le esigenze di un client. Il principio di segregazione dell'interfaccia non è semplicemente la tipizzazione manuale delle anatre.

Ma l'ISP non è semplicemente una richiesta di interfacce di ruoli "coerenti" che possono essere riutilizzate. Nessun progetto di interfaccia di ruolo "coerente" può difendersi perfettamente dall'aggiunta di un nuovo client con le proprie esigenze di ruolo.

ISP è un modo per isolare i clienti dall'impatto delle modifiche al servizio. È stato progettato per rendere la compilazione più veloce mentre si apportano modifiche. Sicuramente ha altri vantaggi, come non rompere i clienti, ma quello era il punto principale. Se sto cambiando la count()firma della funzione servizi è bello se i client che non usano count()non hanno bisogno di essere modificati e ricompilati.

Questo è il motivo per cui mi interessa il principio di segregazione dell'interfaccia. Non è qualcosa che credo nella fede così importante. Risolve un vero problema.

Quindi il modo in cui dovrebbe essere applicato dovrebbe risolvere un problema per te. Non esiste un modo di morte cerebrale per applicare l'ISP che non può essere sconfitto con il giusto esempio di un cambiamento necessario. Dovresti guardare come sta cambiando il sistema e fare delle scelte che permetteranno di calmare le cose. Esploriamo le opzioni.

Prima chiediti: in questo momento è difficile apportare modifiche all'interfaccia di servizio? Altrimenti, esci e gioca fino a quando non ti calmi. Questo non è un esercizio intellettuale. Assicurati che la cura non sia peggiore della malattia.

  1. Se molti client utilizzano lo stesso sottoinsieme di funzioni, ciò richiede interfacce riutilizzabili "coerenti". Il sottoinsieme probabilmente si concentra su un'idea che possiamo pensare al ruolo che il servizio fornisce al cliente. È bello quando funziona. Questo non funziona sempre.

  2.  

    1. Se molti client utilizzano diversi sottogruppi di funzioni, è possibile che il client stia effettivamente utilizzando il servizio attraverso più ruoli. Va bene, ma rende i ruoli difficili da vedere. Trovali e prova a stuzzicarli. Ciò può riportarci nel caso 1. Il client utilizza semplicemente il servizio attraverso più di un'interfaccia. Per favore, non iniziare a trasmettere il servizio. Semmai ciò significherebbe passare il servizio nel client più di una volta. Funziona ma mi viene da chiedersi se il servizio non è una grossa palla di fango che deve essere rotta.

    2. Se molti client usano sottoinsiemi diversi ma non vedete ruoli nemmeno permettendo che i client possano utilizzarne più di uno, allora non avete niente di meglio della digitazione duck per progettare le vostre interfacce. Questo modo di progettare le interfacce assicura che il client non sia esposto a nemmeno una funzione che non sta utilizzando ma garantisce quasi che l'aggiunta di un nuovo client comporterà sempre l'aggiunta di una nuova interfaccia che mentre l'implementazione del servizio non deve sapere su di esso sarà l'interfaccia che aggrega il ruolo interfacce. Abbiamo semplicemente scambiato un dolore per un altro.

  3. Se molti client utilizzano diversi sottogruppi, si sovrappongono, si prevede che verranno aggiunti nuovi client che avranno bisogno di sottoinsiemi imprevedibili e non si è disposti a interrompere il servizio, quindi prendere in considerazione una soluzione più funzionale. Dal momento che le prime due opzioni non hanno funzionato e sei davvero in una brutta posizione in cui nulla sta seguendo uno schema e stanno arrivando più cambiamenti quindi considera di fornire a ciascuna funzione la propria interfaccia. Finire qui non significa che l'ISP non sia riuscito. Se qualcosa falliva, era il paradigma orientato agli oggetti. Le interfacce a metodo singolo seguono l'ISP all'estremo. È un bel po 'di digitazione della tastiera, ma potresti scoprire che improvvisamente rende le interfacce riutilizzabili. Ancora una volta, assicurati che non ci sia

Quindi si scopre che possono diventare davvero molto piccoli.

Ho preso questa domanda come una sfida per applicare l'ISP nei casi più estremi. Ma tieni presente che è meglio evitare gli estremi. In una progettazione ben ponderata che applica altri principi SOLID questi problemi di solito non si verificano o contano, quasi altrettanto.


Un'altra domanda senza risposta è:

Chi possiede queste interfacce?

Vedo ripetutamente interfacce progettate con quella che chiamo mentalità da "biblioteca". Siamo stati tutti colpevoli della codifica Monkey-See-Monkey-Do in cui stai facendo qualcosa perché è così che l'hai vista. Siamo colpevoli della stessa cosa con le interfacce.

Quando guardo un'interfaccia progettata per una lezione in una biblioteca pensavo: oh, questi ragazzi sono dei professionisti. Questo deve essere il modo giusto di fare un'interfaccia. Quello che non riuscivo a capire è che un limite della biblioteca ha i suoi bisogni e problemi. Per prima cosa, una biblioteca ignora completamente il design dei suoi clienti. Non tutti i confini sono uguali. E a volte anche lo stesso confine ha modi diversi di attraversarlo.

Ecco due semplici modi per esaminare il design dell'interfaccia:

  • Interfaccia di proprietà del servizio. Alcune persone progettano ogni interfaccia per esporre tutto ciò che un servizio può fare. Puoi anche trovare opzioni di refactoring negli IDE che scriveranno un'interfaccia per te usando qualunque classe la dai.

  • Interfaccia di proprietà del cliente. L'ISP sembra sostenere che questo è giusto e che il servizio di proprietà è sbagliato. Dovresti interrompere ogni interfaccia tenendo presente le esigenze dei clienti. Poiché il client possiede l'interfaccia, dovrebbe definirla.

Allora chi ha ragione?

Prendi in considerazione i plugin:

inserisci qui la descrizione dell'immagine

Chi possiede le interfacce qui? I clienti? I servizi?

Risulta entrambi.

I colori qui sono strati. Il livello rosso (a destra) non dovrebbe sapere nulla sul livello verde (a sinistra). Il livello verde può essere modificato o sostituito senza toccare il livello rosso. In questo modo qualsiasi livello verde può essere inserito nel livello rosso.

Mi piace sapere cosa dovrebbe sapere su cosa, e cosa non dovrebbe sapere. Per me, "cosa sa di cosa?", È la domanda architettonica più importante.

Cerchiamo di chiarire un po 'di vocabolario:

[Client] --> [Interface] <|-- [Service]

----- Flow ----- of ----- control ---->

Un client è qualcosa che usa.

Un servizio è qualcosa che viene utilizzato.

Interactor sembra essere entrambi.

L'ISP afferma di interrompere le interfacce per i client. Bene, applichiamolo qui:

  • Presenter(un servizio) non dovrebbe dettare Output Port <I>all'interfaccia. L'interfaccia dovrebbe essere ristretta a ciò di cui Interactor(qui agendo come cliente). Ciò significa che l'interfaccia CONOSCE riguardo Interactorall'ISP e, per seguire l'ISP, deve cambiare con essa. E questo va bene.

  • Interactor(qui agendo come un servizio) non dovrebbe dettare Input Port <I>all'interfaccia. L'interfaccia dovrebbe essere ristretta a ciò di cui Controller(un cliente) ha bisogno. Ciò significa che l'interfaccia CONOSCE riguardo Controllerall'ISP e, per seguire l'ISP, deve cambiare con essa. E questo non va bene.

Il secondo non va bene perché il livello rosso non dovrebbe conoscere il livello verde. Quindi l'ISP è sbagliato? Beh un pò. Nessun principio è assoluto. Questo è un caso in cui gli sciocchi a cui piace l'interfaccia per mostrare tutto ciò che il servizio può fare risultano essere giusti.

Almeno, hanno ragione se il Interactornon fa nulla di diverso da questo caso d'uso necessario. Se lo Interactorfa per altri casi d'uso, non c'è motivo che questo Input Port <I>debba conoscerli. Non sono sicuro del perché Interactornon possa concentrarsi solo su un caso d'uso, quindi questo non è un problema, ma succede qualcosa.

Ma l' input port <I>interfaccia semplicemente non può essere asservita al Controllerclient e avere questo come un vero plugin. Questo è un limite di "biblioteca". Un negozio di programmazione completamente diverso potrebbe scrivere il livello verde anni dopo la pubblicazione del livello rosso.

Se stai attraversando un limite di "libreria" e senti la necessità di applicare l'ISP anche se non possiedi l'interfaccia dall'altra parte, dovrai trovare un modo per restringere l'interfaccia senza cambiarla.

Un modo per farlo è un adattatore. Mettilo tra client come Controlere l' Input Port <I>interfaccia. L'adattatore accetta Interactorcome Input Port <I>e delega il suo lavoro. Tuttavia, espone solo ciò di cui i clienti hanno Controllerbisogno attraverso un'interfaccia di ruolo o interfacce di proprietà del livello verde. L'adattatore non segue l'ISP da solo, ma consente a classi più complesse come Controllergodersi l'ISP. Ciò è utile se ci sono meno adattatori rispetto a client come quelli Controllerche li usano e quando ti trovi in ​​una situazione insolita in cui stai attraversando un limite della libreria e, nonostante sia pubblicata, la libreria non smetterà di cambiare. Ti guardo Firefox. Ora quei cambiamenti rompono solo i tuoi adattatori.

Che cosa significa questo? Significa onestamente che non mi hai fornito informazioni sufficienti per dirti cosa dovresti fare. Non so se non seguire l'ISP ti sta causando un problema. Non so se seguirlo non finirebbe per causarti ulteriori problemi.

So che stai cercando un semplice principio guida. L'ISP cerca di essere quello. Ma lascia molto non detto. Ci credo. Sì, per favore non forzare i clienti a dipendere da metodi che non usano, senza una buona ragione!

Se hai una buona ragione, come progettare qualcosa per accettare plug-in, fai attenzione ai problemi che non seguono le cause dell'ISP (è difficile cambiare senza rompere i client) e ai modi per mitigarli (mantieni Interactoro almeno Input Port <I>focalizzato su uno stabile caso d'uso).


Grazie per l'input. Ho un modulo di fornitura di servizi che ha più clienti. Il suo spazio dei nomi ha confini logicamente coerenti ma il client deve attraversare questi confini logici. Quindi dividere lo spazio dei nomi sulla base di confini logici non aiuta con l'ISP. Pertanto ho diviso lo spazio dei nomi in base alle esigenze del cliente, come mostrato nel diagramma nella domanda. Ma questo lo rende dipendente dai client e un modo scadente di associare i client al servizio, poiché i client potrebbero essere aggiunti / rimossi relativamente frequentemente, ma le modifiche al servizio saranno minime.
work.bin,

Ora mi sto orientando verso il servizio fornendo un'interfaccia complessa, come nel suo intero spazio dei nomi ed è compito del cliente accedere a questi servizi tramite adattatori specifici del cliente. In termini C sarebbe un file di wrapper di funzioni di proprietà del client. Le modifiche al servizio forzerebbero la ricompilazione dell'adattatore ma non necessariamente del client. .. <
contd

<contd> .. Ciò manterrà sicuramente i tempi di costruzione minimi e manterrà 'libero' l'accoppiamento tra il client e il servizio a costo di runtime (chiamando una funzione wrapper intermedio), aumenta lo spazio del codice, aumenta l'utilizzo dello stack e probabilmente più spazio mentale (programmatore) nel mantenimento degli adattatori.
work.bin

La mia attuale soluzione soddisfa ora le mie esigenze, il nuovo approccio richiederà più sforzi e potrebbe violare YAGNI. Dovrò valutare i pro e i contro di ciascun metodo e decidere in che modo andare qui.
work.bin,

1

Quindi questo punto:

existent clients are unaffected by the addition (or deletion) of more clients.

Rinuncia che stai violando un altro principio importante che è YAGNI. Mi importerebbe quando avrò centinaia di clienti. Pensare a qualcosa in anticipo e poi si scoprirà che non hai altri client per questo codice batte lo scopo.

Secondo

 partitioning depends on the nature of clients

Perché il tuo codice non utilizza DI, inversione di dipendenza, niente, niente nella tua libreria dovrebbe dipendere dalla natura del tuo client.

Alla fine sembra che tu abbia bisogno di un livello aggiuntivo sotto il tuo codice per soddisfare le esigenze di cose sovrapposte (DI quindi il tuo codice frontale dipende solo da questo livello aggiuntivo e i tuoi clienti dipendono solo dalla tua interfaccia frontale) in questo modo batti il ​​DRY.
Questo lo faresti davvero. Quindi fai le stesse cose che usi nel tuo livello di modulo sotto un altro modulo. In questo modo, disponendo il livello sottostante puoi ottenere:

per qualsiasi client sono visibili solo i dati e le API richiesti; il resto dello spazio dei nomi del modulo è nascosto dal client, ovvero aderisce al principio di segregazione dell'interfaccia.

una dichiarazione non viene ripetuta in più file di intestazione, ovvero non viola DRY. il modulo M non ha dipendenze dai suoi client.

un client non è influenzato dalle modifiche apportate in alcune parti del modulo M che non è utilizzato da esso.

i client esistenti non sono interessati dall'aggiunta (o eliminazione) di più client.


1

Le stesse informazioni fornite nella dichiarazione sono sempre ripetute nella definizione. È solo il modo in cui funziona questa lingua. Anche, ripetere una dichiarazione in più file di intestazione non viola DRY . È una tecnica piuttosto comunemente usata (almeno nella libreria standard).

Ripetere la documentazione o l'implementazione violerebbe DRY .

Non mi preoccuperei di questo a meno che il codice client non sia stato scritto da me.


0

Disconosco la mia confusione. Tuttavia, il tuo esempio pratico disegna una soluzione nella mia testa. Se posso esprimere le mie parole: tutte le partizioni nel modulo ne Mhanno molte o molte relazione esclusiva di con tutti i client.

Struttura del campione

M.h      // fat header
 - P1    // Partition 1
 - P2    // ... 2
   - P21 // ... 2 section 1
 - P3    // ... 3
C1.c     // Client 1 (Needs to include P1, P3)
C2.c     // ... 2 (Needs to include P2)
C3.c     // ... 3 (Needs to include P1, P21, P3)

mh

#ifdef P1
#define _PREF_ P1_             // Define Prefix ("PREF") = P1_
 void _PREF_init();            // Some partition specific function
#endif /* P1 */

#ifdef P2
#define _PREF_ P2_
 void _PREF_init();
#endif /* P2 */

#if defined(P21) || defined (P2) // Part 2.1
#define _PREF_ P2_1_
 void _PREF_oddone();
#endif /* P21 */

#ifdef P3
#define _PREF_ P3_
 void _PREF_init();
#endif /* P3 */

Mc

Nel file Mc, non dovresti effettivamente usare #ifdefs perché ciò che metti nel file .c non ha effetto sui file client fintanto che le funzioni che i file client usano sono definite.

#include "M.h"
#define _PREF_ P1_        
void _PREF_init() { ... };

#define _PREF_ P2_
void _PREF_init() { ... }

#define _PREF_ P2_1_
void _PREF_oddone() { ... }

#define _PREF_ P3_
void _PREF_init() { ... }

C1.c

#define P1     // "invite" P1
#define P3     // "invite" P3
#include "M.h" // Open the door, but only the invited come in.

void main()
{
    P1_init();
    //P2_init();
    //P2_1_oddone();
    P3_init();
}

C2.c

#define P2
#include "M.h

void main()
{
    //P1_init();
    P2_init();
    P2_1_oddone();
    //P3_init();
}

C3.c

#define P1
#define P21
#define P3  
#include "M.h" 

void main()
{
    P1_init();
    //P2_init();
    P2_1_oddone();
    P3_init();
}

Ancora una volta, non sono sicuro se questo è quello che stai chiedendo. Quindi prendilo con un granello di sale.


Che aspetto ha Mc? Definisci P1_init() e P2_init() ?
work.bin,

@ work.bin Presumo che Mc sembrerebbe un semplice file .c con l'eccezione di definire lo spazio dei nomi tra le funzioni.
Sanchke Dellowar,

Supponendo che esistano sia C1 che C2 - che cosa fa P1_init()e si P2_init()collega a?
work.bin,

Nel file Mh / Mc, il preprocessore sostituirà _PREF_qualunque cosa sia stata definita l'ultima volta. Quindi _PREF_init()sarà a P1_init()causa dell'ultima dichiarazione #define. Poi l'istruzione successiva definire imposterà PREF uguale a P2_, generando così P2_init().
Sanchke Dellowar,
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.