Im Idealfall ist es natürlich besser, überhaupt nicht zu viel Code zu schreiben . Und wenn Sie schreiben, müssen Sie, wie Sie wissen, gut denken Knochensystem Systemarchitektur und implementieren Fleischsystem Systemlogik. In dieser Notiz geben wir Rezepte für die bequeme Implementierung der letzteren.
Wir werden Beispiele für die Clojure-Sprache geben, das Prinzip selbst kann jedoch auch in anderen funktionalen Programmiersprachen angewendet werden (zum Beispiel verwenden wir in Erlang genau dieselbe Idee).
Idee
Die Idee selbst ist einfach und basiert auf folgenden Aussagen:
- Jede Logik besteht immer aus elementaren Schritten.
- Für jeden Schritt werden bestimmte Daten benötigt, auf die sie ihre Logik anwenden und entweder ein erfolgreiches oder ein erfolgloses Ergebnis erzielen.
Auf der Pseudocode-Ebene kann dies wie folgt dargestellt werden:
do-something-elementary(context) -> [:ok updated_context] | [:error reason]
Wo:
do-something-elementary
- Funktionsname;context
- Funktionsargument, Datenstruktur mit dem Anfangskontext, aus dem die Funktion alle erforderlichen Daten entnimmt;updated_context
- Datenstruktur mit aktualisiertem Kontext, falls erfolgreich, wobei die Funktion das Ergebnis ihrer Ausführung hinzufügt;reason
- Datenstruktur, Fehlerursache, mit Fehler.
Das ist die ganze Idee. Und dann - eine Frage der Technologie. Mit 100.500 Millionen Teilen.
Beispiel: Benutzer, der einen Kauf tätigt
Wir werden die Details auf ein konkretes einfaches Beispiel schreiben, das hier auf GitHub verfügbar ist .
Nehmen wir an, wir haben Benutzer mit Geld und Losen, die Geld kosten und die Benutzer kaufen können. Wir möchten einen Code schreiben, der den Kauf des Loses durchführt:
buy-lot(user_id, lot_id) -> [:ok updated_user] | [:error reason]
Der Einfachheit halber speichern wir den Geldbetrag und viele Benutzer in der Benutzerstruktur.
Für die Implementierung benötigen wir mehrere Hilfsfunktionen.
Funktion until-first-error
In den allermeisten Fällen kann die Geschäftslogik als eine Folge von Schritten dargestellt werden, die ausgeführt werden müssen, bis ein Fehler aufgetreten ist. Dazu erstellen wir eine Funktion:
until-first-error(fs, init_context) -> [:ok updated_context] | [:error reason]
Wo:
fs
ist eine Folge von Funktionen (Elementaraktionen);init_context
ist der anfängliche Kontext.
Die Implementierung dieser Funktion kann hier auf GitHub eingesehen werden .
Funktion with-result-or-error
Sehr oft besteht eine elementare Aktion darin, dass Sie nur eine Funktion ausführen und, wenn sie erfolgreich war, das Ergebnis zum Kontext hinzufügen müssen. Dazu haben wir eine Funktion:
with-result-or-error(f, key, context) -> [:ok updated_context] | [:error reason]
Im Allgemeinen besteht der einzige Zweck dieser Funktion darin, die Codegröße zu reduzieren.
Und schließlich unsere "Schönheit" ...
Die Funktion, die den Kauf implementiert
1. (defn buy-lot [user_id lot_id] 2. (let [with-lot-fn (partial 3. util/with-result-or-error 4. #(lot-db/find-by-id lot_id) 5. :lot) 6. 7. buy-lot-fn (fn [{:keys [lot] :as ctx}] 8. (util/with-result-or-error 9. #(user-db/update-by-id! 10. user_id 11. (fn [user] 12. (let [wallet_v (get-in user [:wallet :value]) 13. price_v (get-in lot [:price :value])] 14. (if (>= wallet_v price_v) 15. (let [updated_user (-> user 16. (update-in [:wallet :value] 17. - 18. price_v) 19. (update-in [:lots] 20. conj 21. {:lot_id lot_id 22. :price price_v}))] 23. [:ok updated_user]) 24. [:error {:type :invalid_wallet_value 25. :details {:code :not_enough 26. :provided wallet_v 27. :required price_v}}])))) 28. :user 29. ctx)) 30. 31. fs [with-lot-fn 32. buy-lot-fn]] 33. 34. (match (util/until-first-error fs {}) 35. 36. [:ok {:user updated_user}] 37. [:ok updated_user] 38. 39. [:error reason] 40. [:error reason])))
Lassen Sie uns den Code durchgehen:
- S. 34:
match
ist ein Makro zum Abgleichen von Werten aus einer Vorlage aus der Bibliothek clojure.core.match
. - S. 34-40: Wir wenden die versprochene Funktion bis zum
until-first-error
auf die Elementarschritte fs
, nehmen die benötigten Daten aus dem Kontext und geben sie zurück oder werfen den Fehler auf; - S. 2-5: Wir erstellen die erste elementare Aktion (auf die nur der aktuelle Kontext angewendet wird), bei der einfach Daten zum Schlüssel hinzugefügt werden
:lot
zum aktuellen Kontext; - S. 7-29: Hier verwenden wir die bekannte Funktion
with-result-or-error
, aber die Aktion, die sie umschließt, erwies sich als etwas kniffliger: In einer Transaktion überprüfen wir, ob der Benutzer über genügend Geld verfügt, und tätigen bei Erfolg einen Kauf (weil Standardmäßig ist unsere Anwendung Multithread-fähig (Und wer hat irgendwo das letzte Mal eine Single-Threaded-Anwendung gesehen?) und darauf müssen wir vorbereitet sein).
Und ein paar Worte zu den anderen Funktionen, die wir verwendet haben:
lot-db/find-by-id(id)
- gibt viel nach id
;user-db/update-by-id!(user_id, update-user-fn)
- wendet die Funktion update-user-fn
auf user user_id
(in einer imaginären Datenbank) an.
Und zu testen? ...
Testen Sie diese Beispielanwendung von clojure REPL. Wir starten REPL von der Konsole aus im Projektstamm:
lein repl
Was sind unsere Benutzer mit Finanzen:
context-aware-app.core=> (context-aware-app.user.db/enumerate) [:ok ({:id "1", :name "Vasya", :wallet {:value 100}, :lots []} {:id "2", :name "Petya", :wallet {:value 100}, :lots []})]
Was wir haben viel (Waren):
context-aware-app.core=> (context-aware-app.lot.db/enumerate) [:ok ({:id "1", :name "Apple", :price {:value 10}} {:id "2", :name "Banana", :price {:value 20}} {:id "3", :name "Nuts", :price {:value 80}})]
Vasya kauft einen Apfel:
context-aware-app.core=>(context-aware-app.processing/buy-lot "1" "1") [:ok {:id "1", :name "Vasya", :wallet {:value 90}, :lots [{:lot_id "1", :price 10}]}]
Und die Banane:
context-aware-app.core=> (context-aware-app.processing/buy-lot "1" "2") [:ok {:id "1", :name "Vasya", :wallet {:value 70}, :lots [{:lot_id "1", :price 10} {:lot_id "2", :price 20}]}]
Und die "Nüsse":
context-aware-app.core=> (context-aware-app.processing/buy-lot "1" "3") [:error {:type :invalid_wallet_value, :details {:code :not_enough, :provided 70, :required 80}}]
Es gab nicht genug Geld für Nüsse.
Insgesamt
Infolgedessen gibt es bei kontextbezogener Programmierung keine großen Codeteile mehr (die nicht in einen Bildschirm passen) sowie „lange Methoden“, „große Klassen“ und „lange Listen von Parametern“. Und es gibt:
- Zeit sparen beim Lesen und Verstehen des Codes;
- Vereinfachtes Testen des Codes
- die Möglichkeit, den Code wiederzuverwenden (einschließlich der Verwendung von Copy-Paste + Dopilivanie-Datei);
- Vereinfachung des Code-Refactorings.
Das heißt, alles was wir lieben und üben.