Dependency Management in einer komplexen Multi-Team Codebase – TECH’N’DRINKS @MYPOSTER

Im April haben wir Benedikt Terhechte bei uns in München zu einem grandiosen hybriden Tech-Meetup bei MYPOSTER begrüßt. Unterhaltsam, kurzweilig und gleichzeitig sehr spannend und aufschlussreich war es. In unserem Blog fassen wir die Insights aus Benedikt’s Talk über Dependency Management in einer komplexen Multi-Team Codebase zusammen und klären, wie am besten mit Abhängigkeiten in der iOS Entwicklung umgegangen werden kann.

Wo liegt eigentlich das Problem?

Aus technischen Gründen ist die Verwaltung von Abhängigkeiten unter iOS von höherer Relevanz als in anderen Sprachen / Plattformen. Das bedeutet aber nicht, dass sie nicht gelöst werden können. Die Erkenntnisse im Streben nach einer wohl-strukturierten Anwendung in anderen Sprachen können und sollten auch hier angewandt werden.

Die Herangehensweise: Creating Frameworks

Um die Abhängigkeiten unter iOS so überschaubar wie möglich zu halten, empfiehlt Benedikt die Aufteilung des Source Code in Module – sogenannte Frameworks oder Packages. Das kann beispielsweise ein Framework für die Chat Funktionalität oder ein Framework für die Settings / Einstellungen sein. Die Vorteile eines solchen Vorgehens sind eindeutig:

  • Unterschiedliche Teams können gleichzeitig, quasi konfliktfrei an unterschiedlichen Features arbeiten
  • Der entwickelte Code kann für mehrere Produkte (Targets) genutzt werden (z.B. Apple Watch, iOS, Apple TV, macOS). Grund hierfür ist, dass das Target nur den spezifischen Target-Code enthält, während die Frameworks den generischen Applikations-Code beinhalten.
  • Die Kompilier-Zeit sinkt durch die Aufteilung in Frameworks deutlich. Dauert das Kompilieren unter iOS mitunter sehr lange, ermöglicht die Aufteilung in Frameworks, dass Swift die jeweiligen Frameworks für sich genommen kompiliert. Gleichzeitig wird der Kompilier-Prozess durch Parallelisieren und Cachen optimiert.

Framework bedingt Framework bedingt Framework im Quadrat

Der aufmerksame Leser denkt sich nun: “Klingt gut, aber dann bau’ ich mir so schnell Abhängigkeiten zwischen den Frameworks. Wo ist der Gewinn?”

Das stimmt natürlich, theoretisch. Beispielsweise muss eine Homepage wissen, wie viele Chats der Nutzer hat, oder ein Button auf dem Profile erlaubt einen neuen Chat zu öffnen, usw. Dies wiederum führt dazu, dass die unterschiedlichen Frameworks sich gegenseitig importieren.

In seinem Talk hat Benedikt das Prinzip einmal exemplarisch an seiner fiktiven App „Dackel Dackel Go“, Deutschlands führender App für Dackelfreunde, aufgezeigt. 🤓 🐶

In der App gibt es mehrere Sektionen, die der User besuchen kann – alles dabei, was der geneigte Dackelfreund eben so braucht:

_Daheim: Die Startseite

_Profil: Profile von Nutzern

_DackelTV: Dackel Livestreams

_Schnack: Chat zwischen Nutzern

_Nachrichten: Neuigkeiten und Nachrichten

_Sammlung: Dackel-Sammlung

Und schon allein mit diesen wenigen Frameworks ergibt sich schnell ein Bild wie folgender Abbildung dargestellt:

Es liegen zyklische Abhängigkeiten vor, die drei gravierende Nachteile mit sich bringen:

  1. Da sich die Abhängigkeiten bedingen, kann der Compiler nicht ausschließen, dass eine Änderung an einer Abhängigkeit Einfluss auf andere Abhängigkeiten haben kann. Daher muss er zwangsläufig alle Frameworks neu kompilieren, was sehr lange dauert.
  2. Da es kein definiertes Öffentliches Interface zwischen den Abhängigkeiten gibt, muss ein Entwickler, der an einem Framework arbeitet (etwa am Chat), im Nachgang immer kontrollieren und sicherstellen, dass alle anderen Frameworks (die Chat importieren) noch korrekt funktionieren.
  3. Jedes weitere neue Framework (welches zwangsläufig viele andere importiert) erhöht die Komplexität der Codebase – nicht nur linear, sondern quadratisch.

How to break zyklische Abhängigkeiten: Dependency Injection Pattern

Um zyklische Abhängigkeiten zwischen Frameworks zu verhindern, gibt es laut Benedikt einen sinnvollen Weg: Dependency Injection Pattern. Dabei definiert jedes Framework ein Öffentliches Interface, und nur dieses kann genutzt werden, um auf die Funktionalität eines Frameworks zuzugreifen.

Darüber hinaus verwendet dieser Pattern eine zentrale Stelle, an der alle Abhängigkeiten vorgehalten werden. Somit muss z.B. in unserer fiktiven App „Dackel Dackel Go” die Sektion Schnack niemals DackelTV importieren. Stattdessen importieren sie beide den Dependency Injector (in Benedikt’s Beispiel „Belum“ genannt). Das Vorgehen ist dabei wie folgt:

  1. Beim Start der App wird der Dependency Injector Belum initialisiert. Dabei werden alle existierenden Frameworks / Abhängigkeiten in ihm registriert. Dadurch hat nur Belum Zugriff auf alle Abhängigkeiten.
  2. Die Individuellen Frameworks werden nun nach und nach initialisiert (je nachdem wann sie das erste mal benötigt werden). Diese bekommen dabei eine Referenz auf den Dependency Injector Belum zugewiesen.
  3. Will später Sektion Schnack auf das Profil zugreifen, so fragt er Belum nach dem Profil. Schnack selber importiert aber nie das Profil, es wird nur über Belum zugewiesen.

Dieses Vorgehen erlaubt es, dass zur Zeit des Kompilierens keine Abhängigkeiten zwischen den Frameworks existieren müssen. Diese werden zur Laufzeit aufgelöst, genau dann, wenn die Anwendung beim Start Belum initialisiert.

Darüber hinaus ist es durch die Anwendung des Dependency Injectors möglich, den Import eines Frameworks durch ein anderes Framework individuell zu definieren. So werden bestimmte Basis-Frameworks (z.B. Kryptographie) nicht einfach von jedem beliebigen anderem Framework importiert. Das Spart Zeit und Kapazität.

Wie das im Resultat aussieht, zeigt folgende Abbildung:

Den exemplarischen Code für den Dependency Injector aus Benedikt’s Dackel-App findet ihr in folgendem Repository auf GitHub:

<https://github.com/terhechte/belum>

Danke, Benedikt, für Deinen spannenden Talk und den guten Abend, den wir zusammen hatten. Bis bald hoffentlich!

ÜBER BENEDIKT

Benedikt ist als Entwickler für ein Berliner Stealth Startup tätig und bastelt in der Freizeit an der Präsentations-App „HyperDeck“. Zuvor war er als Team Lead an der Entwicklung der XING iOS App beteiligt, und davor viele Jahre mit der Instagram App „PhotoDesk“ als macOS Indie unterwegs. Wenn er nicht gerade Swift macht, macht er Rust.

Mehr über Benedikt:

terhech.de

Twitter

GitHub