Schlechte Tipps für einen Go-Programmierer

Nach Jahrzehnten der Programmierung in Java habe ich in den letzten Jahren hauptsächlich an Go gearbeitet. Die Arbeit mit Go ist großartig, vor allem, weil der Code sehr einfach zu befolgen ist. Java hat das C ++ - Programmiermodell vereinfacht, indem Mehrfachvererbung, manuelle Speicherverwaltung und Operatorüberladung entfernt wurden. Go macht dasselbe und bewegt sich weiterhin in Richtung eines einfachen und verständlichen Programmierstils, wobei Vererbung und Funktionsüberladung vollständig beseitigt werden. Einfacher Code ist lesbarer Code, und lesbarer Code ist unterstützter Code. Und das ist großartig für das Unternehmen und meine Mitarbeiter.
Wie in allen Kulturen hat die Softwareentwicklung ihre eigenen Legenden, Geschichten, die vom Wasserkühler nacherzählt werden. Wir haben alle von Entwicklern gehört, die sich nicht darauf konzentrieren, ein Qualitätsprodukt zu entwickeln, sondern ihre eigene Arbeit vor Außenstehenden schützen wollen. Sie benötigen keinen unterstützten Code, da dies bedeutet, dass andere Personen ihn verstehen und ändern können. Ist es auf Go möglich? Ist es möglich, Go-Code so kompliziert zu machen? Ich werde gleich sagen - das ist keine leichte Aufgabe. Schauen wir uns die möglichen Optionen an.
Sie denken: „
Wie viel können Sie Code in einer Programmiersprache korrodieren? Ist es möglich, so schrecklichen Code auf Go zu schreiben, dass sein Autor im Unternehmen unverzichtbar wird? " Keine Sorge. Als ich Student war, hatte ich ein Projekt, in dem ich den Lisp-e-Code eines anderen unterstützte, der von einem Doktoranden geschrieben wurde. Tatsächlich gelang es ihm, Fortran-e-Code mit Lisp zu schreiben. Der Code sah ungefähr so aus:
(defun add-mult-pi (in1 in2) (setq a in1) (setq b in2) (setq c (+ ab)) (setq d (* 3.1415 c) d )
Es gab Dutzende von Dateien mit solchem Code. Er war absolut schrecklich und absolut brillant zugleich. Ich habe Monate damit verbracht, es herauszufinden. Im Vergleich dazu ist das Schreiben von schlechtem Code auf Go nur ein Spucke.
Es gibt viele verschiedene Möglichkeiten, Ihren Code nicht zu unterstützen, aber wir werden nur einige betrachten. Um Böses zu tun, müssen Sie zuerst lernen, Gutes zu tun. Deshalb schauen wir uns zuerst an, wie die "guten" Go-Programmierer schreiben, und dann schauen wir uns an, wie man das Gegenteil macht.
Schlechte Verpackung
Pakete sind ein praktisches Thema für den Einstieg. Wie kann die Code-Organisation die Lesbarkeit beeinträchtigen?
In Go wird der Paketname verwendet, um auf die exportierte Entität zu verweisen (z. B. "
fmt.Println" oder "
http.RegisterFunc" ). Da wir den Namen des Pakets sehen können, stellen die "guten" Go-Programmierer sicher, dass dieser Name die exportierten Entitäten beschreibt. Wir sollten keine util-Pakete haben, da Namen wie "
util.JSONMarshal" für uns nicht funktionieren - wir brauchen "
json.Marshal" .
Die "guten" Go-Entwickler erstellen auch kein separates Paket für das DAO oder Modell. Für diejenigen, die mit diesem Begriff nicht vertraut sind, ist ein DAO ein „
Datenzugriffsobjekt “ - eine Codeebene, die mit Ihrer Datenbank interagiert. Ich habe für ein Unternehmen gearbeitet, in dem 6 Java-Dienste dieselbe DAO-Bibliothek importiert haben, um auf dieselbe Datenbank zuzugreifen, die sie gemeinsam genutzt haben, weil "
... nun, Sie wissen, Microservices sind dieselben ... ".
Wenn Sie ein separates Paket mit all Ihren DAOs haben, ist es wahrscheinlicher, dass Sie eine zirkuläre Abhängigkeit zwischen Paketen erhalten, die in Go verboten ist. Wenn Sie über mehrere Dienste verfügen, die dieses DAO-Paket als Bibliothek verbinden, kann es auch vorkommen, dass bei einer Änderung eines Dienstes alle Ihre Dienste aktualisiert werden müssen, da sonst ein Fehler auftritt. Dies wird als verteilter Monolith bezeichnet und ist unglaublich schwer zu aktualisieren.
Wenn Sie wissen, wie Verpackungen funktionieren sollten und was sie noch schlimmer macht, wird es einfach, „dem Bösen zu dienen“. Organisieren Sie Ihren Code schlecht und geben Sie Ihren Paketen schlechte Namen.
Teilen Sie Ihren Code in Pakete wie
model ,
util und
dao auf . Wenn Sie wirklich Chaos schaffen möchten, versuchen Sie, Pakete zu Ehren Ihrer Katze oder Ihrer Lieblingsfarbe zu erstellen. Wenn Menschen mit zyklischen Abhängigkeiten oder verteilten Monolithen konfrontiert sind, weil sie versuchen, Ihren Code zu verwenden, müssen Sie seufzen, die Augen verdrehen und ihnen sagen, dass sie einfach falsch machen ...
Unangemessene Schnittstellen
Nachdem alle unsere Pakete beschädigt sind, können wir zu den Schnittstellen übergehen. Schnittstellen in Go sind nicht wie Schnittstellen in anderen Sprachen. Die Tatsache, dass Sie nicht explizit deklarieren, dass dieser Typ die Schnittstelle implementiert, scheint zunächst unbedeutend zu sein, kehrt jedoch das Konzept der Schnittstellen vollständig um.
In den meisten Sprachen mit abstrakten Typen wird eine Schnittstelle vor oder gleichzeitig mit der Implementierung definiert. Sie müssen dies zumindest zum Testen tun. Wenn Sie die Schnittstelle nicht im Voraus erstellen, können Sie sie später nicht einfügen, ohne den gesamten Code zu beschädigen, der diese Klasse verwendet. Weil Sie es mit einem Link zur Schnittstelle anstelle eines bestimmten Typs neu schreiben müssen.
Aus diesem Grund verfügt Java-Code häufig über gigantische Dienstschnittstellen mit vielen Methoden. Klassen, die diese Schnittstellen implementieren, verwenden dann die benötigten Methoden und ignorieren den Rest. Das Schreiben von Tests ist möglich, Sie fügen jedoch eine zusätzliche Abstraktionsebene hinzu. Beim Schreiben von Tests verwenden Sie häufig Tools, um Implementierungen der Methoden zu generieren, die Sie nicht benötigen.
In Go bestimmen implizite Schnittstellen, welche Methoden Sie verwenden müssen. Code besitzt eine Schnittstelle, nicht umgekehrt. Selbst wenn Sie einen Typ mit vielen darin definierten Methoden verwenden, können Sie eine Schnittstelle angeben, die nur die von Ihnen benötigten Methoden enthält. Ein anderer Code, der separate Felder desselben Typs verwendet, definiert andere Schnittstellen, die nur die erforderliche Funktionalität abdecken. In der Regel verfügen diese Schnittstellen nur über einige Methoden.
Dies erleichtert das Verständnis Ihres Codes, da eine Methodendeklaration nicht nur bestimmt, welche Daten benötigt werden, sondern auch genau angibt, welche Funktionen verwendet werden sollen. Dies ist einer der Gründe, warum gute Go-Entwickler den Rat befolgen: "
Schnittstellen akzeptieren, Strukturen zurückgeben ."
Aber nur weil dies eine gute Praxis ist, heißt das nicht, dass Sie das tun sollten ...
Der beste Weg, um Ihre Schnittstellen "böse" zu machen, besteht darin, zu den Prinzipien der Verwendung von Schnittstellen aus anderen Sprachen zurückzukehren, d. H. Definieren Sie Schnittstellen im Voraus als Teil des aufgerufenen Codes. Definieren Sie große Schnittstellen mit vielen Methoden, die von allen Service-Clients verwendet werden. Es wird unklar, welche Methoden wirklich benötigt werden. Dies verkompliziert den Code, und wie Sie wissen, ist die Komplikation der beste Freund eines „bösen“ Programmierers.
Übergeben Sie Heap-Zeiger
Bevor Sie erklären, was dies bedeutet, müssen Sie ein wenig philosophieren. Wenn Sie ablenken und nachdenken, macht jedes geschriebene Programm dasselbe. Es empfängt Daten, verarbeitet sie und sendet die verarbeiteten Daten an einen anderen Ort. Dies gilt unabhängig davon, ob Sie ein Abrechnungssystem schreiben, HTTP-Anforderungen akzeptieren und Webseiten zurückgeben oder sogar den Joystick überprüfen, um einen Klick auf eine Schaltfläche zu verfolgen - Programme verarbeiten die Daten.
Wenn wir die Programme auf diese Weise betrachten, ist es am wichtigsten sicherzustellen, dass wir leicht verstehen, wie die Daten konvertiert werden. Es empfiehlt sich daher, die Daten während des Programms so lange wie möglich unverändert zu lassen. Denn Daten, die sich nicht ändern, sind Daten, die leicht zu verfolgen sind.
In Go haben wir Referenztypen und Werttypen. Der Unterschied zwischen den beiden besteht darin, ob sich die Variable auf eine Kopie der Daten oder auf den Speicherort der Daten im Speicher bezieht. Zeiger, Slices, Maps, Kanäle, Schnittstellen und Funktionen sind Referenztypen, und alles andere ist ein Werttyp. Wenn Sie einer anderen Variablen eine Werttypvariable zuweisen, wird eine Kopie des Werts erstellt. Durch Ändern einer Variablen wird der Wert einer anderen Variablen nicht geändert.
Das Zuweisen einer Variablen eines Referenztyps zu einer anderen Variablen eines Referenztyps bedeutet, dass beide denselben Speicherbereich verwenden. Wenn Sie also die Daten ändern, auf die der erste zeigt, ändern Sie die Daten, auf die der zweite zeigt. Dies gilt sowohl für lokale Variablen als auch für Funktionsparameter.
func main() {
Kind Go-Entwickler möchten es einfacher machen zu verstehen, wie Daten gesammelt werden. Sie versuchen, den Wertetyp so oft wie möglich als Parameter für Funktionen zu verwenden. In Go gibt es keine Möglichkeit, Felder in Strukturen oder Funktionsparametern als endgültig zu markieren. Wenn eine Funktion Wertparameter verwendet, werden durch Ändern der Parameter die Variablen in der aufrufenden Funktion nicht geändert. Die aufgerufene Funktion kann lediglich den Wert an die aufrufende Funktion zurückgeben. Wenn Sie also eine Struktur ausfüllen, indem Sie eine Funktion mit Wertparametern aufrufen, können Sie keine Angst haben, Daten an die Struktur zu übertragen, da Sie verstehen, woher jedes Feld in der Struktur stammt.
type Foo struct { A int B string } func getA() int { return 20 } func getB(i int) string { return fmt.Sprintf("%d",i*2) } func main() { f := Foo{} fA = getA() fB = getB(fA)
Wie werden wir "böse"? Sehr einfach - dieses Modell umdrehen.
Anstatt Funktionen aufzurufen, die die gewünschten Werte zurückgeben, übergeben Sie einen Zeiger auf die Struktur in der Funktion und lassen sie Änderungen an der Struktur vornehmen. Da jede Funktion ihre eigene Struktur hat, können Sie nur herausfinden, welche Felder sich ändern, indem Sie den gesamten Code betrachten. Möglicherweise haben Sie auch implizite Abhängigkeiten zwischen Funktionen - die 1. Funktion überträgt die von der 2. Funktion benötigten Daten. Aber im Code selbst gibt nichts an, dass Sie zuerst die 1. Funktion aufrufen sollten. Wenn Sie Ihre Datenstrukturen auf diese Weise erstellen, können Sie sicher sein, dass niemand versteht, was Ihr Code tut.
type Foo struct { A int B string } func setA(f *Foo) { fA = 20 }
Panik taucht auf
Jetzt fangen wir an, mit Fehlern umzugehen. Sie denken wahrscheinlich, dass es schlecht ist, Programme zu schreiben, die Fehler zu 75% behandeln, und ich kann nicht sagen, dass Sie falsch liegen. Go-Code wird häufig mit einer Fehlerbehandlung von Kopf bis Fuß gefüllt. Und natürlich wäre es bequem, sie nicht so einfach zu verarbeiten. Fehler passieren, und der Umgang mit ihnen unterscheidet Profis von Anfängern. Eine verschwommene Fehlerbehandlung führt zu instabilen Programmen, die schwer zu debuggen und schwer zu warten sind. Manchmal bedeutet ein "guter" Programmierer "anstrengen".
func (dus DBUserService) Load(id int) (User, error) { rows, err := dus.DB.Query("SELECT name FROM USERS WHERE ID = ?", id) if err != nil { return User{}, err } if !rows.Next() { return User{}, fmt.Errorf("no user for id %d", id) } var name string err = rows.Scan(&name) if err != nil { return User{}, err } err = rows.Close() if err != nil { return User{}, err } return User{Id: id, Name: name}, nil }
Viele Sprachen wie C ++, Python, Ruby und Java verwenden Ausnahmen, um Fehler zu behandeln. Wenn etwas schief geht, lösen Entwickler in diesen Sprachen eine Ausnahme aus oder erwarten, dass Code damit umgeht. Natürlich erwartet das Programm, dass dem Client ein möglicher Fehler bekannt ist, der an einem bestimmten Ort ausgelöst wird, so dass es möglich ist, eine Ausnahme auszulösen. Da außer (ohne Wortspiel beabsichtigt) von Java geprüften Ausnahmen nichts in der Methodensignatur in Sprachen oder Funktionen darauf hinweist, dass eine Ausnahme auftreten kann. Woher wissen Entwickler, über welche Ausnahmen sie sich Sorgen machen müssen? Sie haben zwei Möglichkeiten:
- Erstens können sie den gesamten Quellcode aller Bibliotheken lesen, die ihr Code aufruft, und alle Bibliotheken, die die aufgerufenen Bibliotheken usw. aufrufen.
- Zweitens können sie der Dokumentation vertrauen. Ich bin vielleicht voreingenommen, aber aufgrund meiner persönlichen Erfahrung kann ich der Dokumentation nicht voll vertrauen.
Also, wie bringen wir dieses Böse zum Gehen? Natürlich Panik (
Panik ) und Genesung (
Genesung ) missbrauchen! Die Panik ist für Situationen wie "Das Laufwerk ist heruntergefallen" oder "Die Netzwerkkarte ist explodiert" ausgelegt. Aber nicht für solche - "jemand hat String statt int übergeben".
Leider geben andere, "weniger aufgeklärte Entwickler" Fehler aus ihrem Code zurück. Hier ist eine kleine Hilfsfunktion von PanicIfErr. Verwenden Sie es, um die Fehler anderer Entwickler in Panik zu versetzen.
func PanicIfErr(err error) { if err != nil { panic(err) } }
Sie können PanicIfErr verwenden, um die Fehler anderer Personen zu verpacken und Code zu komprimieren. Keine hässliche Fehlerbehandlung mehr! Jeder Fehler ist jetzt eine Panik. Es ist so produktiv!
func (dus DBUserService) LoadEvil(id int) User { rows, err := dus.DB.Query( "SELECT name FROM USERS WHERE ID = ?", id) PanicIfErr(err) if !rows.Next() { panic(fmt.Sprintf("no user for id %d", id)) } var name string PanicIfErr(rows.Scan(&name)) PanicIfErr(rows.Close()) return User{Id: id, Name: name} }
Sie können die Wiederherstellung irgendwo näher am Anfang des Programms platzieren, möglicherweise in Ihrer eigenen
Middleware . Und dann sagen Sie, dass Sie nicht nur Fehler verarbeiten, sondern auch den Code eines anderen sauberer machen. Böses durch Gutes tun ist die beste Art von Bösem.
func PanicMiddleware(h http.Handler) http.Handler { return http.HandlerFunc( func(rw http.ResponseWriter, req *http.Request){ defer func() { if r := recover(); r != nil { fmt.Println(", - .") } }() h.ServeHTTP(rw, req) } ) }
Nebenwirkungen einstellen
Als nächstes werden wir einen Nebeneffekt erzeugen. Denken Sie daran, dass der „gute“ Go-Entwickler verstehen möchte, wie die Daten das Programm durchlaufen. Der beste Weg, um zu wissen, was die Daten durchlaufen, besteht darin, explizite Abhängigkeiten in der Anwendung einzurichten. Selbst Entitäten, die derselben Schnittstelle entsprechen, können sich im Verhalten stark unterscheiden. Zum Beispiel ein Code, der Daten im Speicher speichert, und ein Code, der für dieselbe Arbeit auf die Datenbank zugreift. Es gibt jedoch Möglichkeiten, Abhängigkeiten in Go ohne explizite Aufrufe zu installieren.
Wie viele andere Sprachen bietet Go die Möglichkeit, Code auf magische Weise auszuführen, ohne ihn direkt aufzurufen. Wenn Sie eine Funktion namens init ohne Parameter erstellen, wird diese automatisch gestartet, wenn das Paket geladen wird. Und um noch weiter zu verwirren: Wenn in einer Datei mehrere Funktionen mit dem Namen init oder mehrere Dateien in einem Paket vorhanden sind, werden alle gestartet.
package account type Account struct{ Id int UserId int } func init() { fmt.Println(" !") } func init() { fmt.Println(" , init()") }
Init-Funktionen sind häufig mit leeren Importen verbunden. Go hat eine spezielle Methode zum Deklarieren von Importen, die wie "import _" github.com / lib / pq "aussieht. Wenn Sie eine leere Namenskennung für ein importiertes Paket festlegen, wird die init-Methode darin ausgeführt, es werden jedoch keine der Paketkennungen angezeigt. Bei einigen Go-Bibliotheken - wie Datenbanktreibern oder Bildformaten - müssen Sie diese laden, indem Sie den Import leerer Pakete aktivieren, um die Init-Funktion aufzurufen, damit das Paket seinen Code registrieren kann.
package main import _ "github.com/lib/pq" func main() { db, err := sql.Open( "postgres", "postgres://jon@localhost/evil?sslmode=disable") }
Und dies ist eindeutig eine „böse“ Option. Wenn Sie die Initialisierung verwenden, liegt Code, der magisch funktioniert, vollständig außerhalb der Kontrolle des Entwicklers. In Best Practices wird die Verwendung der Initialisierungsfunktionen nicht empfohlen. Dies sind nicht offensichtliche Funktionen, sie verwirren den Code und lassen sich leicht in der Bibliothek ausblenden.
Mit anderen Worten, Init-Funktionen sind ideal für unsere bösen Zwecke. Anstatt Entitäten explizit in Paketen zu konfigurieren oder zu registrieren, können Sie die Initialisierungs- und Leerimportfunktionen verwenden, um den Status Ihrer Anwendung zu konfigurieren. In diesem Beispiel stellen wir das Konto dem Rest der Anwendung über die Registrierung zur Verfügung, und das Paket selbst wird mithilfe der Init-Funktion in der Registrierung abgelegt.
package account import ( "fmt" "github.com/evil-go/example/registry" ) type StubAccountService struct {} func (a StubAccountService) GetBalance(accountId int) int { return 1000000 } func init() { registry.Register("account", StubAccountService{}) }
Wenn Sie ein Konto verwenden möchten, fügen Sie einen leeren Import in Ihr Programm ein. Es muss nicht der Haupt- oder verwandte Code sein - es muss nur "irgendwo" sein. Das ist Magie!
package main import ( _ "github.com/evil-go/example/account" "github.com/evil-go/example/registry" ) type Balancer interface { GetBalance(int) int } func main() { a := registry.Get("account").(Balancer) money := a.GetBalance(12345) }
Wenn Sie inits in Ihren Bibliotheken verwenden, um Abhängigkeiten zu konfigurieren, werden Sie sofort feststellen, dass andere Entwickler sich nicht sicher sind, wie diese Abhängigkeiten installiert wurden und wie sie geändert werden können. Und niemand wird klüger sein als Sie.
Komplizierte Konfiguration
Es gibt noch viel von allem, was wir mit der Konfiguration tun können. Wenn Sie ein "guter" Go-Entwickler sind, sollten Sie die Konfiguration vom Rest des Programms isolieren. In der Funktion main () erhalten Sie Variablen aus der Umgebung und konvertieren sie in die Werte, die für Komponenten benötigt werden, die explizit miteinander verknüpft sind. Ihre Komponenten wissen nichts über Konfigurationsdateien oder deren Eigenschaften. Für einfache Komponenten legen Sie öffentliche Eigenschaften fest, und für komplexere können Sie eine Factory-Funktion erstellen, die Konfigurationsinformationen empfängt und eine korrekt konfigurierte Komponente zurückgibt.
func main() { b, err := ioutil.ReadFile("account.json") if err != nil { fmt.Errorf("error reading config file: %v", err) os.Exit(1) } m := map[string]interface{}{} json.Unmarshal(b, &m) prefix := m["account.prefix"].(string) maker := account.NewMaker(prefix) } type Maker struct { prefix string } func (m Maker) NewAccount(name string) Account { return Account{Name: name, Id: m.prefix + "-12345"} } func NewMaker(prefix string) Maker { return Maker{prefix: prefix} }
Aber die "bösen" Entwickler wissen, dass es besser ist, die Informationen über die Konfiguration im gesamten Programm zu verteilen. Verwenden Sie anstelle einer Funktion in einem Paket, die die Namen und Werttypen für Ihr Paket definiert, eine Funktion, die die Konfiguration unverändert verwendet und selbst konvertiert.
Wenn dies zu "böse" erscheint, verwenden Sie die Init-Funktion, um die Eigenschaftendatei aus Ihrem Paket zu laden und die Werte selbst festzulegen. Es mag den Anschein haben, als hätten Sie anderen Entwicklern das Leben leichter gemacht, aber Sie und ich wissen ...
Mit der Init-Funktion können Sie neue Eigenschaften im hinteren Teil des Codes definieren, und niemand wird sie jemals finden, bis sie in Produktion gehen und alles abfällt, da etwas nicht in eine der Dutzenden von Eigenschaftendateien gelangt, die zum Ausführen benötigt werden. Wenn Sie noch mehr "böse Macht" wollen, können Sie vorschlagen, ein Wiki zu erstellen, um alle Eigenschaften in allen Bibliotheken zu verfolgen und regelmäßig neue zu "vergessen". Als Property Keeper sind Sie die einzige Person, die die Software ausführen kann.
func (m maker) NewAccount(name string) Account { return Account{Name: name, Id: m.prefix + "-12345"} } var Maker maker func init() { b, _ := ioutil.ReadFile("account.json") m := map[string]interface{}{} json.Unmarshal(b, &m) Maker.prefix = m["account.prefix"].(string) }
Funktionsrahmen
Schließlich kommen wir zum Thema Frameworks vs Bibliotheken. Der Unterschied ist sehr subtil. Es geht nicht nur um Größe; Sie können große Bibliotheken und kleine Frameworks haben. Das Framework ruft Ihren Code auf, während Sie den Bibliothekscode selbst aufrufen. Bei Frameworks müssen Sie Ihren Code auf eine bestimmte Weise schreiben, unabhängig davon, ob Sie Ihre Methoden nach bestimmten Regeln benennen oder ob sie bestimmten Schnittstellen entsprechen, oder Sie müssen Ihren Code im Framework registrieren. Frameworks haben ihre eigenen Anforderungen für Ihren gesamten Code. Das heißt, Frameworks befehlen Ihnen im Allgemeinen.
Go fördert die Verwendung von Bibliotheken, da Bibliotheken verknüpft sind. Obwohl natürlich jede Bibliothek erwartet, dass Daten in einem bestimmten Format übertragen werden, können Sie einen Verbindungscode schreiben, um die Ausgabe einer Bibliothek in eine Eingabe für eine andere umzuwandeln.
Es ist schwierig, Frameworks dazu zu bringen, nahtlos zusammenzuarbeiten, da jedes Framework die vollständige Kontrolle über den Codelebenszyklus wünscht. Oft besteht die einzige Möglichkeit, Frameworks zur Zusammenarbeit zu bringen, darin, dass die Framework-Autoren zusammenkommen und die gegenseitige Unterstützung klar organisieren.
Und der beste Weg, die „bösen Frameworks“ zu nutzen, um langfristig an Macht zu gewinnen, besteht darin, ein eigenes Framework zu schreiben, das nur innerhalb des Unternehmens verwendet wird.Gegenwärtiges und zukünftiges Übel
Wenn Sie diese Tricks gemeistert haben, werden Sie sich für immer auf den Weg des Bösen begeben. Im zweiten Teil werde ich zeigen, wie man all dieses "Böse" einsetzt und wie man den "guten" Code korrekt in "böse" umwandelt.