Child-Parent Communication in Elm: OutMsg vs Translator vs NoMap Patterns

Wanneer je app-iep begint te groeien, wil je hem in kleinere stukjes breken om te kunnen schalen. Ik heb dit behandeld in een andere blogpost: gestructureerd TodoMVC-voorbeeld met Elm.

Gedeeltelijk als deze schaling uiteindelijk de noodzaak inhoudt om een ​​MSG van een module naar de bovenliggende module te verzenden, zoals wanneer navigatieknoppen in een specifieke weergave een bericht naar de router op het hoogste niveau moeten verzenden.

Na enige tijd begon ik 3 verschillende patronen op te merken om dit af te handelen, en ik refacteerde Elm TodoMVC naar al die verschillende benaderingen in deze repository, zodat je ze naast elkaar kunt vergelijken.

Het OutMsg-patroon

Ik geloof dat Folkertdev de eerste was die ik zag schrijven over kind-oudercommunicatie in Elm, zijn blogpost legt deze benadering vrij goed uit.

Maar om samen te vatten, u retourneert in feite een extra waarde in uw updatefunctie. Dus in plaats van dit te retourneren:

(Model, Cmd Msg)

U retourneert dit:

(Model, Cmd Msg, OutMsg)

Vervolgens is de ouderupdatefunctie verantwoordelijk voor de afhandeling daarvan. Op deze manier hoeft het kind niets te weten over zijn ouder, maar moet de ouder weten over de OutMsgs van zijn kind.

Ik heb TodoMVC met deze aanpak geïmplementeerd. Maar als je een echte schaal hiervan wilt bekijken, heeft Richard Feldman het voorbeeld van de iep-spa op deze manier geïmplementeerd.

Een ander voorbeeld dat deze aanpak gebruikt, is iep-datepicker.

Het vertaalpatroon

Het Translator-patroon lijkt erg op dat van OutMsg, maar in plaats van dat de ouder op de hoogte is van de Msgs-typen van het kind, is het de ouder die doorgeeft welke Mgsgs zal worden gegenereerd, via een vertaler. Alex Lew legt zijn aanpak hier veel beter uit.

Kortom, je hebt een vertaler die zo'n record is:

type alias TranslationDictionary msg =
  {onInternalMessage: InternalMsg -> msg
  , onPlayerWin: Int -> msg
  , onPlayerLose: msg
  }

Ik heb TodoMVC ook met deze aanpak geïmplementeerd en ik geloof dat iep-autocomplete ook een goed voorbeeld is.

De iep-ouder-kind-update is een bibliotheek die u helpt met kind-ouder-update die dit patroon lijkt te volgen.

Het NoMap-patroon

Dit is iets wat ik merkte dat ik aan het doen was. Het basisidee is om Cmd.map en Html.map te vermijden, dus in plaats daarvan moet iedereen dezelfde taal spreken, met andere woorden, uw update- en weergavefuncties moeten het hoogste MSG-type retourneren.

Hiermee heb je waarschijnlijk Msgs zoals MsgForLogin, MsgForRouter, etc, dus in je View zou je zoiets doen als:

knop [onClick (MsgForLogin SignUp)] []

Dit is hoe ik TodoMVC voor het eerst refacteerde, in feite, de eerste keer dat ik OutMsg zag, begreep ik de reden niet, omdat ik mijn MSG's niet in kaart bracht.

Bekijk de bliksem-praat-app voor een groter voorbeeld met deze aanpak. Deze app lijkt ook de manier van Kris Jenkins te volgen om Elm-apps te structureren, wat deze benadering begunstigt als hij de Msgs-typen in een bestand Types.elm scheidt.

De elm-taco-bibliotheek gebruikt nogal een combinatie van OutMsg- en NoMap-patronen door een "taco" op het hoogste niveau te hebben waarnaar u berichten kunt sturen.

Observaties en vergelijkingen

Tijdens het onderzoeken en refactoren voor die patronen, merkte ik iets op, wat voor- of nadelen kunnen zijn, afhankelijk van uw behoeften:

  • Op NoMap blijft de updatefunctie van de ouder ongeveer hetzelfde als uw app groeit, terwijl op OutMsg en Translate de updatefunctie van de ouder erg groot kan worden, omdat u de OutMsg van elk kind moet verwerken (voorbeeld)
  • Op OutMsg en Translate hoeven de geneste modules niets van de bovenliggende ouders te importeren, waardoor ze meer ingekapseld zijn, het zou gemakkelijker zijn om een ​​submodule uit te pakken en te publiceren als een bibliotheek bijvoorbeeld
  • Om NoMap te laten werken, moeten uw MSG's in een afzonderlijk bestand van Update leven, anders heeft u een afhankelijkheidslus. Dit is een goede reden om dingen te splitsen, maar tegelijkertijd slecht als je voor elke module een enkel bestand wilt hebben (Home.elm, Login.elm, Router.elm)
  • In NoMap is het eenvoudiger om Msgs ergens anders heen te sturen, maar het kan moeilijker zijn om alle statusveranderingen die hierdoor worden veroorzaakt te volgen.
  • Zoals gemeten op het moment van dit schrijven, voor de TodoMVC-refactors, heeft de NoMap-aanpak 546 LOC, OutMsg 561 en Translator 612 als dit belangrijk voor u is
  • Op NoMap moet je uiteindelijk de _ catch-all case gebruiken om Msgs te negeren van andere plaatsen die je niet wilt verwerken, dus er is minder hulp van de compiler, het kan niet vertellen wat je mist (bedankt voor @mordrax voor het aanwijzen dat op elm slack)
  • In OutMsg en Translator kun je gewoon naar de typen of vertalers kijken om te ontdekken welke kind-oudercommunicatie nodig is, zodat de compiler je kan begeleiden bij het implementeren daarvan, terwijl op NoMap deze communicatie implicieter is
  • De vertalerbenadering lijkt een goed idee om uw eigen Msgs aan een externe component te geven, zoals iep-autocomplete
  • Ik vond het vertaalpatroon moeilijk te volgen met moeilijker te begrijpen foutmeldingen van Elm compiler tijdens het bouwen
  • Als u de standaard (Model, Cmd Msg) niet wijzigt, kunt u de bibliotheek voor fijne iep-retour gebruiken
  • Sommige mensen beschouwen Html.map niet als een goede gewoonte om te voorkomen dat er "componenten" worden gemaakt
  • Je kunt veel voordeel halen uit het combineren van die benaderingen, je kunt bijvoorbeeld Html.map gewoon vermijden voor views, terwijl je nog steeds OutMsg gebruikt voor updates, of je kunt NoMap alleen gebruiken voor de hoogste Msgs, met OutMsgs hieronder, tijdens het renderen van een externe vertaalde component

Middelen

Ik geloof dat communicatie tussen kinderen en ouders vaak belangrijker is bij het schalen en SPA's doen, daarom vond ik veel dingen die ik las over deze reddit-thread over Scaling Elm Apps:

Proost!