In dieser Notiz beschreibe ich die Grundprinzipien des bekannten klassischen CRUD-Musters und die Implementierung in der Swift-Sprache. Swift ist eine offene, plattformübergreifende Sprache, die für Windows, Linux, macOS, iOS und Android verfügbar ist.
Es gibt viele Lösungen zur Abstraktion von Datenspeicherung und Anwendungslogik. Eine solche Lösung ist der CRUD-Ansatz, ein Akronym für C– Erstellen, R -Lesen, U – Update, D– Löschen.
Typischerweise wird dieses Prinzip durch die Implementierung einer Schnittstelle zur Datenbank implementiert, in der Elemente mithilfe einer eindeutigen Kennung, beispielsweise einer ID, manipuliert werden. Für jeden Buchstaben CRUD – wird eine Schnittstelle erstellt. Erstellen (Objekt, ID), Lesen (ID), Aktualisieren (Objekt, ID), Löschen (Objekt, ID).
Wenn ein Objekt eine ID in sich selbst enthält, kann das ID-Argument in den Methoden (Create, Update, Delete) weggelassen werden, da das gesamte Objekt zusammen mit seinem Feld – Ausweis. Aber für – Für das Lesen ist eine ID erforderlich, da wir anhand der ID ein Objekt aus der Datenbank abrufen möchten.
Alle Namen sind fiktiv
Stellen wir uns vor, dass eine hypothetische AssistantAI-Anwendung mit dem kostenlosen EtherRelm-Datenbank-SDK erstellt wurde. Die Integration war einfach, die API war sehr praktisch und als Ergebnis wurde die Anwendung auf den Markt gebracht.
Plötzlich beschließt der SDK-Entwickler EtherRelm, es kostenpflichtig zu machen und legt den Preis auf 100 US-Dollar pro Jahr und Anwendungsbenutzer fest.
Was? Ja! Was sollen die Entwickler von AssistantAI jetzt tun, da sie bereits 1 Million aktive Benutzer haben! 100 Millionen US-Dollar zahlen?
Stattdessen wird beschlossen, die Übertragung der Anwendung in die plattformeigene RootData-Datenbank zu evaluieren. Nach Angaben der Programmierer wird eine solche Übertragung etwa sechs Monate dauern, wobei die Implementierung neuer Funktionen in der Anwendung nicht berücksichtigt ist. Nach einigem Überlegen wurde beschlossen, die Anwendung vom Markt zu nehmen und sie auf einem anderen kostenlosen plattformübergreifenden Framework mit integrierter BueMS-Datenbank neu zu schreiben. Dadurch wird das Problem mit der kostenpflichtigen Datenbank gelöst und die Entwicklung auf anderen Plattformen vereinfacht.
Ein Jahr später wurde die Anwendung in BueMS umgeschrieben, doch dann beschließt der Framework-Entwickler plötzlich, sie kostenpflichtig zu machen. Es stellt sich heraus, dass das Team zweimal in die gleiche Falle getappt ist; ob es beim zweiten Mal wieder herauskommt, ist eine ganz andere Geschichte.
Abstraktion zur Rettung
Diese Probleme hätten vermieden werden können, wenn Entwickler eine Abstraktion von Schnittstellen innerhalb der Anwendung verwendet hätten. Zu den drei Säulen von OOP – Polymorphismus, Kapselung, Vererbung, vor nicht allzu langer Zeit haben sie ein weiteres – Abstraktion.
Mit der Datenabstraktion können Sie Ideen und Modelle allgemein und mit einem Minimum an Details beschreiben und gleichzeitig genau genug sein, um spezifische Implementierungen zu implementieren, die zur Lösung von Geschäftsproblemen verwendet werden.
Wie können wir den Datenbankbetrieb abstrahieren, sodass die Anwendungslogik nicht davon abhängt? Wir verwenden den CRUD-Ansatz!
Ein vereinfachtes UML CRUD-Diagramm sieht so aus:

Beispiel mit einer fiktiven EtherRelm-Datenbank:

Beispiel mit einer echten SQLite-Datenbank:

Wie Sie bereits bemerkt haben, ändert sich beim Wechseln der Datenbank nur die CRUD-Schnittstelle, mit der die Anwendung interagiert. CRUD ist eine Implementierung des GoF-Musters – Adapter, weil Damit passen wir Anwendungsschnittstellen an beliebige Datenbanken an und kombinieren inkompatible Schnittstellen.
Worte sind leer, zeig mir den Code
Um Abstraktionen in Programmiersprachen zu implementieren, werden Schnittstellen/Protokolle/abstrakte Klassen verwendet. All dies sind Phänomene der gleichen Art. In Interviews werden Sie jedoch möglicherweise gebeten, den Unterschied zwischen ihnen zu benennen. Ich persönlich denke, dass dies nicht viel Sinn macht, weil Der einzige Verwendungszweck besteht darin, eine Datenabstraktion zu implementieren, andernfalls dient es dazu, das Gedächtnis des Befragten zu testen.
CRUD wird oft im Rahmen des Repository-Musters implementiert. Das Repository kann jedoch die CRUD-Schnittstelle implementieren oder auch nicht, alles hängt vom Einfallsreichtum des Entwicklers ab.
Stellen Sie sich einen ziemlich typischen Swift-Code für das Book-Struktur-Repository vor, der direkt mit der UserDefaults-Datenbank arbeitet:
struct Book: Codable {
let title: String
let author: String
}
class BookRepository {
func save(book: Book) {
let record = try! JSONEncoder().encode(book)
UserDefaults.standard.set(record, forKey: book.title)
}
func get(bookWithTitle title: String) -> Book? {
guard let data = UserDefaults.standard.data(forKey: title) else { return nil }
let book = try! JSONDecoder().decode(Book.self, from: data)
return book
}
func delete(book: Book) {
UserDefaults.standard.removeObject(forKey: book.title)
}
}
let book = Book(title: "Fear and Loathing in COBOL", author: "Sir Edsger ZX Spectrum")
let repository = BookRepository()
repository.save(book: book)
print(repository.get(bookWithTitle: book.title)!)
repository.delete(book: book)
guard repository.get(bookWithTitle: book.title) == nil else {
print("Error: can't delete Book from repository!")
exit(1)
}
Der obige Code scheint einfach zu sein, aber zählen wir die Anzahl der Verstöße gegen das DRY-Prinzip (Do not Repeat Yourself) und die Kohärenz des Codes:
Konnektivität zur UserDefaults-Datenbank
Konnektivität mit JSON-Encodern und -Decodern – JSONEncoder, JSONDecoder
Verbunden mit der Book-Struktur, aber wir benötigen ein abstraktes Repository, um nicht für jede Struktur, die wir in der Datenbank speichern, eine Repository-Klasse zu erstellen (DRY-Verletzung)
Ich sehe diese Art von CRUD-Repository-Code ziemlich oft, er kann verwendet werden, aber eine hohe Kopplung und Duplizierung des Codes führt dazu, dass seine Unterstützung mit der Zeit sehr kompliziert wird. Dies macht sich besonders bemerkbar, wenn Sie versuchen, zu einer anderen Datenbank zu wechseln, oder wenn Sie die interne Logik der Arbeit mit der Datenbank in allen in der Anwendung erstellten Repositorys ändern.
Anstatt Code zu duplizieren, halten Sie die Kopplung hoch – Schreiben wir ein Protokoll für das CRUD-Repository und abstrahieren so die Datenbankschnittstelle und die Anwendungsgeschäftslogik unter Berücksichtigung von DRY und implementieren eine geringe Kopplung:
typealias Item = Codable
typealias ItemIdentifier = String
func create<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier, item: T) async throws
func read<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier) async throws -> T
func update<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier, item: T) async throws
func delete(id: CRUDRepository.ItemIdentifier) async throws
}
Das CRUDRepository-Protokoll beschreibt Schnittstellen und zugehörige Datentypen für die weitere Implementierung eines bestimmten CRUD-Repositorys.
Als nächstes schreiben wir eine spezifische Implementierung für die UserDefaults-Datenbank:
private typealias RecordIdentifier = String
let tableName: String
let dataTransformer: DataTransformer
init(
tableName: String = "",
dataTransformer: DataTransformer = JSONDataTransformer()
) {
self.tableName = tableName
self.dataTransformer = dataTransformer
}
private func key(id: CRUDRepository.ItemIdentifier) -> RecordIdentifier {
"database_\(tableName)_item_\(id)"
}
private func isExists(id: CRUDRepository.ItemIdentifier) async throws -> Bool {
UserDefaults.standard.data(forKey: key(id: id)) != nil
}
func create<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier, item: T) async throws {
let data = try await dataTransformer.encode(item)
UserDefaults.standard.set(data, forKey: key(id: id))
UserDefaults.standard.synchronize()
}
func read<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier) async throws -> T {
guard let data = UserDefaults.standard.data(forKey: key(id: id)) else {
throw CRUDRepositoryError.recordNotFound(id: id)
}
let item: T = try await dataTransformer.decode(data: data)
return item
}
func update<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier, item: T) async throws {
guard try await isExists(id: id) else {
throw CRUDRepositoryError.recordNotFound(id: id)
}
let data = try await dataTransformer.encode(item)
UserDefaults.standard.set(data, forKey: key(id: id))
UserDefaults.standard.synchronize()
}
func delete(id: CRUDRepository.ItemIdentifier) async throws {
guard try await isExists(id: id) else {
throw CRUDRepositoryError.recordNotFound(id: id)
}
UserDefaults.standard.removeObject(forKey: key(id: id))
UserDefaults.standard.synchronize()
}
}
Der Code sieht lang aus, enthält aber eine vollständige konkrete Implementierung eines CRUD-Repositorys mit loser Kopplung, Details unten.
Typalias wurden zur Selbstdokumentation des Codes hinzugefügt.
Schwache Kopplung und starke Kopplung
Die Loslösung von einer bestimmten Struktur (Struktur) wird mithilfe des generischen T implementiert, das wiederum die Codable-Protokolle implementieren muss. Mit Codable können Sie Strukturen mithilfe von Klassen konvertieren, die die Protokolle TopLevelEncoder und TopLevelDecoder implementieren, beispielsweise JSONEncoder und JSONDecoder. Bei Verwendung von Basistypen (Int, String, Float usw.) ist es nicht erforderlich, zusätzlichen Code zum Konvertieren von Strukturen zu schreiben. p>
Die Entkopplung von einem bestimmten Encoder und Decoder erfolgt mithilfe der Abstraktion im DataTransformer-Protokoll:
func encode<T: Encodable>(_ object: T) async throws -> Data
func decode<T: Decodable>(data: Data) async throws -> T
}
Durch die Implementierung eines Datentransformators haben wir eine Abstraktion der Encoder- und Decoder-Schnittstellen implementiert und eine lose Kopplung implementiert, um die Arbeit mit verschiedenen Arten von Datenformaten sicherzustellen.
Das Folgende ist der Code für einen bestimmten DataTransformer, nämlich für JSON:
func encode<T>(_ object: T) async throws -> Data where T : Encodable {
let data = try JSONEncoder().encode(object)
return data
}
func decode<T>(data: Data) async throws -> T where T : Decodable {
let item: T = try JSONDecoder().decode(T.self, from: data)
return item
}
}
War es möglich?
Was hat sich geändert? Jetzt reicht es aus, ein bestimmtes Repository zu initialisieren, um mit jeder Struktur zu arbeiten, die das Codable-Protokoll implementiert, wodurch die Notwendigkeit entfällt, Code zu duplizieren und eine lose Kopplung der Anwendung zu implementieren.
Ein Beispiel für ein Client-CRUD mit einem bestimmten Repository, UserDefaults ist die Datenbank, JSON-Datenformat, Client-Struktur, außerdem ein Beispiel für das Schreiben und Lesen eines Arrays:
print("One item access example")
do {
let clientRecordIdentifier = "client"
let clientOne = Client(name: "Chill Client")
let repository = UserDefaultsRepository(
tableName: "Clients Database",
dataTransformer: JSONDataTransformer()
)
try await repository.create(id: clientRecordIdentifier, item: clientOne)
var clientRecord: Client = try await repository.read(id: clientRecordIdentifier)
print("Client Name: \(clientRecord.name)")
clientRecord.name = "Busy Client"
try await repository.update(id: clientRecordIdentifier, item: clientRecord)
let updatedClient: Client = try await repository.read(id: clientRecordIdentifier)
print("Updated Client Name: \(updatedClient.name)")
try await repository.delete(id: clientRecordIdentifier)
let removedClientRecord: Client = try await repository.read(id: clientRecordIdentifier)
print(removedClientRecord)
}
catch {
print(error.localizedDescription)
}
print("Array access example")
let clientArrayRecordIdentifier = "clientArray"
let clientOne = Client(name: "Chill Client")
let repository = UserDefaultsRepository(
tableName: "Clients Database",
dataTransformer: JSONDataTransformer()
)
let array = [clientOne]
try await repository.create(id: clientArrayRecordIdentifier, item: array)
let savedArray: [Client] = try await repository.read(id: clientArrayRecordIdentifier)
print(savedArray.first!)
Während der ersten CRUD-Prüfung wurde eine Ausnahmebehandlung implementiert, bei der das Lesen des Remote-Elements nicht mehr möglich ist.
Datenbank wechseln
Jetzt zeige ich Ihnen, wie Sie Ihren aktuellen Code in eine andere Datenbank übertragen. Als Beispiel nehme ich den SQLite-Repository-Code, den ChatGPT generiert hat:
class SQLiteRepository: CRUDRepository {
private typealias RecordIdentifier = String
let tableName: String
let dataTransformer: DataTransformer
private var db: OpaquePointer?
init(
tableName: String,
dataTransformer: DataTransformer = JSONDataTransformer()
) {
self.tableName = tableName
self.dataTransformer = dataTransformer
self.db = openDatabase()
createTableIfNeeded()
}
private func openDatabase() -> OpaquePointer? {
var db: OpaquePointer? = nil
let fileURL = try! FileManager.default
.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
.appendingPathComponent("\(tableName).sqlite")
if sqlite3_open(fileURL.path, &db) != SQLITE_OK {
print("error opening database")
return nil
}
return db
}
private func createTableIfNeeded() {
let createTableString = """
CREATE TABLE IF NOT EXISTS \(tableName) (
id TEXT PRIMARY KEY NOT NULL,
data BLOB NOT NULL
);
"""
var createTableStatement: OpaquePointer? = nil
if sqlite3_prepare_v2(db, createTableString, -1, &createTableStatement, nil) == SQLITE_OK {
if sqlite3_step(createTableStatement) == SQLITE_DONE {
print("\(tableName) table created.")
} else {
print("\(tableName) table could not be created.")
}
} else {
print("CREATE TABLE statement could not be prepared.")
}
sqlite3_finalize(createTableStatement)
}
private func isExists(id: CRUDRepository.ItemIdentifier) async throws -> Bool {
let queryStatementString = "SELECT data FROM \(tableName) WHERE id = ?;"
var queryStatement: OpaquePointer? = nil
if sqlite3_prepare_v2(db, queryStatementString, -1, &queryStatement, nil) == SQLITE_OK {
sqlite3_bind_text(queryStatement, 1, id, -1, nil)
if sqlite3_step(queryStatement) == SQLITE_ROW {
sqlite3_finalize(queryStatement)
return true
} else {
sqlite3_finalize(queryStatement)
return false
}
} else {
print("SELECT statement could not be prepared.")
throw CRUDRepositoryError.databaseError
}
}
func create<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier, item: T) async throws {
let insertStatementString = "INSERT INTO \(tableName) (id, data) VALUES (?, ?);"
var insertStatement: OpaquePointer? = nil
if sqlite3_prepare_v2(db, insertStatementString, -1, &insertStatement, nil) == SQLITE_OK {
let data = try await dataTransformer.encode(item)
sqlite3_bind_text(insertStatement, 1, id, -1, nil)
sqlite3_bind_blob(insertStatement, 2, (data as NSData).bytes, Int32(data.count), nil)
if sqlite3_step(insertStatement) == SQLITE_DONE {
print("Successfully inserted row.")
} else {
print("Could not insert row.")
throw CRUDRepositoryError.databaseError
}
} else {
print("INSERT statement could not be prepared.")
throw CRUDRepositoryError.databaseError
}
sqlite3_finalize(insertStatement)
}
func read<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier) async throws -> T {
let queryStatementString = "SELECT data FROM \(tableName) WHERE id = ?;"
var queryStatement: OpaquePointer? = nil
var item: T?
if sqlite3_prepare_v2(db, queryStatementString, -1, &queryStatement, nil) == SQLITE_OK {
sqlite3_bind_text(queryStatement, 1, id, -1, nil)
if sqlite3_step(queryStatement) == SQLITE_ROW {
let queryResultCol1 = sqlite3_column_blob(queryStatement, 0)
let queryResultCol1Length = sqlite3_column_bytes(queryStatement, 0)
let data = Data(bytes: queryResultCol1, count: Int(queryResultCol1Length))
item = try await dataTransformer.decode(data: data)
} else {
throw CRUDRepositoryError.recordNotFound(id: id)
}
} else {
print("SELECT statement could not be prepared")
throw CRUDRepositoryError.databaseError
}
sqlite3_finalize(queryStatement)
return item!
}
func update<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier, item: T) async throws {
guard try await isExists(id: id) else {
throw CRUDRepositoryError.recordNotFound(id: id)
}
let updateStatementString = "UPDATE \(tableName) SET data = ? WHERE id = ?;"
var updateStatement: OpaquePointer? = nil
if sqlite3_prepare_v2(db, updateStatementString, -1, &updateStatement, nil) == SQLITE_OK {
let data = try await dataTransformer.encode(item)
sqlite3_bind_blob(updateStatement, 1, (data as NSData).bytes, Int32(data.count), nil)
sqlite3_bind_text(updateStatement, 2, id, -1, nil)
if sqlite3_step(updateStatement) == SQLITE_DONE {
print("Successfully updated row.")
} else {
print("Could not update row.")
throw CRUDRepositoryError.databaseError
}
} else {
print("UPDATE statement could not be prepared.")
throw CRUDRepositoryError.databaseError
}
sqlite3_finalize(updateStatement)
}
func delete(id: CRUDRepository.ItemIdentifier) async throws {
guard try await isExists(id: id) else {
throw CRUDRepositoryError.recordNotFound(id: id)
}
let deleteStatementString = "DELETE FROM \(tableName) WHERE id = ?;"
var deleteStatement: OpaquePointer? = nil
if sqlite3_prepare_v2(db, deleteStatementString, -1, &deleteStatement, nil) == SQLITE_OK {
sqlite3_bind_text(deleteStatement, 1, id, -1, nil)
if sqlite3_step(deleteStatement) == SQLITE_DONE {
print("Successfully deleted row.")
} else {
print("Could not delete row.")
throw CRUDRepositoryError.databaseError
}
} else {
print("DELETE statement could not be prepared.")
throw CRUDRepositoryError.databaseError
}
sqlite3_finalize(deleteStatement)
}
}
Oder der CRUD-Code des Repositorys für das Dateisystem, der ebenfalls von ChatGPT generiert wurde:
class FileSystemRepository: CRUDRepository {
private typealias RecordIdentifier = String
let directoryName: String
let dataTransformer: DataTransformer
private let fileManager = FileManager.default
private var directoryURL: URL
init(
directoryName: String = "Database",
dataTransformer: DataTransformer = JSONDataTransformer()
) {
self.directoryName = directoryName
self.dataTransformer = dataTransformer
let paths = fileManager.urls(for: .documentDirectory, in: .userDomainMask)
directoryURL = paths.first!.appendingPathComponent(directoryName)
if !fileManager.fileExists(atPath: directoryURL.path) {
try? fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil)
}
}
private func fileURL(id: CRUDRepository.ItemIdentifier) -> URL {
return directoryURL.appendingPathComponent("item_\(id).json")
}
private func isExists(id: CRUDRepository.ItemIdentifier) async throws -> Bool {
return fileManager.fileExists(atPath: fileURL(id: id).path)
}
func create<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier, item: T) async throws {
let data = try await dataTransformer.encode(item)
let url = fileURL(id: id)
try data.write(to: url)
}
func read<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier) async throws -> T {
let url = fileURL(id: id)
guard let data = fileManager.contents(atPath: url.path) else {
throw CRUDRepositoryError.recordNotFound(id: id)
}
let item: T = try await dataTransformer.decode(data: data)
return item
}
func update<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier, item: T) async throws {
guard try await isExists(id: id) else {
throw CRUDRepositoryError.recordNotFound(id: id)
}
let data = try await dataTransformer.encode(item)
let url = fileURL(id: id)
try data.write(to: url)
}
func delete(id: CRUDRepository.ItemIdentifier) async throws {
guard try await isExists(id: id) else {
throw CRUDRepositoryError.recordNotFound(id: id)
}
let url = fileURL(id: id)
try fileManager.removeItem(at: url)
}
}
Ersetzen Sie das Repository im Client-Code:
print("One item access example")
do {
let clientRecordIdentifier = "client"
let clientOne = Client(name: "Chill Client")
let repository = FileSystemRepository(
directoryName: "Clients Database",
dataTransformer: JSONDataTransformer()
)
try await repository.create(id: clientRecordIdentifier, item: clientOne)
var clientRecord: Client = try await repository.read(id: clientRecordIdentifier)
print("Client Name: \(clientRecord.name)")
clientRecord.name = "Busy Client"
try await repository.update(id: clientRecordIdentifier, item: clientRecord)
let updatedClient: Client = try await repository.read(id: clientRecordIdentifier)
print("Updated Client Name: \(updatedClient.name)")
try await repository.delete(id: clientRecordIdentifier)
let removedClientRecord: Client = try await repository.read(id: clientRecordIdentifier)
print(removedClientRecord)
}
catch {
print(error.localizedDescription)
}
print("Array access example")
let clientArrayRecordIdentifier = "clientArray"
let clientOne = Client(name: "Chill Client")
let repository = FileSystemRepository(
directoryName: "Clients Database",
dataTransformer: JSONDataTransformer()
)
let array = [clientOne]
try await repository.create(id: clientArrayRecordIdentifier, item: array)
let savedArray: [Client] = try await repository.read(id: clientArrayRecordIdentifier)
print(savedArray.first!)
Die Initialisierung von UserDefaultsRepository wurde durch FileSystemRepository mit den entsprechenden Argumenten ersetzt.
Nachdem Sie die zweite Version des Client-Codes ausgeführt haben, finden Sie im Dokumentenordner ein Verzeichnis „Clients Database“, das eine Datei eines in JSON serialisierten Arrays mit einer Client-Struktur enthält.
Datenspeicherformat wechseln
Jetzt bitten wir ChatGPT, einen Encoder und Decoder für XML zu generieren:
let formatExtension = "xml"
func encode<T: Encodable>(_ item: T) async throws -> Data {
let encoder = PropertyListEncoder()
encoder.outputFormat = .xml
return try encoder.encode(item)
}
func decode<T: Decodable>(data: Data) async throws -> T {
let decoder = PropertyListDecoder()
return try decoder.decode(T.self, from: data)
}
}
Dank der in Swift integrierten Typen wird die Aufgabe für ein neuronales Netzwerk elementar.
Ersetzen Sie JSON durch XML im Client-Code:
print("One item access example")
do {
let clientRecordIdentifier = "client"
let clientOne = Client(name: "Chill Client")
let repository = FileSystemRepository(
directoryName: "Clients Database",
dataTransformer: XMLDataTransformer()
)
try await repository.create(id: clientRecordIdentifier, item: clientOne)
var clientRecord: Client = try await repository.read(id: clientRecordIdentifier)
print("Client Name: \(clientRecord.name)")
clientRecord.name = "Busy Client"
try await repository.update(id: clientRecordIdentifier, item: clientRecord)
let updatedClient: Client = try await repository.read(id: clientRecordIdentifier)
print("Updated Client Name: \(updatedClient.name)")
try await repository.delete(id: clientRecordIdentifier)
let removedClientRecord: Client = try await repository.read(id: clientRecordIdentifier)
print(removedClientRecord)
}
catch {
print(error.localizedDescription)
}
print("Array access example")
let clientArrayRecordIdentifier = "clientArray"
let clientOne = Client(name: "Chill Client")
let repository = FileSystemRepository(
directoryName: "Clients Database",
dataTransformer: XMLDataTransformer()
)
let array = [clientOne]
try await repository.create(id: clientArrayRecordIdentifier, item: array)
let savedArray: [Client] = try await repository.read(id: clientArrayRecordIdentifier)
print(savedArray.first!)
Der Client-Code wurde in nur einen Ausdruck JSONDataTransformer geändert -> XMLDataTransformer
Gesamt
CRUD-Repositorys sind eines der Entwurfsmuster, die zur Implementierung einer losen Kopplung von Anwendungsarchitekturkomponenten verwendet werden können. Eine weitere Lösung – unter Verwendung von ORM (Object-Relational Mapping), kurz gesagt, ORM verwendet einen Ansatz, bei dem Strukturen vollständig auf die Datenbank abgebildet werden und dann Änderungen an Modellen in der Datenbank angezeigt (zugeordnet (!)) werden sollen.
Aber das ist eine ganz andere Geschichte.
Eine vollständige Implementierung von CRUD-Repositorys für Swift ist verfügbar unter:
https://gitlab.com/demensdeum/crud-example
Swift wird übrigens schon lange außerhalb von macOS unterstützt; der Code aus dem Artikel wurde vollständig unter Arch Linux geschrieben und getestet.
Quellen
https://developer.apple.com/documentation/combine/topleveldecoder
https://developer.apple.com/documentation/combine/toplevelencoder
https://en.wikipedia.org/wiki/Create,_read,_update_and_delete