Continuing the discussion from [APP] Nextcloud Vereins-App (Alpha Release):
In dem o.g. Therad gab es eine Diskussion zwischen @Wacken2012 @luflow und mir bzgl der Kommunikation zwischen verschiendenen Apps. Um das andere Thema nicht zu ĂŒberladen, habe ich dieses getrennte Thema erstellt.
Ich versuche das mal hier zu darzulegen.
Die wichtigsten Zeilen in deinem Code hier zusammen kopiert:
<?php
// ...
use OCP\Notification\IManager as INotificationManager;
//...
class NotificationService {
private INotificationManager $notificationManager;
// ...
public function __construct(
INotificationManager $notificationManager,
// ...
) {
// ...
}
Du nutzt das Interface \OCP\Notification\IManager. Das ist aber kein Interface einer App sondern des Kerns (erkennbar am Namensraum \OCP). WĂŒrdest du direkt die andere App verwenden, mĂŒsstest du den Namensraum \OCA angeben.
Das funktioneirt deshalb, weil die Benachrichtigungsapp schon sehr alt ist und eng mit dem Kern verwoben wurde. Jeder kann eigene Apps schreiben, die auch Benachrichtigungen ĂŒber beliebige KanĂ€le senden können. Dazu mĂŒssen sie sich nur selber registrieren. Der Kern bietet dazu eine Infrastruktur auf abstrakter Ebene an, um derartige Funktionen bereit zu stellen.
Wenn der Kern das nicht tut (weil der jeweilige Use-Case zu spezifisch ist), dann kann das nicht auf diese Art und Weise realisiert werden. FĂŒr den angedachten Fall (Auftrennen der Alpha-Version in 10 Teil-Apps), werden sicherlich nicht alle FunktionalitĂ€ten direkt ĂŒber den Kern abgewickelt werden können (ich wĂŒrde vermuten keine, aber vllt gibt es die eine oder andere Ausnahme).
Es bleibt die Frage: Was kann man statt dessen tun?
Hier muss ich gesetehen, dass ich nur Optionen bisher gesammelt habe und mir meine Gedanken dazu gemacht habe. Das ist weder eine (offizielle) Empfehlung noch bin ich sicher, dass ich alle Optionen oder alle zu berĂŒcksichtigenden EinschrĂ€nkungen erfasst habe.
Der Einfachheit halber gehen wir von 2 Apps aus, A und B und A versucht Daten an B zu senden.
Zugriff auf Klassen einer anderen App
GrundsÀtzlich kann A beliebige Klassen und Methoden nutzen, die der Server kennt. Innerhalb von A könnte man also use OCA\B\Interfaces\v1\IfA; schreiben. Dann kann man mittels Dependency Injection im Construktor sich ein Objekt vom Typ IfA anfordern und darin Methoden aufrufen.
Ganz so einfach wird es dann aber doch nicht. Ein paar einschrĂ€nkende Ăberlegungen:
- Was passiert, wenn die App B bei einem Nutzer nicht installiert ist (egal ob bewusst oder nicht)? In diesem Fall wird der DI schief gehen und die App A wird mit einem Fehler 500 an die Wand fahren. Ende der Geschichte.
- Man könnte versuchen, den Eintrag nullable zu machen. Also derart, dass der DI auch
IfAnicht einsetzen kann. In diesem Fall muss man das innerhalb von A sauber abprĂŒfen. Zudem erinnere ich mich daran, dass ich mal einen nervigen Bug gefunden hatte, wo das nicht ganz so geschmeidig geklappt hatte, wie intuitiv angenommen. Das muss man also genau durch testen. - Wenn man doch mal das Interface von
IfAanpassen muss, dann wird es kompliziert. Die Version 1 darf nicht angefasst werden. Also muss man ein weiteres Interface mit neuem Namen/Namespace (z.B.OCA\B\Interfaces\v2\IfB) erstellen. Dann muss aber A nun alle Versionen einzeln vom DI anfordern und im User-Code aussortieren, wie man denn nun die Daten irgendwie an die passende Methode ĂŒbergeben kann. - Das Update der einzelnen Apps ist kritisch. Wenn das Interface mehr umfasst als einfaches hin- und herschubsen von elementaren Daten, dann mĂŒssen die Versionen der Apps A und B recht genau zueinander passen. Dies wird durch den Upgrader bzw schlimmstenfalls durch den Nutzer realisiert. Da kann man dann gar nix mehr garantieren.
- Eine klare Kommunikation ist notwendig seitens B, welche Klassen/Interfaces denn als stabil einzuschĂ€tzen sind, damit die Apps sich nicht gegenseitig in die FĂŒĂe hauen.
- Wenn nur A installiert wird, muss man höllisch aufpassen, dass man nicht versehentlich irgendwo die Klasse aus B aufruft oder auch nur erwÀhnt (Typisierung in Metoden-/Funktionsparametern!). Andernfalls kann es sein, dass der PHP-Interpreter eine nicht definierte Klasse anmÀckelt.
Ich sehe diese Methode nicht wirklich produktiv und denke, dass es eher KrĂŒcke als Lösung ist. FĂŒr einen PoC (proof of concept) mag das noch gehen. FĂŒr die langfristige Verwaltung einer App ist es nur noch ein Krampf.
Nutzung von Events
Der Kern bietet die Möglichkeit, Events zu senden. Ein Event ist dabei ein Objekt einer Klasse, in der die relevanten Informationen abjelegt/spezifiziert werden. B wĂŒrde einen Listener registrieren, der also auf alle Events einer bestimmten Klasse reagieren soll. A wĂŒrde die entsprechenden Events abfeuern.
- Durch die Definition der Datenstruktur in der Event-Klasse ist es kaum nötig, in fremden Namespaces zu wildern (s.o.). Das Event wĂŒrde innerhalb von A definiert, damit kennt A die genaue Struktur der Klasse sicher. Der Listener in B wĂŒrde nur ausgefĂŒhrt, wenn auch ein Event gefeuert wurde, wenn also A eh installiert ist. Dann kann der PHP-Parser aber die entsprechende Klasse auch finden, also alles safe.
- Problematisch ist, dass A keine Ahnung hat, ob B da ist und ob das Event auch korrekt verarbeitet wurde. Es ist also eher fire&forget, wenn man nicht manuell noch was drumrum baut.
Bidirektionale Verbindung
Soll eine bidirektionale Kommunikation zwischen A und B aufgebaut werden, dann wird es wĂŒst. Mal angenommen wir wollen so was machen
sequenceDiagram
A ->> B: Hello
B -->> A: B in Version 2 present
A ->> B: Open entity 452
B -->> A: <content>
Ich sehe mehrere Möglichkeiten:
1. VollstÀndige Event-basierte Programmierung
Es werden wechselseitig Events durch die Gegend geschickt. Damit wird aber jeder Funktionsaufruf und jeder RĂŒckgabewert in einem eigenen Event verpackt. Entsprechend steigt die Anzahl der Events stark an, wenn die KomplexitĂ€t der Aufgabe steigt.
sequenceDiagram
A ->>+ B: Ev1("Hello")
B ->>+ A: Ev2("B in Version 2 present")
A ->>+ B: EV3("Open entity 452")
B ->>+ A: EV4(<content>)
A -->>- B: <EV4 finished>
B -->>- A: <EV3 finished>
A -->>- B: <EV2 finished>
B -->>- A: <EV1 finished>
Zudem wird die Programmierung komplett umgestellt, da nun vollstĂ€ndig Event-basiert gearbeitet werden muss. Ich muss also den Kontext immer sinnvoll mit transportieren (offene Fragestellung). Zudem sind eine ganze Reihe an Listener zu schreiben, die alle individuell die aktuelle Situation beurteilen mĂŒssen.
2. Events mit Callbacks
Hierbei handelt es sich um einen hybriden Ansatz. Das Erste Event liefert einen Callback, den der EmpfĂ€nger aufrufen kann. Also kann mit dem Empfangen des Events durch B auch ein Callable mit ĂŒbergeben werden, der in A etwas auslöst. Das kann dann so immer weiter genutzt werden, um Daten hin- und her zu spielen.
Der Vorteil ist, dass der Kontext implizit schon dabei sein kann: Durch geeignete Wahl und Erstellung der Callables kann der Kontext von A bei A und von B bei B verbleiben.
Das waren mal die Themen als ganz schnelle Zusammenfassung. Ich sehe, dass die Konzepte nicht unbedingt klar abzugrenzen sind und dass es sich da auch um teilweise marginale Unterscheiede. Ich weiĂ nicht, in wie weit ich die Probleme korrekt und vollstĂ€ndig identifiziert habe. Wahrscheinlich wĂ€re es auch sinnvoll, einmal die verschiedenen Konzepte als PoC mal aufzubauen (tatsĂ€chlich zwischen A und B), damit man mal den Aufwand fĂŒr die Implementierung sehen kann.
Gerne können wir das Thema auch noch mal diskutieren. Mittelfristig wĂŒrde ich auch das ganze gerne auf Englisch diskutieren: Dann können wir auch die anderen nicht-deutschsprachigen Experten mit ins Boot holen. In der letzten NC-Konferenz hatte sich da mehrere Diskussionen ergeben.
Christian