Erstellen Aufzählungen spröde Schnittstellen?


17

Betrachten Sie das folgende Beispiel. Jede Änderung an der ColorChoice-Enumeration wirkt sich auf alle IWindowColor-Unterklassen aus.

Neigen Aufzählungen dazu, spröde Schnittstellen zu verursachen? Gibt es etwas Besseres als eine Aufzählung, um mehr polymorphe Flexibilität zu ermöglichen?

enum class ColorChoice
{
    Blue = 0,
    Red = 1
};

class IWindowColor
{
public:
    ColorChoice getColor() const=0;
    void setColor( const ColorChoice value )=0;
};

Bearbeiten: Entschuldigung für die Verwendung von Farbe als mein Beispiel, das ist nicht, was die Frage ist. Hier ist ein anderes Beispiel, das den roten Hering vermeidet und mehr Informationen darüber liefert, was ich unter Flexibilität verstehe.

enum class CharacterType
{
    orc = 0,
    elf = 1
};

class ISomethingThatNeedsToKnowWhatTypeOfCharacter
{
public:
    CharacterType getCharacterType() const;
    void setCharacterType( const CharacterType value );
};

Stellen Sie sich außerdem vor, dass Handles für die entsprechende Unterklasse ISomethingThatNeedsToKnowWhatTypeOfCharacter durch ein werkseitiges Entwurfsmuster ausgegeben werden. Jetzt habe ich eine API, die in Zukunft nicht für eine andere Anwendung erweitert werden kann, bei der die zulässigen Zeichentypen {Mensch, Zwerg} sind.

Bearbeiten: Nur um konkreter zu sein, woran ich arbeite. Ich entwerfe eine starke Bindung dieser ( MusicXML- ) Spezifikation und verwende Enum-Klassen, um die Typen in der Spezifikation darzustellen, die mit xs: enumeration deklariert sind. Ich versuche darüber nachzudenken, was passiert, wenn die nächste Version (4.0) herauskommt. Könnte meine Klassenbibliothek in einem 3.0-Modus und in einem 4.0-Modus funktionieren? Wenn die nächste Version zu 100% abwärtskompatibel ist, dann vielleicht. Aber wenn Aufzählungswerte aus der Spezifikation entfernt wurden, bin ich tot im Wasser.


2
Welche Fähigkeiten haben Sie genau im Sinn, wenn Sie "polymorphe Flexibilität" sagen?
Ixrec


3
Die Verwendung einer Aufzählung für Farben erzeugt spröde Schnittstellen, nicht nur die Verwendung einer Aufzählung.
Doc Brown

3
Das Hinzufügen einer neuen Aufzählungsvariante unterbricht den Code mit dieser Aufzählung. Das Hinzufügen einer neuen Operation zu einer Enumeration ist dagegen ziemlich eigenständig, da alle Fälle, die behandelt werden müssen, genau dort sind (im Gegensatz zu Interfaces und Superklassen, bei denen das Hinzufügen einer nicht standardmäßigen Methode eine schwerwiegende Änderung darstellt). Es hängt von der Art der Änderungen ab, die wirklich notwendig sind.

1
Zu MusicXML: Wenn es nicht einfach ist, anhand der XML-Dateien zu erkennen, welche Version des Schemas von den einzelnen verwendet wird, ist dies für mich ein kritischer Konstruktionsfehler in der Spezifikation. Wenn Sie es irgendwie umgehen müssen, gibt es wahrscheinlich keine Möglichkeit für uns zu helfen, bis wir genau wissen, was sie wählen, um 4.0 zu brechen, und Sie können nach einem bestimmten Problem fragen, das es verursacht.
Ixrec

Antworten:


25

Bei richtiger Verwendung sind Aufzählungen weitaus lesbarer und robuster als die "magischen Zahlen", die sie ersetzen. Ich sehe normalerweise nicht, dass sie Code spröder machen. Zum Beispiel:

  • setColor () muss keine Zeit verschwenden, um zu überprüfen, ob valueein gültiger Farbwert vorliegt oder nicht. Der Compiler hat das schon gemacht.
  • Sie können setColor (Color :: Red) anstelle von setColor (0) schreiben. Ich glaube, mit der enum classFunktion in modernem C ++ können Sie sogar Leute zwingen, immer das erstere anstelle des letzteren zu schreiben.
  • Normalerweise nicht wichtig, aber die meisten Aufzählungen können mit einem ganzzahligen Typ jeder Größe implementiert werden, sodass der Compiler auswählen kann, welche Größe am bequemsten ist, ohne dass Sie über solche Dinge nachdenken müssen.

Die Verwendung einer Aufzählung für Farben ist jedoch fraglich, da es in vielen (den meisten?) Situationen keinen Grund gibt, den Benutzer auf einen so kleinen Satz von Farben zu beschränken. Sie können sie auch in beliebigen RGB-Werten übergeben. Bei den Projekten, mit denen ich arbeite, taucht eine kleine Liste solcher Farben immer nur als Teil einer Reihe von "Themen" oder "Stilen" auf, die als dünne Abstraktion über konkrete Farben fungieren sollen.

Ich bin mir nicht sicher, worauf sich Ihre Frage nach der "polymorphen Flexibilität" bezieht. Enums haben keinen ausführbaren Code, daher gibt es nichts, was man polymorph machen könnte. Vielleicht suchen Sie nach dem Befehlsmuster ?

Bearbeiten: Nach der Bearbeitung ist mir immer noch nicht klar, nach welcher Art von Erweiterbarkeit Sie suchen, aber ich denke immer noch, dass das Befehlsmuster dem "polymorphen Enum" am nächsten kommt.


Wo kann ich mehr darüber erfahren, wie ich verhindern kann, dass 0 als Enumeration übergeben wird?
TankorSmash

5
@TankorSmash C ++ 11 führte die "enum class" ein, auch "scoped enums" genannt, die nicht implizit in ihren zugrunde liegenden numerischen Typ konvertiert werden können. Sie vermeiden auch, den Namespace zu verschmutzen, wie dies bei den alten C-artigen Aufzählungstypen der Fall war.
Matthew James Briggs

2
Aufzählungen werden normalerweise durch ganze Zahlen unterstützt. Es gibt viele seltsame Dinge, die bei der Serialisierung / Deserialisierung oder beim Casting zwischen ganzen Zahlen und Aufzählungen passieren können. Es ist nicht immer sicher anzunehmen, dass eine Aufzählung immer einen gültigen Wert hat.
Eric

1
Du hast recht, Eric (und das ist ein Problem, auf das ich selbst schon einige Male gestoßen bin). Sie müssen sich jedoch nur bei der Deserialisierung Gedanken über einen möglicherweise ungültigen Wert machen. In allen anderen Fällen, in denen Sie Aufzählungen verwenden, können Sie davon ausgehen, dass der Wert gültig ist (zumindest für Aufzählungen in Sprachen wie Scala - einige Sprachen verfügen nicht über eine sehr starke Typüberprüfung für Aufzählungen).
Kat

1
@Ixrec "weil es in vielen (den meisten?) Situationen keinen Grund gibt, den Benutzer auf einen so kleinen Satz von Farben zu beschränken" Es gibt legitime Fälle. Die .NET - Konsole emuliert die im alten Stil Fenster Konsole, die nur Text in einem von 16 Farben (16 Farben des CGA - Standard) hat msdn.microsoft.com/en-us/library/...
Pharap

15

Jede Änderung an der ColorChoice-Enumeration wirkt sich auf alle IWindowColor-Unterklassen aus.

Nein, tut es nicht. Es gibt zwei Fälle: Implementierer werden entweder

  • Speichern, Zurückgeben und Weiterleiten von Enum-Werten, die niemals bearbeitet werden. In diesem Fall bleiben sie von Änderungen der Enum-Werte unberührt

  • arbeite mit individuellen enum - Werten, wobei jede Änderung in der ENUM muß natürlich natürlich unausweichlich, notwendigerweise , für mit einer entsprechenden Änderung in der Logik der Implementierer berücksichtigt werden.

Wenn Sie "Sphere", "Rectangle" und "Pyramid" in eine "Shape" -Aufzählung einfügen und eine solche Aufzählung an eine drawSolid()Funktion übergeben, die Sie zum Zeichnen des entsprechenden Volumenkörpers geschrieben haben, und dann eines Morgens entscheiden, eine " "Ikositetrahedron", können Sie nicht erwarten, dass die drawSolid()Funktion davon unberührt bleibt. Wenn Sie damit gerechnet haben, dass es irgendwie icositetrahedrons zeichnet, ohne dass Sie zuerst den eigentlichen Code schreiben müssen, um icositetrahedrons zu zeichnen, ist dies Ihre Schuld, nicht die Schuld der Aufzählung. So:

Neigen Aufzählungen dazu, spröde Schnittstellen zu verursachen?

Nein, das tun sie nicht. Was spröde Schnittstellen verursacht, ist, dass Programmierer sich als Ninjas betrachten und versuchen, ihren Code zu kompilieren, ohne dass ausreichende Warnungen aktiviert sind. Der Compiler warnt sie dann nicht, dass ihre drawSolid()Funktion eine switchAnweisung enthält , in der eine caseKlausel für den neu hinzugefügten Enum-Wert "Ikositetrahedron" fehlt .

Die Art und Weise, wie es funktionieren soll, ist analog zum Hinzufügen einer neuen rein virtuellen Methode zu einer Basisklasse: Sie müssen diese Methode dann auf jedem einzelnen Erben implementieren, oder das Projekt wird und sollte nicht erstellt.


Um ehrlich zu sein, sind Aufzählungen kein objektorientiertes Konstrukt. Sie sind eher ein pragmatischer Kompromiss zwischen dem objektorientierten Paradigma und dem strukturierten Programmierparadigma.

Die reine objektorientierte Art, Dinge zu tun, besteht darin, überhaupt keine Aufzählungen zu haben und stattdessen Objekte zu haben.

Die rein objektorientierte Art, das Beispiel mit den Festkörpern zu implementieren, ist natürlich die Verwendung des Polymorphismus: Statt einer einzigen monströsen zentralisierten Methode, die alles zu zeichnen weiß und zu sagen hat, welcher Festkörper zu zeichnen ist, deklarieren Sie ein " Solide "Klasse mit einer abstrakten (rein virtuellen) draw()Methode, und dann fügen Sie die Unterklassen" Sphere "," Rectangle "und" Pyramid "hinzu, von denen jede eine eigene Implementierung hat, draw()die weiß, wie sie sich selbst zeichnet.

Auf diese Weise müssen Sie, wenn Sie die Unterklasse "Ikositetrahedron" einführen, nur eine draw()Funktion dafür bereitstellen , und der Compiler wird Sie daran erinnern, dies zu tun, indem Sie sonst nicht "Icositetrahedron" instanziieren lassen.


Tipps zum Auslösen einer Kompilierungszeitwarnung für diese Schalter? Normalerweise werfe ich nur Laufzeitausnahmen. aber die kompilierzeit könnte großartig sein! Nicht ganz so toll .. aber Unit-Tests kommen in den Sinn.
Vaughan Hilts

1
Es ist schon eine Weile her, dass ich C ++ zum letzten Mal verwendet habe, daher bin ich mir nicht sicher, aber ich würde erwarten, dass die Angabe des Parameters -Wall für den Compiler und das Weglassen der default:Klausel dies tun sollte. Eine schnelle Suche nach dem Thema ergab keine genaueren Ergebnisse, so dass dies als Thema einer anderen programmers SEFrage geeignet sein könnte .
Mike Nakis

In C ++ kann es eine Überprüfung der Kompilierungszeit geben, wenn Sie diese aktivieren. In C # müsste jede Überprüfung zur Laufzeit erfolgen. Jedes Mal, wenn ich eine Nicht-Flag-Aufzählung als Parameter nehme, stelle ich sicher, dass sie mit Enum.IsDefined validiert wird. Das bedeutet jedoch weiterhin, dass Sie alle Verwendungen der Aufzählung manuell aktualisieren müssen, wenn Sie einen neuen Wert hinzufügen. Siehe: stackoverflow.com/questions/12531132/…
Mike unterstützt Monica am

1
Ich würde hoffen, dass ein objektorientierter Programmierer niemals sein Datenübertragungsobjekt machen würde, wie man es Pyramideigentlich von draw()einer Pyramide kennt . Bestenfalls könnte es von Solideiner GetTriangles()Methode stammen und sie haben , und Sie könnten sie an einen SolidDrawerDienst übergeben. Ich dachte, wir würden uns von den Beispielen für physische Objekte als Beispiele für Objekte in OOP entfernen.
Scott Whitlock

1
Es ist in Ordnung, es hat einfach niemand gemocht, der eine gute alte objektorientierte Programmierung auf die Probe gestellt hat. :) Zumal die meisten von uns funktionale Programmierung und OOP mehr als nur noch OOP kombinieren.
Scott Whitlock

14

Aufzählungen erzeugen keine spröden Schnittstellen. Missbrauch von Aufzählungen tut.

Wofür sind Aufzählungen?

Aufzählungen sind so konzipiert, dass sie als Mengen von Konstanten mit aussagekräftigem Namen verwendet werden. Sie sind zu verwenden, wenn:

  • Sie wissen, dass keine Werte entfernt werden.
  • (Und) Sie wissen, dass es sehr unwahrscheinlich ist, dass ein neuer Wert benötigt wird.
  • (Oder) Sie akzeptieren, dass ein neuer Wert benötigt wird, aber selten genug, um den gesamten Code zu reparieren, der dadurch beschädigt wird.

Gute Verwendungen von Aufzählungen:

  • Wochentage: (gemäß .Net's System.DayOfWeek) Sofern Sie nicht mit einem unglaublich undurchsichtigen Kalender zu tun haben, wird es immer nur 7 Wochentage geben.
  • Nicht erweiterbare Farben: (wie in .Net angegeben System.ConsoleColor) Einige stimmen dem möglicherweise nicht zu, aber .Net hat sich aus einem bestimmten Grund dafür entschieden. Im Konsolensystem von .Net stehen der Konsole nur 16 Farben zur Verfügung. Diese 16 Farben entsprechen der älteren Farbpalette, die als CGA oder "Color Graphics Adapter" bezeichnet wird . Es werden nie neue Werte hinzugefügt, was bedeutet, dass dies tatsächlich eine sinnvolle Anwendung einer Aufzählung ist.
  • Aufzählungen, die feste Zustände darstellen: (gemäß Java Thread.State) Die Entwickler von Java haben entschieden, dass es im Java-Threading-Modell immer nur einen festen Satz von Zuständen geben wird, in denen sich a befinden Threadkann. Um die Sache zu vereinfachen, werden diese verschiedenen Zustände als Aufzählungen dargestellt . Dies bedeutet, dass viele zustandsbasierte Prüfungen einfache ifs und Schalter sind, die in der Praxis mit ganzzahligen Werten arbeiten, ohne dass sich der Programmierer um die tatsächlichen Werte kümmern muss.
  • Bitflags, die sich nicht gegenseitig ausschließende Optionen darstellen: (gemäß .Net's System.Text.RegularExpressions.RegexOptions) Bitflags sind eine sehr häufige Verwendung von Enums. In der Tat so verbreitet, dass in .Net alle Enums eine integrierte HasFlag(Enum flag)Methode haben. Sie unterstützen auch die bitweisen Operatoren und es gibt ein FlagsAttribute, um eine Enumeration als einen Satz von Bitflags zu kennzeichnen. Wenn Sie eine Aufzählung als Satz von Flags verwenden, können Sie eine Gruppe von Booleschen Werten in einem einzelnen Wert darstellen und die Flags der Einfachheit halber klar benennen. Dies wäre äußerst vorteilhaft für die Darstellung der Flags eines Statusregisters in einem Emulator oder für die Darstellung der Berechtigungen einer Datei (Lesen, Schreiben, Ausführen) oder in nahezu jeder Situation, in der sich eine Reihe verwandter Optionen nicht gegenseitig ausschließen.

Schlechte Verwendung von Aufzählungen:

  • Charakterklassen / -typen in einem Spiel: Sofern es sich bei dem Spiel nicht um eine einmalige Demo handelt, die Sie wahrscheinlich nicht mehr verwenden werden, sollten für Charakterklassen keine Aufzählungen verwendet werden, da Sie wahrscheinlich viel mehr Klassen hinzufügen möchten. Es ist besser, eine einzige Klasse zu haben, die einen Charakter repräsentiert, und einen im Spiel befindlichen Charaktertyp, der anders dargestellt wird. Eine Möglichkeit, dies zu handhaben, ist das TypeObject- Muster. Andere Lösungen umfassen das Registrieren von Zeichentypen bei einer Wörterbuch- / Typregistrierung oder einer Mischung aus beiden.
  • Erweiterbare Farben: Wenn Sie eine Aufzählung für Farben verwenden, die später hinzugefügt werden können, ist es keine gute Idee, diese in Aufzählungsform darzustellen, da Sie sonst für immer Farben hinzufügen müssen. Dies ähnelt dem obigen Problem, daher sollte eine ähnliche Lösung verwendet werden (dh eine Variation von TypeObject).
  • Erweiterbarer Zustand: Wenn Sie eine Zustandsmaschine haben, die möglicherweise wesentlich mehr Zustände einführt, ist es keine gute Idee, diese Zustände durch Aufzählungen darzustellen. Die bevorzugte Methode besteht darin, eine Schnittstelle für den Maschinenzustand zu definieren, eine Implementierung oder Klasse bereitzustellen, die die Schnittstelle umschließt und ihre Methodenaufrufe (ähnlich dem Strategiemuster ) delegiert , und dann ihren Zustand zu ändern, indem geändert wird, welche Implementierung derzeit aktiv ist.
  • Bitflags, die sich gegenseitig ausschließende Optionen darstellen: Wenn Sie zur Darstellung von Flags Enums verwenden und zwei dieser Flags niemals zusammen auftreten sollten, haben Sie sich selbst in den Fuß geschossen. Alles, was so programmiert ist, dass es auf eines der Flags reagiert, reagiert plötzlich auf dasjenige Flag, für das es als erstes programmiert ist - oder schlimmer noch, es reagiert möglicherweise auf beide. Diese Art von Situation bittet nur um Ärger. Der beste Ansatz besteht darin, das Fehlen einer Flagge als alternative Bedingung zu behandeln, wenn dies möglich ist (dh das Fehlen der TrueFlagge impliziert dies False). Dieses Verhalten kann durch die Verwendung spezieller Funktionen (dh IsTrue(flags)und IsFalse(flags)) weiter unterstützt werden .

Ich werde Beispiele für Bitflags hinzufügen, wenn jemand ein funktionierendes oder bekanntes Beispiel für Enums findet, die als Bitflags verwendet werden. Mir ist bekannt, dass es sie gibt, aber ich kann mich derzeit leider an keine erinnern.
Pharap


@BLSully Exzellentes Beispiel. Ich kann nicht sagen, dass ich sie jemals benutzt habe, aber sie existieren aus einem bestimmten Grund.
Pharap

4

Aufzählungen sind eine große Verbesserung gegenüber magischen Identifikationsnummern für geschlossene Mengen von Werten, denen nicht viele Funktionen zugeordnet sind. Normalerweise ist es Ihnen egal, welche Nummer tatsächlich mit der Aufzählung verknüpft ist. In diesem Fall ist es einfach, durch Hinzufügen neuer Einträge am Ende zu erweitern, es sollte keine Sprödigkeit auftreten.

Das Problem liegt vor, wenn Sie über umfangreiche Funktionen verfügen, die mit der Aufzählung verknüpft sind. Das heißt, Sie haben diese Art von Code herumliegen:

switch (my_enum) {
case orc: growl(); break;
case elf: sing(); break;
...
}

Ein oder zwei davon sind in Ordnung, aber sobald Sie diese Art von aussagekräftiger Aufzählung haben, switchneigen diese Aussagen dazu, sich wie Tribbles zu vermehren. Jetzt müssen Sie jedes Mal, wenn Sie die Aufzählung erweitern, alle dazugehörigen switchEinträge suchen, um sicherzustellen, dass Sie alles abgedeckt haben. Das ist spröde. Quellen wie Clean Code schlagen vor, dass Sie switchhöchstens eine pro Enumeration haben sollten .

In diesem Fall sollten Sie stattdessen die OO-Prinzipien verwenden und eine Schnittstelle für diesen Typ erstellen . Möglicherweise behalten Sie die Aufzählung für die Kommunikation dieses Typs bei, aber sobald Sie etwas damit tun müssen, erstellen Sie ein mit der Aufzählung verknüpftes Objekt, möglicherweise unter Verwendung einer Factory. Dies ist viel einfacher zu warten, da Sie nur einen Ort für die Aktualisierung suchen müssen: Ihre Factory, und indem Sie eine neue Klasse hinzufügen.


1

Wenn Sie vorsichtig sind, wie Sie sie verwenden, würde ich Enums nicht als schädlich betrachten. Es gibt jedoch ein paar Dinge zu beachten, wenn Sie sie in einem Bibliothekscode verwenden möchten, im Gegensatz zu einer einzelnen Anwendung.

  1. Niemals Werte entfernen oder neu anordnen. Wenn Ihre Liste irgendwann einen Aufzählungswert enthält, sollte dieser Wert für alle Ewigkeit mit diesem Namen verknüpft sein. Wenn Sie möchten, können Sie Werte zu einem bestimmten Zeitpunkt umbenennen. Wenn Sie sie deprecated_orcjedoch nicht entfernen, können Sie die Kompatibilität mit altem Code, der mit den alten Aufzählungen kompiliert wurde, einfacher aufrechterhalten. Wenn ein neuer Code nicht mit alten Enum-Konstanten umgehen kann, generieren Sie entweder dort einen geeigneten Fehler oder stellen Sie sicher, dass kein solcher Wert diesen Codeteil erreicht.

  2. Rechne nicht mit ihnen. Führen Sie insbesondere keine Bestellvergleiche durch. Warum? Denn dann könnten Sie in eine Situation geraten, in der Sie vorhandene Werte nicht beibehalten und gleichzeitig eine vernünftige Reihenfolge beibehalten können. Nehmen Sie zum Beispiel eine Aufzählung für Kompassrichtungen: N = 0, NE = 1, E = 2, SE = 3,… Wenn Sie nach einer Aktualisierung NNE usw. zwischen den vorhandenen Richtungen einfügen, können Sie diese entweder am Ende hinzufügen der Liste und damit die Reihenfolge zu brechen, oder Sie verschachteln sie mit vorhandenen Schlüsseln und brechen damit etablierte Zuordnungen im Legacy-Code. Oder Sie verwerfen alle alten Schlüssel und haben einen komplett neuen Schlüsselsatz zusammen mit einem Kompatibilitätscode, der aus Gründen des Legacy-Codes zwischen alten und neuen Schlüsseln übersetzt.

  3. Wählen Sie eine ausreichende Größe. Standardmäßig verwendet der Compiler die kleinste Ganzzahl, die alle Aufzählungswerte enthalten kann. Das bedeutet, dass Sie plötzlich 2 Byte für jeden Aufzählungswert anstelle von einem benötigen, wenn Ihr Satz möglicher Aufzählungen bei einem Update von 254 auf 259 erweitert wird. Dies könnte die Struktur- und Klassenlayouts überall zerstören. Versuchen Sie daher, dies zu vermeiden, indem Sie im ersten Entwurf eine ausreichende Größe verwenden. C ++ 11 gibt Ihnen hier viel Kontrolle, aber ansonsten sollte die Angabe eines Eintrags ebenfalls LAST_SPECIES_VALUE=65535hilfreich sein.

  4. Habe eine zentrale Registry. Da Sie die Zuordnung zwischen Namen und Werten korrigieren möchten, ist es schlecht, wenn Sie zulassen, dass Drittbenutzer Ihres Codes neue Konstanten hinzufügen. Kein Projekt, das Ihren Code verwendet, sollte berechtigt sein, diesen Header zu ändern, um neue Zuordnungen hinzuzufügen. Stattdessen sollten sie dich nerven, um sie hinzuzufügen. Dies bedeutet, dass für das von Ihnen erwähnte Beispiel von Mensch und Zwerg Aufzählungen in der Tat schlecht passen. Dort ist es besser, zur Laufzeit eine Art Registrierung zu haben, in der Benutzer Ihres Codes eine Zeichenfolge einfügen und eine eindeutige Zahl zurückerhalten können, die in einen undurchsichtigen Typ eingepackt ist. Die "Zahl" könnte sogar ein Zeiger auf die betreffende Zeichenfolge sein, es spielt keine Rolle.

Trotz der Tatsache, dass mein letzter Punkt oben Aufzählungen für Ihre hypothetische Situation schlecht geeignet macht, scheint Ihre tatsächliche Situation, in der sich einige Spezifikationen ändern könnten und Sie Code aktualisieren müssten, recht gut zu einer zentralen Registrierung für Ihre Bibliothek zu passen. Da sollten also Aufzählungen angebracht sein, wenn Sie sich meine anderen Vorschläge zu Herzen nehmen.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.