Ich habe ungefähr 20-40 Stunden damit verbracht, einen Teil eines Spiels mit JavaScript und HTML5 Canvas zu entwickeln. Als ich anfing, hatte ich keine Ahnung, was ich tat. Es begann also als Proof of Concept und kommt jetzt gut voran, aber es gibt keine automatisierten Tests. Das Spiel wird langsam so komplex, dass es von einigen automatisierten Tests profitieren könnte, aber es scheint schwierig zu sein, da der Code von der Mutation des globalen Zustands abhängt.
Ich möchte das Ganze mit Underscore.js , einer funktionalen Programmierbibliothek für JavaScript, überarbeiten. Ein Teil von mir meint, ich sollte einfach bei Null anfangen und einen funktionalen Programmierstil verwenden und testen. Ich denke jedoch, dass die Umgestaltung des imperativen Codes in deklarativen Code eine bessere Lernerfahrung und eine sicherere Möglichkeit sein könnte, zu meinem aktuellen Funktionsstatus zu gelangen.
Das Problem ist, ich weiß, wie mein Code am Ende aussehen soll, aber ich weiß nicht, wie ich meinen aktuellen Code in ihn umwandeln soll. Ich hoffe, einige Leute hier könnten mir einige Tipps zum Refactoring-Buch und zum effektiven Arbeiten mit Legacy-Code geben.
Zum Beispiel denke ich als ersten Schritt darüber nach, den globalen Staat zu "verbieten". Nehmen Sie jede Funktion, die eine globale Variable verwendet, und übergeben Sie sie stattdessen als Parameter.
Der nächste Schritt kann darin bestehen, die Mutation zu "verbieten" und immer ein neues Objekt zurückzugeben.
Jeder Rat wäre dankbar. Ich habe noch nie OO-Code genommen und ihn in Funktionscode umgestaltet.
quelle
Antworten:
Nachdem ich mehrere Projekte in funktionalem JavaScript verfasst habe, kann ich meine Erfahrungen teilen. Ich werde mich auf allgemeine Überlegungen konzentrieren, nicht auf spezifische Implementierungsprobleme. Denken Sie daran, es gibt keine Silberkugeln. Tun Sie, was für Ihre spezielle Situation am besten funktioniert.
Pure Functional vs Functional Style
Überlegen Sie, was Sie von diesem funktionalen Refactoring profitieren möchten. Möchten Sie wirklich eine reine funktionale Implementierung oder nur einige der Vorteile, die ein funktionaler Programmierstil bieten kann, wie z. B. referenzielle Transparenz?
Ich habe den letzteren Ansatz gefunden, den ich als funktionalen Stil bezeichne, der sich gut in JavaScript einfügt und normalerweise das ist, was ich will. Es ermöglicht auch die sorgfältige Verwendung selektiver nicht funktionaler Konzepte in Code, wo sie sinnvoll sind.
Das Schreiben von Code in einem rein funktionalen Stil ist auch in JavaScript möglich, jedoch nicht immer die am besten geeignete Lösung. Und Javascript kann niemals rein funktional sein.
Funktionsstil-Schnittstellen
Im funktionalen Stil von Javascript ist die Grenze zwischen funktionalem und imperativem Code die wichtigste Überlegung. Es ist wichtig, diese Grenze klar zu verstehen und zu definieren.
Ich organisiere meinen Code um funktionale Schnittstellen. Hierbei handelt es sich um Sammlungen von Logik, die sich in einem funktionalen Stil verhalten und von einzelnen Funktionen bis zu ganzen Modulen reichen. Die konzeptionelle Trennung von Schnittstelle und Implementierung ist wichtig. Eine Schnittstelle im funktionalen Stil kann zwingend implementiert werden, solange die zwingende Implementierung nicht über die Grenze hinaus leckt.
Leider liegt die Last für die Durchsetzung von Schnittstellen für funktionale Stile in Javascript vollständig beim Entwickler. Auch Sprachmerkmale wie Ausnahmen machen eine wirklich saubere Trennung unmöglich.
Da Sie eine vorhandene Codebasis umgestalten, identifizieren Sie zunächst vorhandene logische Schnittstellen in Ihrem Code, die sauber in einen funktionalen Stil verschoben werden können. Eine überraschende Menge an Code kann bereits funktional sein. Gute Ausgangspunkte sind kleine Schnittstellen mit wenigen zwingenden Abhängigkeiten. Jedes Mal, wenn Sie über Code wechseln, werden vorhandene zwingende Abhängigkeiten beseitigt und es kann mehr Code verschoben werden.
Iterieren Sie weiter und arbeiten Sie schrittweise nach außen, um große Teile Ihres Projekts auf die funktionale Seite der Grenze zu verschieben, bis Sie mit den Ergebnissen zufrieden sind. Dieser inkrementelle Ansatz ermöglicht das Testen einzelner Änderungen und erleichtert das Identifizieren und Durchsetzen der Grenze.
Algorithmen, Datenstrukturen und Design überdenken
Imperative Lösungen und Designmuster sind im funktionalen Stil oft nicht sinnvoll. Insbesondere für Datenstrukturen können direkte Ports von imperativem Code zum Funktionsstil hässlich und langsam sein. Ein einfaches Beispiel: Möchten Sie lieber jedes Element einer Liste für ein Voranstellen kopieren oder das Element in die vorhandene Liste aufnehmen?
Erforschung und Bewertung bestehender funktionaler Problemansätze. Funktionale Programmierung ist mehr als nur eine Programmiertechnik, sie ist eine andere Art, über Probleme nachzudenken und sie zu lösen. Projekte, die in den traditionelleren funktionalen Programmiersprachen wie lisp oder haskell geschrieben wurden, sind in dieser Hinsicht eine große Ressource.
Verstehe die Sprache und die Builtins
Dies ist ein wichtiger Teil des Verständnisses der funktionalen Imperativgrenze und wirkt sich auf das Schnittstellendesign aus. Verstehen Sie, welche Funktionen im funktionalen Stilcode sicher verwendet werden können und welche nicht. Alles, von dem Builtins Ausnahmen auslösen können und die
[].push
Objekte mutieren, zu Objekten, die als Referenz übergeben werden, und wie Prototypenketten funktionieren. Ein ähnliches Verständnis aller anderen Bibliotheken, die Sie in Ihrem Projekt verwenden, ist ebenfalls erforderlich.Dieses Wissen ermöglicht fundierte Entwurfsentscheidungen, z. B. wie mit fehlerhaften Eingaben und Ausnahmen umgegangen werden soll, oder was passiert, wenn eine Rückruffunktion etwas Unerwartetes bewirkt? Einige davon können im Code erzwungen werden, andere erfordern lediglich eine Dokumentation zur ordnungsgemäßen Verwendung.
Ein weiterer Punkt ist, wie streng Ihre Implementierung sein sollte. Möchten Sie wirklich versuchen, das korrekte Verhalten bei maximalen Call-Stack-Fehlern zu erzwingen, oder wenn der Benutzer eine integrierte Funktion neu definiert?
Verwenden von nicht funktionalen Konzepten, wo dies angemessen ist
Funktionale Ansätze sind nicht immer angemessen, insbesondere in einer Sprache wie JavaScript. Die rekursive Lösung kann elegant sein, bis Sie maximale Call-Stack-Ausnahmen für größere Eingaben erhalten. Daher wäre möglicherweise ein iterativer Ansatz besser. Ebenso verwende ich die Vererbung für die Codeorganisation, die Zuweisung in Konstruktoren und unveränderliche Objekte, wenn sie sinnvoll sind.
Solange die funktionalen Schnittstellen nicht verletzt werden, tun Sie, was Sinn macht und was am einfachsten zu testen und zu warten ist.
Beispiel
Hier ist ein Beispiel für die Konvertierung eines sehr einfachen realen Codes in einen funktionalen Stil. Ich habe kein sehr gutes großes Beispiel für diesen Prozess, aber dieses kleine berührt einige interessante Punkte.
Dieser Code erstellt einen Versuch aus einer Zeichenfolge. Ich beginne mit einem Code, der auf dem oberen Abschnitt von John Resigs Trie-Js Build-Trie- Modul basiert . Viele Vereinfachungen / Formatierung Änderungen wurden vorgenommen , und meine Absicht ist nicht zu kommentieren die Qualität des Original - Code (Es gibt viel klarer und saubere Weise ein Trie zu bauen, also warum diese c Artcode kommt zuerst in Google interessant ist. Hier ein ist schnelle Implementierung, die reduzieren verwendet ).
Ich mag dieses Beispiel, weil ich es nicht bis zu einer echten funktionalen Implementierung führen muss, sondern nur zu funktionalen Schnittstellen. Hier ist der Ausgangspunkt:
Stellen wir uns vor, dies sei das gesamte Programm. Dieses Programm führt zwei Dinge aus: Konvertieren einer Zeichenfolge in eine Liste von Wörtern und Erstellen eines Versuchs aus der Liste von Wörtern. Dies sind die vorhandenen Schnittstellen im Programm. Hier ist unser Programm mit den Schnittstellen deutlicher ausgedrückt:
Nur durch die Definition unserer Schnittstellen können wir
_get_words
zur funktionalen Stilseite der Grenze wechseln . Weder nochreplace
odersplit
ändert die ursprüngliche Zeichenfolge. Und unsere öffentliche Oberflächebuild_trie
ist auch funktional, obwohl sie mit einem sehr wichtigen Code interagiert. Tatsächlich ist dies in den meisten Fällen ein guter Haltepunkt. Mehr Code würde überwältigend werden, also lassen Sie mich ein paar andere Änderungen überblicken.Stellen Sie zunächst alle Schnittstellen in einen funktionalen Stil. Dies ist in diesem Fall trivial.
_build_trie_from_list
Geben Sie einfach ein Objekt zurück, anstatt eines zu mutieren.Umgang mit fehlerhaften Eingaben
Überlegen Sie, was passiert, wenn wir
build_trie
mit einer Reihe von Zeichen aufrufenbuild_trie(['a', ' ', 'a', 'b', 'c', ' ', 'f'])
. Der Aufrufer ging davon aus, dass sich dies in einem funktionalen Stil verhalten würde, löst jedoch eine Ausnahme aus, wenn.replace
er für das Array aufgerufen wird. Dies kann das beabsichtigte Verhalten sein. Oder wir könnten explizite Typprüfungen durchführen und sicherstellen, dass die Eingaben wie erwartet sind. Aber ich schreibe lieber Enten.Strings sind nur Arrays von Zeichen und Arrays sind nur Objekte mit einer
length
Eigenschaft und nicht negativen Ganzzahlen als Schlüssel. Also , wenn wir Refactoring und schrieb neuereplace
undsplit
Methoden , die auf Array-ähnliche Objekte von Strings funktioniert, sie haben nicht einmal Strings sein müssen, könnte unser Code tun genau das Richtige. (String.prototype.*
funktioniert hier nicht, es konvertiert die Ausgabe in einen String). Das Tippen von Enten ist völlig unabhängig von der funktionalen Programmierung, aber der größere Punkt ist, dass schlechte Eingaben immer berücksichtigt werden sollten.Grundlegende Neugestaltung
Es gibt auch grundlegendere Überlegungen. Angenommen, wir möchten den Trie auch im funktionalen Stil bauen. Im imperativen Code bestand der Ansatz darin, den Versuch wortweise zu konstruieren. Ein direkter Port würde uns jedes Mal den gesamten Versuch kopieren lassen, wenn wir eine Einfügung vornehmen müssen, um zu vermeiden, dass Objekte mutieren. Das wird natürlich nicht funktionieren. Stattdessen könnte der Versuch Knoten für Knoten von unten nach oben konstruiert werden, so dass ein Knoten, sobald er fertiggestellt ist, nie wieder berührt werden muss. Oder ein anderer Ansatz ist möglicherweise besser. Eine schnelle Suche zeigt viele vorhandene funktionale Implementierungen von Versuchen.
Hoffe, das Beispiel hat die Dinge ein wenig geklärt.
Insgesamt finde ich, dass das Schreiben von funktionalem Stilcode in Javascript ein lohnendes und unterhaltsames Unterfangen ist. Javascript ist eine überraschend leistungsfähige funktionale Sprache, wenn auch im Standardzustand zu ausführlich.
Viel Glück bei Ihrem Projekt.
Einige persönliche Projekte im funktionalen Stil Javascript:
quelle