Pracuję nad faktoringiem niektórych aspektów istniejącej usługi internetowej. Interfejsy API usług są implementowane poprzez rodzaj „potoku przetwarzania”, w którym są zadania wykonywane sekwencyjnie. Nic dziwnego, że późniejsze zadania mogą wymagać informacji obliczonych na podstawie wcześniejszych zadań, a obecnie sposób ten jest wykonywany przez dodanie pól do klasy „stan potoku”.
Myślałem (i mam nadzieję?), Że istnieje lepszy sposób na dzielenie się informacjami między krokami potoku niż posiadanie obiektu danych z polami zillionów, z których niektóre mają sens dla niektórych kroków przetwarzania, a nie dla innych. Sprawienie, by klasa ta była bezpieczna dla wątków, byłoby bardzo uciążliwe (nie wiem, czy byłoby to w ogóle możliwe), nie ma sposobu na uzasadnienie jej niezmienników (i prawdopodobnie nie ma żadnych).
Szukałem inspiracji w książce wzorów Gang of Four, ale nie czułem, żeby było tam jakieś rozwiązanie (Memento było w tym samym duchu, ale nie do końca). Szukałem również w trybie online, ale za drugim razem, gdy wyszukujesz „potok” lub „przepływ pracy”, zalewa Cię informacja o potoku Unix lub zastrzeżone silniki i struktury przepływu pracy.
Moje pytanie brzmi - jak podejmiesz kwestię rejestrowania stanu wykonania potoku przetwarzania oprogramowania, aby późniejsze zadania mogły wykorzystywać informacje obliczone przez wcześniejsze? Wydaje mi się, że główną różnicą w potokach uniksowych jest to, że nie zależy ci tylko na wynikach bezpośrednio poprzedzającego zadania.
Zgodnie z żądaniem, niektóre pseudokody ilustrujące mój przypadek użycia:
Obiekt „kontekstu potoku” ma kilka pól, które różne pola potoku mogą wypełnić / odczytać:
public class PipelineCtx {
... // fields
public Foo getFoo() { return this.foo; }
public void setFoo(Foo aFoo) { this.foo = aFoo; }
public Bar getBar() { return this.bar; }
public void setBar(Bar aBar) { this.bar = aBar; }
... // more methods
}
Każdy etap potoku jest również obiektem:
public abstract class PipelineStep {
public abstract PipelineCtx doWork(PipelineCtx ctx);
}
public class BarStep extends PipelineStep {
@Override
public PipelineCtx doWork(PipelieCtx ctx) {
// do work based on the stuff in ctx
Bar theBar = ...; // compute it
ctx.setBar(theBar);
return ctx;
}
}
Podobnie w przypadku hipotetycznego FooStep
, który może wymagać paska obliczonego przez BarStep przed nim, wraz z innymi danymi. A potem mamy prawdziwe wywołanie API:
public class BlahOperation extends ProprietaryWebServiceApiBase {
public BlahResponse handle(BlahRequest request) {
PipelineCtx ctx = PipelineCtx.from(request);
// some steps happen here
// ...
BarStep barStep = new BarStep();
barStep.doWork(crx);
// some more steps maybe
// ...
FooStep fooStep = new FooStep();
fooStep.doWork(ctx);
// final steps ...
return BlahResponse.from(ctx);
}
}
źródło
Odpowiedzi:
Głównym powodem zastosowania projektu potoku jest to, że chcesz rozdzielić etapy. Albo dlatego, że jeden etap może być użyty w wielu potokach (jak narzędzia powłoki Unix), albo dlatego, że zyskujesz pewne korzyści skalowania (tj. Możesz łatwo przejść z architektury z jednym węzłem do architektury z wieloma węzłami).
W obu przypadkach na każdym etapie rurociągu należy zapewnić wszystko, czego potrzeba, aby wykonać swoją pracę. Nie ma powodu, dla którego nie można korzystać z zewnętrznego sklepu (np. Bazy danych), ale w większości przypadków lepiej jest przekazywać dane z jednego etapu na drugi.
Nie oznacza to jednak, że musisz lub powinieneś przekazać jeden duży obiekt wiadomości z każdym możliwym polem (chociaż patrz poniżej). Zamiast tego każdy etap w potoku powinien definiować interfejsy dla komunikatów wejściowych i wyjściowych, które identyfikują tylko dane, których potrzebuje dany etap.
Masz wtedy dużą elastyczność w implementowaniu rzeczywistych obiektów wiadomości. Jednym z podejść jest użycie ogromnego obiektu danych, który implementuje wszystkie niezbędne interfejsy. Innym jest tworzenie klas opakowań wokół prostego
Map
. Jeszcze innym jest utworzenie klasy opakowania wokół bazy danych.źródło
Przypomina mi się kilka myśli, z których pierwszą jest to, że nie mam wystarczającej ilości informacji.
Odpowiedzi prawdopodobnie skłoniłyby mnie do dokładniejszego przemyślenia projektu, jednak w oparciu o to, co powiedziałeś, są dwa podejścia, które prawdopodobnie rozważę najpierw.
Zbuduj każdy etap jako własny obiekt. Etap n-ty składałby się z 1 do n-1 etapów jako lista delegatów. Każdy etap obejmuje dane i ich przetwarzanie; zmniejszając ogólną złożoność i pola w każdym obiekcie. Możesz również mieć dostęp do danych w późniejszych etapach w razie potrzeby z dużo wcześniejszych etapów, przechodząc przez delegatów. Nadal masz dość ścisłe sprzężenie między wszystkimi obiektami, ponieważ ważne są wyniki etapów (tj. Wszystkich attrów), ale są one znacznie zmniejszone, a każdy etap / obiekt jest prawdopodobnie bardziej czytelny i zrozumiały. Możesz sprawić, by wątek był bezpieczny, czyniąc listę delegatów leniwymi i używając bezpiecznej kolejki wątków, aby zapełnić listę delegatów w każdym obiekcie w razie potrzeby.
Alternatywnie prawdopodobnie zrobiłbym coś podobnego do tego, co robisz. Ogromny obiekt danych, który przechodzi przez funkcje reprezentujące każdy etap. Jest to często znacznie szybsze i lekkie, ale bardziej złożone i podatne na błędy, ponieważ jest to tylko duży stos atrybutów danych. Oczywiście nie jest bezpieczny dla wątków.
Szczerze mówiąc, później robiłem częściej dla ETL i innych podobnych problemów. Skoncentrowałem się na wydajności ze względu na ilość danych, a nie na łatwość konserwacji. Ponadto były to zdarzenia jednorazowe, których nie można ponownie wykorzystać.
źródło
To wygląda jak wzór łańcucha w GoF.
Dobrym punktem wyjścia byłoby przyjrzenie się temu, co robi łańcuch wspólny .
źródło
Pierwszym rozwiązaniem, jakie mogę sobie wyobrazić, jest wyraźne określenie kroków. Każdy z nich staje się obiektem zdolnym do przetworzenia kawałka danych i przekazania go do następnego obiektu przetwarzania. Każdy proces tworzy nowy (idealnie niezmienny) produkt, dzięki czemu nie zachodzi interakcja między procesami, a zatem nie ma ryzyka związanego z udostępnianiem danych. Jeśli niektóre procesy są bardziej czasochłonne niż inne, możesz umieścić bufor między dwoma procesami. Jeśli poprawnie wykorzystasz harmonogram do wielowątkowości, przydzieli on więcej zasobów do opróżnienia buforów.
Drugim rozwiązaniem może być myślenie „komunikat” zamiast potoku, być może z dedykowanym szkieletem. Masz wtedy „aktorów” odbierających wiadomości od innych aktorów i wysyłających inne wiadomości do innych aktorów. Organizujesz aktorów w potoku i przekazujesz swoje podstawowe dane pierwszemu aktorowi, który inicjuje łańcuch. Udostępnianie danych nie jest możliwe, ponieważ zastępuje się je wysyłaniem wiadomości. Wiem, że model aktorski Scali może być używany w Javie, ponieważ nie ma tu nic specyficznego dla Scali, ale nigdy nie korzystałem z niego w programie Java.
Rozwiązania są podobne i możesz wdrożyć drugie z pierwszym. Zasadniczo główne koncepcje dotyczą radzenia sobie z niezmiennymi danymi w celu uniknięcia tradycyjnych problemów związanych z udostępnianiem danych oraz tworzenia wyraźnych i niezależnych podmiotów reprezentujących procesy w potoku. Jeśli spełniasz te warunki, możesz łatwo utworzyć jasne, proste potoki i używać ich w programie równoległym.
źródło