Jak uniknąć gadatliwych interfejsów

10

Tło: Projektuję aplikację serwera i tworzę osobne biblioteki DLL dla różnych podsystemów. Dla uproszczenia załóżmy, że mam dwa podsystemy: 1) Users2)Projects

Publiczny interfejs użytkownika ma metodę taką jak:

IEnumerable<User> GetUser(int id);

A publiczny interfejs projektów ma metodę taką jak:

IEnumerable<User> GetProjectUsers(int projectId);

Na przykład, kiedy musimy wyświetlić użytkowników dla określonego projektu, możemy zadzwonić GetProjectUsers, co da obiekty z wystarczającą ilością informacji do wyświetlenia w siatce danych lub podobnej.

Problem: Idealnie, Projectspodsystem nie powinien również przechowywać informacji o użytkownikach i powinien po prostu przechowywać identyfikatory użytkowników uczestniczących w projekcie. Aby służyć GetProjectUsers, musi wywołać GetUserw Userssystemie dla każdego identyfikatora użytkownika przechowywane we własnej bazie danych. Wymaga to jednak wielu osobnych GetUserwywołań, powodując wiele osobnych zapytań SQL w Userpodsystemie. Tak naprawdę nie testowałem tego, ale ten gadatliwy projekt wpłynie na skalowalność systemu.

Jeśli odłożyłbym na bok separację podsystemów, mógłbym przechowywać wszystkie informacje w jednym schemacie dostępnym dla obu systemów i Projectspo prostu zrobić, JOINaby uzyskać wszystkich użytkowników projektu w jednym zapytaniu. Projectsmusiałby także wiedzieć, jak generować Userobiekty z wyników zapytania. Ale to łamie separację, która ma wiele zalet.

Pytanie: Czy ktoś może zasugerować sposób na zachowanie separacji przy jednoczesnym unikaniu wszystkich tych indywidualnych GetUserpołączeń podczas GetProjectUsers?


Na przykład, pomyślałem, że miałem dla użytkowników, aby dać systemom zewnętrznym możliwość „otagowania” użytkowników parą wartość-etykieta oraz żądania użytkowników o określonej wartości, np .:

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

Następnie system Projekty mógłby oznaczyć każdego użytkownika dodawanym do projektu:

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

a podczas GetProjectUsers może poprosić wszystkich użytkowników projektu w jednym wywołaniu:

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

część, której nie jestem pewien, to: tak, użytkownicy są agnostycy projektów, ale tak naprawdę informacje o członkostwie projektu są przechowywane w systemie użytkowników, a nie projektów. Po prostu nie czuję się naturalnie, więc próbuję ustalić, czy brakuje mi tutaj dużej wady.

Eren Ersönmez
źródło

Odpowiedzi:

10

W twoim systemie brakuje pamięci podręcznej.

Mówisz:

Wymaga to jednak wielu osobnych GetUserwywołań, powodując wiele osobnych zapytań SQL w Userpodsystemie.

Liczba wywołań metody nie musi być taka sama jak liczba zapytań SQL. Raz otrzymujesz informacje o użytkowniku, dlaczego ponownie zapytałbyś o te same informacje, gdyby się nie zmieniły? Prawdopodobnie możesz nawet buforować wszystkich użytkowników w pamięci, co spowodowałoby zerowe zapytania SQL (chyba że użytkownik zmieni).

Z drugiej strony, wykonując Projectszapytanie podsystemowe zarówno dla projektów, jak i dla użytkowników INNER JOIN, wprowadzasz dodatkowy problem: przeszukujesz tę samą informację w dwóch różnych lokalizacjach w kodzie, co sprawia, że ​​unieważnienie pamięci podręcznej jest niezwykle trudne. W konsekwencji:

  • Albo później nie wprowadzisz pamięci podręcznej,

  • Lub spędzisz tygodnie lub miesiące studiując, co powinno zostać unieważnione, gdy zmieni się informacja,

  • Lub dodasz unieważnienie pamięci podręcznej w prostych lokalizacjach, zapominając o innych i powodując trudne do znalezienia błędy.


Ponownie czytając twoje pytanie, zauważam słowo kluczowe, które przegapiłem za pierwszym razem: skalowalność . Zasadniczo możesz postępować zgodnie z następnym wzorem:

  1. Zadaj sobie pytanie, czy system działa wolno (tzn. Narusza niefunkcjonalny wymóg wydajności lub jest po prostu koszmarem w użyciu).

    Jeśli system nie jest wolny, nie przejmuj się wydajnością. Zadbaj o czysty kod, czytelność, łatwość konserwacji, testowanie, pokrycie gałęzi, czysty projekt, szczegółową i łatwą do zrozumienia dokumentację, dobre komentarze do kodu.

  2. Jeśli tak, poszukaj wąskiego gardła. Robisz to nie przez zgadywanie, ale przez profilowanie . Profilując, określasz dokładną lokalizację wąskiego gardła (biorąc pod uwagę, że kiedy zgadniesz , prawie zawsze możesz go pomylić) i możesz teraz skupić się na tej części kodu.

  3. Po znalezieniu wąskiego gardła wyszukaj rozwiązania. Robisz to poprzez zgadywanie, testy porównawcze, profilowanie, pisanie alternatyw, zrozumienie optymalizacji kompilatora, zrozumienie optymalizacji, które możesz podjąć, zadawanie pytań na temat przepełnienia stosu i przejście na języki niskiego poziomu (w tym asemblera, jeśli to konieczne).

Jaki jest faktyczny problem z Projectspodsystemem proszącym o informacje do Userspodsystemu?

Ewentualny problem skalowalności w przyszłości? To nie jest problem. Skalowalność może stać się koszmarem, jeśli zaczniesz scalać wszystko w jedno rozwiązanie monolityczne lub sprawdzać te same dane z wielu lokalizacji (jak wyjaśniono poniżej, z powodu trudności z wprowadzeniem pamięci podręcznej).

Jeśli występuje już zauważalny problem z wydajnością, to w kroku 2 wyszukaj wąskie gardło.

Jeśli wydaje się, że rzeczywiście wąskie gardło istnieje i wynika z faktu, że Projectsżądania dla użytkowników za pośrednictwem Userspodsystemu (i znajdują się na poziomie zapytań do bazy danych), tylko wtedy należy poszukać alternatywy.

Najczęstszą alternatywą byłoby wdrożenie buforowania, drastycznie zmniejszając liczbę zapytań. Jeśli znajdujesz się w sytuacji, w której buforowanie nie pomaga, dalsze profilowanie może pokazać, że musisz zmniejszyć liczbę zapytań, dodać (lub usunąć) indeksy bazy danych, rzucić więcej sprzętu lub całkowicie przeprojektować cały system .

Arseni Mourzenko
źródło
O ile cię nie rozumiem, mówisz: „zachowuj poszczególne połączenia GetUser, ale używaj buforowania, aby uniknąć obchodzenia bazy danych”.
Eren Ersönmez
@ ErenErsönmez: GetUserzamiast zapytania do bazy danych, zajrzy do pamięci podręcznej. Oznacza to, że tak naprawdę nie ma znaczenia, ile razy zadzwonisz GetUser, ponieważ ładuje dane z pamięci zamiast z bazy danych (chyba że pamięć podręczna została unieważniona).
Arseni Mourzenko,
to dobra sugestia, biorąc pod uwagę, że nie wykonałem dobrej pracy, podkreślając główny problem, którym jest „pozbyć się otchłani bez łączenia systemów w jeden system”. Mój przykład użytkowników i projektów w naturalny sposób doprowadziłby cię do przekonania, że ​​istnieje stosunkowo niewielka liczba i rzadko zmieniających się użytkowników. Być może lepszym przykładem byłyby Dokumenty i Projekty. Wyobraź sobie, że masz kilka milionów dokumentów, tysiące dodawanych codziennie, a system Project używa systemu dokumentów do przechowywania swoich dokumentów. Czy nadal zalecałbyś buforowanie? Prawdopodobnie nie, prawda?
Eren Ersönmez,
@ ErenErsönmez: im więcej masz danych, tym bardziej krytyczne buforowanie pojawia się. Zasadniczo porównuj liczbę odczytów z liczbą zapisów. Jeśli dodawane są „tysiące” dokumentów dziennie i są miliony selectzapytań dziennie, lepiej skorzystaj z buforowania. Z drugiej strony, jeśli dodajesz miliardy jednostek do bazy danych, ale otrzymujesz tylko kilka tysięcy selectsz bardzo selektywnymi wheres, buforowanie może nie być tak przydatne.
Arseni Mourzenko,
prawdopodobnie masz rację - prawdopodobnie próbuję rozwiązać problem, którego jeszcze nie mam. Prawdopodobnie wdrożę tak, jak jest i w razie potrzeby spróbuję ulepszyć ją później. Jeśli buforowanie nie jest odpowiednie, ponieważ np. Jednostki mogą być czytane tylko 1-2 razy po dodaniu, czy uważasz, że możliwe rozwiązanie I, które dodałem do pytania, może działać? Czy widzisz z tym ogromny problem?
Eren Ersönmez,