このノートでは、よく知られている古典的な CRUD パターン、Swift 言語での実装の基本原則について説明します。 Swift は、Windows、Linux、macOS、iOS、Android で利用できるオープンなクロスプラットフォーム言語です。
データ ストレージとアプリケーション ロジックを抽象化するためのソリューションは数多くあります。そのようなソリューションの 1 つが CRUD アプローチです。これは C– の頭字語です。作成、R -読み取り、U –最新情報、D–削除
 します。通常、この原則はデータベースへのインターフェイスの実装を通じて実装され、要素は id などの一意の識別子を使用して操作されます。 CRUD – 文字ごとにインターフェイスが作成されます。作成(オブジェクト、ID)、読み取り(ID)、更新(オブジェクト、ID)、削除(オブジェクト、ID)。
オブジェクトの内部に ID が含まれている場合、オブジェクト全体がフィールド – とともに渡されるため、メソッド (Create、Update、Delete) から id 引数を省略できます。 ID。しかし、– についてはID によってデータベースからオブジェクトを取得したいため、読み取りには ID が必要です。
名前はすべて架空のものです
仮想の AssistantAI アプリケーションが無料の EtherRelm データベース SDK を使用して作成され、統合は簡単で、API は非常に便利で、その結果、アプリケーションが市場にリリースされたと想像してみましょう。
突然、SDK 開発者の EtherRelm は有料化を決定し、アプリケーション ユーザーあたりの価格を年間 100 ドルに設定しました。
何?はい! AssistantAI の開発者は、すでに 100 万人のアクティブ ユーザーを抱えているため、今何をすべきでしょうか。 1億ドル支払う?
代わりに、プラットフォーム固有の RootData データベースへのアプリケーションの転送を評価する決定が行われます。プログラマーによると、そのような転送には約 6 か月かかりますが、これにはアプリケーションの新機能の実装は考慮されていません。いくつか考えた結果、アプリケーションを市場から削除し、BueMS データベースが組み込まれた別の無料のクロスプラットフォーム フレームワークに書き直すことが決定されました。これにより、有料データベースの問題が解決され、他のプラットフォームでの開発が簡素化されます。 
1 年後、アプリケーションは BueMS で書き直されましたが、突然フレームワーク開発者が有料化を決定しました。チームは同じ罠に 2 度陥ったことが判明しました。2 度目から抜け出せるかどうかはまったく別の話です。
抽象化が助けになります
開発者がアプリケーション内でインターフェイスの抽象化を使用していれば、これらの問題は回避できたはずです。 OOPの3つの柱へ–ポリモーフィズム、カプセル化、継承、少し前に別の – が追加されました。抽象化
 します。データ抽象化により、ビジネス上の問題を解決するために使用される特定の実装を実装するのに十分な正確さを保ちながら、アイデアやモデルを最小限の詳細で一般的な用語で記述することができます。
アプリケーションロジックが依存しないようにデータベース操作を抽象化するにはどうすればよいでしょうか?私たちは CRUD アプローチを使用しています!
簡略化された UML CRUD 図は次のようになります。

架空の EtherRelm データベースの例:

実際の SQLite データベースの例:

すでにお気づきのとおり、データベースを切り替えると、アプリケーションがやり取りする CRUD インターフェイスのみが変更され、変更されません。 CRUD は GoF パターンの実装です。アダプターなので、これを使用して、アプリケーション インターフェイスをあらゆるデータベースに適合させ、互換性のないインターフェイスを結合します。
言葉は空です、コードを見せてください
プログラミング言語で抽象化を実装するには、インターフェイス/プロトコル/抽象クラスが使用されます。これらはすべて同じ程度の現象ですが、インタビュー中にそれらの違いを挙げるように求められることがありますが、個人的にはこれはあまり意味がないと考えています。使用の唯一の目的はデータ抽象化を実装することです。それ以外の場合は、インタビュー対象者の記憶をテストすることです。
CRUD は多くの場合、リポジトリ パターンのフレームワーク内で実装されますが、リポジトリは CRUD インターフェイスを実装する場合と実装しない場合があり、すべて開発者の創意工夫に依存します。
UserDefaults データベースを直接操作する、Book 構造リポジトリの非常に典型的な Swift コードを考えてみましょう。
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)
}
上記のコードは単純に見えますが、DRY (Do notrepeat Yourself) 原則の違反の数とコードの一貫性を数えてみましょう。
UserDefaults データベースへの接続
JSON エンコーダーおよびデコーダーとの接続 – #8211; JSONエンコーダ、JSONデコーダ
Book 構造に接続されていますが、データベースに保存する構造ごとにリポジトリ クラスを作成しないように抽象リポジトリが必要です (DRY 違反)
この種の CRUD リポジトリ コードはよく見かけます。使用することはできますが、コードの高度な結合と重複により、時間の経過とともにそのサポートが非常に複雑になるという事実につながります。これは、別のデータベースに切り替えようとする場合、またはアプリケーションで作成されたすべてのリポジトリでデータベースを操作する内部ロジックを変更する場合に特に顕著になります。
コードを複製する代わりに、カップリングを高く保ちます。 CRUD リポジトリのプロトコルを作成して、データベース インターフェイスとアプリケーション ビジネス ロジックを抽象化し、DRY を尊重し、低結合を実装しましょう。
    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
}
CRUDRepository プロトコルは、特定の CRUD リポジトリをさらに実装するためのインターフェイスと関連するデータ型を記述します。
次に、UserDefaults データベースの特定の実装を作成します。
    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()
    }
}
コードは長く見えますが、疎結合を含む CRUD リポジトリの完全な具体的な実装が含まれています。詳細は以下で説明します。
コードの自己文書化のために typealias が追加されました。
弱い結合と強い結合
 について。特定の構造 (struct) からの切り離しは、汎用 T を使用して実装されます。これには、Codable プロトコルを実装する必要があります。 Codable では、TopLevelEncoder および TopLevelDecoder プロトコルを実装するクラス (JSONEncoder や JSONDecoder など) を使用して構造を変換できます。基本型 (Int、String、Float など) を使用する場合、構造を変換するために追加のコードを記述する必要はありません。 p>
特定のエンコーダとデコーダからの分離は、DataTransformer プロトコルの抽象化を使用して行われます。
	func encode<T: Encodable>(_ object: T) async throws -> Data
	func decode<T: Decodable>(data: Data) async throws -> T
}
データ トランスフォーマーの実装を使用して、エンコーダーとデコーダーのインターフェイスの抽象化を実装し、さまざまなタイプのデータ形式で確実に動作するように疎結合を実装しました。
以下は、特定の DataTransformer、つまり 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
	}
}
それは可能でしたか?
何が変わりましたか?今後は、Codable プロトコルを実装する構造を操作できるように特定のリポジトリを初期化するだけで十分です。そのため、コードを複製する必要がなく、アプリケーションの疎結合を実装できます。
特定のリポジトリを使用したクライアント CRUD の例。UserDefaults はデータベース、JSON データ形式、クライアント構造であり、配列の書き込みと読み取りの例でもあります。
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!)
最初の CRUD チェック中に例外処理が実装されており、リモート アイテムの読み取りは利用できなくなります。
データベースの切り替え
次に、現在のコードを別のデータベースに転送する方法を説明します。たとえば、ChatGPT が生成した SQLite リポジトリ コードを取り上げます。
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)
    }
}
または、同じく ChatGPT によって生成された、ファイル システムのリポジトリの CRUD コード:
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)
	}
}
クライアント コード内のリポジトリを置き換えます。
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!)
UserDefaultsRepository の初期化は、適切な引数を備えた FileSystemRepository に置き換えられました。
クライアント コードの 2 番目のバージョンを実行すると、ドキュメント フォルダーに「Clients Database」ディレクトリが表示されます。このディレクトリには、1 つのクライアント構造を持つ JSON でシリアル化された配列のファイルが含まれています。
データ保存形式の切り替え
次に、ChatGPT に XML のエンコーダーとデコーダーを生成するよう依頼しましょう。
	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)
	}
}
Swift の組み込み型のおかげで、ニューラル ネットワークのタスクが初歩的になります。
クライアント コードで JSON を XML に置き換えます。
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!)
クライアント コードは 1 つの式 JSONDataTransformer -> のみに変更されました。 XMLDataTransformer
合計
CRUD リポジトリは、アプリケーション アーキテクチャ コンポーネントの疎結合を実装するために使用できる設計パターンの 1 つです。もう 1 つの解決策 – ORM (Object-Relational Mapping) を使用すると、ORM は構造をデータベースに完全にマッピングし、モデルによる変更をデータベース上に表示 (マッピング (!)) するアプローチを使用します。
しかし、それはまったく別の話です。
Swift 用の CRUD リポジトリの完全な実装は、次の場所から入手できます。
https://gitlab.com/demensdeum/crud-example
ところで、Swift は macOS 以外でも長い間サポートされてきましたが、この記事のコードはすべて Arch Linux 上で作成され、テストされました。
ソース
https://developer.apple.com/documentation/combine/topleveldecoder 
https://developer.apple.com/documentation/combine/toplevelencoder
https://en.wikipedia.org/wiki/Create,_read,_update_and_delete