Keine Angst vor Zauberei: Aspektorientierte Programmierung #1

Wenn in Workshops oder auch bei der täglichen Arbeit das Thema Aspekte oder AOP (Aspektorientierte Programmierung) angesprochen wird, zucken viele Entwickler kurz vor Schreck zusammen und gehen auf Tauchstation. AOP hat immer noch den Ruf von schwarzer Magie: Keiner weiß so genau warum, geschweige denn wie sie funktioniert, und um einen Aspekt selbst zu schreiben muss man mindestens ein Jahr Hogwarts besucht haben. Oftmals wird dann in Frage gestellt, dass man AOP überhaupt benötigt. Dabei wird allerdings ignoriert, dass man – ohne eigenes Wissen – täglich damit zu tun hat.

Es gibt aber eigentlich keinen Grund AOP zu verteufeln – sie ist gar nicht so schwierig, wenn man einmal die Grundlagen verstanden hat.

Motivation

Wozu braucht man überhaupt AOP?
Am besten lässt sich dies am Beispiel von Transaktionsbehandlung erklären. Ziel von Transaktionen ist es bekanntlich sicherzustellen, dass logisch zusammengehörende Operationen gar nicht oder alle gemeinsam durchgeführt werden, also im Prinzip die Einhaltung der ACID (atomar, consistent, isolated, durable) Eigenschaften.
In einer 08/15 Schichtenarchitektur (UI -> Service -> Persistenz) wird dies oft so realisiert, dass beim Eintritt in eine Methode der Serviceschicht eine Transaktion aufgespannt wird und beim Austritt der Methode diese Transaktion entweder abgeschlossen („committed“) oder im Fehlerfall zurückgerollt wird („rollbacked“). Wie lässt sich dies nun umsetzen?

Variante #1: Manuelle Umsetzung

Eine manuelle Umsetzung könnte mit Spring wie folgt aussehen:

import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;

class SuperService {
	@Autowired
	private PlatformTransactionManager txMgr;

        @Autowired 
        private SuperDao dao;

	void complexOperation(ComplexObject obj) {
		final TransactionStatus tx = txMgr.getTransaction(new DefaultTransactionDefinition(TransactionDefinition.PROPAGATION_REQUIRES_NEW));
		try {
			calculate(obj);
			dao.save(obj);

			txMgr.commit(tx);
		} finally {
			if (!tx.isCompleted()) {
				txMgr.rollback(tx);
			}
		}
	}
}

Das hier nur exemplarisch für eine Methode gezeigte Muster müssten wir nun für alle Service-Methoden implementieren. Daraus ergeben sich eine Reihe von Nachteilen:

  1. In (mindestens) 95% der Fälle ist der Code für die Transaktionsbehandlung immer gleich. Wir verstossen damit massiv gegen das DRY-Prinzip (Don´t repeat yourself)!
  2. Die Implementierung ist sehr aufwendig; nicht unbedingt komplex, aber eben ein relativ großer Aufwand für die Implementierung.
  3. Die Sicht auf die eigentliche Aufgabe der complexOperation-Methode ist durch die Transaktionsbehandlung vernebelt. Die eigentliche Geschäftslogik beansprucht zwei Zeilen der Methode, wohingehend die Transaktionsbehandlung insgesamt 8 Zeilen in Anspruch nimmt. Dies widerspricht dem Single-Responsibility-Prinzip.
  4. Die Transaktionsbehandlung ist über den gesamten Code der Serviceschicht zerstreut. Sollten hier Änderungen notwendig sein, muss an vielen Stellen Hand angelegt werden.
  5. Die Transaktionsbehandlung erzeugt Abhängigkeiten in der Serviceschicht. Wie in den Import-Statements zu sehen, werden 4 Klassen aus dem Spring-Framework benötigt. Dies widerspricht dem Prinzip der losen Kopplung.

In der Fachliteratur werden für diese Probleme oftmals auch die Begriffe „code tangling“ (to tangle – verwirren) und „code scattering“ (to scatter – zerstreuen) verwendet.

Nun ist Transaction-Handling nicht die einzige Querschnittsfunktion, die in Softwaresystemen benötigt wird. Hier ist eine natürlich beliebig erweiterbare Liste weiterer möglicher Querschnittsfunktionen („crosscutting concerns“):

  • Tracing – Ein- und Austritt von Methoden protokollieren, um im Fehlerfall den Programmfluss nachvollziehen zu können
  • Security – Prüfen, ob ein User berechtigt ist eine Funktion auszuführen
  • Fehlerbehandlung – Protokollieren von internen Exceptions und Übersetzen in allgemeingültige Exceptions
  • Profiling – Performance-Messungen

Eine manuelle Implementierung dieser Querschnittsfunktionen für unser Beispiel würde in etwa so aussehen:

import blog.aop.errorhandling.ErrorHandler;
import blog.aop.security.SecurityService;
import blog.aop.security.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;

@Service
class SuperService {
   private static final Logger LOG = LoggerFactory.getLogger(SuperService.class);

   @Autowired
   private PlatformTransactionManager txMgr;

   @Autowired
   private SecurityService securityService;

   @Autowired
   private ErrorHandler errorHandler;

   @Autowired
   private SuperDao dao;

   public void complexOperation(ComplexObject obj) {
      final long start = System.currentTimeMillis();
      try {
         final User user = securityService.getUserFromCurrentSession();
         if (!securityService.isAuthorized(user, "SuperService.complexOperation")) {
            throw new UserNotAuthorizedException();
         }

         if (LOG.isTraceEnabled()) {
            LOG.trace("Start " + SuperService.class + ".complexOperation()");
         }
         final TransactionStatus tx = txMgr.getTransaction(new DefaultTransactionDefinition(TransactionDefinition.PROPAGATION_REQUIRES_NEW));
         try {
            calculate(obj);
            dao.save(obj);

            txMgr.commit(tx);
         } finally {
            if (!tx.isCompleted()) {
               txMgr.rollback(tx);
            }
         }
      } catch (final Throwable t) {
         errorHandler.handleError();
      } finally {
         final long took = System.currentTimeMillis() - start;
         LOG.info(SuperService.class + ".complexOperation() took" + took + "ms");
      }
   }
}

Es fällt schon schwer im obigen Listing die Anweisungen zu finden, die den eigentlichen Sinn und Zweck der Methode widerspiegeln. Wir haben also 2 Zeilen für die eigentliche Business Logik und über 40 Zeilen Code, die sich um die Querschnittsfunktionen kümmern. Projiziert man dieses auf den gesamten Quellcode der Serviceschicht, wird deutlich, dass eine andere Form der Umsetzung von Querschnittsfunktionen wünschenswert wäre.

Variante #2: Aspektorientierte Programmierung

Ziel der AOP ist es, Querschnittfsunktionen in separate Bausteine, sogenannte Aspekte, auszulagern. Die Klassen der Anwendung sollten dann dann nur noch die Kernlogik enthalten, wodurch das Single-Responsibility-Prinzip eingehalten würde. Darüber hinaus sollten keine Abhängigkeiten zu Codefragmenten mehr existieren, die für die Realisierung von Nicht-Kernaufgaben benötigt werden. Dies begünstigt die lose Kopplung und würde das „code tangling“ verhindern.

Durch die Auslagerung in die Aspekte sollten die Querschnittsfunktionen zudem nicht mehr über den gesamten Quellcode verteilt werden, wodurch das „code scattering“ eliminiert würde. Darüber hinaus könnten Querschnittsfunktionen wiederverwendet werden.

Wie gelangen die Querschnitts-Anweisungen aus den Aspekten aber wieder in den Code? Dies ist Aufgabe des „Weaving“-Prozesses. Beim Weaving werden die Anweisungen der Aspekte, die sogenannten „Advices“, wieder in den Code der Anwendung eingewebt. Dies erfolgt automatisiert zur Compilezeit oder zur Ladezeit der Klassen durch einen gesonderten Classloader oder Dynamic Proxies (später mehr dazu). Damit der Weaver weiß, wo und wann die Advices in den Code eingewebt werden sollen, müssen die Codestellen mit Hilfe von sog. Pointcuts definiert werden.

Wichtig ist am Ende, dass sich das System mit eingewebten Aspekten genauso verhält, als wenn die Anweisungen manuell implementiert wurden.

Im nächsten Blog-Beitrag geht es dann mit AOP – Technologien weiter…


Quellen / Verweise:
Beitragsbild: photo credit: aldisley Shrewsbury LEGO Fest 2005: Hogwarts hall via photopin (license)

Verfasst von Björn Seebeck am 4. Juli 2017