Come evitare le interfacce loquaci


10

Background: sto progettando un'applicazione server e creando dll separate per diversi sottosistemi. Per semplificare le cose, diciamo che ho due sottosistemi: 1) Users2)Projects

L'interfaccia pubblica degli utenti ha un metodo come:

IEnumerable<User> GetUser(int id);

E l'interfaccia pubblica di Projects ha un metodo come:

IEnumerable<User> GetProjectUsers(int projectId);

Quindi, ad esempio, quando abbiamo bisogno di mostrare gli utenti per un determinato progetto, possiamo chiamare GetProjectUserse questo restituirà oggetti con informazioni sufficienti per mostrarli in un datagrid o simile.

Problema: idealmente, il Projectssottosistema non dovrebbe anche memorizzare le informazioni sugli utenti e dovrebbe semplicemente archiviare gli ID degli utenti che partecipano a un progetto. Per servire il GetProjectUsers, deve chiamare GetUseril Userssistema per ogni ID utente memorizzato nel proprio database. Tuttavia, ciò richiede molte GetUserchiamate separate , causando molte query sql separate all'interno del Usersottosistema. Non l'ho davvero provato ma avere questo design loquace influenzerà la scalabilità del sistema.

Se metto da parte la separazione dei sottosistemi, potrei archiviare tutte le informazioni in un singolo schema accessibile da entrambi i sistemi e Projectspotrei semplicemente fare una cosa JOINper ottenere tutti gli utenti del progetto in una singola query. Projectsdovrebbe anche sapere come generare Useroggetti dai risultati della query. Ma questo rompe la separazione che ha molti vantaggi.

Domanda: Qualcuno può suggerire un modo per mantenere la separazione evitando tutte queste singole GetUserchiamate durante GetProjectUsers?


Ad esempio, una di quelle che penso di avere era che gli utenti potessero dare ai sistemi esterni la possibilità di "taggare" gli utenti con una coppia etichetta-valore e di richiedere agli utenti un certo valore, ad esempio:

void AddUserTag(int userId, string tag, string value);
IEnumerable<User> GetUsersByTag(string tag, string value);

Quindi il sistema Progetti potrebbe taggare ciascun utente man mano che vengono aggiunti al progetto:

AddUserTag(userId,"project id", myProjectId.ToString());

e durante GetProjectUsers, potrebbe richiedere tutti gli utenti del progetto in una singola chiamata:

var projectUsers = usersService.GetUsersByTag("project id", myProjectId.ToString());

la parte di cui non sono sicuro è: sì, gli utenti sono agnostici rispetto ai progetti, ma in realtà le informazioni sull'appartenenza al progetto sono archiviate nel sistema Users, non in Progetti. Semplicemente non mi sento naturale, quindi sto cercando di determinare se c'è un grosso svantaggio che mi manca.

Risposte:


10

Ciò che manca nel sistema è la cache.

Tu dici:

Tuttavia, ciò richiede molte GetUserchiamate separate , causando molte query sql separate all'interno del Usersottosistema.

Il numero di chiamate a un metodo non deve essere uguale al numero di query SQL. È possibile ottenere le informazioni relative all'utente, una volta, perché si ricerca per le stesse informazioni di nuovo se non il cambiamento? Molto probabilmente, potresti persino memorizzare nella cache tutti gli utenti in memoria, il che comporterebbe zero query SQL (a meno che un utente non cambi).

D'altra parte, facendo una Projectsquery sul sottosistema sia i progetti che gli utenti con un INNER JOIN, si introduce un ulteriore problema: si sta interrogando la stessa informazione in due posizioni diverse nel codice, rendendo estremamente difficile l'invalidazione della cache. Come conseguenza:

  • O non intendi introdurre la cache in qualsiasi momento dopo,

  • Oppure passerai settimane o mesi a studiare ciò che dovrebbe essere invalidato quando cambia un'informazione,

  • Oppure aggiungerai l'annullamento della cache in posizioni semplici, dimenticando le altre e risultando in bug difficili da trovare.


Rileggendo la tua domanda, noto una parola chiave che ho perso la prima volta: scalabilità . Come regola generale, è possibile seguire il modello successivo:

  1. Chiediti se il sistema è lento (ovvero viola un requisito non funzionale delle prestazioni o è semplicemente un incubo da utilizzare).

    Se il sistema non è lento, non preoccuparti delle prestazioni. Preoccupati di codice pulito, leggibilità, manutenibilità, test, copertura delle filiali, design pulito, documentazione dettagliata e di facile comprensione, buoni commenti sul codice.

  2. Se sì, cerca il collo di bottiglia. Lo fai non indovinando, ma profilando . Tramite la profilazione, si determina la posizione esatta del collo di bottiglia (dato che, quando si indovina , si può quasi sempre sbagliare) e ora è possibile concentrarsi su quella parte del codice.

  3. Una volta trovato il collo di bottiglia, cercare soluzioni. Puoi farlo indovinando, confrontando, profilando, scrivendo alternative, comprendendo le ottimizzazioni del compilatore, comprendendo le ottimizzazioni che dipendono da te, ponendo domande su Stack Overflow e passando a linguaggi di basso livello (incluso Assembler, se necessario).

Qual è il problema reale con il Projectssottosistema che richiede informazioni al Userssottosistema?

L'eventuale futuro problema di scalabilità? Questo non è un problema. La scalabilità può diventare un incubo se si inizia a fondere tutto in un'unica soluzione monolitica o eseguire query per gli stessi dati da più posizioni (come spiegato di seguito, a causa della difficoltà di introdurre la cache).

Se c'è già un notevole problema di prestazioni, quindi, passaggio 2, cercare il collo di bottiglia.

Se sembra che, in effetti, il collo di bottiglia esiste ed è dovuto al fatto che le Projectsrichieste per gli utenti attraverso il Userssottosistema (e si trova a livello di query del database), solo allora dovresti cercare un'alternativa.

L'alternativa più comune sarebbe implementare la memorizzazione nella cache, riducendo drasticamente il numero di query. Se ti trovi in ​​una situazione in cui la memorizzazione nella cache non aiuta, un'ulteriore profilazione potrebbe mostrarti la necessità di ridurre il numero di query o aggiungere (o rimuovere) indici di database, o lanciare più hardware o riprogettare completamente l'intero sistema .


A meno che non ti fraintenda, stai dicendo "mantieni le singole chiamate GetUser, ma usa la memorizzazione nella cache per evitare i db roundtrip".
Eren Ersönmez,

@ ErenErsönmez: GetUserinvece di interrogare il database, cercherà nella cache. Ciò significa che in realtà non importa quante volte chiamerai GetUser, poiché caricherà i dati dalla memoria anziché dal database (a meno che la cache non sia stata invalidata).
Arseni Mourzenko,

questo è un buon suggerimento, dato che non ho fatto un buon lavoro mettendo in evidenza il problema principale, che è "sbarazzarsi della chiacchierata senza fondere i sistemi in un singolo sistema". Il mio esempio di utenti e progetti ti indurrebbe naturalmente a credere che ci sia un numero relativamente piccolo di utenti che cambiano raramente. Forse un esempio migliore sarebbe stato Documenti e progetti. Immagina di avere un paio di milioni di documenti, migliaia che vengono aggiunti ogni giorno e il sistema Project utilizza il sistema Document per archiviare i suoi documenti. Consiglieresti comunque la memorizzazione nella cache? Probabilmente no, vero?
Eren Ersönmez,

@ ErenErsönmez: più dati hai, più appare la cache critica. Come regola generale, confrontare il numero di letture con il numero di scritture. Se "migliaia" di documenti vengono aggiunti al giorno e ci sono milioni di selectquery al giorno, è meglio utilizzare la memorizzazione nella cache. D'altra parte, se si aggiungono miliardi di entità a un database ma si ottengono solo poche migliaia di selects con messaggi molto selettivi where, la memorizzazione nella cache potrebbe non essere così utile.
Arseni Mourzenko,

probabilmente hai ragione - probabilmente sto cercando di risolvere un problema che non ho ancora. Probabilmente implementerò così com'è e proverò a migliorare in seguito, se necessario. Se la memorizzazione nella cache non è appropriata perché, ad esempio, è probabile che le entità vengano lette solo 1-2 volte dopo essere state aggiunte, pensi che la possibile soluzione I che ho aggiunto alla domanda potrebbe funzionare? Vedi un grosso problema con quello?
Eren Ersönmez,
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.