Workflow Pattern – Sequence

Tworząc aplikacje często mam doczynienia z sekwencją pewnych czynności – procesem. Samowolnie i wręcz automatycznie, nazywamy takie zachowanie angielskim słowem workflow (polskie tłumaczenie „przepływ pracy” brzmi co najmniej głupio). Przykładem procesu może być składanie zamówienia w sklepie internetowym, wypełnianie wieloetapowego formularza, po sprawy teoretycznie mniej skomplikowane – jak przetwarzanie danych do formatu wyjściowego (niejednokrotnie wymagające kilku etapów np. ETL).

Dla zobrazowania sytuacji:

A -> B -> C

Wykonujemy akcję A, po niej następuje wykonanie akcji B, następnie akcji C. Na tym kończy się nasz workflow.

Modelując niedawno jedną z funkcji aplikacji, spotkałem się ponownie z podobnym problemem – musiałem wywoływać akcje jedna po drugiej z zachowaniem ich odpowiedniej kolejność oraz współdzieleniem pewnych danych pomiędzy poszczególnymi krokami. Pierwsza akcja generowała dane które były niezbędne do przeprowadzenia kolejnej akcji. Po początkowej fazie stworzenia szkicu, zabrałem się za implementacja rozwiązania. Nie wyglądało ono sympatycznie, a jego poziom skomplikowania nie był adekwatny do tego co faktycznie dzieje się w systemie. Natomiast druga iteracja doprowadziła kod do bardziej czytelnej formy. Poniżej prezentuję PoC rozwiązania, odcięte od produkcyjnego kodu, jednak dające zobrazowanie organizacji kodu.

Najpierw zdefiniowałem context, czyli obiekt który będzie współdzielony pomiędzy poszczególnymi akcjami. Taki „pojemnik” na dane. Dla przykładu, mój context będzie zawierał tylko jedną właściwość.

<?php

class WorkflowContext
{
    private $sequenceId;

    public function getSequenceId() : ?int
    {
        return $this->sequenceId;
    }

    public function setSequenceId(int $sequenceId) : void
    {
        $this->sequenceId = $sequenceId;
    }
}

Każda akcja będzie implementować następujący interfejs:

<?php

interface WorkflowAction
{
    public function execute(WorkflowContext $context);
}

Szkielet implementacji trzech niemal identycznych akcji:

<?php

class FirstWorkflowAction implements WorkflowAction
{
    public function execute(WorkflowContext $context)
    {
        echo 'FirstWorkflowAction. SequenceId = ' . $context->getSequenceId() . PHP_EOL;
        $context->setSequenceId(rand(1, 100));
        return new SecondWorkflowAction();
    }
}

class SecondWorkflowAction implements WorkflowAction
{
    public function execute(WorkflowContext $context)
    {
        echo 'SecondWorkflowAction. SequenceId = ' . $context->getSequenceId() . PHP_EOL;
        return new ThirdWorkflowAction();
    }
}

class ThirdWorkflowAction implements WorkflowAction
{
    public function execute(WorkflowContext $context)
    {
        echo 'ThirdWorkflowAction. SequenceId = ' . $context->getSequenceId() . PHP_EOL;
        return null;
    }
}

Każde wykonanie akcji, czyli wywołanie metody execute zwraca nam instancję obiektu kolejnej do wykonania akcji (za wyjątkiem ostatniej akcji w procesie – ona zwraca wartość null). Implementacja poszczególnych akcji może zostać oddelegowana do zupełnie innego miejsca – nowej klasy. W ten sposób wszystko co implementuje WorkflowAction może być tylko opakowaniem na sekwencyjny proces, bez implementacji konkretnej logiki.

Pozostaje jedynie uruchomienie workflow:

<?php

$context = new WorkflowContext();
$action = new FirstWorkflowAction();

while ($action) {
    $action = $action->execute($context);
}

Tworzę instancję WorkflowContext, ustawiam pierwszą akcję do wykonania FirstWorkflowAction. Następnie wykorzystując pętlę while wywoływana jest metoda execute (na kolejnych akcjach), tak długo aż otrzymamy wartość niespełniającą warunku pętli. W tym przypadku jest to wartość null, która zwracana jest przez ostatnią akcję procesu.

Rezultatem końcowym będzie wyrzucenie na konsolę następujących informacji:

$: php sequence.php
FirstWorkflowAction. SequenceId = 
SecondWorkflowAction. SequenceId = 5
ThirdWorkflowAction. SequenceId = 5

Podsumowując. Wykonane zostały sekwencyjnie trzy akcje. Współdzielony context umożliwia transport danych pomiędzy poszczególnymi akcjami. Zaprezentowany kod jest jedynie szkieletem. Umożliwia łatwe odseparowanie sterowania procesem (workflow) od logiki wykonywanej w poszczególnych akcjach.

PS. Więcej o Workflow Patterns możesz poczytać na stronie workflowpatterns.com. Problem o którym piszę można sklasyfikować jako „Basic Control Flow Pattern – Sequence”.

Na co dzień Software Engineer, jeszcze programujący CTO w firmie Emphie Solutions. Fascynat programowania, architektury, metodyk zwinnych i dobrych praktyk w szerokim ujęciu. Polyglot Programer kochający poznawać nowe języki jednocześnie wykorzystując ich najlepsze strony. Założyciel DevEnv i współautor podcastu DevEnv.
PODZIEL SIĘ

1 KOMENTARZ

  1. Workflow jest bardzo przydatnym wzorcem, sam go implementowałem już parę razy. Chciałem jednak zwrócić uwagę na jedną rzecz. Twoja implementacja konkretnych akcji jest silnie zależna od innych akcji: FirstWorkflowAction zawsze zwraca instancję SecondWorkflowAction (moim zdaniem łamie to SRP). Jeśli dojdzie do sytuacji, że pomiędzy pierwszą a druga akcją będzie trzeba dodać coś nowego to wprowadzisz kolejną akcję modyfikując przy tym kod pierwszej (łamie OpenClose Principle). W obecnym projekcie przyjęliśmy rozwiązanie polegające na dodaniu klasy, którą można nazwać WorkflowProcess. Klasa to miała zdefiniowaną listę wszystkich akcji w kolejności w jakiej mają się wykonać i tylko jedną metodę Execute(), która inicjalizowała Context i uruchamiała cały proces wywołując akcję po akcji w kolejności w jakiej zostały zadeklarowane. Dodatkowy benefit poza rozluźnieniem zależności to możliwość budowania wielu procesów z wykorzystaniem tych samych akcji oraz enkapsulacja inicjalizacji Contextu i uruchamiania procesu.

    Co o tym sądzisz?

Comments are closed.