NET 5 WebAPI - jaki jest cel interfejsów do serwisów

0

Witam.
Post z serii - "To jest głupie, po co mam to robić..."

Wydaje mi się, że jest to kompletnie niepotrzebne klepanie kodu, które nie ma żadnego mądrego celu. Dlaczego jest to konieczne? Wzorzec? Microsoft tak chce? Oglądam różnego rodzaju projekty na GH i w większości z nich jest interfejs i klasa z logiką do tego interfejsu.

0

Z takich podstaw jakie przychodzą mi do głowy to przede wszystkim kwestia dziedziczenia i unit testów. Aczkolwiek też chętnie się dowiem czy są jakieś wymogi bardziej formalne

0

Testy jeszcze jestem w stanie zrozumieć ale dziedziczenie do mnie nie przemawia. Robiąc klasę, która dziedziczy po interfejsie, zmusza cię to do zaimplementowania wszystkiego w pełni i to może być dobre, bo IDE "pomyśli" za ciebie żeby ci nic nie umknęło ale... Jak chcesz coś do interfejsu dopisać, to musisz to zrobić w dwóch miejscach, bo jak wpiszesz tylko w klasie to jest ta metoda niewidoczna.

0

Ale masz możliwość dziedziczenia po wielu interfejsach :) a tylko po jednej klasie. W dużych projektach zaczyna mieć to znaczenie bo w małych to można sobie darować i też to będzie chodzić

0

Co to jest duży projekt i w jakich przypadkach potrzebuje dziedziczyć po wielu interfejsach? 🤔

3
AdamWox napisał(a):

Robiąc klasę, która dziedziczy po interfejsie, zmusza cię to do zaimplementowania wszystkiego w pełni

Tutaj wracamy do jakiś podstawowych zasad, które jak wiemy lubisz kwestionować - Interface segregation principle

Programowanie do interfejsu rozluźnia połączenia między fragmentami kodu, pozwala m.in. na odzielenie domeny od infrastruktury, przez co łatwo przetestować sensowne fragmenty w izolacji.
I mała uwaga nt. nomentklatury - interfejsy się implementuje, nie **dziedziczy **się po nich.

0

Tak myślałem, że znalazłem nową rzecz, której mogę się czepić 😁
Czyli wszystko sprowadza się do wielkich projektów, w których takie "implementacje" jest dobrze stosować. Te CRUDy na GH są zwyczajnym przyzwyczajaniem potencjalnego czytelnika do takich zabiegów w projekcie, a nie koniecznością.

Czyli zapis

services.AddScoped<IProductsService, ProductsService>();

Może być taki

services.AddScoped<ProductsService>();
1

Jeśli masz jedną implementację to robienie interfejsu na zaś nie ma sensu, bo każde IDE pozwala na wyekstrahowanie interfejsu w razie potrzeby. Poza tym nie sądzę, żeby CRUDy z GH były dobrym źrodłem inspiracji.

1

Nawet jeśli masz jedną implementację to czasem tworzenie interfejsu ma sens, głównie ze względu na łatwość mockowania tej implementacji w testach i izolowaniu reszty kodu od tej zależności. Oczywiście da się to zrobić i bez interfejsu ale tak jest najłatwiej.

Co do samych interfejsów to nic innego jak kontrakty które mówią, ze dana klasa wystawia jakieś składowe (np metody czy właściwości) - a przynajmniej było tak do czasu wprowadzenia domyślnych implementacji interfejsów które zacierają nieco granicę pomiędzy klasami abstrakcyjnymi a interfejsami.

Przykładem jest np. interfejs IDisposable, który mówi że dana klasa w jakiś sposób implementuje metodę Dispose i można ją wywołać jawnie lub mniej jawnie przez użycie bloku using.
Po interfejsach się nie dziedziczy, interfesy się implementuje.
Co do bezmyślnego klepania kodu zarówno w klasie jak i interfejsie - poczytaj sobie o segregacji interfejsów.
Interfejsy można wykorzystać jako "markery", które pozwolą na implementację ciekawych mechanizmów na bazie tychże właśnie interfejsów i metod przez nie implementowanych bez użycia dziedziczenia (które mogłoby nie mieć w tym przypadku sensu). Przykładami nie ma sensu chyba rzucać bo będą zbyt ogólnikowe ale z drugiej strony sam temat nie jest zbyt konkretny. Ja używałem interfejsów np do budowania dynamicznego filtrowania albo renderowania widoków.

Z moich własnych obserwacji wynika, że ludzie często wpychają interfejsy na siłę tam gdzie ich zupełnie nie potrzeba ale to nie znaczy, że:

  1. interfejsy są nieprzydatne
  2. że należy brać ich poczynania za wyznacznik

Można napisać skomplikowany system bez dokładania do niego żadnych interfejsów, tyle że mądre ich użycie może to znacznie ułatwić.

2
AdamWox napisał(a):

Wydaje mi się, że jest to kompletnie niepotrzebne klepanie kodu, które nie ma żadnego mądrego celu. Dlaczego jest to konieczne? Wzorzec? Microsoft tak chce? Oglądam różnego rodzaju projekty na GH i w większości z nich jest interfejs i klasa z logiką do tego interfejsu.

A dlaczego uważasz, że jest to konieczne? To jest absolutnie zbędne. Autorzy tych repozytoriów nie myślą, tylko powielają kulty cargo.

AdamWox napisał(a):

Co to jest duży projekt i w jakich przypadkach potrzebuje dziedziczyć po wielu interfejsach? 🤔

Np. w przypadku fluent step by step bulidera.
Albo gdy masz klasę, która realizuje np. logikę interakcji z jakimś zewnętrznym API i jest używana przez wiele innych klas, z których każda potrzebuje jedynie jakiegoś wycinka funkcjonalności całego API. Wówczas do klas używających możesz wstrzykiwać wyspecjalizowane interfejsy, ale jednocześnie całą interakcję z jednym konkretnym API zamknąć w jednej klasie.

0

Pamiętam jak ludzie zastanawiali się do czego właściwie są te interfejsy zanim TDD stało się modne.

0

@gosc_z_pytaniem:
Do TDD także tego ich nie potrzebujesz, możesz skorzystać z biblioteki takiej jak ta https://github.com/tonerdo/pose

0

Z góry zaznaczam, że mój przykład może być bardzo kiepski, ponieważ to wszystko cały czas obraca wokół Comarch Optima i stworzenia do tego aplikacji webowej. To nie moja baza, nie moje "zasady". Mam przeczucie, że taki "wrapper" wokół czegoś istniejącego może być tworzony na innych zasadach i te interfejsy mogą nie mieć sensu, szczególnie, że one tylko odzwierciedlają to co ma być oprogramowane w klasie. Dla mnie akurat te interfejsy nie pełnią żadnej ważnej funkcji. Jest to kompletnie niepotrzebny boilerplate code 🤔

1

@AdamWox: jeżeli Twoja klasa służy do interakcji z jakimś API, to wydzielenie z niej interfejsu/inferfejsów, które później można zamockować np. w testach logiki biznesowej ma sens.
Ale serwis brzmi jak klasa realizująca jakąś logikę biznesowa, i w takim przypadku dorabianie do niej interfejsu nie ma raczej sensu.

0

No właśnie to jest ten problem i specyfika tego projektu. Serwis, który odpowiada za produkty/towary ProductsService, pobiera dane za pomocą Dappera, na czysto, bezpośrednio z bazy danych, ale zapisuje je korzystając z bibliotek (Obiektów COM) Comarchu. Tylko HttpGet jest bezpośrednio z bazy, HttpPost, HttpPatch oraz HttpDelete to operacje używając "API" Optimy. Czy w takim przypadku daje mi coś interfejs?

1
gosc_z_pytaniem napisał(a):

Pamiętam jak ludzie zastanawiali się do czego właściwie są te interfejsy zanim TDD stało się modne.

Nie wiązałbym tego. Ludzie korzystają z interfejsów, bo nauczyli się korzystać z możliwości jakie daje język. Bądź co bądź, jakość kodu rośnie i ludzie dochodzą w końcu do jakiś sensownych rzeczy, jak podział na warstwy czy odwrócenie zależności.

0

ale zapisuje je korzystając z bibliotek (Obiektów COM) Comarchu.

to operacje używając "API" Optimy

Czy w takim przypadku daje mi coś interfejs?

raczej nie, bo tego i tak nie da się sensownie testować (koszt/wartość) :P

jeżeli twoje operacje biznesowe to tylko wywołanie jakichś zewnętrznych bibliotek

0

Muszę swój model przekonwertować na model z biblioteki i zapisać za pomocą tej biblioteki. I tutaj nawet zastawiam się czy zrezygnować ze swoich modeli i korzystać tylko i wyłącznie z tych z biblioteki. Na te chwilę, moje modele to takie trochę, oszukane DTO.

0

Ja kieruję się prostą zasadą, jeśli interfejs ma tylko jedną implementację i w nazwie interfejsu/implementacji mamy I lub Impl to znaczy, że interfejs jest zbędny.

2
rad1317 napisał(a):

Ja kieruję się prostą zasadą, jeśli interfejs ma tylko jedną implementację i w nazwie interfejsu/implementacji mamy I lub Impl to znaczy, że interfejs jest zbędny.

Ogólnie stosowana konwencja nazewnictwa interfejsów jawnie mówi o nazywaniu ich w taki sposób, że pierwszą literą jest właśnie I, więc jeden z warunków tej zasady można chyba zmodyfikować.

Ale uważam, że reguła określająca zasadność stosowania jakiegoś elementu języka wyłącznie na podstawie użytej nomenklatury a nie rzeczywistych wymagań wygląda co najmniej dziwnie i trzymanie się jej jest bezcelowe.
Co do pojedynczej implementacji to może to nie mieć sensu a może jednak mieć - patrz np. na segregację interfejsów, łatwość izolacji kodu w testach jednostkowych.

0
AdamWox napisał(a):

No właśnie to jest ten problem i specyfika tego projektu. Serwis, który odpowiada za produkty/towary ProductsService, pobiera dane za pomocą Dappera, na czysto, bezpośrednio z bazy danych, ale zapisuje je korzystając z bibliotek (Obiektów COM) Comarchu. Tylko HttpGet jest bezpośrednio z bazy, HttpPost, HttpPatch oraz HttpDelete to operacje używając "API" Optimy. Czy w takim przypadku daje mi coś interfejs?

Nie, ale gdybyś podzielił w taki sposób:

  1. ProductService (aczkolwiek to brzmi jak jakiś God-Object, raczej bym szedł w stronę iluś klas w rodzaju Product<UseCaseName>), który zarządza logiką biznesową i interakcją pozostałych komponentów.
  2. ProductsDataReader, (bo zakładam, że czytasz produkty z jakiejś tabeli?) który używa Dappera do pobierania danych.
  3. OptimaApiClient do manipulacji danymi.

To zarówno data reader jak i API client mogłyby mieć interfejsy, dzięki czemu logika biznesowa w ProductService mogłaby być przetestowana w oderwaniu od zewnętrznych zależności, w szybki sposób nie wymagający uruchamiania tej całej Optimy. Tym większy byłby z tego zysk, im więcej testowalnej logiki masz w tej klasie.

0

@somekind:

mogłaby być przetestowana w oderwaniu od zewnętrznych zależności, w szybki sposób nie wymagający uruchamiania tej całej Optimy.

znając życie takie testy będą bezużyteczne

chyba że chcesz testować czy C# działa (pętelki, typy, lub bcl typu linq), to wtedy spoko.

3
WeiXiao napisał(a):

znając życie takie testy będą bezużyteczne

Ja wiem, że swoje życie znasz głównie z tego, że jest bezużyteczne.

Nie wiem jaka jest tam logika, ale jeśli są to jakiekolwiek filtrowanie, agregacje, bądź cokolwiek, co się ma stać po spełnieniu określonego warunku, to zawsze jest to coś, co warto testować. Tym bardziej warto testować, im rozgałęzień/obliczeń w kodzie jest więcej.

A nawet jeśli nie jest to logika warta testów, to i tak warto oddzielać od siebie klasy odpowiedzialne za sterowanie przepływem, pobieranie danych i ich składowanie w celu:

  1. skrócenia kodu w każdej klasie;
  2. trzymania razem kodu powiązanego ze sobą funkcjonalnie oraz mającego wspólne elementy;
  3. ułatwienia w nawigacji po projekcie.
0

@somekind: Co do testów to się nie czepiam. Co do rozbijania tego na pierdyliard klas i interfejsów to już jest coś czego lubię się czepiać. Dlaczego powinno się to tak szczegółowo rozbijać (oprócz testów)? Czytelność kodu? Jak rozbije to na kilka klas to nie mam całego bagna z logiką w jednym miejscu? Co miałeś na myśli z tym Product<UseCaseName>? Jak to praktycznie wykorzystać, bo chyba nie rozumiem? Wszystko i tak sprowadza się do wstrzyknięcia każdego z UseCase'ów do kontrolera 🤔

0
rad1317 napisał(a)):

Gdyby dostawca zmienił coś w swoim API i moje testy by to wyłapały to ja raczej bym się cieszył. Ale jak rozumiem to tutaj bardziej chodziło Ci o dostępność samego API. Ja rozwiązuje to w ten sposób, że symuluję działanie takiego API. Czasem narzędzie jest już dostępne, czasem tworzę je sam, zależy od konkretnego przypadku. Rozwiązuje nam to problem od razu dla wszystkich testów, od jednostkowych, po automatyczne. Nawet jeśli z jakiegoś powodu nie możemy symulować działania takiego api, to dla mnie mniejszym złem jest stworzenie fejkowych implementacji niż mokowanie.

Nie zrozumieliśmy się chyba, posłużę się więc przykładem z projektu w którym teraz pracuję.
Aplikacja stanowiąca jeden z elementów systemu jest odpowiedzialna za procesowanie transakcji różnych typów. Jedną ze składowych transakcji jest nazwa zasobu jakiego ta transakcja dotyczy. W tym przypadku jest to nazwa domenowa komputera. W chwili kiedy dana transakcja wpada do systemu to aplikacja musi pobrać dane z odpowiedniego forestu ActiveDirectory oraz konfigurację dla danej grupy zasobów z bazy danych.
Logika, której zadaniem jest wybór odpowiedniej ścieżki procesowania transakcji ma zaimplementowane drzewo decyzyjne, które na podstawie tego co dostaje z AD i bazy określa co ma się dalej stać.
Aby przetestować działanie drzewa decyzyjnego nie potrzebuję ani połączenia a ActiveDirectory ani z bazą. Potrzebuję natomiast przygotować sobie różne scenariusze, tj zestawy danych dla których wiem w jaki sposób powinien zachować się system - co jest opisane w wymaganiach biznesowych, specyfikacji czy jakkolwiek zostanie to nazwane. W ten sposób będąc w pełni odizolowanym od tych zewnętrznych zależności mogę przetesować każdy interesujący mnie wariant.
Jeśli nie użyłbym mocka żadnego rodzaju to byłbym w pełni zależny od konfiguracji zarówno AD jaki i bazodanowej.

Oznacza to, że każda zmiana w konfiguracji systemu, czy to zmian w bazie czy AD może wywalić moje testy bo zwyczajnie zmienią mi się parametry wejściowe. Wydaje mi się, że można byłoby to ograć wykorzystując sprytny mechanizm wyszukiwania obiektów o określonej konfiguracji ale to zwyczajnie głupi pomysł.

O ile mogę od biedy ustawić sobie konfigurację w bazie dla celów testowych to przygotowanie AD tylko dla testów byłoby trudne i czasochłonne. Natomiast mock załatwia sprawę w sposób idealny ponieważ mogę sobie ustawić konfigurację w dowolny sposób i odciąć się w pełni od tych zewnętrznych zasobów.

W ramach tych testów nie jestem zainteresowany integracją. To czy coś się zmieni po stronie dostawcy mnie kompletnie nie interesuje na tym poziomie testów.
Nie obchodzi mnie zupełnie kwestia dostępności API czy czegokolwiek innego a wyłącznie kawałek kodu który stanowi drzewo decyzyjne i jest jednostką testowaną.

3
AdamWox napisał(a):

@somekind: Co do testów to się nie czepiam. Co do rozbijania tego na pierdyliard klas i interfejsów to już jest coś czego lubię się czepiać. Dlaczego powinno się to tak szczegółowo rozbijać (oprócz testów)? Czytelność kodu? Jak rozbije to na kilka klas to nie mam całego bagna z logiką w jednym miejscu?

Masz logikę w jednym miejscu, interakcję ze źródłem danych w innym, a z miejscem zapisu w innym. To trochę tak jak w domu - piwo trzymamy w jednym miejscu, brudne skarpetki w innym, samochód w jeszcze innym.
Dzięki temu jest czytelnie - jeśli mam błąd związany z pobraniem danych z API, to szukam w klasie pobierającej dane z API, a nie w spaghetti kodzie, w którym odczyt, zapis i sterowanie wymieszane są razem.

Co miałeś na myśli z tym Product<UseCaseName>? Jak to praktycznie wykorzystać, bo chyba nie rozumiem?

No bardziej może <UseCaseName>Product, np. ImportProduct, CreateProduct, GetProduct. Chodzi o to, aby zamiast jednej klasy ProductService, która odpowiada za wszystkie przypadki użycia związane z produktami, ma około 15 tys linii kodu i 200 zależności, mieć zestaw małych klas, z których każda jest krótka i ma tylko te zależności, których faktycznie potrzebuje.

Wszystko i tak sprowadza się do wstrzyknięcia każdego z UseCase'ów do kontrolera 🤔

Tak, no chyba, że użyjemy wzorca mediator i np. biblioteki MediatR. Wówczas mamy tylko jedną zależność w kontrolerze, do mediatora wysyłamy polecenie związane z danym przypadkiem użycia, a on wywołuje odpowiedni kawałek kodu odpowiedzialny za realizację tego przypadku użycia.

var napisał(a):

Aby przetestować działanie drzewa decyzyjnego nie potrzebuję ani połączenia a ActiveDirectory ani z bazą. Potrzebuję natomiast przygotować sobie różne scenariusze, tj zestawy danych dla których wiem w jaki sposób powinien zachować się system - co jest opisane w wymaganiach biznesowych, specyfikacji czy jakkolwiek zostanie to nazwane. W ten sposób będąc w pełni odizolowanym od tych zewnętrznych zależności mogę przetesować każdy interesujący mnie wariant.
Jeśli nie użyłbym mocka żadnego rodzaju to byłbym w pełni zależny od konfiguracji zarówno AD jaki i bazodanowej.

To zależy od tego, co to drzewo robi:

  1. jeżeli wynikiem działania drzewa są jakieś dane, to sam algorytm można wydzielić do jednostki zależnej jedynie od danych wejściowych i ją przetestować bez używania mocków.
  2. jeżeli wynikiem działania drzewa są interakcje z zewnętrznymi zależnościami dokonywane w trakcie przetwarzania danych, to mocki w testach są przydatne.

Mam wrażenie, że fanatycy nieużywania mocków w ogóle nie zdają sobie sprawy z tego, że może istnieć kod, którego wynikiem działania nie są dane lecz interakcje. Jakby zaczęli swoje światłe idee implementować w życiu w systemie tego typu, to szybko by zmienili zdanie (albo się bardzo męczyli w imię bezsensownej ideologii).
Z drugiej strony wielu ludzi nawet nie myśli o tym, że w wielu przypadkach można wydzielić kod przetwarzający dane od kodu operującego na zależnościach i elegancko testować go jednostkowo bez mocków. I też się męczą tracąc czas na mockowanie, którego mogłoby nie być.

1 użytkowników online, w tym zalogowanych: 0, gości: 1