Freitag, 8. Oktober 2010

Model meets Implementation

Serviceorientierte Architekturen (SOA) setzen sich aus Services zusammen. Ein Service erfüllt einen Vertrag für eine spezifizierte Geschäftslogik. Services sind kohäsiv und lose miteinander gekoppelt. Häufig werden Services prozessorientiert entworfen und in der Folge mit anderen Services orchestriert.

Der wesentliche Vorteil von prozessorientierten Services ist, dass die Service-Definition in der Spezifikationsphase von Fachspezialisten, den Business Analysten entworfen wird. Die Service-Definition wird dabei werkzeugunterstützt entworfen und dient in der Implementierungsphase als Vorlage für die Programmierung. Prozessschritte in Service-Definitionen sind häufig besser verständlich als Use Cases und UML-Diagramme. Die Fachabteilung und Entwickler haben ein gemeinsames Verständnis für Prozessdiagramme, was bei technisch getriebenen UML-Diagrammen nicht immer der Fall ist.

Praxistauglich sind zwei Ansätze zur prozessorientierten Implementierung von Services. Der eine Ansatz definiert Prozessschritte über Datenbankstrukturen. Dabei wird der Status in jedem Prozessschritt neu gesetzt und entsprechend  in nachfolgenden Prozessschritten zur Verarbeitung ausgewertet. Bei dieser Vorgehensweise werden EJBs entworfen, die einzelnen Prozessschritten entsprechen. Diese einfache Art der prozessorientierten Verarbeitung bietet sich dann an, wenn keine Business Process Manegement (BPM) Engine als weitere Komplexitätsstufe in ein Java EE Projekt integriert werden soll. Nachteil der Lösung ist allerdings, dass zwar Prozessschritte abgebildet werden können, aber praktisch keine Möglichkeit zur Orchestrierung von Services besteht.

Die Anforderung der Service-Orchestrierung ist  nur bedingt notwendig und je nach Plattform stärker oder schwächer ausgeprägt. Im Enterprise Service Bus (ESB) Kontext, bietet der ESB selbst die Möglichkeit zur Orchestrierung von Services an, sodass die einzelnen Services nicht zwingend die Fähigkeit der Orchestrierung besitzen müssen. Aufgrund der hohen Komplexität von ESB-Lösungen kann es deshalb sinnvoll sein, prozessorientierte Datenbankstrukturen einer komplexeren BPM Lösung vorzuziehen.

BPM beinhaltet eine weitere Komplexitätsstufe für ein Java EE Projekt. Auf der anderen Seite erfüllt eine BPM Engine folgende wesentliche Anforderungen: Separation of Concerns (Delegierung von Verantwortlichkeiten) und Trennen der Prozessdefinition von der Implementierung. Ein Service ist aufgrund der Anforderungserfüllung flexibel änderbar, sodass sowohl Prozessschritte als auch die Implementierung eines Prozessschrittes ohne Seiteneffekte ausgetauscht werden können. Die Definitionssprache für Services (BPEL) ist standardisiert und die gängigen BPM Engines sind in ähnlicherweise nutzbar. Graphische Werkzeuge stehen zur Prozessdefinition zur Verfügung. Diese Werkzeuge erlauben es, ActionHandler, Events und Exceptions für einen Prozessschritt zu definieren. Die Prozessdefinition ist deshalb nahtlos mit der Implementierung eines Prozesses verknüpfbar. Darüber hinaus fügen sich BPM Engines in das Transaktionskonzept einer Java EE Anwendung ein und können neben entfernter Geschäftslogik, Datenbanken und MOM-Systeme transaktionssicher ansprechen.

Es ist sicherlich Gewohnheit prozessorientiert zu denken und Prozesse zu implementieren. Der Einstieg lohnt sich allerdings, weil Prozessschritte aus kleinen Einheiten bestehen, die nacheinander abgearbeitet werden und flexibel miteinander verknüpfbar sind. Konform zu Design by Contract stellt sich ein sicheres Gefühl für eine Anwendung ein, bei der wesentliche fachliche Überlegungen stattgefunden haben. Das Angst, dass bei fachlichen Änderungen die zuvor implementierte Softwarestruktur zerstört werden könnte, ist beim prozessorientierten Ansatz weniger stark ausgeprägt. Die Fachlichkeit ist im Prozessdiagramm klar definiert.  Das Prozessdiagramm wird zur Laufzeit einer Anwendung intepretiert und per Delegierung (Command Pattern) in einzelnen Prozessschritten mit Quellcode, welcher den Geschäftsanforderungen genügt, angereichert.

BPM ist bei der Programmierung von Web Services nicht mehr wegzudenken und besonders für Anwendungen im Web Services Umfeld empehlenswert. Prozessschritte können dabei synchron oder asynchron verarbeitet werden. Die Orchestrierung von Web Services mit BPEL ist weit verbreitet und erfreut sich großer Beliebtheit.

In dem folgenden Szenario wird aus Sicht des Reviewers der Review-Prozess prozessorientiert mit der JBoss JBPM Engine abgebildet. Die JBPM Engine von JBoss ist als eigenständige Applikation in eine JBoss-Umgebung integrierbar und bietet für Eclipse ein graphisches Werkzeug zur Prozessdefinition an. Prozessdefinitionen können mit JBoss JBPM in einer Datenbank abgelegt werden, sodass langlaufende Prozesse abbildbar sind. Persistente Prozessdefinitionen mit entsprechender Interaktion zur Laufzeit einer prozessorientierten Anwendung sind ein wesentliches Kriterium für Quality of Services.

JBoss JBPM Prozessdefinition für den Review-Prozess

Prozessdefiniiton des Review-Prozesses
 

In den Prozessschritten, die durch Knoten gekennzeichnet sind, werden ActionHandler registriert. ActionHandler implementieren die Geschäftslogik und tauschen Daten zur Verarbeitung und Steuerung von Prozessschritten über den Ausführungskontext aus.

MessageHandler zur Anzeige von Statusübergängen

package ccd.jee.jbpm.handler;

import org.apache.log4j.Logger;
import org.jbpm.graph.def.ActionHandler;
import org.jbpm.graph.exe.ExecutionContext;

public class MessageHandler implements ActionHandler {

    private Logger log = Logger.getLogger(MessageHandler.class);

    private String message;

    @Override
    public void execute(ExecutionContext context) throws Exception {

        processMessage(context);
    }

    private void processMessage(ExecutionContext context) {

        message = (String) context.getVariable("MESSAGE");
        log.info(message);
    }
}

DecisionHandler zur Steuerung des Review-Prozesses

package ccd.jee.jbpm.handler;

import org.jbpm.graph.exe.ExecutionContext;
import org.jbpm.graph.node.DecisionHandler;

public class ReviewDecisionHandler implements DecisionHandler {

    @Override
    public String decide(ExecutionContext executionContext) throws Exception {
       
        boolean result = (Boolean) executionContext.getVariable("SOURCE_CHANGE_OK");
       
        if(result) {
           
            return "OK";
        }
       
        return "nicht OK";
    }
}

Unit-Test des Review-Prozesses

package ccd.jee.jbpm.handler;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import java.util.HashMap;
import java.util.Map;
import org.jbpm.graph.def.ProcessDefinition;
import org.jbpm.graph.exe.ProcessInstance;
import org.junit.Before;
import org.junit.Test;

public class CCDReviewProcessTest {

    private ProcessDefinition processDefinition;
    private ProcessInstance processInstance;

    @Before
    public void setUp() {

       processDefinition = ProcessDefinition.parseXmlResource("CCDReviewProcess/processdefinition.xml");
       assertNotNull("Definition should not be null", processDefinition);

       processInstance = new ProcessInstance(processDefinition);
    }

    @Test
    public void testCCDReviewProcessOK() {

        startState();

        identifyCodeSmellState();

        signCodeSmellState();

        commitSourcesState();

        informAutorState();

        waitForSourceChangeState();

        signalSourceChangeOk();

        sourceChangeOkState();

        endState();
    }

    @Test
    public void testCCDReviewProcessNotOK() {

        startState();

        identifyCodeSmellState();

        signCodeSmellState();

        commitSourcesState();

        informAutorState();

        waitForSourceChangeState();

        signalSourceChangeNotOk();

        sourceChangeNotOkState();

        repeatState();
    }

    private void startState() {

        assertEquals("Instance is in start state", processInstance.getRootToken()
                .getNode().getName(), "Review starten");

        assertNull("Message variable should not exist yet", processInstance
                .getContextInstance().getVariable("MESSAGE"));

        processInstance.getContextInstance().setVariable("MESSAGE",
                "gehe zu Code Smell identifizieren");
       
        processInstance.signal();
    }

    private void identifyCodeSmellState() {

        assertEquals("Instance is in state", processInstance.getRootToken().getNode()
                .getName(), "Code Smells identifizieren");

        processInstance.getContextInstance().setVariable("MESSAGE",
                "gehe zu Code Smell kennzeichnen");
       
        processInstance.signal();
    }
   
    private void signCodeSmellState() {

        assertEquals("Instance is in state", processInstance.getRootToken().getNode()
                .getName(), "Code Smells kennzeichnen");

        processInstance.getContextInstance().setVariable("MESSAGE",
                "gehe zu Quellcode einchecken");
       
        processInstance.signal();
    }
   
    private void commitSourcesState() {

        assertEquals("Instance is in state", processInstance.getRootToken().getNode()
                .getName(), "Quellcode einchecken");

        processInstance.getContextInstance().setVariable("MESSAGE",
                "gehe zu Autor informieren");
       
        processInstance.signal();
    }
   
    private void informAutorState() {

        assertEquals("Instance is in state", processInstance.getRootToken().getNode()
                .getName(), "Autor informieren");

        processInstance.signal();
    }
   
    private void waitForSourceChangeState() {

        assertEquals("Instance is in state", processInstance.getRootToken().getNode()
                .getName(), "Warten auf Korrektur");
    }
   
    private void sourceChangeOkState() {
       
        assertEquals("Instance is in state", processInstance.getRootToken().getNode()
                .getName(), "Korrektur in Ordnung");
       
        processInstance.signal();
    }
   
    private void sourceChangeNotOkState() {

        assertEquals("Instance is in state", processInstance.getRootToken().getNode()
                .getName(), "Korrektur nicht in Ordnung");

        processInstance.signal();
    }

    private void signalSourceChangeOk() {

        final Map<String, Boolean> contextVariables = sourceChangeOK();

        processInstance.addInitialContextVariables(contextVariables);
        processInstance.getContextInstance().setVariable("MESSAGE",
                "gehe zu Korrektur in Ordnung");
       
        processInstance.signal();
    }

    private void signalSourceChangeNotOk() {

        final Map<String, Boolean> contextVariables = sourceChangeNotOK();

        processInstance.addInitialContextVariables(contextVariables);
        processInstance.getContextInstance().setVariable("MESSAGE",
                "gehe zu Korrektur nicht in Ordnung");
       
        processInstance.signal();
    }

    private Map<String, Boolean> sourceChangeOK() {

        final Map<String, Boolean> contextVariables = new HashMap<String, Boolean>();
        contextVariables.put("SOURCE_CHANGE_OK", Boolean.TRUE);
        return contextVariables;
    }

    private Map<String, Boolean> sourceChangeNotOK() {

        final Map<String, Boolean> contextVariables = new HashMap<String, Boolean>();
        contextVariables.put("SOURCE_CHANGE_OK", Boolean.FALSE);
        return contextVariables;
    }
   
    private void endState() {

        assertEquals("Instance is in state", processInstance.getRootToken().getNode()
                .getName(), "Review beenden");
    }

    private void repeatState() {

        assertEquals("Instance is in state", processInstance.getRootToken().getNode()
                .getName(), "Review wiederholen");
    }
}

Das implementierte Szenario verdeutlicht die Trennung der Prozessdefinition und der eigentlichen Implementierung in den Handlern. Zur Laufzeit wird über die JBoss JBPM Engine die Prozessdefinition interpretiert, Handler angesprochen und Ereignisse im Prozessfortschritt ausgelöst. Prozessorientierte Anwendungen können mit Unit-Tests gesichert werden, sodass noch vor der Implementierung der Geschäftslogik der grundsätzliche Prozessablauf testbar ist.

Die Prozessdefinition in EJBs zu integrieren ist einfach und mit ein paar Zeilen Quellcode bewerkstelligt. Dadurch ergeben sich gute Bedingungen für den Einsatz einer BPM Engine in Java EE Anwendungen.

Model first is still a best practice for Web Service development!


Der Rechtshinweis des Java Blog für Clean Code Developer ist bei der Verwendung und Weiterentwicklung des Quellcodes des Blogeintrages zu beachten.