Wichtige Basis-Typen für jedes Projekt
AP020-0200 • • Florian Pfisterer
Dieser Artikel soll eine Reihe an wichtigen Basis-Typen vorstellen, die man in so gut wie jedem Swift-Projekt nutzen kann. Diese Typen, wenn man sie intelligent benutzt, machen den Code sauberer, aussagekräftiger und einfacher zu testen.
1. Der Result Typ
In so gut wie jedem Projekt gibt es Aufrufe von Funktionen, die fehlschlagen können. In Swift ist es in so einem Fall
üblich, dass die Funktion entweder einen Error wirft oder nil
zurück gibt. Auch wenn das in einigen Fällen passend
ist, möchte man in anderen Fällen weder Errors werfen und weitergeben (rethrow
) noch möchte man die ganze Zeit
Optionals unwrappen.
Um dieses Problem zu lösen, betrachte man den folgenden generischen Result Typ:
enum Result<T> {
case success(T)
case failure(ErrorType)
}
Dieser Typ, wenn man ihn als Ergebnis eines Funktionsaufrufs benutzt beispielsweise, repräsentiert einen der folgenden Fälle:
- Der Aufruf war erfolgreich, und hier ist das Ergebnis: ein Objekt des Typs
T
- Irgendwo ist ein Error passiert, und hier ist er: ein
ErrorType
case
(man könnte auchNSError
benutzen)
Der Result Typ erweitert eigentlich nur den Swift Standard-Optional-Typ, indem bei Misserfolg auch gleich der Error
mitgeliefert wird, warum kein Ergebnisobjekt (T
) vorliegt.
enum Optional<T> {
case some(T)
case none
}
Ein Swift Optional ist wirklich nicht mehr als das, die “?”s und “!”s sind nur syntaktische Vereinfachungen.
Beispiel-Implementierung
Man betrachte die folgende Funktion, die asynchron Users
aus einer Netzwerkdatenbank lädt (zum Beispiel innerhalb
einer Klasse DatabaseClient
).
static func loadUsers(completion: (Result<[User]>) -> Void) {
let queue =
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)
dispatch_async(queue, {
do {
// aufwändiger Netzwerkaufruf, der einen Error werfen könnte
let users: [User] = try networkOperationThatMightThrow()
completion(.success(users))
} catch let error {
completion(.failure(error))
}
})
}
Wenn wir diese Funktion dann zum Beispiel in unserem ViewController
benutzen, müssen wir den Aufruf weder in einem
do-try-catch
Block einbauen, noch müssen wir Optionals unwrappen. Unser Code bleibt sauber, wartbar und verständlich.
DatabaseClient.loadUsers { result in
switch result {
case .failure(let error):
// einen Alert zeigen etc.
case .success(let users):
// die UI updaten und die Users zeigen
}
}
Erweiterung
In manchen Fällen kann es günstig sein, wenn man einfach nur kurz prüfen kann, ob es ein Objekt gibt (.success
) oder
nicht (.failure
). Dafür können wir den Result Typ folgendermaßen erweitern:
extension Result {
var optional: T? {
switch self {
case .success(let object): return object
default: return nil
}
}
}
2. Ein Model Typ
Die allermeisten iOS Apps speichern und laden Daten aus einer Datenbank.
Wenn man Realm oder CoreData als lokale Datenbank benutzt, bekommt man ‘von Haus aus’ bereits eine Superklasse, von der
die eigenen Model-Typen erben (Object
bei Realm und NSManagedObject
bei CoreData). Aber wenn man diese Bibliotheken
nicht benutzt - beispielsweise wenn man mit einer selbst definierten REST API auf einem Server arbeitet - muss man einen
solchen Basis-Typ für seine Model-Typen selbst erstellen.
Warum? Weil dies der Wiederholung von Code vorbeugt und den Code so einfacher zu warten macht. Anstatt, dass man eine
save
Funktion für jeden einzelnen Model-Typ schreiben muss, können alle davon die dafür notwendigen Properties teilen
und so alle eine Implementierung einer save
Funktion nutzen.
Wie implementiere ich diesen Model Basis-Typ?
Die erste Idee könnte sein, einfach eine Klasse namens Model zu erstellen, von der die anderen Model-Typen alle erben.
Diese Klasse könnte dann bestimmte Instanzvariablen wie eine id
sowie Funktionalität wie eine save
Funktion
beinhalten. Das Problem mit dieser Herangehensweise ist, dass es in den allermeisten Fällen besser ist ‘value-type’
structs
anstatt von ‘reference-type’ classes
zu nutzen. Hier werde ich nicht näher auf diese Thematik eingehen, aber
man kann in einem anderen Artikel mehr über den Unterschied zwischen ‘value-types’ und ‘reference-types’ erfahren
TODO: Link einfügen.
Wie also teilen wir Funktionalität zwischen structs
? Vererbung ist hier keine Option.
Wir benutzen ‘Protocol Extensions’
In Swift kann man nun protocol extensions
benutzen, um Funktionalität zu protocols
hinzuzufügen. Man betrachte die
folgende erste Implementierung unseres Model-Typs:
protocol Model {
var id: String { get }
var createdAt: NSDate { get }
var updatedAt: NSDate { get set }
}
Jeder Model-Typ (wie zum Beispiel Kunde, Produkt, Rechnung, etc.) muss jetzt diese Properties implementieren. Noch wird
keine Funktionalität geteilt, diese fügen wir jetzt aber mit Hilfe von Swifts protocol extensions
hinzu:
extension Model {
func save(completion: Result<Self>) {
// das Objekt z.B. mithilfe der `id` speichern
}
}
Man könnte weitere Funktionen wie update
, delete
, etc. implementieren, aber der Punkt ist, dass jeder Model-Typ, den
wir für unsere Anwendung brauchen nun diese Funktionalität definiert in unserem Model protocol
nutzen kann. Wenn sich
etwas an der Datenbank etc. ändert, müssen wir somit den Code nur an einer Stelle verändern.
Eine kleine Zusatzinformation
Wenn manche Model-Typen aufgrund der Business-Logik ihre eigene Implementierung der save
Funktion benötigen, muss man
die Deklaration dieser Funktion neben der extension
auch in der protocol
Deklaration direkt einbauen. So wird dann,
wenn man save
auf einem Objekt jener Klassen aufruft, die richtige Implementierung verwendet.