Objekte, Funktionen und Modelle Ingenieurwissenschaften

Das Denken in Objekten und Funktionen verfolgt die Menschheit seit den ersten überlieferten Aufzeichnungen und zeigte sich meist als unvereinbarer Dualismus. Für Platon bestand alles im Kern aus Ideen und ihren materiellen Abbildern, die stets unvollständig sind. Für Demokrit bestand hingegen alles aus materiellen Elementen, die unveränderlich und unteilbar sind und aus deren spezifischer Verhakung alle Phänomene resultieren.

Im philosophischen Konzept des Dualismus geht es um unvereinbare Entitäten wie Körper und Seele oder das Erkennen von Licht mal als Teilchen oder mal als Welle – ganz allgemein also um die Unvereinbarkeit von materiell und immateriell.

Wenn man von der immateriellen Seite kommt, sieht man kein Bestehen von etwas, sondern nur eine stetige Änderung von Beziehungen und redet über Zustände, Systeme etc. Alles lässt sich folglich funktional und dynamisch denken. Alles ist im Fluss. Wenn man von der materiellen Seite kommt, sieht man Objekte und deren Beziehung. Alles lässt sich gedanklich anfassen, sortieren und einfrieren.

Die populärsten Dualismen ließen sich übrigens im letzten Jahrhunderten auflösen, in dem sich häufig einfach die Frage nach der Unvereinbarkeit erübrigte. Es entstand meist ein drittes Bewusstsein.

Aus der Seele wurde ein neuronales Nervengeflecht von Zelle, die ihre Funktionen materiell über Proteine ausführen, die wiederum per DNA codiert sind, sowie der Kommunikation zwischen den Zellen insgesamt, z.B. über Potentialentladungen an den Zell-Interfaces. In diesem Erklärungsansatz werden materielle Objekte und Funktionen in mehrere Ebenen verschachtelt. Es entsteht Komplexität.  Im Umkehrschluss bedeutet das jedoch auch, dass sich jede Komplexität in seine elementaren Bausteine und deren Beziehung zerlegen, isolieren und untersuchen lässt. Es handelt sich um das wissenschaftliche Erkenntnis Prinzip nach Descartes, das zum systematischen Denken führt.

Es entwickelte sich jedoch auch eine andere Strategie, den probabilistischen Ansatz.

Das Problem von Welle und Teilchen wurde in der Quanten-Elektrodynamik aus einer gemeinsamen Theorie heraus erklärt. Licht ist seitdem eine Wahrscheinlichkeitsamplitude eines Photons, mit der es durch Raum und Zeit (auch rückwärts) mit Elektronen und anderen Elementarteilchen interagiert.

Sowohl die System-Theorie also auch die Probabilistische Theorie (Theorie = absolute Erklärung) sind formal und intuitiv schwer zugänglich, so dass man aus ihnen heraus stets vereinfachte Konstrukte und Analogien erarbeiten muss, was als Modellbildung bezeichnet wird. Die Modellierung hat dabei im modernen physikalischen Verständnis das Ziel, konkrete Berechnungen durchzuführen, um zu quantitativen Aussagen zu gelangen, die sich experimentell überprüfen lassen, womit die Theorien dann stückweise bestätigt werden können.

Es liegt auf der Hand, dass es sich im naturwissenschaftlichen Sinne stets um mathematische Modelle handelt. Der Modellbegriff ist allerdings nicht von den Naturwissenschaftlern geschützt, so entlehnen ihn auch Geistes- und Meta-Wissenschaftler für ihre Erklärungsansätze. Als schlimmes Beispiel ist hier der Definitionsversuch von Herbert Stachowiak anzuführen, der prominent auf Wikipedia zum Thema Modell vertreten ist.

Aber das soll hier kein Thema sein. Es gibt nichts zu definieren. In der Programmierung sind Objekte, Funktionen und Modelle so gebräuchliche Dinge, dass es wenig Sinn macht, ihnen etwas Metaphysisches anzudichten. Es macht eindeutig mehr Sinn, sich anzuschauen, wie sie funktionieren und gebraucht werden.

Damit ein Modell wie ein Kästchen-Pfeilchen-Diagramm funktioniert, bedarf es vieler Programmierkniffe, um von der eigentlichen quasi seriellen Abarbeitung von einzelnen Anweisungen eines Computer zu einer Art Gleichzeitigkeit zu kommen.

Betrachten wir zunächst einmal die Funktion. Sie ist der digitale Repräsentant der mathematischen Funktion. Man übergibt eine Information an eine Funktion und die Funktion gibt eine Information zurück.

y=f(x)

Man kann eine reine Funktion (engl: pure function) als Box mit Eingängen und Ausgängen darstellen. Dann darf sie aber auch wirklich nichts anderes machen, was in eventbasierten Programmen (z.B. alle grafischen Anwendungen, die übrigens nur ca. 20% aller Programme überhaupt ausmachen) häufig nicht der Fall ist. Im Gegensatz zu einer Gleichung und analog zur Mathematik lässt sich eine Funktion nicht umstellen. Eingang und Ausgang lassen sich nicht vertauschen, es sei denn, man findet irgendwie eine inverse Funktion. Inverse Events gibt es jedoch nicht.

Da jede Berechnung mindestens einen Rechenschritt dauert, folgt die Ausgabe stets der Eingabe. Eine klassische Funktion ist damit kausal und genügt nicht dem probabilistischen Ansatz.

Eine Funktion ist häufig eine Abstraktion von Basisoperationen und Kontrollstrukturen (Kontrollstruktur: if-else Anweisungen und while- bzw. for-Schleifen), die wiederum Abstraktionen von einfachen Maschinenanweisungen sind, die wiederum Abstraktionen von Gatteranweisungen usw. sind. Eine Abstraktion ist ein Programmcode, der eine häufig benötigte Funktionalität möglich effizient verallgemeinert und durch einfache Befehle dem Nutzer auf einer höheren sprachlichen Ebene zur Verfügung stellt,  bzw. den eigentlichen Maschinenmechanismus vor dem Nutzer elegant verbirgt. Dadurch wird der Code insgesamt einfacher zu lesen und zu warten. Das Vokabular und die Möglichkeiten werden hingegen umfangreicher.

Wie im Folgenden gezeigt, kann man z.B. in Matlab eine Kontrollstruktur oder eine Vektor-Abstraktion für das gleiche Ergebnis nutzen.


M=[1,2,3,4,5];

N=[6,6,8,8,10];

% als klassische Kontrollstruktur

G = zeros(length(M),1); %initialisiert das zu befühlende Array.

for i=1:length(M) % defininiert die Wiederholung ...

G(i) = M(i) + N(i) % ... einer einfachen Addition

end

%% als Vektor-Abstraktion.

G = M + N; % Vektoraddition.

Ein weiteres Beispiel: das Herausfiltern von Primzahlen aus dem vorherigen Ergebnis


%% als Kontrollstruktur

primes=[]; % Initialisiert das Ausgabearray.

for i=1:length(M)

if isprime(G(i)) %== true % prüft jeden Eintrag ...

primes(end+1)= G(i); %... und fügt ggf. dem Array hinzu.

end

end

%% Als Abstraktion

primes = G(isprime(G))

Warum funktioniert die letztere Schreibweise? Zunächst erzeugt isprime(G) das logische Array [true, false, true, false, false] bzw. [1, 0, 1, 0, 0] equivalent zur Abarbeitung in einer Schleife. Wenn man nun in Matlab ein Werte-Array mit einem logischen Array anspricht, gibt dieser Aufruf ein Array zurück, das die entsprechenden Werte enthält, denen ein true gegenüber steht. Diese Abstraktion nennt sich logische Indizierung.

Hochlevelsprachen zeichnen sich durch eine Reihe solcher Abstraktionen aus, die bestimmte Funktionen bereits in der Syntax der Sprache ermöglichen, die man ansonsten „umständlich“ über Kontrollstrukturen schreiben müsste.

Funktionen lassen sich neben Typen zu sogenannten digitalen Bürgern erster Klasse erheben, indem man Funktionen neben einfachen Werten und Arrays selbst als Ein-und Ausgabe-Information von Funktionen zulässt. Hierbei spricht man von Funktionen höherer Ordnung und funktionaler Programmierung. Eine Funktion kann auf diese Weise eine andere Funktion modellieren.

In Matlab ist diese Sprachenfeatures nicht direkt möglich, aber auch nicht unmöglich zu realisieren. Man muss zunächst ein sogenanntes Function Handle auf eine Funktion definieren, das einem spezieller Verweis auf diese Funktion entspricht und einen eigenen Informations-Type in Matlab darstellt.

Nehmen wir einmal a als Parametervektor.


a=[1,2,3] %  -->; a(1)=1, a(2)=2, a(3)=3

Function Handles lassen sich erzeugen, in dem man die Funktion in eine separate .m Datei schreibt und sie dann per vorangestellten @ aufruft. Alternativ kann sie auch, falls dies als Einzeiler möglich ist, direkt zur Laufzeit erstellt werden.


function [y] = polyfunction(x)

  y = x + x^2 + x^3;

end

polynom = @polyfunction

% als Einzeiler

polynom = @(x) x + x^2 +x^3 %

% in beiden Fällen

// polynom(3) = (3 + 3^2 + 3^3) = 39

Die Polynomfunktion soll nun über eine andere Funktion parametrisiert, bzw modelliert werden


createPolynomFunction = @(a) (@(x) a(1)*x + a(2)*x^2 + a(3)*x^3)

polynom = createPolynomFunction([1,2,3])

% erstellt die Funktion polynom(x) = 1*x + 2*x^2 + 3*x^3, die dann ganz normal verwendet werden kann.

% // polynom(3) = 102

Eine kleine Sache muss noch korrigiert werden. Da Matlab defaultmäßig alle mathematischen Operationen als Matrizenopertionen ausführen will, sollten die Potenzen explizit als elementweise Operationen deklariert werden, was durch einen vorangestellten Punkt bewerkstelligt wird.


createPolynomFunction= @(a) (@(x) a(1)*x + a(2)*x.^2 +a(3)*x.^3)

polynom =createPolynomFunction([1,2,3])

Damit lässt sich nun die Funktion für verschiedene Werte in einer vektorisierten Deklaration ausführen.


x=linspace(-20,20,41) % erstellt einen Vektor mit 41 Werte beginnen bei -20 bis +20.

y=polynom(x)

plot(x,y)

Funktion Handles kommen bei Optimierung- und Integralalgorithmen in Matlab standardmäßig zum Einsatz, also genau da, wo Funktionen aufeinander einwirken.

Für die Berechnung des Integrals des Polynoms von -20 bis +20, oder der Nullstellensuche, angefangen bei 0 …


integral(polynom,-20,20)

fzero(polynom,0)

Das bisherige Polynom-Modell ist noch relativ statisch. Die Werte und die Anzahl der Parameter a, b und c müssen manuell eingegeben werden. Beides soll im Folgenden automatisiert werden. Es soll die Polynomreihe als Modell für ein Wertefitting verwendet werden. Es ist die Frage, wie man die Parameter ermitteln kann, ohne den Quellcode manuell zu editieren und ohne ein Script, also ein klassisches, imperatives Programm zu schreiben, das speziell diesen Fall ermittelt. Mit dem funktionalen Paradigma geht das wie folgt:

Man programmiert eine Funktion, der man die Polynomfunktion, sowie die Fittingdaten übergibt. Anschließend werden die Parameter optimiert. In einem abschließenden Schritt werden diese dann wieder benutzt, um eine passende Funktion zu erstellen, die dann das „angelernte“ Modell sein soll.


polyFun= @(a,x) a(1)*x + a(2)*x.^2 + a(3)*x.^3

afit = lsqcurvefit(polyFun,[1,1,1],xdata,ydata) %liefert die optimierten Parameter.

polyModel = createPolynomFunction(afit) % erstellt aus den optimierten Parametern wieder ein Polynom.

load simplefit_dataset.mat
xdata=simplefitInputs;
ydata=simplefitTargets;
plot(xdata,ydata,'+',xdata,polyModel(xdata),'-') % plottet beides zum Vergleich. 

Das ist zugegebenermaßen etwas tricky und bedarf dem Reindenken, zumal das eigentliche Modell auf zwei verschiedene Weisen definiert werden muss, wenn man als Ergebnis das Modell als anwendbare Funktion erhalten möchte. Die Funktionsaufrufe werden nun verschachtelt:


createPolyFun = @(a) (@(x) a(1)*x + a(2)*x.^2 +a(3)*x.^3)

polyFun  = @(a,x) a(1)*x + a(2)*x.^2 + a(3)*x.^3

polyModel = createPolyFun(lsqcurvefit(polyFun,[1,1,1],xdata,ydata))

Die Optimierungsfunktion wird mit einem Modell und Referenzdaten „gefüttert“, die die Information enthalten, wie sich x zu y verhält. Das Ergebnis wird direkt genutzt, um das Modell zu parametrisieren. Mit diesem Modell lassen sich nun auf Basis von weiteren x-Werten passende y-Werte zuweisen.

In dem gezeigten Weg ist die Topologie des Modells noch starr, es werden lediglich die Parameter optimiert. Die Topologie wird zwar indirekt durch das Vorzeichen beeinflusst, doch der wesentliche Parameter ist die Anzahl der Glieder. Wir wollen deshalb einmal die Anzahl der Polynomkoeffizienten als Parameter für ein allgemeineres Modell verwenden.


function [ fh1, fh2 ] = polyN( N )

functionString = [''];

for i=1:N

functionString = horzcat(functionString,[' +a(' , mat2str(i) ')*x.^' , mat2str(i)]); % schreibt die Polynomfunktion als Code...

end

functionString1= horzcat(['@(a) (@(x) '],functionString, ')');
functionString2= horzcat(['@(a,x) '],functionString);

fh1 = str2func(functionString1); % ... und evaluiert den Code.
fh2 = str2func(functionString2);

end

Das Modell lässt sich nun wie folgt mit der zusätzlichen Angabe der N-Parameters gewinnen.

N=5

[fh1, fh2] = polyN(N)
polyModel = fh1(lsqcurvefit(fh2,zeros(N),xdata,ydata))

Abschließend wird das Ganze noch in eine einzige Abstraktion verpackt.

function [fh] = polyModel(xdata,ydata,N,startwerte)

if nargin <3
N = 5; % Standardwert, falls nicht angegeben
end
if nargin <4
startwerte = zeros(N); % falls nicht angeben, startet die Suche mit Nullen
end

[fh1, fh2] = polyget(N);

fh = fh1(lsqcurvefit(fh2,startwerte,xdata,ydata));

function [ fh1, fh2 ] = polyget( N )

functionString = [''];

for i=1:N

functionString = horzcat(functionString,[' +a(' , mat2str(i),  ')*x.^' , mat2str(i)]);

end

functionString1= horzcat(['@(a) (@(x) '],functionString, ')');
functionString2= horzcat(['@(a,x) '],functionString);

fh1 = str2func(functionString1);
fh2 = str2func(functionString2);

end

end

Die gewünschte Funktion lässt sich nun einfach aufrufen.

fh = polyModel(xdata,ydata,8)
plot(xdata,fh(xdata),'-',xdata,ydata,'+')

Wenn Sie Code ausgeführt haben, und ein bisschen mit dem N-Wert variieren, werden Sie ein Problem mit der Modellierung feststellen. Woran könnte es liegen?

(Tipp: der Werte um x=0 wird nie getroffen, egal welches N man wählt.)

Neben der funktionalen Programmierung hat sich noch ein anderes Paradigma zur Modellierung etabliert, die objektorientierte Programmierung.

Hier erzeugt man zunächst ein Objekt, übergibt an das Objekt die Daten, ruft eine Optimierungsfunktion des Objekts auf und kann fortan das Objekt für Fittings verwenden, indem man die Methode wie eine Funktion verwendet; denn sie nichts anderes als eine Funktion, die jedoch an die passenden Datentypen und Strukturen gebunden ist, so dass man sich beim Funktionsaufruf nicht um die Ein- und Ausgaben der Zwischenschritte kümmern muss. Das Objekt lässt sich wie eine Blackbox verwenden.

Damit das funktioniert, programmiert man zunächst eine Klasse, in der beschrieben ist, wie das Objekts die Daten aufnimmt, wie diese intern gespeichert werden, was beim Fittingaufruf geschehen soll und wie das letztlich auf weitere Daten angewendet werden kann. Man kapselt folglich die Abfolge in modular aufgebaute Funktionen und passende Variablen.

classdef polyclassSimple < handle % erbt die Klasse handle

properties % Block für die objektinternen Variablen

xdata

ydata

p

end

methods % Block für die objektinternen Funktionen

function obj = polyclass(x,y) % class constructor, wenn selber Name wie die Klasse. Regelt, was beim Installieren eines Objektes geschehen soll. Speichert hier die Werte in die properties.

obj.xdata = x;

obj.ydata = y;

end

function obj = learn(obj,N) % bei Methoden ist zu beachten, dass obj als erstes mit übergeben wird, ohne dass es als Eingangsvariable beim Aufruf zählt. da die Methode das Objekt verändert wird obj auch wieder zurückgegeben.

obj.p=polyfit(obj.xdata,obj.ydata,N); %ruft die Matlabinterne polynomparameterfittingfunktion auf

end

function [y] = model(obj,x) % execute the model

y = polyval(obj.p,x); % ruft die matlabintere fitting Funktion auf

end

end

end


Der Aufruf funktioniert nun wie folgt in der typischen Punkt-Notation der Objektorientierung.


poly = polyclassSimple(xdata,ydata);

poly.learn(N=8)

y= poly.model(xdata)

Objekte spielen erst ihre volle Stärke aus, wenn sie event-gesteuert und somit dynamisch aufeinander einwirken und Informationen austauschen. Zur Demonstration wollen wir einmal die matlabinterne polyfit Funktion in ein anderes Objekt auslagern. Wir wollen das Modell-Objekt so modifizieren, dass sobald der N-Wert gesetzt wird, ein neues Berechungs-Objekt instanziert wird, die Daten für das Fitting an diese Objekt per weiterem Objekt übermittelt wird, diese Objekt die polyfit Funktion ausführt und sobald das Ergebnis vorliegt wieder als Objekt zurück übermittelt und dort passend in die Properties gespeichert.

Da man Objekte in Matlab nicht zur Laufzeit deklarieren, sondern nur instanzieren kann, müssen wir zunächst einmal zwei Klassen, aus denen die Transportobjekte erzeugt werden, als .m Dateien anlegen.


classdef (ConstructOnLoad) transferData < event.EventData

properties

x

y

N

end

methods

function obj = transferData(x,y,N)

obj.x = x;

obj.y = y;

obj.N = N;

end

end

end

und


classdef (ConstructOnLoad) retransferData < event.EventData

properties

p

 

end

methods

function obj = retransferData(p)

obj.p = p;

end

end

end

Als nächstes soll nun eine weitere Klasse erstellt werden, indem der Aufruf der polyfit Funktion erfolgt.


classdef externalWorker < handle

events

WorkerEvent

Back

end

methods

function obj = externalWorker(model)

addlistener(model,'WorkerEvent',@obj.Callback);

addlistener(obj,'Back',@model.Callback2);

end

function obj =  Callback(obj,eventSrc,eventdata)

p=polyfit(eventdata.x,eventdata.y,eventdata.N);

notify(obj, 'Back', retransferData(p));

end

end

end

Das Objekt erzeugt beim Instanzieren zwei Event-Handler für den Datenaustausch, einen in der Instanz der polyclass, einen in der Instanz des externalWorker.

Die polyclass sieht nun wie folgt aus


classdef polyclass < handle

 

properties

xdata = [];

ydata =[];

p=[];

end

properties (SetObservable)

N

end

events

WorkerEvent

Back

end

methods

function obj = polyclass(x,y) % class constructor

obj.xdata = x;

obj.ydata = y;

addlistener(obj,'N','PostSet',@obj.NchangeCallback); %Überwacht den Zugriff auf N

end

function obj = NchangeCallback(obj,src,evnt)

orgx = obj.xdata;

orgy = obj.ydata;

orgN = obj.N;

externalWorker(obj); % instanziert das Objekt, das polyfit ausführt

notify(obj, 'WorkerEvent', transferData(orgx,orgy,orgN)); Übermittelt die Daten an den Handler im externalWorker

end


function obj = Callback2(obj,src,evnt) % wird ausgeführt, wenn im externalWorker der notify Befehl erfolgt, auf den der "Back"-Listener hört.

obj.p = evnt.p;

end

function [y] = model(obj,x)

y = polyval(obj.p,x);

end

end

end

Leider unterstützt Matlab keine asynchrone Abarbeitung der Events, obwohl es die virtuelle Javamaschine im Hintergrund eigentlich könnte. Da Matlab im Allgemeinen sehr unperformant ist, fällt dies wahrscheinlich auch nicht weiter tragisch auf.

Aus dem externalWorker hätte man alternativ zu dem Rückversand über das Objekt auch direkt in die polyclass Instanz zurück schreiben können. In der Funktion „Callback“ der externalWorker Klasse würde dann einfach folgendes stehen:


eventSrc.p=polyfit(eventdata.x,eventdata.y,eventdata.N)

Wie gezeigt, lassen sich Objekte durch andere Objekte modifizieren, was durch Private Properties und statische Klassen auch wieder unterbunden werden kann. Dass Informationen und Events aber potentiell kreuz und quer  – und von wer weiß was ausgelöst –  durch das Programm „fliegen“ und dynamisch weitere Informationspfade anlegen und Funktionen ausführen können, ist mächtig und tückisch zugleich. Man kann in der Objektorientierten Programmierung also viel falsch und viel richtig machen, um guten und modularen Code zu erzeugen.

Sowohl das Funktionale, als auch das Objektorientierte Paradigma führen zu einem deklarativen Programmierstil und unterscheiden sich so von dem imperativen Stil der direkten Maschinenanweisung und Kontrollstruktur.

Die drei verschiedenen Paradigmen schließen sich nicht gegenseitig aus.

Insbesondere, wenn man bedenkt, wie schwer es sein kann, Bugs ausfindig zu machen, die durch fehlgeleitete oder zeitlich nicht richtig eintreffende Events verursacht werden können, entstehen in der Objektorientierung schnell unschöne Schwierigkeiten. Eine asynchrone Eventabarbeitung, wie performanten Compilern und Interpretern, würde das sogar noch viel schlimmer machen. Hingegen ist die Funktionale Programmierung nicht so schnell anpassbar, da man vermerkt auf die Typen aufpassen muss, die durch die Verschachtelung schnell unübersichtlich werden.

Diese Schwächen lassen sich durch ein weiteres Paradigma, die Reaktive Programmierung weitestgehend vermeiden.

Statt Variablen, werden hier Ereignis- und Daten als Stream-Objekte übergeben. Das führt zu einem rein funktionalen und damit konstanten Zusammenhang des gesamten Programms. Damit muss man sich nicht mehr um das Bug anfällige Management von Zuständen kümmern (z.B. ist der Wert schon vorhanden?), was ansonsten bei interaktiven Anwendungen mit get und set Befehlen durchgeführt werden muss und viele if-Anweisung erfordert.

Das Reaktive Paradigma ist z.B. als Abstraktion in Form der Programmbibliothek ReactivX für verschiedene Sprachen verfügbar, allerdings nicht für Matlab.

Die konsequente Abkehr von Kontrollstrukturen in einem deklarativen Programmierstil bedeutet jedoch auch, dass man sehr wenige Grundprinzipien durch sehr viele neue Abstraktionen für alle möglichen Begebenheiten eintauscht, schließlich gilt es zu klären, wie Events- und Daten-Streams zusammengeführt und bearbeitet werden können. Man muss der reaktiven Programmierung dabei zu Gute halten, dass so auch sehr ausgefeilte Algorithmen in wenigen Codezeilen, die gut lesbar sind, zu realisieren sind.

Zusammenfassend kann man sagen, das es heutzutage sehr ausgereifte Methoden der Programmierung gibt, um auf eine zwar leicht verständliche, aber auch starre Imperative Programmierung zu verzichten. Das ermöglicht eine systematische und sehr flexible Verkapselung von einzelnen Programm-Elementen, die miteinander „in Echtzeit“ kommunizieren. Der Programmcode beschreibt dann nicht mehr eine Abarbeitung von Befehlen, sondern nur noch das gewünschte System- bzw. Modellverhalten. Erst so können überhaupt umfangreiche Modelle mit mehreren 10.000 Zeilen Code vernünftig realisiert werden.

Dabei ist es erst mit pure functions (wie sie z.B. in der reaktiven Programmierung angestrebt werden) möglich, digitale Modelle als konsequente Kästchen-Pfeilchen Abbilder zu sehen.

Etwas einfacher und direkt als Kästchen-Pfeilchen Darstellung funktioniert Simulink. Hier lassen sich allerdings „nur“ numerische Datenströme modelliert. Das schauen wir uns in einem weiterem Blog an.

Objekte, Funktionen und Modelle
Bewerte den Beitrag

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.