Bullet-Proof Fusion

Bullet-Proof Fusion

You can watch this post also on YouTube:


Warum ist es wichtig, den schwierigen Weg der Einfachheit zu gehen? Hört sich philosophisch an? Keine Angst, wir bleiben immer schön beim Thema: Weniger schlechten Code schreiben.

Als erstes möchte ich dir was zeigen:

Vor ein paar Jahren überlegte ich mir ein elektrisches Skateboard zu kaufen – ich hatte schon eins bestellt. Aber dann wurde ich auf das OneWheel aufmerksam. Das Einzigartige dabei ist, dass ich keine Fernbedienung benötige, keine App, kann über Stock und Stein floaten und es fühlt sich an, als wäre es eine Erweiterung von meinem Körper.

Dabei heisst das nicht, das dieses Art von Bedienung einfacher zu erstellen ist – im Gegenteil. Bei einem Skateboard ein Motor darunter zu packen und mit einer Fernbedienung die Geschwindigkeit zu steuern ist sicher einfacher zu bauen als ein Board mit einem Rad, welches selber balancieren muss und durch Gewichtsverlagerung gesteuert wird. Das ist um ein vielfaches komplizierter – aber die Bedienung ist intuitiver und macht unendlich mehr Spass.

Ein weiteres Beispiel ist die neue Nespresso Maschine:

Anstatt mehrere Knöpfe, wo eine Taste eine Art von Kaffee brühen kann und nebenbei noch Fehler zulässt (Einen Espresso in Lungo Menge erstellen), gibt es nur noch eine Taste: Die Maschine liesst einen Strichcode auf der Kapsel und kann so alle Attribute anpassen: 5 Tassengrössen (Von Espresso bis hin zu einer ganzen Karaffe) über Temperatur, Brühzeit: Es wird alles auf den für den verwendeten Kaffee perfektioniert.

Andere Frage: Warum war der iPod erfolgreich? Weil er es zum ersten Mal einfach machte, grosse Musikbibliotheken zu durchstöbern. Warum hat das iPhone eine neue Ära bei den Mobiltelefonen eingeläutet? Es gab doch schon den Nokia Communicator! Weil es einfach zu bedienen war. Das Gleiche bei Google. Ein Suchfeld. Fertig.

Einfache Interfaces sind erfolgreicher und machen mehr Spass als komplizierte. Wenn was weggelassen werden kann ist es ein Gewinn – für den Benutzer wie für den Ersteller des Interfaces.

Wie sagte schon Albert Einstein:

Man muß die Dinge so einfach wie möglich machen.
Aber nicht einfacher.

Take Create it easy

Aber Interfaces sind ja nicht nur Benutzeroberflächen – es sind ebenso Schnittpunkte in der Entwicklung. In der PHP Welt gibt es Interfaces, ebenso im Fusion: Dort sind es die Properties, welche direkt in der Komponente auf der obersten Ebene liegen:

prototype(Foo.Bar:Atom.Link) < prototype(Neos.Fusion:Component) {
	// primary, secondary or link
	appearance = 'secondary'
	href = null
	class = null
	content = null
    
	@if.hasNeededProps = ${this.href && this.content}
  
	renderer = Neos.Fusion:Component {
		@apply.props = ${props}
		appearanceClasses = Neos.Fusion:Match {
			@subject = ${props.appearance}
			@default = 'text-red-600'
			primary = 'text-white bg-red-600'
			secondary = 'text-red-700 bg-red-100'
		}
	
		finalClass = ${['inline-flex items-center justify-center py-2 px-4', props.appearanceClasses, Type.isArray(props.class) ? Array.join(Array.filter(props.class), ' ') : props.class]}
    
		renderer = afx`
			<a href={props.href} class={props.finalClass}>
			    {props.content}
			</a>
		`
	}
}

appearance, href, class und content bilden das Interface. linkClasses und finalClass hingegen nicht. Es ist egal, wie du in Zukunft es innerhalb der Komponente handhabst, solange das Interface das Gleiche bleibt, bist du save. Dabei gilt: So wenig Properties wie möglich, so viel wie nötig.

Ein weiteres gutes Beispiel siehts du im Flowpack.Listable: Dabei wird geprüft, ob collection eine Instanz von Elasticsearch ist. Falls ja, wird dieses verwendet und ist damit performanter, anderenfalls wird FlowQuery verwendet. Super, oder? Keine Property, bei dem ich angeben muss was ich verwende. Es wird einfach mit dem Eel Helper Type.instance() geprüft was vorhanden ist.

Es ist wie gesagt nicht einfach, es einfach zu machen. Aber Andere (und du dir selber zwei Wochen später) werden es dir danken.

Was ist kompliziert?

Alles was schwer verständlich oder veränderbar ist, ist kompliziert. Erkennen tut man das wenn man ständig was ändern muss oder einfach Fehler auftreten, wo sich niemand erklären kann.

Abhängigkeiten und Unklarheit sind meistens die Ursachen für Komplexität.

Modularisierung

Zuerst müssen wir definieren was ein Modul ist. Im Kontext von Fusion ist das ein Eel-Helper, eine FlowQuery Operation, ein Fusion Prototype oder ein ganzes Package.

Generell gibt es flache und tiefe Module. Ein tiefes Modul hat eine kleine Schnittstelle und eine grosse Implementierung. Beim flachen Modul ist es umgekehrt.

Tiefe Module sind generell zu bevorzugen, weil sie mehr Informationen hinter ihrer Schnittstelle verstecken. Dies führt zu einer besseren Informationsverschleierung. Ein Modul, welches automatisch auf verschiedene Gegebenheiten ohne Konfiguration reagieren kann, ist sicher besser als ein Modul, wo alles mögliche immer definiert werden muss. Ein Modul, zum Beispiel ein Teaser Prototype, welcher für News, Blog Einträge und normale Dokumente eingesetzt werden kann ist ebenfalls besser als für jeden Fall ein eigner Prototype zu generieren. Solange die Schnittstelle, nennen wir es API noch zusammenpassen und sich diese nicht zu stark unterscheiden. Ein wichtiger Baustein hierzu ist die Trennung von Präsentation und Integration – dazu aber später mehr.

Wann sollten Module zusammengeführt und wann getrennt werden? Auch hier kannst du die Regeln von vorhin anwenden: Wenn ein Zusammenführen zu einer einfacheren Schnittstelle (und damit zu einem Modul mit größerer Tiefe) führt, sollte dies bevorzugt werden. Wenn ein Modul verschiedene Abstraktionen vermischt, sollten Module auseinandergeteilt werden: Wenn sich Allzweck- und Spezialfunktionalität im selben Modul befinden, sollte dieses aufgespalten werden. Allgemeinere Funktionalität sollte nach unten (in die unteren Schichten) und speziellere Funktionalität nach oben (in die oberen Schichten) gezogen werden.

Es heisst ja: "Ein Modul sollte eine Sache tun." Das kann man folgendermassen erweitern: Ein Modul sollte eine Sache tun, aber vollständig.

Ein Modul sollte eine Sache tun, aber vollständig.

Das kann dir eine Orientierung geben welche Module und Funktionen zusammengeführt werden sollten und welche nicht.

Es kommt drauf an

Die am meisten verwendete Antwort. Das Gleiche im Neos. Was ist node? Kommt darauf an wo du bist. Falls du auf der obersten Seite bist, ist node, documentNode und site das Gleiche. Falls du auf einer Unterseite bist und ein Inhaltselement markiert hast, sind alle drei Kontextvariablen verschieden. Praktisch, wenn man das Prinzip begriffen hat. Die Hölle, wenn nicht. Deshalb hier nochmals zur Übersicht:

Variable Beschreibung
node Die aktuell ausgewählte Node, kann jeder möglicher Typ von Node sein
documentNode Dokument von der aktuell ausgewählten Node
site Der Startseitennode

Zum Glück lässt sich das im Fusion leicht vergleichen:
documentNode == node. Und schon weiss ich, wo ich bin.

Der liebe Kontext

Wo wir beim nächsten Thema sind: Der liebe @context. Wichtig zu wissen: @context wird auf alle Kindelemente vererbt. Super wenn man das will, aber in den meisten Fällen ist das vermeidbar, da unangenehme Seiteneffekte auftreten können. Und dank @apply und den Fusion Components kein Problem:

prototype(Foo.Bar:Atom.Link) < prototype(Neos.Fusion:Component) {
	appearance = null
	href = null
	content = null

	@if.hasHrefAndContent = ${this.href && this.content}

	renderer = Neos.Fusion:Component {
		@apply.props = ${props}
		appearanceClasses = Neos.Fusion:Match {
			@subject = ${props.appearance}
			@default = 'text-red-600'
			primary = 'text-white bg-red-600'
			secondary = 'text-red-700 bg-red-100'
		}
		renderer = afx`
			<a href={props.href} class={props.appearanceClasses}>
				{props.content}
			</a>
		`
	}
}

Warum AFX einfach genial ist

Du fragst dich, warum eigentlich AFX? Es gibt doch Fluid. Ja, das stimmt. Du kannst weiterhin mit Fluid deine Webseite bauen. Aber: Das geniale neben dem das du alles in einer Datei handeln kannst, ist das du direkt Fusion Prototypes im Quelltext verwenden kannst:

<a href="mailto:office@domain.com" aria-label="Write me an email">
	<Foo.Bar:Atom.Icon icon="envelope" />
</a>

Und noch ein Benefit: Du musst dir nicht die Unterschiede zwischen Fluid und Fusion merken. AFX ist nach allem simples Fusion.

Präsentation und Integration trennen

Wenn wir beim Thema sind: Es macht Sinn, Präsentation und Integration zu trennen. Präsentation ist das, was ohne Daten von Neos auskommt, d.h. ohne node, documentNode, site, request usw. Einerseits kannst du dann das Design mit Monocle sauber testen, die visuellen Komponenten sind vielseitiger einsetzbar, die Arbeit lässt sich viel einfacher zwischen Personen aufteilen und du ersparst dir eine Menge Ärger mit dem definieren von dem @caching.

Ich habe schon häufig Code gesehen, wo zum Beispiel ein Teaser gleichzeitig als Inhaltselement und als visuelle Komponente in einem anderen Inhaltselement mit dynamischen News definiert war. Schon nur diese Erklärung hört sich kompliziert an. Die Folge war, das drei mal die gleiche News angezeigt wurde, weil das verschachtelte Inhaltselement ebenfalls einen @cache Eintrag hatte.

Am Anfang braucht es bisschen zu Umgewöhung, aber glaub mir, es lohnt sich! Dabei kann es zum Beispiel helfen, wenn du der visuellen Komponente Daten Strukturen übergibst und diese Strukturen in der Integration mit dynamischen Daten füllst:

prototype(Foo.Bar:Module.Teaser) < prototype(Neos.Fusion:Component) {
	items = null
	
	@if.hasEntries = ${Type.isArray(this.items) && !Array.isEmpty(this.items)}
	
	renderer = afx`
		<section class="flex flex-wrap">
			<Neos.Fusion:Loop items={props.items}>
				<Foo.Bar:Module.Teaser.Fragment.Entry {...item} />
			</Neos.Fusion:Loop>
		</section>
	`
}
prototype(Foo.Bar:Module.Teaser.Fragment.Entry) < prototype(Neos.Fusion:Component) {
	href = null
	imageSource = null
	headline = null
	text = null
	alternativeText = null
	
	@if.hasAllNeededProperties = ${this.href && this.imageSource && this.headline}
	
	renderer = afx`
		<div class="relative flex flex-col p-8">
			<a href={props.href} class="pt-4">
				<span class="absolute inset-0" aria-hidden="true"></span>
				{props.headline}
			</a>
			<figure class="order-first -mt-8 -mx-8">
				<Sitegeist.Lazybones:Image
					imageSource={props.imageSource}
					alt={props.alternativeText}
					srcset="320w, 400w, 600w, 800w, 1000w"
					sizes="auto"
					renderDimensionAttributes={true}
					width={600}
					height={300}
				/>
			</figure>
			<p @if.set={props.text} class="pt-4">
				{props.text}
			</p>
		</div>
	`
}

Eine Integration würde in diesem Beispiel dann so aussehen:

prototype(Foo.Bar:Component.News) < prototype(Neos.Fusion:Component) {
	newsNodes = ${q(site).find('[instanceof Foo.Bar:Document.News]').sort('_hiddenBeforeDateTime', 'DESC').get()}
	
	renderer = Neos.Fusion:Component {
		items = Neos.Fusion:Map {
			items = ${props.newsNodes}
			itemRenderer = Neos.Fusion:DataStructure {
				href = Neos.Neos:NodeUri {
					node = ${item}
				}
				imageSource = Sitegeist.Kaleidoscope:AssetImageSource {
					asset = ${q(item).property('image')}
				}
				headline = ${q(item).property('title')}
				text = ${q(item).property('text')}
				alternativeText = ${q(item).property('alternativeText')}
			}
			@process.filter = ${Array.filter(value, entry => !!(entry.imageSource && entry.headline))}
		}
		renderer = afx`<Foo.Bar:Module.Teaser {...props} />`
	}
	
	@cache {
		mode = 'cached'
		entryIdentifier.node = ${node}
		entryTags {
			1 = ${Neos.Caching.nodeTag(node)}
			2 = ${Neos.Caching.nodeTypeTag('Foo.Bar:Document.News', node)}
		}
	}
}

Eine Sache noch wegen dem @cache Eintrag: Es ist am besten in immer sofort bei der Integration zu schreiben. Häufig denkt man sich, das mache ich dann am Schluss, aber: Machen musst du es so oder so. Und in dem Moment wo du die Integration schreibst, hast du den besten Überblick. Ausserdem dokumentiert dies auch auf welche Nodes und Werte die Integration zugreift – jeder nach dir, inklusive dein eigenes ich in zwei Wochen, wird dir danken.

Erklären, was los ist

Gerade bei Prototypen welche reine Präsentationskomponenten sind, ist es nicht immer ersichtlich, was für ein Typ und/oder Wert für ein Eigenschaft erlaubt ist.

Das kann man natürlich mit Kommentaren lösen, aber hier möchte ich dir @propTypes ans Herz legen:

prototype(Foo.Bar:Atom.Headline) < prototype(Neos.Fusion:Component) {
    @propTypes {
        title = ${PropTypes.string.isRequired}
        subtitle = ${PropTypes.string}
        tagName = ${PropTypes.oneOf(['h1', 'h2', 'h3'])}
    }
}

Auch wenn du das Package nicht kennst, schon beim reinem lesen ist es selbsterklärend. Es erklärt nicht, wie und auf welche Art die Implementierung funktioniert, sondern nur, was erwartet und erlaubt ist. Perfekt!

Eine verrückte Idee

Ein spannender Ansatz wäre, zuerst die @propTypes zu definieren. Genau zu überlegen, wie die API des Fusion Prototypen aussehen sollte. Ich glaube, dies würde helfen, die Module weniger breit zu gestalten und in der Implementierung mehr in die Tiefe zu gehen.

Erkläre die Implementierung

Wenn es jedoch um eine Erklärung einer Implementierung geht, sollte diese nicht auf oberster Ebene geschrieben werden. Sondern dort, wo die Implementierung passiert. Sie tragen zur Präzision bei und erklären das Was und Warum eines bestimmten Code-Details. Ein sehr schönes Beispiel, welches zwar nicht im Fusion ist, sondern in einer PHP Klasse, kannst die hier sehen:
ImageService vom Jonnitto.PrettyEmbedHelper

Sebastian Kurfürst von sandstorm ist sicher den meisten hier ein Begriff. Er hat in diesem Service nicht nur das Problem gelöst, sondern direkt im Code auch genau erklärt was passiert.

Do it, Fix it, Break it

Die besten Module sind nicht die ersten Würfe, sondern die, welche mehrmals entworfen wurde. Eine krasse Behauptung, ich weiss. Aber wir haben ja zum Beispiel das  UI auch drei mal neu gebaut, bis dann wirklich gepasst hat.

Um die Ecke denken

Manchmal ist es einfach gut um die Ecke zu denken. Schliesslich ist unser Kopf rund, damit wir unsere Denkrichtung ändern können. Gerade in der Webentwicklung passiert enorm viel, das wir alle immer und immer wieder herausgefordert sind, neue Sachen zu lernen und Probleme auf eine neue Art zu lösen. Auch bei uns, im Neos Universum ist extrem viel in den letzten Jahren passiert: Best Practices wurden wiederholt über Bord geworfen und durch neue Herangehensweisen ersetzt. Aber macht das nicht den Reiz der Entwicklung aus? Wir leben in einer enorm spannenden aber ebenso herausfordernden Zeit. Niemals war so viel möglich und noch nie stand die Menschheit vor solchen Herausforderungen (Klimawandel). Ich bin fest davon überzeugt, das die, welche immer wieder Prinzipien über den Haufen werfen und neue Wege gehen, diese Welt verändern können. Das die, welche sich nicht auf den gewohnten Pfaden bewegen, sondern Wege gehen damit diese entstehen.