DIGITAL
Auf den vergangenen Blättern wurde bereits mit std::cin
, std::cout
und Dateistreams gearbeitet. In dieser Aufgabe sehen wir uns an, wie man die Funktionalität dieser Objekte erweitern kann. Dazu definieren wir als erstes ein Interface (stream
), das am Ende alle Klassen implementieren werden.
Da es leider nicht so einfach ist, C++-Streams objektorientiert direkt zu erweitern, geben wir für diese Aufgabe einen einfachen Wrapper (BasicStreamWrapper
) für std::ostream
vor. Nun, mit einer eigenen C++-Implementierung können wir eigene Klassen definieren, die davon ableiten: Wir implementieren drei sogenannte Dekoratoren, die das Verhalten des Streams im Gegensatz zum Wrapper auch tatsächlich verändern.
Interface:
Als Basis definieren wir die abstrakte Klasse stream
, die das Interface eines Streams festlegt: Jeder Stream stellt eine Methode print
bereit, mit der Daten in den Stream geschrieben werden können. Wohin die Daten dann tatsächlich geschrieben werden, ist durch dieses Interface noch nicht festgelegt (theoretisch darf ein Stream auch Daten wegwerfen).
class stream {
public:
virtual void print(const std::string&) = 0;
virtual ~stream() = default;
};
Wrapper für std::ostream
:
Wir nutzen nun das Interface stream
, um eine konkrete Implementierung eines Streams vorzugeben. Die unterhalb definierte Klasse basic_stream_wrapper
gibt den übergebenen String auf einem C++-kompatiblen Output-Stream (etwa std::cout
) aus und wrappt damit wie die Verwendung von C++-Streams. (Eine mögliche Verwendung wird weiter unten in einem Beispielcodeschnipsel vorgeführt).
// .h
class basic_stream_wrapper : public stream {
private:
std::ostream& stream;
public:
explicit basic_stream_wrapper(std::ostream& s): stream(s) {};
void print(const std::string&) override;
~basic_stream_wrapper() override = default;
};
// .cpp
void basic_stream_wrapper::print(const std::string& string) {
stream << string;
}
Dekoratoren: Wir können jetzt Modifikationen implementieren, die den auszugebenen String mit Zeilennummern versehen (count_decorator
), ein Trennzeichen hinten anfügen (postfix_decorator
) oder sogar den eigentlichen Inhalt des Strings überschreiben (caesar_decorator
). Um solche Modifikatoren nach einem Baukasten-System zusammensetzen zu können, bedienen wir uns des Decorator-Patterns: Wir leiten für jede Modifikation einen weiteren Stream ab, der in seiner print
-Methode die gewünschten Änderungen am String durchführt, dann aber das Ergebnis nicht sofort ausgibt, sondern an einen anderen Stream weitergibt. Der "Dekorator"-Stream hat also dasselbe Interface wie ein normaler Stream, verhält sich aber anders.
class stream_decorator : public stream {
protected:
stream* stream;
public:
explicit stream_decorator(Stream* s) : stream(s) {};
~stream_decorator() override = default;
};
Die Klasse stream_decorator
ist unser Ausgangspunkt für diese Implementierung. stream_decorator
leitet zwar von stream
ab, überschreibt aber nicht die pure virtuelle Methode print
und ist daher selbst eine abstrakte Klasse, kann also nicht instanziiert werden. Jeder stream_decorator
führt einen Zeiger auf den Stream mit, auf den letztendlich geschrieben werden soll.
Beispielverwendung:
Wenn Sie alles implementiert haben, sollte das folgende Codebeispiel funktionieren:
#include <ostream>
#include <string>
// Ihre selbst definierten Header kommen hierher.
void do_something(Stream &stream) {
stream.print("Hello World");
stream.print("Second Line");
}
int main() {
std::ostream& out = std::cout;
basic_stream_wrapper cout(out);
postfix_decorator postfix(&cout, ".\n");
count_decorator count(&postfix);
caesar_decorator caesar(&count, 7);
do_something(count);
do_something(caesar);
}
Die Ausgabe ist dann
[0] Hello World.
[1] Second Line.
[2] Olssv Dvysk.
[3] Zljvuk Spul.
stream_decorator
erben: count_decorator
, der vor jede Ausgabe eine automatisch inkrementierende Ausgabennummer anhängt. class count_decorator : public stream_decorator {
uint64_t counter;
public:
explicit count_decorator(Stream* s, uint64_t initial_counter = 0);
void print(const std::string&) override;
~count_decorator() override = default;
};
count_decorator
vor. Die Syntax ... : stream_decorator(s) ...
ruft den Konstruktor der Basisklasse stream_decorator
mit dem gewrappten Stream auf.count_decorator::count_decorator(stream *s, uint64_t initial_counter) : stream_decorator(s), counter(initial_counter) {}
postfix_decorator
. Dieser soll einen vorher definierten String an jeden String anhängen, der print
übergeben wird. class postfix_decorator : public stream_decorator {
private:
std::string postfix_string;
public:
explicit postfix_decorator(stream* s, const std::string& postfix_string = "\n");;
void print(const std::string&) override;
~postfix_decorator() override = default;
};
caesar_decorator
. Ein caesar_decorator
soll jeden String, der an print
übergeben wird, nach dem Caesar-Chiffre verschlüsseln. Das Caesar-Chiffre ist eine sehr einfache Verschlüsselung, bei der alle Buchstaben einfach verschoben werden. Andere Zeichen bleiben unverändert. Die Zeichen der oberen Reihe werden in das jeweilige Zeichen der unteren Reihe übersetzt. Hier ein Beispiel für eine Verschiebung um 5 Buchstaben: Klar: a b c d e f g h i j k l m n o p q r s t u v w x y z
Geheim: f g h i j k l m n o p q r s t u v w x y z a b c d e
Die Buchstabenverschiebung wird Ihrer Klasse als Konstruktorparameter übergeben.class caesar_decorator : public stream_decorator {
private:
int32_t shift;
public:
explicit caesar_decorator(stream* s, int32_t shift = 0);
void print(const std::string&) override;
~caesar_decorator() override = default;
};
stream_decorator
eine beliebige Klasse vom Typ stream
erwartet können wir auch andere Basisobjekte vom Typ stream
dekorieren ohne unseren Klassen vom Typ stream_decorator
umschreiben zu müssen. Wir tun dies, indem wir eine Klasse stream_print_wrapper
definieren, die statt einem std::ostream
die Methode printf
(Link zur Dokumentation) wrappt. Implementieren Sie also class stream_print_wrapper : public stream { ... }
als zweite Basisklasse.