Dobre strategie wdrażania dla enkapsulacji współdzielonych danych w potoku oprogramowania


13

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);
    }
}

6
nie przechodź przez słupek, ale oznacz mod, aby się przenieść
maniak ratchet

1
Zrobię krok naprzód, myślę, że powinienem poświęcić więcej czasu na zapoznanie się z zasadami. Dzięki!
RuslanD,

1
Czy unikasz trwałego przechowywania danych dla swojej implementacji, czy jest coś w tym momencie do zdobycia?
CokoBWare,

1
Cześć RuslanD i witaj! Jest to rzeczywiście bardziej odpowiednie dla programistów niż przepełnienie stosu, dlatego usunęliśmy wersję SO. Pamiętaj, o czym wspomniał @ratchetfreak, możesz oflagować uwagę moderatora i poprosić o migrację pytania na bardziej odpowiednią stronę, bez konieczności przechodzenia przez post. Przy wybieraniu między tymi dwoma stronami obowiązuje zasada, że ​​programiści zajmują się problemami, które napotykasz, gdy znajdujesz się przed tablicą projektującą projekty, a przepełnienie stosu - więcej problemów technicznych (np. Problemy z implementacją). Aby uzyskać więcej informacji, zobacz nasze FAQ .
yannis,

1
Jeśli zmienisz architekturę na przetwarzający DAG (ukierunkowany wykres acykliczny) zamiast potoku, możesz jawnie przekazać wyniki wcześniejszych kroków.
Patrick

Odpowiedzi:


4

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.


1

Przypomina mi się kilka myśli, z których pierwszą jest to, że nie mam wystarczającej ilości informacji.

  • Czy na każdym etapie powstają dane wykorzystywane poza rurociągiem, czy zależy nam tylko na wynikach ostatniego etapu?
  • Czy istnieje wiele obaw dotyczących dużych zbiorów danych? to znaczy. problemy z pamięcią, problemy z prędkością itp

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ć.


1

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 .

Popularną techniką organizowania wykonywania złożonych przepływów przetwarzania jest wzorzec „Łańcuch odpowiedzialności”, jak opisano (między innymi) w klasycznej książce „Gang of Four”. Chociaż podstawowe umowy API wymagane do wdrożenia tego projektu są niezwykle proste, przydatne jest posiadanie podstawowego interfejsu API, który ułatwia korzystanie ze wzoru i (co ważniejsze) zachęca do tworzenia implementacji poleceń z wielu różnych źródeł.

W tym celu Chain API modeluje obliczenia jako serię „poleceń”, które można połączyć w „łańcuch”. Interfejs API dla komendy składa się z pojedynczej metody ( execute()), której przekazywany jest parametr „kontekstowy” zawierający stan dynamiczny obliczeń i którego wartość zwracana jest wartością logiczną, która określa, czy przetwarzanie bieżącego łańcucha zostało zakończone ( true) lub czy przetwarzanie powinno być delegowane do następnego polecenia w łańcuchu (false).

Abstrakcja „kontekstowa” ma na celu odizolowanie implementacji komend od środowiska, w którym są uruchamiane (na przykład komenda, której można użyć w serwletie lub portlecie, bez bezpośredniego powiązania z umowami API żadnego z tych środowisk). W przypadku poleceń, które muszą przydzielić zasoby przed delegowaniem, a następnie zwolnić je po powrocie (nawet jeśli polecenie delegowane do zgłosi wyjątek), rozszerzenie „filtruj” do „polecenia” zapewnia postprocess()metodę tego czyszczenia. Wreszcie komendy mogą być przechowywane i wyszukiwane w „katalogu”, aby umożliwić odroczenie decyzji, która komenda (lub łańcuch) jest faktycznie wykonywana.

Aby zmaksymalizować użyteczność interfejsów API wzorca łańcucha odpowiedzialności, podstawowe umowy dotyczące interfejsów są definiowane w sposób zerowy zależny od innych niż odpowiedni JDK. Zapewniono wygodną implementację tych podstawowych interfejsów API, a także bardziej wyspecjalizowane (ale opcjonalne) implementacje dla środowiska WWW (tj. Serwlety i portlety).

Biorąc pod uwagę, że implementacje poleceń są zaprojektowane w taki sposób, aby były zgodne z tymi zaleceniami, powinno być możliwe wykorzystanie interfejsów API łańcucha odpowiedzialności w „przednim kontrolerze” struktury aplikacji internetowych (takich jak Struts), ale także możliwość używania go w biznesie warstwy logiki i trwałości do modelowania złożonych wymagań obliczeniowych za pomocą kompozycji. Ponadto rozdzielenie obliczeń na dyskretne polecenia działające w kontekście ogólnego przeznaczenia umożliwia łatwiejsze tworzenie poleceń, które można testować jednostkowo, ponieważ wpływ wykonania polecenia można zmierzyć bezpośrednio, obserwując odpowiednie zmiany stanu w dostarczonym kontekście ...


0

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.


Hej, zaktualizowałem moje pytanie za pomocą jakiegoś pseudokodu - w rzeczywistości mamy wyraźne kroki.
RuslanD
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.