Sentencja jest zwięzła i gdyby się nad nią dłużej zastanowić, niesie za sobą olbrzymią ilość treści. Cała prelekcja jest bardzo dobra, a niżej linkuję do fragmentu który szczególnie mi się spodobał.
If you’re good at the debugger it means you spent a lot of time debugging. I don’t want you to be good at the debugger.
To był wcześniejszy tytuł tego tekstu, który nie za bardzo się kliknął, ale zostawiłem go, ponieważ jeśli już ktoś dotarł tutaj, to ten tytuł w maksymalnie skondensowanej formie wyjaśnia na czym polega pomysł rozwiązania i równie zwięźle podsumowuje wyżej linkowany artykuł o projektowaniu REST API.
Domena
Jako przykład operacji biznesowej posłuży mi domena nauki wraz z jej artykułami naukowymi, które podlegają procesowi recenzji przed opublikowaniem. W tej domenie miałem okazję pracować przez ostatnie dwa lata.
Praca naukowa, która pomyślnie przeszła proces recenzji jest publikowana w czasopiśmie naukowym. Manuskryptem będziemy nazywać pracę naukową, która jest w trakcie recenzji. W celu przeprowadzenia recenzji, recenzenci są zapraszani dla konkretnego manuskryptu. Oni decydują o jego akceptacji lub odrzuceniu. Manuskrypt zaakceptowany jest następnie wysyłany do czasopisma naukowego, aby mógł zostać opublikowany jako artykuł.
Implementacja
Ogólny wzorzec operacji biznesowych na poziomie API przedstawia się następująco: na poziomie zasobów (resources) dodaje się nowy zasób o nazwie „actions„. Następnie, w obrębie „actions„, dodawana jest konkretna operacja biznesowa, czyli czasownik (action) w formie rozkazującej.
POST /{resource}/{id}/actions/{action}
Przykładem synchronicznej operacji biznesowej może być zaproszenie recenzenta oraz akceptacja lub odrzucenie manuskryptu. Każda z tych operacji biznesowych może być realizowana przez inny proces, który zależy od konfiguracji specyficznej dla poszczególnych magazynów naukowych.
POST /manuscripts/256/actions/invite
POST /manuscripts/256/actions/accept
POST /manuscripts/256/actions/reject
Sync
W odpowiedzi zwracany jest status 204 (No Content).
204 No Content
Async
W odpowiedzi zwracany jest status 202 (Accepted), a w nagłówku „Location” podawany jest link, za pomocą którego można sprawdzić status realizowanego procesu.
Powodem modelowania asynchronicznej operacji biznesowej może być proces, w którym wymagane są więcej niż jeden krok (na przykład dodatkowa zgoda redaktora magazynu naukowego) lub gdy operacja jest czasochłonna i nie oczekuje się, że klient będzie czekać na jej zakończenie.
Przykładowo, publikację artykułu naukowego w magazynie naukowym można zamodelować w ten sposób.
Następnie klient, korzystając z otrzymanego linku, wykonuje zapytanie.
GET /journals/16384/actions/submit/4096
W przypadku, gdy akcja nadal jest w trakcie wykonywania, wówczas zwracany jest status 102 (Processing) wraz z nagłówkiem „Retry-After„, który informuje, kiedy klient powinien wykonać ponowne sprawdzenie statusu.
102 Processing Retry-After: 30
W przypadku, gdy akcja została wykonana w całości, wówczas zwracany jest status 204 (No Content).
204 No content
Sync lub Async
W szczególnych przypadkach możemy dać klientowi możliwość wyboru, czy dana operacja ma być wykonywana synchronicznie, czy asynchronicznie.
Operacja zostanie wykonana synchronicznie, jeśli klient podczas wywoływania API ustawi nagłówek „Expect” z wartością statusu 204 (No Content).
POST /journals/16384/actions/submit Expected: 204-no-content
Operacja zostanie wykonana asynchronicznie, jeśli klient podczas wywoływania API ustawi nagłówek ‚Expect’ z wartością statusu 202 (Accepted).
POST /journals/16384/actions/submit Expected: 202-accepted
W przypadku, gdy wybrany przez klienta oczekiwany sposób wykonania nie jest dostępny, wówczas akcja zwraca status 417 (Expectation Failed).
417 Expectation Failed
Acknowledgement
W ostatnim roku pracowałem nad projektem, w którym modelowaliśmy HTTP API w stylu Remote Procedure Call (RPC). Architektem i pomysłodawcą tego podejścia był Konrad Kwiatkowski, który kompletnie zmienił moje postrzeganie świata, przekonując mnie, że nie musimy dążyć do osiągnięcia czwartej, a nawet półtorej wersji dojrzałości modelu Richardsona dla REST API. Za tę perspektywę jestem mu niezmiernie wdzięczny.
Wyluzujcie z tym kodem, mam coś, co zrobi za Was całą brudną robotę. CleanupCode Command-Line Tool w połączeniu z GitHub Actions – to jest to. Wystarczy kilka kliknięć i… voilà, kod sam się czyści. W README repo ReSharper CLI CleanupCode znajdziecie wszystko, czego potrzeba, żeby to ustawić.
Rzućcie okiem na ten link: ReSharper CLI CleanupCode – tak, to moje dzieło, które rok temu wylądowało na GitHub Marketplace. To właśnie tam zaczarowałem GitHub Actions, żeby sprzątały kod za Was.
Jeśli ciekawi Was, jak naprawdę działa ta GitHub Action, sprawdźcie repo Blef, mój open source projekt robiony po godzinach.To właśnie tam też, wśród skomplikowanych workflows GitHub, testowałem to rozwiązanie przez ostatni rok. Jest to open source więc mam możliwość podzielić się z Wami tym kawałkiem pracy. Zapraszam do repo Blef – i może nawet do dołączenia do gry?
Więc dajcie sobie szansę na nowe lepsze życie. W końcu kodowanie ma być przyjemnością, a nie sprzątaniem. Niech kod czyści się sam a wy miejcie czas na to, co naprawdę lubicie. Odpalcie sobie tego CleanupCode’a i GitHub Actions, a potem już tylko relaks i kodowanie, jak lubicie!
Jakby co, piszcie, chętnie podzielę się większymi szczegółami!
Update 03-01-2024: Zamieszczam link do rozmowy na Twitterze, która stanowi znakomite uzupełnienie informacji o narzędziu oraz zawiera cenne spostrzeżenia dotyczące samego konceptu. Dla osób, które nie posiadają konta na Twitterze, każda z odpowiedzi została opublikowana osobno.
Update 04-01-2024: Jeden z problemów, który rozwiązuje ta GitHub Action.
Spotykamy się w bardzo luźnej atmosferze, gdzie w trakcie spotkania możemy poczęstować się darmową pizzą oraz browarem. Rozmowy mają charakter nieformalny. Zdobywamy nową wiedzę, nie zapominając o miłej atmosferze.
Po zakończeniu drugiej prelekcji często zostajemy na dłużej, aby nawiązać nowe znajomości oraz wymienić spostrzeżenia, często również są ścisłe, specjalistyczne, techniczne i często trudne z dziedziny programowania.
Przekazywanie zmiennej typu wartościowego, takiej jak int czy czy struct do metody przez referencję oraz zwracanie wartości z metody przez referencję to coś, co rzadko wykorzystuje się na co dzień, dlatego od czasu do czasu warto odświeżyć sobie tę wiedzę.
W załączonym kodzie możesz prześledzić różnice między przekazywaniem argumentów przez wartość oraz przez referencję. Przedstawiam przykłady przekazywania typów wartościowych oraz referencyjnych jako argumenty do metod i modyfikowania ich wartości wewnątrz metod.
Pomysł na ten wpis jest taki, że na początek, napiszę testy jednostkowe, które będą palić się na czerwono, w których zdefiniuje problem. Testy zapalę na zielono poprzez implementację wzorca Adapter.
Adapter przekształca interfejs klas na inny, oczekiwany przez klienta. Adapter umożliwia współdziałanie klasom, które z uwagi na niezgodne interfejsy standardowo nie mogą współdziałać ze sobą.
„Wzorce Projektowe”, Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides
Adapter dokonuje konwersji danej klasy do postaci innego interfejsu, zgodnie z oczekiwaniami klienta. Adapter pozwala na współpracę klas, które ze względu na niekompatybilne interfejsy wcześniej nie mogły ze sobą współpracować. {…}. Dzięki temu kod klienta nie musi być modyfikowany za każdym razem, kiedy będzie współpracować z innym interfejsem.
„Head First Design Patterns” Erich Freeman, Elisabeth Freeman, Bert Bates, Kathy Sierra
Adapter Obiektów
Na początek, napiszę test, w którym zdefiniuję problem. Po czym napiszę kod produkcyjny, który zapali ten test na zielono.
public class AdapterTester
{
private readonly Client client;
public AdapterTester()
{
client = new Client();
}
[Fact]
public void call_request_method_in_target()
{
// arrange
ITarget target = A.Fake();
// act
client.CallRequest(target);
// assert
A.CallTo(() => target.Request()).MustHaveHappened();
}
}
Clinet posiada metodę CallRequest, która w argumencie pobiera obiekt spełniający interfejs ITarget. W teście sprawdzamy, czy w wyniku wywołania metody CallRequest została wywołana metoda Request na obiekcie target.
Aby ten test zapalił się na zielono potrzebna jest definicja interfejsu ITarget oraz implementacja klasy Client.
public interface ITarget
{
void Request();
}
public class Client
{
public void CallRequest(ITarget target)
{
target.Request();
}
}
Teraz zdefiniuję kolejny test, którego zapalenie na zielono będzie wymagało napisania wzorca Adapter.
public class AdapterTester
{
// ... pozostały kod jest niezmieniony
[Fact]
public void call_specific_request_method_in_adaptee()
{
// arrange
IAdaptee adaptee = A.Fake();
ITarget adapter = new Adapter(adaptee);
// act
client.CallRequest(adapter);
// assert
A.CallTo(() => adaptee.SpecificRequest()).MustHaveHappened();
}
}
W teście tym pojawił się obiekt typu IAdaptee. IAdaptee posiada metodę SpecificRequest, co powoduje, że interfejs IAdaptee jest inny od ITarget. Klient nie będzie umieć współpracować z obiektem typu IAdaptee. IAdaptee wymaga adaptacji do współdziałania z metodą CallRequest z klasy Client.
IAdaptee przekazany jest do konstruktora nowej klasy Adapter, która implementuje interfejs ITarget. Wewnątrz klasy Adapter nastąpi przetłumaczenie interfejsu IAdaptee na interfejs ITarget. W ostatniej linijce testu jest asercja mówiąca o tym, że w wyniku przetłumaczenia IAdaptee na ITarget, jeśli klient wywoła metodę Request na obiekcie typu ITarget to w rzeczywistości wywoła się metoda SpecificRequest na obiekcie typu IAdaptee.
Jak widać w teście, klasa Clientpozostaje bez zmian — to ważne założenie. Adaptacja nowego interfejsu wykonywana jest bez najmniejszej zmiany klasy, do której ten nowy interfejs adaptujemy.
Test pali się teraz na czerwono. Kod się nie kompiluje. Definiuję teraz ten nowy interfejs, który będę adaptować.
public interface IAdaptee
{
void SpecificRequest();
}
Teraz definiuję klasę Adapter, która w konstruktorze przyjmuje obiekt typu IAdaptee. Klasa Adapter implementuje interfejs ITarget, czyli ten pierwotny, do którego się adaptujemy.
public class Adapter : ITarget
{
private readonly IAdaptee adaptee;
public Adapter(IAdaptee adaptee)
{
this.adaptee = adaptee;
}
public void Request()
{
adaptee.SpecificRequest();
}
}