kramann.info
© Guido Kramann

Login: Passwort:










2.2 Bezugnahme auf Entwurfsmuster bei der Software-Entwicklung für mobile autonome Systeme

2.2 (EN google-translate)

2.2 (PL google-translate)


entwurfsmuster.zip - C++ und Java-BlueJ-Projekt zu den nachfolgenden Entwurfsmustern.

entwurfsmuster2.zip - Modifikation des Fassade-Beispiels hin zum Adapter-Muster (vergl. weiter unten).

Die als Architekturen eingeführte Konzepte waren noch ziemlich abstrakt. Ein Schritt weiter hin auf eine konkrete Umsetzung soll hier durch die Anwendung s.g. Entwurfsmuster geschehen.

Hinweis: Als sinnvolles "Zusatzelement" im Zusammenhang einer Software-Architektur für Autonome Vehikel wird noch am Ende dieses Kapitels "Sensorsynthese" behandelt. Dies ist kein klassisches Entwurfsmuster, passt aber in diesen Zusammenhang.

Im Übersichtsdiagramm in der Einführung wurde ja als Zielsetzung für die Einführung bestimmter Architekturen angedeutet, dass damit...

  • Erweiterbarkeit
  • Fehlertoleranz
  • Robustheit

... erreicht werden.

Es wurde auch angedeutet, dass dies konkret durch ...

  • Modularisierung
  • Hierarchisierung
  • Datenreduktion
  • Anpassung (Adaption)
  • Lernen

erreicht werden kann.

Was sind Entwurfsmuster und warum können Sie nützlich sein die hier angestrebten Programm-Eigenschaften zu erreichen?

Bei Entwurfsmustern handelt es sich um eine Sammlung von Lösungen für bestimmte Programmierprobleme. Grundsätzlich steht dabei im Hintergrund eine Objekt orientierte Programmiersprache, aber nicht immer zwingend. Gefunden und zusammengestellt wurden diese Konzepte von den Informatikern Erich Gamma, Richard Helm, Ralph Johnson und John Vlissides in "Design Pattern - Elements of Reusable Object-Oriented Software". Mittlerweile gibt es viele Autoren, die diese Konzepte beispielhaft für verschiedene Programmiersprachen dargestellt haben, z.B. für Java von Geirhos, Matthias, "Entwurfsmuster - Das umfassende Handbuch", Rheinwerk (ehem. Galileo Press), Bonn 2015.

Im Rahmen dieser LV wird eine Auswahl an Erzeugungsmuster, Strukturmuster und Verhaltensmustern verwendet. Drei weitere Kategorien (Muster verteilter Architekturen, Datenmuster, GUI-Muster) werden hier nicht behandelt.

Muster, die unmittelbar Eingang in den Software-Entwurf eines Beispiel-Projekts fließen sind mit einem Stern (*) versehen und werden hier theoretisch behandelt und in C++ für ein AVR-Mikrocontroller-Programm auf der Arduino-Plattform (Arduino-Micro) implementiert.

Übersicht der Entwurfsmuster

Erzeugungsmuster
    - Fabrikmethode
    * Singleton *
    - Multiton
    - Abstrakte Fabrik
    - Erbauer
    - Prototyp

Strukturmuster
    - Brücke
    - Komposition
    - Dekorierer
    * Fassade *
    - Fliegengewicht
    - Proxy
    * Adapter *

Verhaltensmuster
    - Zuständigkeitskette
    * Befehl *
    - Interceptor
    * Interpreter *
    - Iterator
    - Vermittler
    - Memento
    * Beobachter *
    * Zustand *
    * Strategie *
    - Schablonenmethode
    - Besucher


Code 2.2-1: Überblick der Entwurfsmuster.

Nachfolgend werden die für uns hier interessantesten Entwurfsmuster (Design-Pattern) genauer beschrieben und je eine mögliche Implementierung in Java gezeigt und eine für die Anwendung in einem Mikrocontroller angemessene Implementierung in C++. Insbesondere wird in C++ versucht, den Code möglichst kurz und einfach zu halten, da ja der Programm-Speicherplatz (32 Kilobyte), die Performance (16MHz) und die Stacktiefe (3) bei dem verwendeten AVR-Mikrocontroller (Arduino-Micro / ATmega32U4) recht begrenzt sind und die dynamische Erzeugung von Objekten weder elaubt noch sinnvoll ist: Der benötigte Speicher sollte komplett direkt beim Programmstart allokiert werden, um bei der begrenzten Menge an Arbeitsspeicher (2,5 Kilobyte) keinen Speicherüberlauf und Absturz des Programms zu riskieren.

Singleton

Beim Singleton-Muster geht es darum zu gewährleisten, dass es von einer bestimmten Klasse nur jeweils genau ein Objekt geben darf. Dies kann durch Verwendung eines privaten Konstruktors und einer speziellen statischen Methode realisiert werden, die eine Instanz bei Bedarf liefert. Wird diese Instanz auch erst bei Aufruf dieser Gebermethode erzeugt, hat man hier außerdem ein Beispiel für LAZY LOADING vor sich. Dies spart Resourcen, wenn der Fall zur Laufzeit auftritt, dass letztlich gar keine Instanz benötigt wird.

Anwendungsfälle sind solche, wo auf physikalische Resourcen zugegriffen wird, die nur einmal vorhanden sind, wie Kamera, Soundkarte, usw. Insbesondere beim Mikrocontroller sind alle internen Peripherien typischerweise nur einmal vorhanden, bzw. bei einem Roboter, oder Autonomen Vehikel gibt es einen bestimmten Sensor, oder eine Anzeige nur einmal und entsprechend sollte es nur ein Objekt geben, das diese repräsentiert, d.h. welches Methoden bereitstellt, um auf dieses zuzugreifen.

UML-Klassendiagramm der nachfolgenden Implementierung des Singleton-Entwurfsmusters.

Bild 2.2-1: UML-Klassendiagramm der nachfolgenden Implementierung des Singleton-Entwurfsmusters.

/**
Singleton - Es wird gewährleistet, dass nur eine Instanz der Klasse erzeugt werden kann.
*/

public class Singleton
{
    private static Singleton instanz = null;

    private Singleton()
    {
        
    }

    public static Singleton holeInstanz()
    {
        if(instanz==null)
        {
            instanz = new Singleton();
        }

        return instanz;
    } 

    /** fiktive Anwendungsmethode */
    public void zeigeAufDisplay(String text)
    {
         //Hier einfache Ausgabe auf der Konsole
         System.out.println(text);
    }

    /** Achtung: Dass hier in der Klasse eine main-Methode ist, hat nichts zu sagen.
    Diese könnte auch ich einer anderen Klasse stehen, um die Methode Singleton.holeInstanz() zu testen.
    */
   
    public static void main(String[] args)
    {
        Singleton s = Singleton.holeInstanz();
        s.zeigeAufDisplay("Hier wird die Anwendungsmethode der Singleton-Instanz verwendet.");
    }
}

Code 2.2-2: Singleton-Implementierung mit Java.

#include<iostream>
#include<string>

using namespace std;

/*
Eigentlich ist dies keine richtige implementierung von
Singleton, da hier nicht sicher gewährleistet wird, dass nur eine
Instanz der KLasse zur Laufzeit existiert.

Jedoch wird als Konvention vorausgesetzt, dass beim Mikrocontrollerprogramm sowieso keine 
Instanzen von Objekten erzeugt werden, außer denjenigen, die direkt am Ende der 
jweiligen Klasse angegeben sind.

Das vorliegende Beispiel wurde so angepaßt, dass es am PC getestet werden kann.
*/
class Singleton
{
    protected:
    public:
        Singleton()
        {
        }
        void zeigeAufDisplay(string text)
        {
            //Hier einfache Ausgabe auf der Konsole
            cout<<text<<endl;
        }        
} s;

int main()
{
    s.zeigeAufDisplay("Hier wird die Anwendungsmethode der Singleton-Instanz verwendet.");
}

Code 2.2-3: Nicht ganz korrekte, aber angemessene Implementierung für die Verwendung in einem Mikrocontroller in C++.

Fassade

Fassade-Implementierungen erlauben es in einfacher Weise auf komplexe Systeme zugreifen zu können. Die Fassade präsentiert nach außen eine einfache Benutzerschnittstelle. Beispiel: Der Aufbau einer seriellen Verbindung erfordert die Festlegung einer Reihe an Parametern: Baudrate, Anzahl an Bits pro übertragenem Wort, Vorhandensein bzw. Anzahl der Start und Stop-Bits, Parität. Meistens aber werden innerhalb einer Anwendung immer die gleichen Einstellungen benutzt. Nun möchte man einerseits die Verwendung anderer Einstellungen nicht verunmöglichen, aber andererseits einen vereinfachten Zugriff auf die serielle Schnittstelle gewährleisten. Hier kann ein Fassade-Pattern helfen, neben dem Standard-Zugriff auch noch einen vereinfachten Zugriff zu gewährleisten.

Bei einem solch einfachen Fall reicht es aber oft schon mehrere Konstruktoren der Klasse anzubieten. Nachfolgend wieder eine "Pseudoimplementierung" in C++ für eine angemessene Nutzung im Mikrocontrollerbereich des Fassade-Pattern:

#define TAKTFREQUENZ 9216000
#define BAUDRATE 115200
#define PUFFERGROESSE 30

class RS232 : public RS232BASIS, CoachBASIS
{
    private:
        uint16_t baudregister;
        char  puffer[PUFFERGROESSE];
        int16_t  i,k;
        double   x,y;
    public:        
        void RS232() //einfacher Konstruktor für typische Verwendung
        {
            baudregister = TAKTFREQUENZ/8/BAUDRATE-1;
            UBRRH = (unsigned char) (baudregister>>8); //Setzen des HIGH-Bytes des Baudraten-Registers
            UBRRL = (unsigned char)  baudregister;     //Setzen des LOW -Bytes des Baudraten-Registers
            //Einschalten des Senders und des Empfängers
            UCSRB = (1<<TXEN) | (1<<RXEN);	        
            //Setzen des Nachrichtenformats: 8 Datenbits, 1 Stopbits
            UCSRC = (1<<URSEL)|(1<<UCSZ0)|(1<<UCSZ1);
            UCSRA = (1<<U2X);
        } 
    
        void RS232(int baudrate, int anzahl_datenbits)//komplizierterer Konstruktor für ausgefallenere Anwendungen
        {
            baudregister = TAKTFREQUENZ/8/baudrate-1;
            UBRRH = (unsigned char) (baudregister>>8); //Setzen des HIGH-Bytes des Baudraten-Registers
            UBRRL = (unsigned char)  baudregister;     //Setzen des LOW -Bytes des Baudraten-Registers
            //Einschalten des Senders und des Empfängers
            UCSRB = (1<<TXEN) | (1<<RXEN);	        
            //Setzen des Nachrichtenformats: 

            switch(anzahl_datenbits)
            {
                case 8:
                    UCSRC = (1<<URSEL)|(1<<UCSZ0)|(1<<UCSZ1);
                    UCSRA = (1<<U2X);
                break;
                case 9:
                    ...
                    ...
                break;
                default:
                    ...
                    ...
                break;
            }

        }
    
        void stop()
        {
            UCSRB &= ~( (1<<TXEN) | (1<<RXEN) );	        
        }

        void sendeZeichen(uint8_t zeichen)
        {
            while( !(UCSRA & (1<<UDRE)) ); 
            UDR = zeichen;
        }

        uint8_t holeZeichen()
        {
			while(!(UCSRA & (1<<RXC)));
			return UDR;
        }
        ...
        ...
        ...
};

Code 2.2-4: Klasse zur Verwendung der seriellen Schnittstelle bei einem ATmega32 mit verschiedenen Konstruktoren (Keine echte Implementierung des Fassade-Pattern, aber die verschiedenen Konstruktoren spielen hier die Rolle verschiedener "Fassaden" für den mehr oder weniger differenzierten, bzw. einfachen Zugriff auf die serielle Schnittstelle).

Noch weiter geschärft wird das Profil einer Fassade, wenn diese nicht nur auf ein hinter ihr liegendes System zugreift, sondern auf mehrere. Beim Entwurf Fehlertoleranter Software haben Sie das Muster "Recovery Blocks", also "Regenerierungs-Blöcke " eingeführt, die mehrere Methoden nacheinander ausprobieren, um ein Ergebnis zu erzielen: Versagt die erste, wird die zweite ausprobiert, versagt diese, dann die dritte usw. Dies ist häufig ein sehr komplizierter Vorgang, bei dem es Sinn macht, diesen vor dem Benutzer des System (kann ein anderer Programmteil sein / Client) zu verbergen.

Bei dem nachfolgenden vollständigen lauffähigen Java-Beispiel kann das Programm durch Angabe des Parameters "w" für Window mit GUI, oder ohne Angabe des Parameters als reine Konsolenanwendung gestartet werden. In beiden Fällen soll es eine einfache Möglichkeit geben, dem Nutzer Nachrichten zu übermitteln. Ein Fassade-Pattern verbirgt die Komplxität, die darin besteht, zu unterscheiden, ob die Anwendung als GUI oder nicht läuft bietet eine einfache Methode "print(...)" nach außen an:

Um eine weiter verbesserte Transparenz des Codes zu erhalten, werden die unterschiedlichen Nachrichten-Methoden in unterschiedliche Klassen gesteckt (PrinterKonsole, PrinterGUI) und über eine gemeinsame Schnittstelle gewährleistet, dass sie die gleichen Methoden anbieten (iPrinter) und somit von der Fassade in gleicher Weise benutzt werden können. Je nach Art des Starts (Konsole oder GUI) zieht die Fassade entweder eine Instanz von PrinterKonsole oder eine von PrinterGUI an. Diese spezielle Vorgehen ist wiederum auch eine Implementierung des Entwurfsmusters "Adapter". Der Dozent hat hiervon intensiv gebrauch gemacht, um für Processing-Anwendungen einfach zu nutzende Methoden bereitzustellen, die Hardware sowohl im Android- als auch im Java-Mode nutzbar machen. Beispielsweise wird eine Webcam auf dem PC anders eingebunden als auf einem Android-Tablet. Durch eine Fassade, die abhängig vom Betriebssystem unterschiedliche Instanzen anzieht ("KameraLINUX", "KameraANDROID", gemeinsame Schnittstelle "iKamera") kann diese Komplexität vor dem Anwender / Anwenderentwickler verborgen werden. Jedoch wären diese Beispiele ein wenig komplizierter um sie an dieser Stelle rein zum Verständnis des Prinzips vorzustellen, darum das einfachere Beispiel GUI / Konsole.

Hinweis: das beigefügte ZIP-File "entwurfsmuster.zip" kann nach dem Entpacken als BlueJ-Projekt geöffnet werden. Da das Fassade-Beispiel in einem Unterpackage "fassade" liegt, gestaltet sich das Kompilieren und Starten von der Konsole aus folgendermaßen:

cd PFAD_IN_ORDNER_entwurfsmuster
javac fassade/*.java
java fassade/Hybrid

Code 2.2-5: Kompilieren und Aufruf des Fassade-Beispiels von der Konsole aus.

Vererbungs- und Benutzungs-Beziehungen des Fassade/Adapter-Beispiels

Bild 2.2-2: Vererbungs- und Benutzungs-Beziehungen des Fassade/Adapter-Beispiels

public interface iPrinter
{   
    void print(String text);
}

public class PrinterGUI implements iPrinter
{
    TextField tf;
    
    public PrinterGUI(Hybrid hybrid)
    {
          tf = new TextField();          
          hybrid.add(tf);
          tf.setBounds(50,50,400,20);    
          tf.setVisible(true);
    }
    
    public void print(String text)
    {
        tf.setText(text);
    }
}

public class PrinterKonsole implements iPrinter
{
    public void print(String text)
    {
        System.out.println(text);
    }
}

public class Printer
{
    private iPrinter iprinter;
    
    public Printer(Hybrid hybrid, boolean mitGUI)
    {
         if(mitGUI==true)
         {
              iprinter = new PrinterGUI(hybrid);
         }
         else
         {
              iprinter = new PrinterKonsole();
         }
    }


    public void print(String text)
    {
         iprinter.print(text);
    }
}

public class Hybrid extends Frame
{
     private boolean mitGUI;
     private Printer printer;
     public Hybrid(boolean mitGUI)
     {
          this.mitGUI = mitGUI;
          
          if(mitGUI==true)
          {
              setSize(500,500);
              setLayout(null);
              setVisible(true);                            
          }    
         
          printer = new Printer(this,mitGUI);
          
          //Testweise Ausgabe einer Nachricht:
          printer.print("Der Printer kann nun im Hybrid-Objekt genutzt werden.");
     }
    
     public static void main(String[] args)
     {
          Hybrid h = null;
          if(args!=null && args.length>=1 && args[0].equals("w"))
          {
              h = new Hybrid(true); //mit GUI
          }    
          else
          {
              h = new Hybrid(false); //ohne GUI
          }                   
     }
}


Code 2.2-6: Alle Klassen und Interfaces des Fassade/Adapter-Beispiels.

Ohne die zugehörigen Klassen, also unvollständig, ist hier auch noch die Fassade-Klasse "Kamera" zum Kamera-Beispiel angfügt, also eine Klasse, die je nachdem auf welchem Betriebssystem der Beispielcode läuft (Linux oder Android) ein anderes Betriebssystem-abhängiges Kamera-Objekt anzieht (camLINUX der Klasse Capture oder camAndroid der Klasse KetaiCamera) und passend konfiguriert und in beiden Fällen nach außen die gleichen Methoden zur Verwendung der Kamera anbietet.

Jedoch ist die Lösung hier nicht so elegant, weil nicht mit einem gemeinsamen Interface für die Linux-Kameraklasse und die Android-Kameraklasse gearbeitet wird, was zur Folge hat, dass beispielsweise in der einheitlichen Methode "auffrischen()" Fallunterscheidungen bzgl. des Betriebssystems nötig sind:

package info.kramann.crossplattform.system;

//Für den Java-Mode:
import processing.video.*;

//Für den Android-Mode:
import ketai.camera.*;

import processing.core.*; 
import info.kramann.crossplattform.system.Info;

/**
*  <h4>Kamera.java</h4>
*
*  Copyright (C) 2015 Guido Kramann<br/>
*  kramann@fh-brandenburg.de<br/>
*  http://www.kramann.info<br/>
*  <br/>
* 
* WICHTIG: Im Android-Mode Sketchpermission CAMERA setzen!<br/><br/>
* 
* Stellt die Basisfunktionalität zum Abruf des Kamerabildes bereit.<br/>
* Nutzt die Processing-Libraries Ketai für den Android-Mode und video im Java-Mode.<br/>
* Über Angabe der Kamera-ID kann zwischen der Front- und der Back-Cam gewählt werden:<br/>
* 0==Backcam<br/>
* 1==Frontcam<br/>
* (gestestet bei ASUS Fonepad)
* <br/><br/>
* Die verwendete Auflösung für das Kamerabild beträgt fest die halbe VGA-Auflösung, also 320x240 Pixel.<br/>
* 
*/



public class Kamera
{
    private PApplet pap;
    private Info info;
    private boolean ANDROID;
    
    private Capture camLINUX;
    private KetaiCamera camANDROID;
    private PImage kamerabild;
    
    private long ZAEHLER;
    
    public Kamera(PApplet pap, int KAMERA_ID)
    {
        ZAEHLER = 0;
        
        this.pap = pap;
        info = new Info();        
        ANDROID = info.isAndroid();
        
        kamerabild = null;
        
        if(ANDROID)
        {
            camANDROID = new KetaiCamera(pap, 320, 240, 10);
            int anzahl = camANDROID.getNumberOfCameras(); 
            if(camANDROID==null || anzahl==0)
            {
                camANDROID = null;
            }
            else if(anzahl<=KAMERA_ID)
            {
                camANDROID.setCameraID(0);
                int ZAEHLER = 0;
                kamerabild = camANDROID;
            }
            else
            {
                camANDROID.setCameraID(KAMERA_ID);
                int ZAEHLER = 0;
                kamerabild = camANDROID;
            }            
        }
        else
        {
            String[] cameras = Capture.list();
            if(cameras.length==0)  
            {
                camLINUX = null;
            }
            else if(cameras.length<=KAMERA_ID)
            {
                camLINUX = new Capture(pap, 320,240, cameras[0]);
                camLINUX.start();
                kamerabild = camLINUX;
            }
            else
            {
                camLINUX = new Capture(pap, 320,240, cameras[KAMERA_ID]);
                camLINUX.start();
                kamerabild = camLINUX;
            }
        }        
    }
    
    /**
     * Zugreifen auf die Referenz des Kamerabildes als PImage.
     */
    public PImage getKamerabildAsPImageReferenz()
    {
        return kamerabild;
    }

    public boolean isStarted()
    {
        if(ANDROID && camANDROID!=null && camANDROID.isStarted())         
        {
            return true; //wird auch aufgerufen, wenn kein neues Bild verfügbar ist. Noch nicht gut implementiert!
        }         
        else
        {
            return false;
        }
    }
    public void start()
    {
        if(ANDROID && camANDROID!=null && !camANDROID.isStarted())         
        {
            camANDROID.start(); //wird auch aufgerufen, wenn kein neues Bild verfügbar ist. Noch nicht gut implementiert!
        }         
    }
    public void stop()
    {
        if(ANDROID && camANDROID!=null && camANDROID.isStarted())         
        {
            camANDROID.stop(); //wird auch aufgerufen, wenn kein neues Bild verfügbar ist. Noch nicht gut implementiert!
        }         
    }
    
    /**
     * Liefert zurück, ob ein neues Bild bereitsteht (true) oder nicht (false).<br/>
     * Dieses steht als Referenz in PImage kamerabild zur Verfügung und kann mit getKamerabildAsPImageReferenz() geholt werden.
     * 
     * HINWEIS: Der Mechanismus in if(ANDROID && camANDROID!=null && !camANDROID.isStarted() && ZAEHLER>0 && ZAEHLER%20==0)<br/>
     * wurde eingeführt, da bei Versuchen mit einem Asus Fonepad (Android 4.3) das Starten der Kamera<br/>
     * Schwierigkeiten gemacht hat. Bei einem Nexus 7 (2012er, Android 4.2) trat dieses Problem nicht auf.<br/>
     * Auf einem Samsung mit Android 2.3.3 ließ sich die Kamera bisher nicht starten.<br/>
     * 
     */
    public boolean auffrischen()
    {
        if(ANDROID && camANDROID!=null && camANDROID.isStarted())         
        {
            camANDROID.read(); //wird auch aufgerufen, wenn kein neues Bild verfügbar ist. Noch nicht gut implementiert!
            return true;
        }
        else if(ANDROID && camANDROID!=null && !camANDROID.isStarted() && ZAEHLER>0 && ZAEHLER%20==0)
        {
            if(ZAEHLER%40==0)
                start();
            else    
                stop();
        }
        else if(camLINUX!=null && camLINUX.available() == true)
        {
            camLINUX.read();
            return true;
        }
        
        ZAEHLER++;
        
        return false;
    }
}

Code 2.2-7: Kamera - Fassade/Adapter-Klasse für Processing zur glichartigen Verwendung einer Kamera unter Linux und Android.

Schließlich folgt hier noch eine weitere konkrete Implementierung, auch aus dem Crossplattform-Konzept für Processing, in diesem Fall aber für das Speichern oder Laden von Dateien unter Linux und Android im Sketch-Ordner, bzw. im Implementierungspfad einer App auf einem Android-Device (unsichtbar nach Außem), oder im Download-Ordner, wo das Adapter-Konzept in gleicher Weise wie bei dem Hybrid-Beispiel weiter oben unter Einführung eines gemeinsamen Interfaces (iDatei) für die Linux- und die Android-Klasse (DateiLINUX / DateiANDROID) verwirklicht wird. Wieder wird hier nur die Fassade-Klasse wiedergegeben:

package info.kramann.crossplattform.system;

import processing.core.*; 

import info.kramann.crossplattform.system.Info;
import info.kramann.crossplattform.system.iDatei;
import info.kramann.crossplattform.system.DateiLINUX;
import info.kramann.crossplattform.system.DateiANDROID;

/**
*  <h4>Datei.java</h4>
*
*  Copyright (C) 2015 Guido Kramann<br/>
*  kramann@fh-brandenburg.de<br/>
*  http://www.kramann.info<br/>
*  <br/>
*  
*  Datei kann Dateien von data in den home-Ordner kopieren und umgekehrt.<br/>
*  D.h. alle Fileoperationen werden mit der Klasse Data auf den nicht öffentlichen Bereich data angewendet und
*  die Klasse hier bietet als zusätzliche Möglichkeit, Dateien von dort öffentlich zugreifbar und sichtbar zu machen.<br/>
*  
*  Im Android-Mode muß hier die Permission WRITE_EXTERNAL_STORAGE gegeben werden.<br/>
*  
*  Problem: Gespeicherte Dateien werden am PC nicht gleich angezeigt.<br/>
*  Abhilfe mit MediaScannerConnection.scanFile(activity, new String[] {meinFile.getAbsolutePath()}, null, null); <br/>
*  Jedoch trotzdem nötig, das Verzeichnis am PC neu zu laden.
*/
public class Datei
{
    private iDatei idatei;
        
    public Datei(Object pap)
    {
        if( (new Info()).isAndroid() )
            idatei = new DateiANDROID(pap);
        else
            idatei = new DateiLINUX(pap);
    }
    
    /**
     * Kopiert eine bereits vorhandene Datei vom nicht sichtbaren data-Bereich in den
     * sichtbare ~/Downloads Ordner (LINUX) bzw. /mnt/sdcard/Download Ordner.<br/>
     */
    public void veroeffentlichen(String dateiname)    
    {
        idatei.veroeffentlichen(dateiname);
    }    
    
    /**
     * String aus Datei vom home/Downloads-Ordner holen.
     */
    public String ladeString(String dateiname)    
    {
        return idatei.ladeString(dateiname);
    }    
}

Code 2.2-8: Fassade/Adapter-Klasse zur gleichartigen Nutzung des Dateisystems zum Laden und Abspeichern von Daten unter Linux und Android unter Verwendung eines gemeinsamen Interfaces.

"Regenerierungs-Blöcken" sind ein sehr nützliches Pattern zur Gewährleistung eines möglichst Ausfall-freien Betriebs innerhalb einer komplexen unsicheren Umgebung, wie sie bei einem in einer Umwelt agierenden AV vorliegt.

Diese Zugriffsweise auf Resourcen in Form von "Regenerierungs-Blöcken" entspricht dem Entwurfsmuster "Zuständigkeitskette". Somit ist "Zuständigkeitskette" ein Spezialfall des Pattern "Fassade".

Beispiel: Existiert eine serielle Schnittstelle über Kabel und eine weitere über Infrarot-Signale, so kann eine Fassade verbergen, über welche dieser Möglichkeiten kommuniziert wird und lediglich einfache Kommunikationsmethoden zur Verfügung stellen und selbstätig prüfen, welcher Weg funktioniert und diesen dann automatisch nutzen.

#include "Arduino.h"

#define REMOTEPUFFER 17

class Remote : public iModul
{
     public:
         ...
         ...
         void zeitschritt()
         {
               while(Serial.available())  //Kablebefehle übernehmen
               {
                     puffer_kabel[index_kabel] = Serial.read();
                     index_kabel++;
                     index_kabel%=REMOTEPUFFER;
               }
               while(Serial1.available()) //Infrarot-Befehle übernehmen
               {
                     puffer_infrarot[index_infrarot] = Serial1.read();
                     index_infrarot++;
                     index_infrarot%=REMOTEPUFFER;
               }

               auftrag_gefunden = false;  //wenn auf der aktuellen Ebene etwas gefunden wird, die darunter liegenden nicht mehr ansehen, bzw. sogar löschen!
               for(k=0;k<3;k++)  //alle drei Puffer in hierarchischer Reihenfolge durchgehen
               {
                     if(k==0)  //Alle Puffer durchgehen. Kabelbefehle haben die höchste Priorität und unterdrücken anders empfangene Befehle
                         puffer = puffer_kabel;
                     else if(k==1)    
                         puffer = puffer_infrarot;
                     else
                         puffer = puffer_intern;

                     for(i=0;i<REMOTEPUFFER;i++)
                     {
                           //wenn   Zustand, oder       Befehl, oder      Nachricht
                           nummer=-1;
                           panz=-1;
                           if(puffer[i]=='#' || puffer[i]=='$' || puffer[i]=='!')
                           {
                               nummer = (puffer[(i+1)%REMOTEPUFFER]-48)*10 + (puffer[(i+2)%REMOTEPUFFER]-48);
                           }
                           
                           if(puffer[i]=='#' && nummer>=0 && nummer<ANZZUSTAND) //Zustand
                           {
                               zustand = zustaende[nummer];
                               auftrag_gefunden = true;
                               panz = zustand->getPanz();
                           }
                           ...
                           ...
                     }
               ...
               ...
           }//zeitschritt()
} remote;

Code 2.2-9: Beispiel aus den C++-Klassen für das TURTLE-AV: Verarbeitung von Kabel gebundenen Befehlen, Infrarot-Befehlen und intern gesetzter Befehle.

Ein anderer Anwendungsfall besteht darin, die Verwendung verschiedenartiger Dinge zu vereinheitlichen, um sie besser organisieren zu können. Vor allem dieser zweite Aspekt von Fassaden wird hier bei der Entwicklung autonomer mobiler Systeme gebraucht. Tatsächlich kann man das weiter unten besprochene Pattern "Befehl" als Spezialfall einer Fassade auffassen. Dabei wird in einer Schnittstelle festgelegt, welche Methoden ein Befehlsobjekt haben muß und dann können Instanzen mit sehr unterschiedlichen Möglichkeiten hiervon gebildet werden: zum Steuern der LEDs, zur Ansteuerung der Motoren usw. Ein Array mit Zeigern auf alle verfügbaren Befehlsobjekte kann angelegt werden, um den Zugriff weiter zu vereinfachen: So wird es möglich den i-ten Befehl abzurufen.

Nachfolgend wird ein Ausschnitt aus einem Arduino-Sketch ( Hauptprogramms: setup() und loop() ), bei dem Objekte, die Befehle, Nachrichten und Zustände repräsentieren in Zeiger-Arrays gespeichert werden (siehe setup() des Programms), deren Elemente wiederum Hilfszeigern zugewiesen werden können, um das Ausführen zu veranlassen (siehe loop() des Programms).

...
...
void setup() 
{
    //pinMode(6,OUTPUT); //Tontest
  
    zustaende[0] = &zblink;
    zustaende[1] = &zping;
    zustaende[2] = &zstromanalyse;
    zustaende[3] = &zbodenanalyse;
    zustaende[4] = &zbodenfahrt;

    befehle[0] = &bgruenan;
    befehle[1] = &bgruenaus;
    befehle[2] = &bblauan;
    befehle[3] = &bblauaus;
    befehle[4] = &bfahrt;
    befehle[5] = &bvor;
    befehle[6] = &brueck;
    befehle[7] = &blinks;
    befehle[8] = &brechts;
    befehle[9] = &bstop;

    nachrichten[0] = &ngruss;
  
    gruenblau.aktivieren();
    antrieb.aktivieren();
    strom.aktivieren();
    boden.aktivieren();
    remote.aktivieren();
}

void loop() 
{
         remote.zeitschritt();
         
         if(zustand!=0)
         {
               zustand->loop();
         }
         if(befehl!=0)
         {
               //zustand = 0; //keinen Zustandsloop mehr ausführen, wenn ein direkter Befehl herainkommt
               //Nur bei Befehl $00 die Zustände ausklinken, da sonst z.B. Kontrolle des Stromsensors bei verschiedenen Motorzuständen nicht möglich!
               //SIEHE befehle.h bGruenAn

               befehl->perform();
               befehl = 0; //nur einmal ausführen!
         }
         if(nachricht!=0)
         {
               nachricht->reagieren();
               nachricht = 0;
         }
         
              //digitalWrite(6,HIGH);  //Tontest
         while(gZaehler<HALBMAXZAEHLER)
         {
              PORTB |=0;
              
         }
         gZaehler=0;
              //digitalWrite(6,LOW); //Tontest
}
...
...

Code 2.2-10: Ausschnitt des Hauptprogramms eines Arduino-Projekts für ein autonomes Vehikel, das Gebrauch vom Pattern Befehl, bzw. Gebrauch von einem Fassade-Pattern macht, das die Gleichbehandlung heterogener Elemente ermöglicht.

Adapter

In den obigen mittleren Fassade-Beispielen ("Hybrid" und "Datei"), könnten die Fassade-Klassen "Printer", bzw. "Datei" auch als "Objektadapter" aufgefaßt werden, wenn deren Objektmethoden auch noch über ein Interface definiert wären.

In diesem Fall würden sie beispielsweise helfen die Methoden einer als fremd anzusehenden Klasse, z.B. DateiANDROID, aufzurufen, ohne diese und deren Methoden genau zu kennen. Statt dessen gäbe es eine Vereinbarung in einem Interface, welche Methode die Adapterklasse anzubieten hat, damit ein Client deren Methoden aufrufen kann.

Um das verständlicher zu machen, wird das Hybridbeispiel entsprechend modifiziert, siehe Package "fassade" in entwurfsmuster2.zip:

Erweitertes Projekt

Bild 2.2-3: Erweitertes Projekt "Hybrid" hin zu Adapter-Pattern: Hybrid "möchte" die print-Methode so nutzen, wie in iAdapter vereinbart. Dies setzt Printer um, indem das Interface iAdapter implementiert wird und über den Fassade-Mechanismus die "fremdartigen" Print-Methoden nutzbar gemacht werden.

public interface iAdapter
{
    public void print(String text);
}

public class Printer implements iAdapter
{
    private iPrinter iprinter;
    
    public Printer(Hybrid hybrid, boolean mitGUI)
    {
         if(mitGUI==true)
         {
              iprinter = new PrinterGUI(hybrid);
         }
         else
         {
              iprinter = new PrinterKonsole();
         }
    }


    public void print(String text)
    {
         iprinter.print(text);
    }
}

package fassade;

import java.awt.*;

public class Hybrid extends Frame
{
     private boolean mitGUI;
     private iAdapter adapter;
     public Hybrid(boolean mitGUI)
     {
          this.mitGUI = mitGUI;
          
          if(mitGUI==true)
          {
              setSize(500,500);
              setLayout(null);
              setVisible(true);                            
          }    
         
          adapter = new Printer(this,mitGUI);
          
          //Testweise Ausgabe einer Nachricht:
          adapter.print("Der Printer kann nun im Hybrid-Objekt genutzt werden.");
     }
    
     public static void main(String[] args)
     {
          Hybrid h = null;
          if(args!=null && args.length>=1 && args[0].equals("w"))
          {
              h = new Hybrid(true); //mit GUI
          }    
          else
          {
              h = new Hybrid(false); //ohne GUI
          }                   
     }
}


Code 2.2-11: Veränderter Code.

Sinnvolles Anwendungsgebiet: Möchte man fremde Packages bzw. Libraries nutzen, wird man häufig unzufrieden sein, mit der Art und Weise, wie dort bestimmte Objekte zu erzeugen sind, die man selber nutzen möchte und auch damit, wie dann die Objektmethoden aufzurufen sind. Außerdem kann es sein, dass man dieses fremde Paket nur übergangsweise nutzen möchte, solange man keine eigene Implementierung realisiert hat. In diesem Fall kann eine Adapter-Klasse geschrieben werden, deren Objektmethoden für die eigene Anwendung über Interfaces festgelegt sind und die die fremden Pakete nutzt und vor der eigenen Anwendung verbirgt.

Zuständigkeitskette

Das Pattern "Zuständigkeitskette" kann eine besser passende Implementierung von Recovery-Blocks gewährleisten, als das Fassade-Beispiel "Remote" weiter oben.

Bei der Zuständigkeitskette sind die Objekte, die die Methoden zur Lösung der zu bewältigenden Aufgabe bereitstellen in einer einfach verketteten Liste organisiert.

Schafft das erste Glied der Kette, die Aufgabe nicht zu lösen, wird der Aufruf an das Nachfolgeelement weitergereicht usw.

Bei Recovery-Blöcken im Zusammenhang mit Autonomen Vehikeln könnte es sich bei einer solchen Kette z.B. um verschiedene Methoden handeln, die aktuelle Position des Fahrzeugs zu bestimmen. Versagt die Methode im ersten verketteten Objekt, wird versucht, die zweite zu verwenden usw.

Ein solches Programm wäre ziemlich umfangreich. Statt dessen wird als Beispiel für eine Implementierung folgendes genommen: Man übergibt der Bearbeitungsmethode einen Buchstaben. Jede der verketteten Objekte enthält nun ein Wort. Kommt das übergebene Zeichen in dem Wort vor, das in dem ersten verketteten Objektgespeichert ist, so wird dieses zurückgegeben. Ansonsten wird das Wort im Nachfolgeobjekt untersucht usw.

UML-Struktur des Zuständigkeitsketten-Beispiels.

Bild 2.2-4: UML-Struktur des Zuständigkeitsketten-Beispiels.

public interface iProblemloeser
{
    public String loeseProblem(char zeichen);
    public void registriereNachfolger(iProblemloeser problemloeser);
}

public class Wort1 implements iProblemloeser
{
    String wort = "Regen";
    
    public iProblemloeser nachfolger = null;
    
    public void registriereNachfolger(iProblemloeser problemloeser)
    {
            nachfolger = problemloeser;
    }
    
    public String loeseProblem(char zeichen)
    {
        for(int i=0;i<wort.length();i++)
            if(wort.charAt(i)==zeichen)
                 return wort;
                 
        if(nachfolger!=null)         
             return nachfolger.loeseProblem(zeichen);
        else
            return null;
    }
}

public class Wort2 implements iProblemloeser
{
    String wort = "Schnee";
    
    public iProblemloeser nachfolger = null;
    
    public void registriereNachfolger(iProblemloeser problemloeser)
    {
            nachfolger = problemloeser;
    }
        
    public String loeseProblem(char zeichen)
    {
        for(int i=0;i<wort.length();i++)
            if(wort.charAt(i)==zeichen)
                 return wort;
                 
        if(nachfolger!=null)         
             return nachfolger.loeseProblem(zeichen);
        else
            return null;
    }
}

public class Wort3 implements iProblemloeser
{
    String wort = "Sonnenschein";
    
    public iProblemloeser nachfolger = null;
    
    public void registriereNachfolger(iProblemloeser problemloeser)
    {
            nachfolger = problemloeser;
    }
        
    public String loeseProblem(char zeichen)
    {
        for(int i=0;i<wort.length();i++)
            if(wort.charAt(i)==zeichen)
                 return wort;
                 
        if(nachfolger!=null)         
             return nachfolger.loeseProblem(zeichen);
        else
            return null;
    }
}

public class Wortsucher
{
    private iProblemloeser problemloeser;

    public Wortsucher()
    {
         problemloeser = new Wort1();
         iProblemloeser p2 = new Wort2();
         iProblemloeser p3 = new Wort3();         
         
         problemloeser.registriereNachfolger(p2);
         p2.registriereNachfolger(p3);
    }

    public String loeseProblem(char zeichen)
    {
         return problemloeser.loeseProblem(zeichen);
    }
}

public class Hauptprogramm
{
    
    public static void auswerten(String wort, char zeichen)
    {
         if(wort==null)
             System.out.println("Es konnte kein Wort gefunden werden, in dem der Buchstabe "+zeichen+" enthalten ist.");
         else
             System.out.println("Problem gelöst: Das Zeichen "+zeichen+" ist im Wort "+wort+" vorhanden.");
    }
    
    public static void main(String[] args)
    {
          Wortsucher wortsucher = new Wortsucher();
          
          String erg = wortsucher.loeseProblem('e');
          auswerten(erg,'e');
          erg = wortsucher.loeseProblem('y');
          auswerten(erg,'y');
          erg = wortsucher.loeseProblem('o');
          auswerten(erg,'o');          
    }
}

Code 2.2-12: Alle Klassen des Zuständigkeitsketten-Beispiels.

Problem gelöst: Das Zeichen e ist im Wort Regen vorhanden.
Es konnte kein Wort gefunden werden, in dem der Buchstabe y enthalten ist.
Problem gelöst: Das Zeichen o ist im Wort Sonnenschein vorhanden.

Code 2.2-13: Konsolenausgabe des Beispiels

Natürlich könnte man die Klassen Wort1, Wort2 und Wort3 aus dem Beispiel ohne weiteres durch eine Klasse Wort repräsentieren und das gespeicherte Wort dem Konstruktor übergeben. Tatsächlich sollen diese drei Klassen aber modellhaft ganz unterschiedliche Dinge repräsentieren, beispielsweise drei sich grundlegend voneinander unterscheidende Methoden zur Bestimmung der aktuellen Fahrzeugposition. Das Programm soll nur das Grundprinzip veständlich darstellen.

Befehl

"Befehl" oder einen "Befehl ausführen" hat bei einem AV eine ganz konkrete Bedeutung: Ein Befehl kann beispielsweise dazu da sein, eine grüne LED an-, oder auszuschalten. Ein Befehl kann dazu dienen einen Dump der während der letzten Fahrt aufgezeichneten Sensordaten über die serielle Schnittstelle an das Wartungsprogramm zu schicken. Ein Sensor könnte kalibriert werden, alle Aktionen könnten beendet und das Fahrzeug in einen Standby-Modus übergehen.

Die Befehlsebene liegt also hierarchisch etwas über der der verfügbaren Peripherien, wie Sensoren, Aktuatoren, Schnittstellen usw., jedoch weit unterhalb einer Ebene, wo Verhaltensstrategien festgelegt werden. Diese Verhaltensstrategien könnten auf vorgefertigte Befehle zugreifen. Das würde das System modularer und transparenter machen. Insbesondere kann die Wirkung jedes Befehls sehr gut getrennt von allem anderen getestet werden.

Bei dem Entwurfsmuster "Befehl" wird im Wesentlichen ein Befehl durch ein Objekt repräsentiert. Das Objekt besteht dann im Wesentlichen aus einer in einem Interface festgelegten Ausführungs-Methode. Das hat folgende Vorteile: (Mit * gekennzeichnete Vorteile sind im Umfeld der Mirkocontroller-Programmentwicklung relevant, andere machen dort keinen Sinn.)

* Die Befehlsmethode kann parametrisiert und 
  so sehr leicht Befehle in ihrer Wirkung den 
  aktuellen Bedingungen angepasst werden, 
  Beispiel: fahrbefehl.ausfuehren(speed).

* Befehle können in Arrays oder Listen gesammelt, 
  hierarchisch geordnet und  weitergeleitet werden.

* Das, was getan werden muß, kann von dem wie und wer es dann macht getrennt werden:
  Ein in einer Warteschlange abgelegter Befehl, kann von irgnedeiner Instanz als
  passend erkannt und von ihr ausgeführt werden, ohne dass der Befehlsgeber vorher 
  Kenntnis davon hatte, wer ihn ausführt.

+ Als serialisierte Objekte können Befehle auf einem Speichermedium 
  abgelegt und wieder von dort geholt werden, oder über ein Netzwerk 
  verschickt werden.


Code 2.2-14: Vorteile, jeden Befehl in ein Objekt zu verpacken.

Nachfolgend ist ein Ausschnitt aus einem Mikrocontroller-Programm gegeben, der aufzeigt, wie Befehlsklassen aussehen könnten und wie diese verwendet werden könnten:

class iBefehl
{
     public:
         virtual void perform() = 0;
         virtual void konfigurieren(char a, char b, char c, char d) = 0;         
         virtual char getPanz() = 0;         
};

class bGruenAn : public iBefehl
{
  public:
    void perform()
    {
          //Nur bei diesem Befehl die Zustände deaktivieren!!!
          //***********************
          zustand = 0;
          antrieb.fahrt(0,0);
          //***********************
          gruenblau.anGruen();
          Serial.write("GRUEN AN

");
    }
    char getPanz(){         return 0;    }
    void konfigurieren(char a, char b, char c, char d){   }
} bgruenan;

class bGruenAus : public iBefehl
{
  public:
    void perform()
    {
          gruenblau.ausGruen();
          Serial.write("GRUEN AUS

");
    }
    char getPanz(){         return 0;    }
    void konfigurieren(char a, char b, char c, char d){   }
} bgruenaus;

class bBlauAn : public iBefehl
{
  public:
    void perform()
    {
          gruenblau.anBlau();
          Serial.write("BLAU AN

");
    }
    char getPanz(){         return 0;    }
    void konfigurieren(char a, char b, char c, char d){   }
} bblauan;

class bBlauAus : public iBefehl
{
  public:
    void perform()
    {
          gruenblau.ausBlau();
          Serial.write("BLAU AUS

");
    }
    char getPanz(){         return 0;    }
    void konfigurieren(char a, char b, char c, char d){   }
} bblauaus;

//Antriebs-Befehle
class bFahrt : public iBefehl
{
     public:
         char richtungLinks, richtungRechts;
         int links,rechts;
         void perform()
         {
              if(richtungLinks=='v') 
                  links=255;
              else if(richtungLinks=='r')    
                  links=-255;
              else
                  links=0;    
                  
              if(richtungRechts=='v') 
                  rechts=255;
              else if(richtungRechts=='r')    
                  rechts=-255;
              else
                  rechts=0;    

              antrieb.fahrt(links,rechts);                  
         }
         char getPanz(){         return 2;    }
         void konfigurieren(char a, char b, char c, char d)
         {   
              richtungLinks  = a;  //v==vorwärts, r==rückwärts, h==halten
              richtungRechts = b;
         }         
} bfahrt;

...
...

//Hauptprogramm:
...
...
void setup() 
{
    //pinMode(6,OUTPUT); //Tontest
  
    zustaende[0] = &zblink;
    zustaende[1] = &zping;
    zustaende[2] = &zstromanalyse;
    zustaende[3] = &zbodenanalyse;
    zustaende[4] = &zbodenfahrt;

    befehle[0] = &bgruenan;
    befehle[1] = &bgruenaus;
    befehle[2] = &bblauan;
    befehle[3] = &bblauaus;
    befehle[4] = &bfahrt;
    befehle[5] = &bvor;
    befehle[6] = &brueck;
    befehle[7] = &blinks;
    befehle[8] = &brechts;
    befehle[9] = &bstop;

    nachrichten[0] = &ngruss;
  
    gruenblau.aktivieren();
    antrieb.aktivieren();
    strom.aktivieren();
    boden.aktivieren();
    remote.aktivieren();
}

void loop() 
{
         remote.zeitschritt();
         
         if(zustand!=0)
         {
               zustand->loop();
         }
         if(befehl!=0)
         {
               //zustand = 0; //keinen Zustandsloop mehr ausführen, wenn ein direkter Befehl herainkommt
               //Nur bei Befehl $00 die Zustände ausklinken, da sonst z.B. Kontrolle des Stromsensors bei verschiedenen Motorzuständen nicht möglich!
               //SIEHE befehle.h bGruenAn

               befehl->perform();
               befehl = 0; //nur einmal ausführen!
         }
         if(nachricht!=0)
         {
               nachricht->reagieren();
               nachricht = 0;
         }
         
              //digitalWrite(6,HIGH);  //Tontest
         while(gZaehler<HALBMAXZAEHLER)
         {
              PORTB |=0;
              
         }
         gZaehler=0;
              //digitalWrite(6,LOW); //Tontest
}


Code 2.2-15: Beispielimplementierung für Befehlsklassen.

Die Schnittstelle iBefehl legt fest, dass jede Befehlsklasse eine Befehlsmethode perform() besitzt und eine Methode zur Konfiguration eines Befehls. Da Befehle auch in Form von entsprechenden Zeichen über eine serielle Schnittstelle übertragen werden können sollen, gibt es zusätzlich eine Methode getPanz(), die die Anzahl der Parameter, die dieser befehl benötigt liefert. Und so könnte ein Befehl bei der Datenübertragung repräsentiert werden:

$00
Bedeutung: $ ist das Schlüsselzeichen für einen Befehl.
Die darauffolgenden beiden Ziffern bezeichnen die laufende Nummer des gemeinten Befehls.
Das System schlägt nun nach wieviele Parameter Befehl Nummer 0 erwartet.
Ist diese Anzahl == 0, so werden auch keine weiteren Daten erwartet, um den Befehl zu vervollständigen.
Andernfalls kommen noch 1 bis 4 Byte nach.

Code 2.2-16: Befehle kodieren.

Interpreter

Mit der Behandlung des Interpreter-Entwurfsmusters wandert man nun ziemlich weit nach oben in der Hierarchie einer angenommenen AV Software-Architektur.

Das Verhalten eines Roboters oder autonomen mobilen System sollte flexibel sein:

  • Es sollten neue Verhaltensregeln erlernt werden können,
  • es sollte zwischen Verhaltensmustern gewechselt werden können (Jetzt habe ich dreimal ein anderes Fahrzeug angerempelt, jetzt werde ich ' mal ein bischen defensiver fahren),
  • die graduelle Umprogrammierung des Verhaltens sollte nicht die Übertragung der gesamten Software erfordern und eventuell auch über Funk "on the fly" möglich sein.

Über Parametrisierungen, die Kontrollstrukturen verändern ist so etwas zu realisieren wird schnell sehr unübersichtlich und auch irgendwann nicht mehr flexibel genug. Das kann sein: Die Parameter eines Regelkreises, die Schwelle für die Anzahl an Zusammenstößen innerhalb einer festgesetzten Zeitperiode, ab welcher das Tempo gedrosselt wird usw.

Für diese übergeordenten Verhaltensmuster wird eine Art Sprache benötigt. Jedoch auf dem Level der verwendeten Programmiersprache zu operieren würde ein neu Kompilieren der ganzen Software erforderlich machen.

Bei der Programmierung dynamischer Webseiten kommen häufig Skriptsprachen zum Einsatz. Diese haben den Vorteil, dass Codefragmente automatisch generiert und interpretiert werden können. Der Nachteil ist die langsamere Verarbeitung. Jedoch ist dies aber einem gewissen Hierarchielevel gar kein so großes Problem mehr: Wenn es darum geht, zu entscheiden "vorsichtiger" zu fahren, geht es um Sekunden bis Minuten. Anders als beim Regeln einer Linienverfolgung, wo es um Hundertstel Sekunden geht. Das heißt je höher man in den Hierarchieebenen aufsteigt, desto weiter ist auch der Zeithorizont für den entschieden wird und entsprechend größer sind die gültigen Gewährleistungsintervalle.

Beobachter
Zustand
Strategie
Sensorsynthese (kein klassisches Entwurfsmuster, Zusatzelement im Zusammenhang der AV-Software-Architektur)