Alfons Mittelmeyer hat geschrieben: [...] ob diese zentralen Signalsysteme global zur Verfügung stehen dürfen oder ob man sie überall als Parameter mit übergeben soll?
Das kommt darauf an, welche Architektur so ein Programm hat. Man könnte sowas mittels Dependency Injection lösen, also mittels Parameter, die man dann überall mitschleift. Man könnte daraus ein Parameter-Objekt machen, das diese ganzen Sub-Systeme zu einer einzigen Struktur zusammen fasst. Damit würde man aber den Grad der Isolation abschwächen, da dann jede Funktion, die dieses Objekt übergeben bekommt, auf jedes beliebige Sub-System zugreifen könnte. Wenn man genügend Disziplin besitzt, muss das aber kein Problem sein.
Letzteres hatte ich mal testweise in Clojure umgesetzt. Dabei repräsentierten Components (Services) die Aussenwelt, also zB. Datenbanken, und aller veränderbare Zustand war ausschließlich dort zuhause. Die Geschäftslogik war rein funktional und eventbasiert. Das muss man sich so vorstellen: ein Command (ein dummes Dictionary, das ein Type-Tag und alle relevanten Daten enthält) wird empfangen, dazu wird eine passende Handler-Funktion gesucht und aufgerufen. Diese Funktion erzeugt dann aus den Daten im Command und dem aktuellen Zustand der Welt eine Liste von Events, die sie als Ergebnis zurückgibt. Sonst macht diese Handler-Funktion nichts, dh., sie ändert nicht den Zustand der Welt. Die Ergebnis-Events werden dann über einen Eventbus verschickt. Event-Handler können Events vom Bus abonnieren und werden aufgerufen, sobald ein entsprechender Event eintritt. Diese Handler erst dürfen den Zustand der Welt verändern, indem sie zB. Daten in eine Datenbank schreiben.
Zusätzlich habe ich Dependency Injection in den Command-Handlern mittels der Reader-Monade realisiert und diese zur Fehlerbehandlung in einen Error-Monad-Transformer gewickelt. Das sieht dann etwa so aus:[codebox=clojure file=Unbenannt.txt](def-command ::customer/place-order
:req [::customer/id
::order/id])
(def-failure ::customer/has-given-no-address
:req [::customer/id])
(def-failure ::customer/cart-is-empty
:req [::customer/id])
(def-failure ::customer/has-selected-no-payment-method
:req [::customer/id])
(defn subscriptions
[component]
{::order/placed
(store-in (:repository component) ::customer/id)
::customer/place-order
(fn [{customer-id ::customer/id
order-id ::order/id}]
(within (boundary component ::customer/shop)
(fail-if-exists ::order/id order-id)
customer <- (get-entity ::customer/id customer-id)
(mdo-await*
(mwhen (-> @customer ::customer/cart empty?)
(fail-with ::customer/cart-is-empty
::customer/id customer-id))
(mwhen (-> @customer ::customer/address nil?)
(fail-with ::customer/has-given-no-address
::customer/id customer-id))
(mwhen (-> @customer ::customer/payment-method nil?)
(fail-with ::customer/has-selected-no-payment-method
::customer/id customer-id)))
(return [(event ::order/placed
::customer/id customer-id
::order/id order-id
::order/items (@customer ::customer/cart)
::order/address (@customer ::customer/address)
::order/payment-method (@customer ::customer/payment-method))
(event ::customer/cart-cleared
::customer/id customer-id)])))})[/code]
In der Handler-Funktion für
::command/place-order werden nirgends explizit Repositories, Datenbanken, etc. erwähnt, obwohl deren Informationen dort verwendet werden. Wie monadische Funktionen funktionieren kann ich allerdings hier nicht erklären. Monaden kann man IMO überhaupt nicht erklären. Man kann sie auch nicht verstehen. Man kann sich nur an sie gewöhnen.
In specifications, Murphy's Law supersedes Ohm's.