Interfacce separate per i fornitori di dati di dipendenza?

0

Sto tentando di ridefinire un componente di un grande progetto, che attualmente ha molte dipendenze dallo stato globale dell'ambiente del progetto. L'obiettivo (per i miei gusti) è un'architettura "SOLIDA", in cui tutto può essere testato correttamente.

A volte la dipendenza può essere semplicemente una stringa o un valore: una directory specifica, un nome di macchina, a volte forse una matrice.

Tuttavia, la dipendenza non può essere iniettata come valore semplice in fase di costruzione, perché il valore potrebbe non essere ancora disponibile.

Il valore potrebbe provenire da un contenitore di iniezione delle dipendenze, dal sistema di configurazione, ecc. Attualmente la classe stessa chiama a funzioni legacy o metodi statici di questi sottosistemi, per ottenere i rispettivi valori. Ciò è negativo a causa della dipendenza globale dello stato e perché il componente sa troppo di questi sottosistemi.

Idea

Quindi la mia idea era di creare un'interfaccia "fornitore di dati" per dipendenza, con diverse implementazioni per "reale" e "falso". Un'interfaccia esprime uno dei bisogni di un componente che dipende da esso, non di più. Quindi, molti di loro hanno solo un metodo. Alcuni vengono riutilizzati, ma non molto.

Anche la funzionalità principale è suddivisa in sotto-componenti con interfacce. Alcuni di loro hanno decoratori di cache o buffer. Una parte è divisa in una catena di processori, che implementano un "(Name) ProcessorInterface".

Problema: troppe classi e interfacce.

Funziona alla grande, e tutto è super-testabile.

Uso sempre lo stesso stile nei miei progetti e non esiterei a farlo di nuovo qui.

Ma qui non è una mia scelta. Questo è un enorme progetto OSS, con una grande comunità di sviluppatori, che devo convincere, se voglio farlo in questo modo. E il resto del codice, per la maggior parte, non assomiglia a questo.

Il numero di classi e interfacce sta diventando molto alto. E molti di loro sono solo "interni", quindi non sono intesi come parte di un'API pubblica.

Ecco un esempio di classi che sto / stavo per aggiungere . Questo è troppo grande per postare qui. L'originale era semplicemente una chiamata a drupal_get_profile() , che ovviamente rende il test molto più difficile.

O ho sbagliato, o ho bisogno di un modo migliore per venderlo.

Domande

Per riassumere le domande principali:

  • È un problema se alcune interfacce non sono pensate per la "public API", ma solo per "internal"?
  • C'è un problema ci sono troppe classi? La complessità dovrebbe essere misurata in numero di classi o dimensione media di ogni singola classe?
  • C'è un modo per ridurre il numero di classi e avere ancora i vantaggi di un'architettura "solida"?

Ho la mia risposta per la maggior parte di questi, ma ho la sensazione che questo non sia sufficiente per convincere nessuno.

Altri pensieri

Un'alternativa sarebbe avere più interfacce "generiche", ad es. "StringProviderInterface" invece di "ProfileNameInterface". Ma poi perderei la specificità semantica. Solo perché restituiscono tutte le stringhe, non le rende intercambiabili.

Forse le interfacce saranno riutilizzate più spesso, se più codice si trasforma in questo stile.

Aggiornamento

Mentre la discussione procede, una preoccupazione importante e ripetuta che sento è che ogni nuova interfaccia deve essere retrocompatibile, anche se è pensata per qualcosa di "interno". Sono tecnicamente d'accordo con questo, ma penso che si possa dire lo stesso delle classi e dei metodi pubblici. Quindi mi chiedo se questa è davvero una preoccupazione valida. Forse il vero problema è avere delle interfacce un po 'strane che penzolano intorno.

    
posta donquixote 10.05.2016 - 11:39
fonte

1 risposta

2

Thus, most of them have just one method.

Questo aspetto puzza anche in linguaggi controllati staticamente controllati e orientati agli oggetti come Java o C # che non scoraggiano la creazione di un numero sempre maggiore di interfacce e classi. In lingue come PHP, Ruby o Python, questo sembra ancora più sbagliato. Un'interfaccia e due classi solo per una stringa? Sembra troppo architettura e non molto KISS.

Some are being reused, but not a lot.

Anche questo non sembra giusto. Se due interfacce non hanno nulla in comune tranne il tipo che racchiudono, tenerle strettamente separate. Riutilizzare funziona bene per i tipi reali. Supponiamo che il tipo Distance possa essere utilizzato per memorizzare la distanza tra due Building s, ma anche come proprietà Length di Wall class. Ma questo non funziona bene per i valori iniettati di dipendenza, ad esempio quando si inserisce la stringa di connessione del database e l'indirizzo del server della cache.

Mantieni insieme informazioni simili

Ad esempio, se l'applicazione ha una configurazione con alcune decine di valori, non c'è nulla di sbagliato nel mantenerli insieme in una classe AppConfiguration e utilizzare un'interfaccia IAppConfiguration . Il AppConfigurationStub potrebbe consentire di definire valori specifici per i test mantenendo altri non definiti / non impostati.

Se una classe specifica della tua applicazione deve conoscere una stringa di connessione al database e il numero massimo di prodotti da mostrare sulla home page, assegnagli l'oggetto di configurazione dell'applicazione completo, non solo quelle due informazioni. Non importa che la classe non abbia bisogno di conoscere l'indirizzo e-mail dell'amministratore o l'elenco delle lingue disponibili: se la classe non ha bisogno di questa informazione, semplicemente non la accederà.

Naturalmente, se si manipolano dati riservati, le cose sono diverse. Ad esempio, l'interfaccia IUserConfiguration può puntare al colore preferito dall'utente o al numero di prodotti nel carrello-informazioni che non sono particolarmente sensibili, ma anche il numero di carta di credito dell'indirizzo e-mail-informazioni che dovresti tenere al sicuro . Fornire un'interfaccia comune a informazioni sensibili e non sensibili sarebbe un errore; invece, potresti avere un'interfaccia separata con le informazioni personali della persona (e-mail, numero di telefono, data di nascita, ecc.) e una terza interfaccia con informazioni altamente sensibili (numero di carta di credito).

Un'altra eccezione è quando, molto logicamente, una classe non può essere interessata da nient'altro che da un campo di un oggetto. Ad esempio, le classi del livello di accesso ai dati (DAL) potrebbero aver bisogno di conoscere esclusivamente la stringa di connessione e non dovrebbero preoccuparsi del numero massimo di prodotti da mostrare sulla home page (poiché questa è una logica aziendale, e verranno passati successivamente al metodo specifico di DAL sotto forma "dammi 20 prodotti, senza preoccuparmi di dove li visualizzerò".) In questa situazione, DAL non dovrebbe nemmeno sapere nulla sull'aspetto della configurazione dell'applicazione o anche applicazione di per sé . Può essere un servizio. O un cron job. Potrebbero avere una propria configurazione, che ha una struttura molto specifica.

BACIO

Se una classe ha bisogno di un valore scalare (come una stringa o una matrice), non avvolgerla in una struttura di classe / interfaccia / stub solo per il beneficio dell'iniezione di dipendenza. Ci si aspetta che l'iniezione di dipendenza ti semplifichi la vita; non è previsto che ti costringa a sovrascrivere il tuo sistema.

Inietta le informazioni just-in-time

Hai menzionato un problema di non conoscenza di alcune informazioni durante l'inizializzazione di una classe. Se non lo conosci in questa fase, molto probabilmente la classe che viene inizializzata non ne ha bisogno durante l'inizializzazione. Se ne ha bisogno a volte più tardi, puoi posticipare l'iniezione della dipendenza fino a quando non chiami il metodo che ha bisogno delle informazioni.

Ad esempio, invece di:

$productsData = new ProductsData($region);
...
$productsData->loadProducts();

puoi fare:

$productsData = new ProductsData;
...
$productsData->loadProducts($region);

A volte, non puoi farlo. Ad esempio, se la maggior parte dei metodi di una classe richiede un'informazione, preferirai dare queste informazioni una volta durante l'inizializzazione dell'oggetto piuttosto che ogni volta che chiami i metodi di questo oggetto.

In questo caso (e se hai bisogno di passare un singolo valore come una stringa o un array), puoi usare un costrutto simile a C # Lazy<T> , che è un modo per dire alla classe" hey, non conosco il valore esatto adesso (o non lo so voglio calcolarlo proprio ora, perché è computazionalmente costoso e potresti anche non averne bisogno), ma so come ottenerlo quando e se, in seguito, ne hai effettivamente bisogno ". Puoi reimplementare lo schema in tutte le lingue in cui funzioni / metodi sono cittadini di prima classe.

    
risposta data 12.05.2016 - 21:43
fonte

Leggi altre domande sui tag