Donnerstag, 9. September 2010

Java EE support for Separation of Concerns

Eine wesentliche Eigenschaft verständlicher und wartbarer Software ist die Trennung von Verantwortlichkeiten. Objektorientierte Programmiersprachen nutzen für diese Anforderung kohäsive Klassen. Wesentlich für das Klassen- und Methodendesign ist, dass keine Verschmutzung des Quellcodes durch nicht funktionale Anforderungen entsteht. Eine optimal programmierte Klasse beinhaltet deshalb nur fachliche Logik und keine Logik für beispielsweise Trace-Ausgaben. Die aspektorientierte Programmierung (AOP) verwendet Cross-Cutting Concerns als Querschnittsfunktionen zur Behandlung von nicht funktionalen Anforderungen.

Eines der Grundsätze in der Informatik ist: divide et impera (teile und herrsche). Dieser Grundsatz hat Separation of Concerns und die aspektorientierte Programmierung geprägt. Nicht funktionale Anforderungen wie Fehlerbehandlungen, Logging, Tracing, Auditing und Sicherheitsanforderungen sind deshalb getrennt von der fachlichen Logik zu behandeln.

Java EE 5 beinhaltet das Konzept der Interzeptoren. Interzeptoren sind schwächer als AOP-Frameworks, eignen sich aber dennoch sehr gut für die Umsetzung nicht funktionaler Anforderungen. Interzeptoren beziehen sich auf den Lebenszyklus einer EJB oder auf fachliche Methoden. In der nachfolgenden Implementierung ist ein einfaches Stateful Cache Bean als exemplarisches Beispiel für die Verwendung von Interzeptoren programmiert.

Schnittstelle des Cache Beans:

public interface Cache {

    public String getCacheData(String key);
    public void setCacheData(String key, String value);
}

Implementierung des Cache Beans:

import java.util.HashMap;
import java.util.Map;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.ejb.Remote;
import javax.ejb.Stateful;
import javax.interceptor.AroundInvoke;
import javax.interceptor.InvocationContext;
import org.apache.log4j.Logger;

@Stateful
@Remote(Cache.class)
public class CacheBean implements Cache {

    private Logger logger;
    private Map<String,String> cache;
  
    @PostConstruct
    void createCache() {
      
        logger = Logger.getRootLogger();
        cache = new HashMap<String,String>();
    }
  
    @PreDestroy
    void destroyCache() {
      
        cache.clear();
        cache = null;
    }
  
    @AroundInvoke
    protected Object businessInterceptor(InvocationContext ctx) throws Exception {
      
        logger.info("interceptor for method " + ctx.getMethod().getName() + " called.");
      
        return ctx.proceed();
    }

    @Override
    public String getCacheData(String key) {
      
        return cache.get(key);
    }

    @Override
    public void setCacheData(String key, String value) {
      
        cache.put(key, value);
    }
}

Die beiden Methoden @PostConstruct und @PreDestroy sind Lebenszyklusmethoden des Cache Beans. Die Lebenszyklusmethoden folgen einem Vertrag der bestimmt, zu welchem Zeitpunkt die Lebenszyklusmethoden aufgerufen werden. Die @AroundInvoke Methode wird für jede Geschäftsmethode eines Beans aufgerufen. In dieser Interzeptormethode kann das Tracing und die Prüfung von sicherheitsrelevanten Rollen stattfinden. Interzeptoren sind in einem EJB oder einer Interzeptorklasse implementierbar. Der Aufbau von Interzeptorklassenhierarchien und die Annotierung von Interzeptorketten ist möglich, sodass dediziert Querschnittsfunktionen implementiert und angewendet werden können. Ketten von Interzeptoren unterliegen einer streng definierten Aufrufreihenfolge.

Unit-Test des Cache Beans:

import static org.junit.Assert.*;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import org.junit.BeforeClass;
import org.junit.Test;

public class TestCache {

    private static final String VALUE = "value";
    private static final String KEY = "key";
   
    // JNDI_NAME = "Projektname/Beanname/remote"
    private static final String JNDI_NAME = "Interceptor/CacheBean/remote";
   
    private static Context context;
    private static Cache cache;
   
    @BeforeClass
    public static void setUp() throws NamingException {
       
        context = new InitialContext();       
        cache = (Cache) context.lookup(JNDI_NAME);               
    }
   
    @Test
    public void testSetAndGetCacheData() {
       
        cache.setCacheData(KEY, VALUE);
        assertEquals(VALUE,cache.getCacheData(KEY));       
    }
}

Der Unit-Test "testSetAndGetCacheData()"  führt neben den beiden fachlichen Methoden auch den Interzeptor aus, sodass als Log-Ausgabe die folgenden zwei Zeilen geschrieben werden:

18:10:30 INFO interceptor for method setCacheData called.
18:10:30 INFO interceptor for method getCacheData called.

Interzeptoren sind ein gutes Mittel, um den Quellcode von EJBs von Querschnittsfunktionen sauber zu halten. Fachliche Logik sollte allerdings nicht in den Interzeptoren untergebracht werden, weil dadurch ihr Zweck entfremdet werden würde.


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