Erfolg: / Misserfolg: Blöcke vs. Abschluss: Block


23

In Objective-C sehe ich zwei gebräuchliche Muster für Blöcke. Eines ist ein Paar von Erfolg: / Misserfolg: Blöcke, das andere ist eine einzelne Vervollständigung: Block.

Angenommen, ich habe eine Aufgabe, die ein Objekt asynchron zurückgibt, und diese Aufgabe schlägt möglicherweise fehl. Das erste Muster ist -taskWithSuccess:(void (^)(id object))success failure:(void (^)(NSError *error))failure. Das zweite Muster ist -taskWithCompletion:(void (^)(id object, NSError *error))completion.

Erfolg: / Misserfolg:

[target taskWithSuccess:^(id object) {
    // W00t! I've got my object
} failure:^(NSError *error) {
    // Oh noes! report the failure.
}];

Fertigstellung:

[target taskWithCompletion:^(id object, NSError *error) {
    if (object) {
        // W00t! I've got my object
    } else {
        // Oh noes! report the failure.
    }
}];

Welches ist das bevorzugte Muster? Was sind die Stärken und Schwächen? Wann würden Sie eins übereinander verwenden?


Ich bin mir ziemlich sicher, dass Objective-C Ausnahmebehandlung mit throw / catch hat. Gibt es einen Grund, warum Sie das nicht verwenden können?
FrustratedWithFormsDesigner

In beiden Fällen können Sie asynchrone Aufrufe verketten, die Sie ausnahmsweise nicht erhalten.
Frank Shearar

5
@FrustratedWithFormsDesigner: stackoverflow.com/a/3678556/2289 - idiomatisches Objekt verwendet try / catch nicht zur Flusssteuerung.
Ameise

1
Bitte ziehen Sie in Betracht, Ihre Antwort von einer Frage zu einer Antwort zu verschieben. Schließlich handelt es sich um eine Antwort (und Sie können Ihre eigenen Fragen beantworten).

1
Endlich gab ich dem Gruppenzwang nach und bewegte meine Antwort auf eine tatsächliche Antwort.
Jeffery Thomas

Antworten:


8

Der Abschluss-Rückruf (im Gegensatz zum Erfolg / Misserfolg-Paar) ist allgemeiner. Wenn Sie einen Kontext vorbereiten müssen, bevor Sie sich mit dem Rückgabestatus befassen, können Sie dies direkt vor der "if (object)" - Klausel tun. Bei Erfolg / Misserfolg müssen Sie diesen Code duplizieren. Dies hängt natürlich von der Callback-Semantik ab.


Kann die ursprüngliche Frage nicht kommentieren ... Ausnahmen gelten nicht für die Flusskontrolle in Ziel-c (gut, Kakao) und sollten nicht als solche verwendet werden. Ausgelöste Ausnahmen sollten nur abgefangen werden, um ordnungsgemäß zu beenden.

Ja ich kann das sehen. Wenn -task…das Objekt zurückgegeben werden könnte, sich das Objekt jedoch nicht im richtigen Zustand befindet, müsste in der Erfolgsbedingung dennoch eine Fehlerbehandlung durchgeführt werden.
Jeffery Thomas

Ja, und wenn der Block nicht vorhanden ist, sondern als Argument an Ihren Controller übergeben wird, müssen Sie zwei Blöcke herumwerfen. Dies kann langweilig sein, wenn ein Rückruf durch mehrere Ebenen geleitet werden muss. Sie können es jedoch jederzeit wieder aufteilen / zusammenstellen.

Ich verstehe nicht, wie der Completion Handler allgemeiner ist. Durch die Fertigstellung werden im Grunde genommen mehrere Methodenparameter zu einem - in Form von Blockparametern. Auch bedeutet Generika besser? In MVC haben Sie oftmals auch doppelten Code im View-Controller. Dies ist ein notwendiges Übel aufgrund der Trennung von Bedenken. Ich denke nicht, dass dies ein Grund ist, MVC zu meiden.
Boon

@Boon Ein Grund, warum ich sehe, dass der einzelne Handler allgemeiner ist, ist in Fällen, in denen Sie es vorziehen, wenn der Angerufene / Handler / Block selbst feststellt, ob eine Operation erfolgreich war oder fehlgeschlagen ist. Betrachten Sie Teilerfolgsfälle, in denen Sie möglicherweise ein Objekt mit Teildaten haben und Ihr Fehlerobjekt ein Fehler ist, der darauf hinweist, dass nicht alle Daten zurückgegeben wurden. Der Block könnte die Daten selbst untersuchen und prüfen, ob sie ausreichen. Dies ist im binären Success / Fail-Callback-Szenario nicht möglich.
Travis

8

Ich würde sagen, ob die API einen Completion-Handler oder ein Paar Erfolgs- / Fehlerblöcke bereitstellt , ist in erster Linie eine Frage der persönlichen Präferenz.

Beide Ansätze haben Vor- und Nachteile, obwohl es nur geringfügige Unterschiede gibt.

Bedenken Sie, dass es auch weitere Varianten gibt, zum Beispiel, bei denen der eine Completion-Handler möglicherweise nur einen Parameter hat , der das mögliche Ergebnis oder einen möglichen Fehler kombiniert :

typedef void (^completion_t)(id result);

- (void) taskWithCompletion:(completion_t)completionHandler;

[self taskWithCompletion:^(id result){
    if ([result isKindOfError:[NSError class]) {
        NSLog(@"Error: %@", result);
    }
    else {
        ...
    }
}]; 

Der Zweck dieser Signatur besteht darin, dass ein Completion-Handler generisch in anderen APIs verwendet werden kann.

Zum Beispiel gibt es in Category for NSArray eine Methode, forEachApplyTask:completion:die nacheinander eine Aufgabe für jedes Objekt aufruft und die Schleife unterbricht, wenn ein Fehler aufgetreten ist . Da diese Methode selbst ebenfalls asynchron ist, verfügt sie auch über einen Completion-Handler:

typedef void (^completion_t)(id result);
typedef void (^task_t)(id input, completion_t);
- (void) forEachApplyTask:(task_t)task completion:(completion_t);

In der Tat ist completion_twie oben definiert generisch genug und ausreichend, um alle Szenarien zu behandeln.

Es gibt jedoch andere Mittel, mit denen eine asynchrone Task ihre Abschlussbenachrichtigung an die Aufrufstelle signalisiert:

Versprechen

Versprechen, auch „Futures“, „Latente“ oder „Delayed“ stellen die genannten eventuellen Ergebnis einer asynchronen Aufgabe (siehe auch: Wiki Futures und Versprechen ).

Zu Beginn befindet sich ein Versprechen im Status "Ausstehend". Das heißt, sein "Wert" ist noch nicht bewertet und noch nicht verfügbar.

In Objective-C wäre ein Promise ein gewöhnliches Objekt, das von einer asynchronen Methode wie folgt zurückgegeben wird:

- (Promise*) doSomethingAsync;

! Der Ausgangszustand eines Versprechens ist "ausstehend".

In der Zwischenzeit beginnen die asynchronen Tasks mit der Auswertung ihres Ergebnisses.

Beachten Sie auch, dass es keinen Completion-Handler gibt. Stattdessen wird das Versprechen ein leistungsfähigeres Mittel bieten, mit dem die anrufende Site das spätere Ergebnis der asynchronen Aufgabe erhalten kann, die wir bald sehen werden.

Die asynchrone Aufgabe, die das Versprechungsobjekt erstellt hat, MUSS schließlich ihr Versprechen „auflösen“. Das heißt, da eine Aufgabe entweder erfolgreich sein oder fehlschlagen kann, MUSS sie entweder ein Versprechen erfüllen, das das bewertete Ergebnis enthält, oder das Versprechen ablehnen, das ein Fehler ist, der den Grund für den Fehler angibt.

! Eine Aufgabe muss schließlich ihr Versprechen lösen.

Wenn ein Versprechen aufgelöst wurde, kann es seinen Status, einschließlich seines Werts, nicht mehr ändern.

! Ein Versprechen kann nur einmal gelöst werden .

Sobald ein Versprechen gelöst wurde, kann eine Call-Site das Ergebnis erhalten (ob es fehlgeschlagen oder erfolgreich war). Wie dies erreicht wird, hängt davon ab, ob das Versprechen im synchronen oder im asynchronen Stil implementiert wird.

A versprechen kann in einem synchronen oder einem asynchronen Art was zu entweder implementiert wird , blockieren bzw. nicht blockierenden Semantik.

Um den Wert des Versprechens abzurufen, würde eine Call-Site in einem synchronen Stil eine Methode verwenden, die den aktuellen Thread blockiert, bis das Versprechen durch die asynchrone Task aufgelöst wurde und das endgültige Ergebnis verfügbar ist.

In einem asynchronen Stil würde die Call-Site Callbacks oder Handler-Blöcke registrieren, die unmittelbar nach dem Erledigen des Versprechens aufgerufen werden.

Es stellte sich heraus, dass der synchrone Stil eine Reihe von erheblichen Nachteilen aufweist, die die Vorzüge asynchroner Aufgaben effektiv zunichte machen. Ein interessanter Artikel über die derzeit fehlerhafte Implementierung von „Futures“ in der Standard-C ++ 11-Bibliothek ist hier zu lesen: Broken Promises - C ++ 0x-Futures .

Wie würde eine Call-Site in Objective-C das Ergebnis erzielen?

Nun, es ist wahrscheinlich am besten, ein paar Beispiele zu zeigen. Es gibt einige Bibliotheken, die ein Versprechen implementieren (siehe Links unten).

Für die nächsten Codefragmente verwende ich jedoch eine bestimmte Implementierung einer Promise-Bibliothek, die auf GitHub RXPromise verfügbar ist . Ich bin der Autor von RXPromise.

Die anderen Implementierungen verfügen möglicherweise über eine ähnliche API, es kann jedoch kleine und möglicherweise geringfügige Unterschiede in der Syntax geben. RXPromise ist eine Objective-C-Version der Promise / A + -Spezifikation, die einen offenen Standard für die robuste und interoperable Implementierung von Versprechungen in JavaScript definiert.

Alle unten aufgeführten Versprechungsbibliotheken implementieren den asynchronen Stil.

Es gibt erhebliche Unterschiede zwischen den verschiedenen Implementierungen. RXPromise verwendet intern die Versandbibliothek, ist vollständig threadsicher, extrem leicht und bietet eine Reihe zusätzlicher nützlicher Funktionen, wie zum Beispiel die Stornierung.

Eine Call-Site erhält das endgültige Ergebnis der asynchronen Aufgabe durch "Registrieren" von Handlern. Die „Promise / A + -Spezifikation“ definiert die Methode then.

Die Methode then

Mit RXPromise sieht es folgendermaßen aus:

promise.then(successHandler, errorHandler);

wobei successHandler ein Block ist, der aufgerufen wird, wenn die Zusage "erfüllt" wurde, und errorHandler ein Block ist, der aufgerufen wird, wenn die Zusage "abgelehnt" wurde.

! thenwird verwendet, um das endgültige Ergebnis zu erhalten und einen Erfolgs- oder Fehlerhandler zu definieren.

In RXPromise haben die Handlerblöcke die folgende Signatur:

typedef id (^success_handler_t)(id result);
typedef id (^error_handler_t)(NSError* error);

Der success_handler hat einen Parameter Ergebnis , die offensichtlich das schließliche Ergebnis der asynchronen Aufgabe. Ebenso weist der error_handler einen Parameterfehler auf , der der Fehler ist, der von der asynchronen Task gemeldet wurde, als dieser fehlgeschlagen ist.

Beide Blöcke haben einen Rückgabewert. Worum es bei diesem Rückgabewert geht, wird bald klar.

In RXPromise thenist dies eine Eigenschaft, die einen Block zurückgibt. Dieser Block hat zwei Parameter, den Success-Handler-Block und den Error-Handler-Block. Die Handler müssen von der Call-Site definiert werden.

! Die Handler müssen von der Call-Site definiert werden.

Der Ausdruck promise.then(success_handler, error_handler);ist also eine Kurzform von

then_block_t block promise.then;
block(success_handler, error_handler);

Wir können noch präziseren Code schreiben:

doSomethingAsync
.then(^id(id result){
    
    return @“OK”;
}, nil);

Der Code lautet: "DoSomethingAsync ausführen , wenn es erfolgreich ist, dann Erfolgshandler ausführen".

Hier ist der Fehlerbehandler, nilwas bedeutet, dass er im Fehlerfall in diesem Versprechen nicht behandelt wird.

Eine weitere wichtige Tatsache ist, dass der Aufruf des von property zurückgegebenen Blocks thenein Promise zurückgibt:

! then(...)gibt ein Versprechen zurück

Beim Aufruf des von property zurückgegebenen Blocks thengibt der „Empfänger“ ein neues Versprechen zurück, ein untergeordnetes Versprechen. Der Empfänger wird zum übergeordneten Versprechen.

RXPromise* rootPromise = asyncA();
RXPromise* childPromise = rootPromise.then(successHandler, nil);
assert(childPromise.parent == rootPromise);

Was bedeutet das?

Nun, aufgrund dessen können wir asynchrone Aufgaben "verketten", die effektiv sequentiell ausgeführt werden.

Darüber hinaus wird der Rückgabewert eines der Handler zum „Wert“ des zurückgegebenen Versprechens. Wenn die Aufgabe mit dem Ergebnis "OK" erfolgreich ist, wird das zurückgegebene Versprechen mit dem Wert "OK" "gelöst" (dh "erfüllt"):

RXPromise* returnedPromise = asyncA().then(^id(id result){
    return @"OK";
}, nil);

...
assert([[returnedPromise get] isEqualToString:@"OK"]);

Wenn die asynchrone Aufgabe fehlschlägt, wird das zurückgegebene Versprechen ebenfalls mit einem Fehler aufgelöst (dh "abgelehnt").

RXPromise* returnedPromise = asyncA().then(nil, ^id(NSError* error){
    return error;
});

...
assert([[returnedPromise get] isKindOfClass:[NSError class]]);

Der Handler kann auch ein anderes Versprechen zurückgeben. Zum Beispiel, wenn dieser Handler eine andere asynchrone Aufgabe ausführt. Mit diesem Mechanismus können wir asynchrone Aufgaben „verketten“:

RXPromise* returnedPromise = asyncA().then(^id(id result){
    return asyncB(result);
}, nil);

! Der Rückgabewert eines Handlerblocks wird zum Wert des untergeordneten Versprechens.

Wenn es kein untergeordnetes Versprechen gibt, hat der Rückgabewert keine Auswirkung.

Ein komplexeres Beispiel:

Hier führen wir asyncTaskA, asyncTaskB, asyncTaskCund der asyncTaskD Reihe nach - und jede weitere Aufgabe übernimmt das Ergebnis der vorhergehenden Aufgabe als Eingabe:

asyncTaskA()
.then(^id(id result){
    return asyncTaskB(result);
}, nil)
.then(^id(id result){
    return asyncTaskC(result);
}, nil)
.then(^id(id result){
    return asyncTaskD(result);
}, nil)
.then(^id(id result){
    // handle result
    return nil;
}, nil);

Eine solche "Kette" wird auch "Fortsetzung" genannt.

Fehlerbehandlung

Versprechen machen den Umgang mit Fehlern besonders einfach. Fehler werden vom übergeordneten Element an das untergeordnete Element weitergeleitet, wenn im übergeordneten Versprechen kein Fehlerbehandlungsprogramm definiert ist. Der Fehler wird in der Kette weitergeleitet, bis ein Kind ihn bearbeitet. Mit der obigen Kette können wir also die Fehlerbehandlung implementieren, indem wir eine weitere „Fortsetzung“ hinzufügen, die sich mit einem potenziellen Fehler befasst, der irgendwo darüber auftreten kann :

asyncTaskA()
.then(^id(id result){
    return asyncTaskB(result);
}, nil)
.then(^id(id result){
    return asyncTaskC(result);
}, nil)
.then(^id(id result){
    return asyncTaskD(result);
}, nil)
.then(^id(id result){
    // handle result
    return nil;
}, nil);
.then(nil, ^id(NSError*error) {
    NSLog(@“”Error: %@“, error);
    return nil;
});

Dies entspricht dem wahrscheinlich bekannteren synchronen Stil mit Ausnahmebehandlung:

try {
    id a = A();
    id b = B(a);
    id c = C(b);
    id d = D(c);
    // handle d
}
catch (NSError* error) {
    NSLog(@“”Error: %@“, error);
}

Versprechen im Allgemeinen haben andere nützliche Funktionen:

Wenn Sie beispielsweise einen Verweis auf ein Versprechen haben, können Sie über then"registrieren" so viele Handler wie gewünscht. In RXPromise können Handler jederzeit und von jedem Thread aus registriert werden, da sie vollständig threadsicher sind.

RXPromise bietet einige weitere nützliche Funktionen, die von der Promise / A + -Spezifikation nicht benötigt werden. Einer ist "Stornierung".

Es stellte sich heraus, dass "Stornierung" ein unschätzbares und wichtiges Merkmal ist. Zum Beispiel kann eine Call-Site, die einen Verweis auf ein Versprechen enthält, die cancelNachricht an sie senden, um anzuzeigen, dass sie nicht mehr am endgültigen Ergebnis interessiert ist.

Stellen Sie sich eine asynchrone Aufgabe vor, die ein Bild aus dem Web lädt und in einem View-Controller angezeigt werden soll. Wenn sich der Benutzer vom aktuellen Ansichtscontroller entfernt, kann der Entwickler Code implementieren, der eine Abbruchnachricht an imagePromise sendet , die wiederum den durch die HTTP-Anforderungsoperation definierten Fehlerhandler auslöst, bei dem die Anforderung abgebrochen wird.

In RXPromise wird eine Abbruchnachricht nur von einem übergeordneten Element an seine untergeordneten Elemente weitergeleitet, nicht jedoch umgekehrt. Das heißt, ein "Wurzel" -Versprechen hebt alle Kinder-Versprechen auf. Ein Versprechen eines Kindes wird jedoch nur den Zweig stornieren, in dem es sich um den Elternteil handelt. Die Abbruchnachricht wird auch an Kinder weitergeleitet, wenn ein Versprechen bereits gelöst wurde.

Eine asynchrone Task kann selbst einen Handler für ihr eigenes Versprechen registrieren und somit erkennen, wann jemand anderes sie storniert hat. Es kann dann vorzeitig aufhören, eine möglicherweise langwierige und kostspielige Aufgabe auszuführen.

Hier sind einige andere Implementierungen von Promises in Objective-C, die auf GitHub zu finden sind:

https://github.com/Schoonology/aplus-objc
https://github.com/affablebloke/deferred-objective-c
https://github.com/bww/FutureKit
https://github.com/jkubicek/JKPromises
https://github.com/Strilanc/ObjC-CollapsingFutures
https://github.com/b52/OMPromises
https://github.com/mproberts/objc-promise
https://github.com/klaaspieter/Promise
https: //github.com/jameswomack/Promise
https://github.com/nilfs/promise-objc
https://github.com/mxcl/PromiseKit
https://github.com/apleshkov/promises-aplus
https: // github.com/KptainO/Rebelle

und meine eigene Implementierung: RXPromise .

Diese Liste ist wahrscheinlich nicht vollständig!

Überprüfen Sie bei der Auswahl einer dritten Bibliothek für Ihr Projekt sorgfältig, ob die Implementierung der Bibliothek die folgenden Voraussetzungen erfüllt:

  • Eine zuverlässige Versprechungsbibliothek MUSS threadsicher sein!

    Es geht nur um asynchrone Verarbeitung, und wir möchten, wann immer möglich, mehrere CPUs verwenden und auf verschiedenen Threads gleichzeitig ausführen. Seien Sie vorsichtig, die meisten Implementierungen sind nicht threadsicher!

  • Handler MÜSSEN asynchron aufgerufen werden, was die Aufrufstelle betrifft ! Immer und egal was!

    Jede anständige Implementierung sollte auch beim Aufrufen der asynchronen Funktionen einem sehr strengen Muster folgen. Viele Implementierer tendieren dazu, den Fall zu "optimieren", in dem ein Handler synchron aufgerufen wird, wenn das Versprechen bereits gelöst ist, wenn der Handler registriert wird. Dies kann zu allen möglichen Problemen führen. Siehe Zalgo nicht freigeben! .

  • Es sollte auch einen Mechanismus geben, um ein Versprechen zu stornieren.

    Die Möglichkeit, eine asynchrone Aufgabe abzubrechen, wird häufig zu einer Anforderung mit hoher Priorität in der Anforderungsanalyse. Andernfalls wird sicher einige Zeit später nach der Freigabe der App eine Erweiterungsanfrage von einem Benutzer gestellt. Der Grund sollte offensichtlich sein: Jede Aufgabe, die zum Stillstand kommt oder zu lange dauert, sollte vom Benutzer oder durch eine Zeitüberschreitung abgebrochen werden können. Eine anständige Versprechensbibliothek sollte die Stornierung unterstützen.


1
Dies ist der Preis für die längste Nichtantwort aller Zeiten. Aber ein Mühe :-)
Travelling Man

3

Mir ist klar, dass dies eine alte Frage ist, aber ich muss sie beantworten, weil meine Antwort anders ist als die der anderen.

Für diejenigen, die sagen, es ist eine Frage der persönlichen Präferenz, muss ich nicht zustimmen. Es gibt einen guten, logischen Grund, den einen dem anderen vorzuziehen ...

Im Abschlussfall werden Ihrem Block zwei Objekte ausgehändigt, eines steht für Erfolg, während das andere für Misserfolg steht. Was machen Sie also, wenn beide Null sind? Was machst du, wenn beide einen Wert haben? Dies sind Fragen, die bei der Kompilierung vermieden werden können und sollten. Sie vermeiden diese Fragen, indem Sie zwei separate Blöcke haben.

Durch separate Erfolgs- und Fehlerblöcke ist Ihr Code statisch überprüfbar.


Beachten Sie, dass sich die Dinge mit Swift ändern. Darin können wir den Begriff einer EitherAufzählung so implementieren , dass der einzelne Vervollständigungsblock garantiert entweder ein Objekt oder einen Fehler aufweist und genau einen von diesen haben muss. Für Swift ist also ein einzelner Block besser.


1

Ich vermute, es wird eine persönliche Präferenz sein ...

Ich bevorzuge aber die getrennten Erfolgs- / Misserfolgsblöcke. Ich mag es, die Erfolgs- / Misserfolgslogik zu trennen. Wenn Sie Erfolg / Misserfolg verschachtelt hätten, hätten Sie am Ende etwas, das besser lesbar wäre (zumindest meiner Meinung nach).

Als ein relativ extremes Beispiel für eine solche Verschachtelung sehen Sie hier einen Rubin , der dieses Muster zeigt.


1
Ich habe geschachtelte Ketten von beiden gesehen. Ich denke, sie sehen beide schrecklich aus, aber das ist meine persönliche Meinung.
Jeffery Thomas

1
Aber wie sonst könnten Sie asynchrone Anrufe verketten?
Frank Shearar

Ich kenne keinen Mann ... ich weiß nicht. Ein Grund, den ich frage, ist, dass mir keiner meiner asynchronen Codes gefällt.
Jeffery Thomas

Sicher. Am Ende schreiben Sie Ihren Code im Continuation-Passing-Stil, was nicht sonderlich überraschend ist. (Haskell hat genau aus diesem Grund seine Do-Notation: Sie können scheinbar direkt schreiben.)
Frank Shearar

Sie könnten an dieser Implementierung von ObjC Promises interessiert sein: github.com/couchdeveloper/RXPromise
e1985

0

Das fühlt sich an wie eine vollständige Überarbeitung, aber ich glaube nicht, dass es hier eine richtige Antwort gibt. Ich habe mich für den Abschlussblock entschieden, nur weil die Fehlerbehandlung möglicherweise noch im Erfolgszustand durchgeführt werden muss, wenn Erfolgs- / Fehlerblöcke verwendet werden.

Ich denke, der endgültige Code wird ungefähr so ​​aussehen

[target taskWithCompletion:^(id object, NSError *error) {
    if (error) {
        // Oh noes! report the failure.
    } else if (![target validateObject:&object error:&error]) {
        // Oh noes! report the failure.
    } else {
        // W00t! I've got my object
    }
}];

oder einfach

[target taskWithCompletion:^(id object, NSError *error) {
    if (error || ![target validateObject:&object error:&error]) {
        // Oh noes! report the failure.
        return;
    }

    // W00t! I've got my object
}];

Nicht das beste Stück Code und das Verschachteln wird noch schlimmer

[target taskWithCompletion:^(id object, NSError *error) {
    if (error || ![target validateObject:&object error:&error]) {
        // Oh noes! report the failure.
        return;
    }

    [object objectTaskWithCompletion:^(id object2, NSError *error) {
        if (error || ![object validateObject2:&object2 error:&error]) {
            // Oh noes! report the failure.
            return;
        }

        // W00t! I've got object and object 2
    }];
}];

Ich denke, ich werde für eine Weile mope.

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.