Abgabe zu Übungsblatt 1, Sebastian Irsch, 1337932
Wie im Übungsblatt erlaubt, wurden die Aufgaben 1 und 2 zusammen gelöst und das Ergebnis als ein github-Repository abgegeben.
Zunächst zur Beschreibung des für Aufgabe 1 relevanten Teils. Vorausschauend auf Aufgabe 2 kann festgestellt werden, dass eine Kodierung der Daten mittels Protocol Buffers bereitgestellt werden soll, weil das die übliche Kodierung für gRPC ist. Daher bietet es sich an, dieses Format bereits im ersten Aufgabenteil zu verwenden.
Die Anwendung, die als Lösung zu Aufgabe 1 programmiert wurde, unterscheidet zwischen dem
internen Datenmodell, bei dem den Symbolen Werte zugewiesen und Berechnungen durchgeführt werden
können, und dem Wire-Format, das lediglich der Speicherung und Übertragung von Rätseln dient. Das
interne Modell ist dabei für eine effiziente Lösung von Rätseln optimiert. Da dieses Modell jedoch
auf Objektreferenzen basiert, jedes Vorkommen eines Symbols verweist auf dieselbe Instanz, lässt es
sich nicht mit den gängigen Kodierungsmechanismen serialisieren. XML kennt noch den Begriff der
Identität für Objekte. Dazu verwendet es IDs im Dokument. Das ist zwar mächtig, aber auch unhandlich
und die meisten moderneren Formate wie JSON, Yaml oder Protobuf konzentrieren sich auf die Abbildung
von baumartigen Objektstrukturen. Daher wurde im Entwicklungsprozess die Design-Entscheidung
getroffen, das Dateispeicher- und Übertragungsformat von der internen Repräsentation zu trennen.
Damit sind die Aufgaben und die Designvorgaben klar getrennt. Während das interne Datenmodell
bestehend aus den Klassen Puzzle
, Addition
, Cell
und Symbol
eine objektorientierte Abbildung
der Problemstellung ist, dient das Protocol-Buffers-basierte Wire-Format einer möglichst einfachen
Repräsentation der zu übertragenden Daten. Dieses spezielle Übertragungsformat ist nicht besonders
restriktiv umgesetzt. Es ist nicht Sinn und Zweck, alle Regeln eines Zahlenrätsels in das
Wire-Format zu kodieren. Mit dem Wire-Format könnten beispielsweise ungültige Rätsel übertragen
werden. Die Details werden erst bei der Umwandlung durch Protobuf2PuzzleConverter
in das interne
Format geprüft. Weil die interne Datenstruktur des Rätsels, die aus mehreren Klassen besteht,
aufwändig zu erzeugen ist, wird hier das Builder-Muster verwendet. Der PuzzleBuilder
ermöglicht
es, ein Puzzle
zu erzeugen. In der build-Methode werden die übergebenen Symbole und Zellen auf
Konsistenz geprüft. Die Klasse PuzzleSolver
dient der Ermittlung von Lösungen zu den Rätseln
mittels Brute-Force-Ansatz. Zum Erzeugen von Rätseln gibt es die Klasse PuzzleGenerator
.
Das Programm ist mit einer einzigen Main-Methode ausgestattet. Die gewünschte Funktionalität wird
durch den Kommandonamen, der als erster Parameter zu übergeben ist, festgelegt. Nach dem Namen des
Kommandos können weitere Parameter folgen. Für Aufgabe 1 sind die beiden Kommandos generate-puzzle
und solve-puzzle
relevant.
Mit dem Kommando generate-puzzle <filename> <?numberOfDigits>
kann ein neues Rätsel erzeugt und
als Datei abgespeichert werden.
Parameter | Beschreibung |
---|---|
<filename> |
Der Name der zu erzeugenden Datei |
<?numberOfDigits> |
(optional) Die Anzahl der zu erzeugenden Stellen |
Mit dem Kommando solve-puzzle <filename>
kann ein Rätsel aus einer Datei gelesen und gelöst
werden.
Parameter | Beschreibung |
---|---|
<filename> |
Der Name der einzulesenden Datei |
Weil gRPC das Thema von Aufgabe 2 ist, wird hier zunächst näher auf die Code-Generierung mit dem Protobuf-Compiler eingegangen. Dann wird die allgemeine Architektur beschrieben und schließlich die Verwendung des Programms in Bezug auf die Client-Server-Kommunikation erläutert.
Auch der Protocol-Buffer-Code aus Aufgabe 1 wurde bereits mit dem Protobuf-Compiler protoc
erzeugt. Mit dem gRPC-Plugin für protoc können die Stubs und die Basisklasse für die
Server-Implementierung erzeugt werden. Der Code für die Protocol-Buffers-Nachrichten und gRPC kann
in derselben protobuf-Datei verfasst werden. Im Rahmen dieses Aufgabenblatts wurde der Code in die
Datei Puzzles.proto
geschrieben. Für den generierten Code wurde ein separates Java-Paket
vorgesehen, um eine klare Trennung zwischen generiertem und selbstgeschriebenem Code zu
bewerkstelligen.
Mit dem Kommando protoc --java_out=src/main/java src/main/resources/software/sirsch/sa4e/puzzles/protobuf/Puzzles.proto
wurde die Code-Generierung
für die Nachrichten ausgelöst. Dabei wurde die Klasse
software.sirsch.sa4e.puzzles.protobuf.Puzzles
und die Nachrichten als nested Classes erzeugt. Für
die Erzeugung der gRPC-Stubs und der Basisklasse für den Dienst wurde protoc --plugin=protoc-gen-grpc-java=protoc-gen-grpc-java-1.51.1-linux-x86_64.exe --grpc-java_out=src/main/java src/main/resources/software/sirsch/sa4e/puzzles/protobuf/Puzzles.proto
verwendet.
Die Klasse PuzzleSolverService
ist von der generierten, abstrakten Klasse
PuzzleSolverGrpc.PuzzleSolverImplBase
abgeleitet und fügt die Business-Logik in das generierte
Gerüst ein. Für das Lifecycle-Management des Servers ist die Klasse PuzzleSolverServer
zuständig.
Weil es sich bei der Abgabe um eine Konsolenanwendung handelt, wurde, wie im Tutorial
https://grpc.io/docs/languages/java/basics/, ein Shutdown-Hook zum Reagieren auf ein Signal
verwendet. Der Main-Thread wartet derweil auf das Herunterfahren des gRPC-Servers. Man könnte den
automatisch generierten Stub PuzzleSolverGrpc.PuzzleSolverBlockingStub
als Client
verwenden. Dann müsste sich der Aufrufer um die Behandlung des gRPC-Channels kümmern. Insbesondere
muss der Channel
heruntergefahren werden, um Ressourcen freizugeben. Um die Ressourcen-Behandlung
auf Client-Seite zu vereinfachen, wurden Channel und Blocking-Stub hinter der Schnittstelle der
Klasse PuzzleSolverClient
verborgen. Diese Klasse implementiert die Schnittstelle Closeable
,
sodass sie in einem Try-With-Resources verwendet werden kann.
Der Anwendung aus Aufgabe 1 wurden zwei weitere Kommandos hinzugefügt. Mit run-server
kann ein
Server zum Lösen von Rätseln gestartet und betrieben werden. Das Kommando request-solve-puzzle
erzeugt ein neues Rätsel und sendet es zur Lösung an einen Server. Die Antwort wird nach Empfang auf
dem Client ausgegeben.
Mit dem Kommando run-server <port>
wird ein Server gestartet und betrieben. Das Programm läuft
dabei so lange, bis es von außen zum Beispiel mit STRG
+ C
beendet wird. Beim Aufruf aus einer
IDE wie Intellij wird das Programm mit der Stop-Schaltfläche beendet.
Parameter | Beschreibung |
---|---|
<port> |
Der Port, auf dem der Server Verbindungen entgegennehmen soll |
Mit dem Kommando request-solve-puzzle <serverHost> <serverPort> <?numberOfDigits>
wird ein neues
Rätsel erzeugt und zur Lösung an den Server übertragen. Die Antwort des Servers wird im Anschluss
daran auf dem Client ausgegeben.
Parameter | Beschreibung |
---|---|
<serverHost> |
Der Hostname des Servers |
<serverPort> |
Der Port, auf dem der Server Verbindungen entgegennimmt |
<?numberOfDigits> |
(optional) Die Anzahl der zu erzeugenden Stellen |
Das Maven-Projekt dieser Abgabe basiert auf der Parent-POM vom Spring Boot. Diese wird verwendet, weil das Spring-Boot-Projekt eine nützliche und aufeinander abgestimmte Auswahl von gängigen Java-Bibliotheken und dem Test-Framework Junit 5 als Abhängigkeiten liefert. Die Funktionalität von Spring Boot oder Spring Framework wird nicht verwendet.