Added clean_arch

This commit is contained in:
debiankaios 2022-11-30 18:24:39 +01:00
parent 91567c95e6
commit 109aa282c8
2 changed files with 207 additions and 0 deletions

206
_de/quality/clean_arch.md Normal file
View File

@ -0,0 +1,206 @@
---
title: Einführung in saubere Architekturen
layout: default
root: ../..
idx: 8.4
---
## Einleitung <!-- omit in toc -->
Sobald Ihre Mod eine beachtliche Größe erreicht hat, wird es immer schwieriger, den den Code sauber und frei von Fehlern zu halten. Dies ist ein besonders großes Problem bei der Verwendung von einer dynamisch typisierte Sprache wie Lua, da der Compiler Ihnen sehr wenig Hilfe und Möglichkeiten zur Compilerzeit gibt, wenn es darum geht, sicherzustellen, dass Typen ordnungsgemäß verwendet werden.
Dieses Kapitel behandelt wichtige Konzepte, um Ihren Code sauber zu halten und gängige Entwurfsmuster, um dies zu erreichen. Bitte beachten Sie, dass dieses Kapitel nicht als Vorschrift gedacht ist, sondern Ihnen eine Vorstellung von den Möglichkeiten geben soll. Es gibt nicht nur einen guten Weg, eine Mod zu entwerfen und gutes Mod-Design ist sehr subjektiv.
- [Kohäsion, Kopplung und Trennung der Programmbereiche](#kohsion-kopplung-und-trennung-der-programmbereiche)
- [Observer](#observer)
- [Modell-View-Controller](#modell-view-controller)
- [API-Sicht](#api-sicht)
- [Zusammenfassung](#zusammenfassung)
## Kohäsion, Kopplung und Trennung der Programmbereiche
Ohne jegliche Planung neigt ein Programmierprojekt dazu, allmählich in
Spaghetti-Code zu verfallen. Spaghetti-Code zeichnet sich durch einen Mangel an Struktur aus - der gesamte Code wird ohne klare Grenzen zusammengewürfelt. Das macht ein Projekt völlig unwartbar und endet damit, dass es aufgegeben wird.
Das Gegenteil davon ist, dass ein Projekt als eine Sammlung interagierender kleinerer Programme oder Code-Bereiche zu entwickeln. <!-- Weird wording? -->
> Inside every large program, there is a small program trying to get out.
>
> --C.A.R. Hoare
Die Übersetzung davon ist: <!-- Weird wording? -->
> In jedem großen Programm gibt es ein kleines Programm, das versucht, herauszukommen.
Dies sollte so geschehen, dass Sie eine Trennung der Programmteile erreichen - jeder Bereich sollte klar abgegrenzt sein und einem separaten Bedürfnis oder einer Aufgabe entsprechen.
Diese Programme/Bereiche sollten die folgenden zwei Eigenschaften haben:
* **Hohe Kohäsion** - die Bereiche sollten eng miteinander verbunden sein.
* **Niedrige Kopplung** - die Abhängigkeiten zwischen den Bereichen sollten so gering wie möglich sein und es sollte vermieden werden,
sich auf interne Implementierungen zu verlassen. Es ist eine sehr gute Idee, sicherzustellen, dass Sie eine geringe Kopplung zu gewährleisten, da dies bedeutet, dass eine Änderung der APIs bestimmter Bereiche
leichter durchführbar sein wird.
Beachten Sie, dass dies sowohl für die Beziehung zwischen Mods gilt,
als auch für die Beziehung zwischen Bereichen innerhalb eines Mods.
## Observer
Eine einfache Möglichkeit, verschiedene Bereiche des Codes zu trennen, ist die Verwendung des Observer-Musters.
Nehmen wir als Beispiel der Freischaltung einer Leistung, wenn ein Spieler zum ersten Mal ein seltenes Tier tötet. Der naive Ansatz wäre, den Code für die Errungenschaft in der mobkill-Funktion den Mob-Namen überprüfen zu lassen und die Auszeichnung freizuschalten, wenn er übereinstimmt.
Dies ist jedoch eine schlechte Idee, da es den Mobs-Mod an die Errungenschaften gekoppelt macht. Wenn man so weitermacht - zum Beispiel, indem man XP zum Mob-Todescode hinzufügt - könnte man eine Menge chaotischer Abhängigkeiten haben.
Hier kommt das Observer-Muster ins Spiel. Anstatt dass sich die mymobs-Mod um Auszeichnungen kümmert,
erhält die mymobs-Mod eine Möglichkeit für andere Bereiche des Codes, ihr Interesse an an einem Ereignis zu registrieren und Daten über das Ereignis zu erhalten.
```lua
mymobs.registered_on_death = {}
function mymobs.register_on_death(func)
table.insert(mymobs.registered_on_death, func)
end
-- im Mob-Death-Code
for i=1, #mymobs.registered_on_death do
mymobs.registered_on_death[i](entity, reason)
end
```
Dann meldet der andere Code sein Interesse an:
```lua
mymobs.register_on_death(function(mob, reason)
if reason.type == "punch" and reason.object and
reason.object:is_player() then
awards.notify_mob_kill(reason.object, mob.name)
end
end)
```
Vielleicht denken Sie jetzt: Moment mal, das kommt mir doch irgendwie bekannt vor. Und Sie haben Recht!
Die Minetest-API ist stark Observer-basiert, damit sich die Engine nicht darum kümmern muss, was auf wen hört.
## Modell-View-Controller
Im nächsten Kapitel werden wir besprechen, wie Sie Ihren Code automatisch testen können. Eines der Probleme wird sein, wie man die Logik(Berechnungen, was getan werden sollte) von API-Aufrufen (`minetest.*`, andere Mods) so weit wie möglich zu trennen.
Eine Möglichkeit, dies zu tun, ist, darüber nachzudenken:
* Welche **Daten** Sie haben.
* Welche **Aktionen** man mit diesen Daten durchführen kann.
* Wie **Ereignisse** (z.B. Formspecs, Stempel, etc.) diese Aktionen auslösen und wie
diese Aktionen in der Engine etwas bewirken.
Nehmen wir ein Beispiel für einen Landschutz-Mod. Die Daten, die Sie haben, sind die Gebiete und alle zugehörigen Metadaten. Mögliche Aktionen sind `Erzeugen`, `Bearbeiten` oder `löschen`. Die Ereignisse, die diese Aktionen auslösen, sind Chat-Befehle und Formspec-Empfangsfelder. Dies sind 3 Bereiche, die sich in der Regel gut voneinander trennen lassen.
In Ihren Tests können Sie sicherstellen, dass eine Aktion, wenn sie ausgelöst wird,
das Richtige mit den Daten macht. Sie brauchen nicht zu testen, dass ein Ereignis eine
Aktion aufruft (dazu müsste die Minetest-API verwendet werden, und dieser Bereich des Codes sollte ohnehin so klein wie möglich gehalten werden).
Sie sollten Ihre Datendarstellung in reinem Lua schreiben. "Sauber" bedeutet in diesem Zusammenhang, dass die Funktionen außerhalb von Minetest ausgeführt werden können - keine der Funktionen der Engine müssen aufgerufen werden.
```lua
-- Data
function land.create(name, area_name)
land.lands[area_name] = {
name = area_name,
owner = name,
-- mehr Dinge
}
end
function land.get_by_name(area_name)
return land.lands[area_name]
end
```
Ihre Aktionen sollten auch sauber sein, aber der Aufruf anderer Funktionen ist akzeptabler als im obigen Beispiel.
```lua
-- Controller
function land.handle_create_submit(name, area_name)
-- Prozessmaterial
-- (d.h.: auf Überschneidungen prüfen, Quoten prüfen, erechtigungen prüfen)
land.create(name, area_name)
end
function land.handle_creation_request(name)
-- Dies ist ein schlechtes Beispiel, wie später erklärt wird
land.show_create_formspec(name)
end
```
Ihre Event-Handler müssen mit der Minetest-API interagieren. Sie sollten die die Anzahl der Berechnungen auf ein Minimum beschränken, da Sie diesen Bereich nicht sehr gut testen können.
```lua
-- Siehe
function land.show_create_formspec(name)
-- Beachten Sie, dass es hier keine komplexen Berechnungen gibt!
return [[
size[4,3]
label[1,0;This is an example]
field[0,1;3,1;area_name;]
button_exit[0,2;1,1;exit;Exit]
]]
end
minetest.register_chatcommand("/land", {
privs = { land = true },
func = function(name)
land.handle_creation_request(name)
end,
})
minetest.register_on_player_receive_fields(function(player,
formname, fields)
land.handle_create_submit(player:get_player_name(),
fields.area_name)
end)
```
Das obige Muster ist das Model-View-Controller-Muster. Das Modell ist eine Sammlung von Daten mit minimalen Funktionen. Der View ist eine Sammlung von Funktionen, die auf
Ereignisse abhören und an den Controller weiterleiten und auch Aufrufe vom Controller erhalten, um etwas mit der Minetest-API zu tun. Der Controller ist der Ort, an dem die Entscheidungen und die meisten Berechnungen getroffen werden.
Der Controller sollte keine Kenntnisse über die Minetest-API haben - beachten Sie, dass
es keine Minetest-Aufrufe oder View-Funktionen, die ihnen ähneln, gibt. Sie sollten *NICHT* eine Funktion wie `view.hud_add(player, def)` haben. Stattdessen definiert der View einige Aktionen, die der Controller dem View mitteilen kann, wie z. B. `view.add_hud(info)`, wobei info ein Wert oder eine Tabelle ist, die in keiner Weise mit der Minetest-API etwas zu tun hat.
<figure class="right_image">
<img
width="100%"
src="{{ page.root }}/static/mvc_diagram.svg"
alt="Diagram showing a centered text element">
</figure>
Es ist wichtig, dass jeder Bereich nur mit seinen direkten Nachbarn kommuniziert,
wie oben gezeigt, um die Anzahl der Änderungen zu reduzieren, die Sie vornehmen müssen, wenn Sie die Interna oder Externa eines Bereichs ändern. Um beispielsweise die Formularvorgabe zu ändern, müssen Sie nur die Ansicht bearbeiten. Um die View-API zu ändern, müssten Sie nur nur den View und den Controller ändern, nicht aber das Modell.
In der Praxis wird dieses Design nur selten verwendet, da es die Komplexität erhöht
und weil es für die meisten Arten von Mods nicht viele Vorteile bietet. Stattdessen,
wird man häufig eine weniger formale und strenge Art von Design sehen -
Varianten der API-Ansicht.
### API-Sicht
In einer idealen Welt würden Sie die oben genannten 3 Bereiche perfekt getrennt haben, wobei alle Ereignisse in den Controller gehen, bevor sie in die normale Ansicht zurückkehren. Aber das ist nicht die reale Welt. Ein guter Kompromiss ist die Reduzierung der Mod in zwei
Teile:
**API** - Dies war das Modell und der Controller oben. Es sollte keine Verwendung von
`minetest.` geben.
* **View** - Dies war auch die obige Ansicht. Es ist eine gute Idee, dies in separate Dateien für jede Art von Ereignis zu strukturieren.
rubenwardy's [crafting mod](https://github.com/rubenwardy/crafting 🇬🇧) folgt ungefähr diesem Design. Die Datei `api.lua` besteht fast ausschließlich aus reinen Lua-Funktionen, die die Daten Speicherung und Controller-ähnliche Berechnungen handhaben. `gui.lua` ist die Ansicht für Formspecs und Formspec-Übermittlung, und `async_crafter.lua` ist der View und Controller für einen Knoten formspec und Nodezeitgeber.
Wenn man die Mods auf diese Weise trennt, kann man den API-Teil sehr einfach testen,
da er keine Minetest-APIs verwendet - wie im [nächstes Kapitel](unit_testing.html) und in der Crafting-Mod zu sehen.
## Zusammenfassung
Gutes Code-Design ist subjektiv und hängt stark von dem Projekt ab, an dem Sie arbeiten. Generell sollte man versuchen, die Kohäsion hoch und die Kopplung niedrig zu halten. Anders formuliert, Halten Sie verwandten Code zusammen und nicht verwandten Code auseinander und halten Sie Abhängigkeiten einfach.
Ich empfehle dringend die Lektüre des [Game Programming Patterns 🇬🇧](http://gameprogrammingpatterns.com/) Buch. Es ist frei verfügbar, [online (auf Englisch) lesbar](http://gameprogrammingpatterns.com/contents.html)
und geht viel detaillierter als in diesem Buch auf allgemeine Programmiermuster ein, die für Spiele relevant sind.

View File

@ -12,6 +12,7 @@ Chapter = Kapitel
chat message = Chat-Nachricht
child = Kind
client = Client
Cohesion = Kohäsion
Command = Befehl
Context = Context
Cubic Nodes = Würfelförmiger Block