Nesta nota descreverei os princípios básicos do conhecido padrão clássico CRUD, implementado na linguagem Swift. Swift é uma linguagem aberta e multiplataforma disponível para Windows, Linux, macOS, iOS, Android.
Existem muitas soluções para abstrair o armazenamento de dados e a lógica do aplicativo. Uma dessas soluções é a abordagem CRUD, que é um acrônimo para C– Criar, R -Leia, U– Atualização, D– Excluir.
Normalmente, este princípio é implementado através da implementação de uma interface para o banco de dados, na qual os elementos são manipulados usando um identificador único, como o id. Uma interface é criada para cada letra CRUD – Criar(objeto, id), Ler(id), Atualizar(objeto, id), Excluir(objeto, id).
Se um objeto contém um id dentro de si, então o argumento id pode ser omitido dos métodos (Create, Update, Delete), já que todo o objeto é passado para lá junto com seu campo – eu ia. Mas para – A leitura requer id porque queremos obter um objeto do banco de dados por id.
Todos os nomes são fictícios
Vamos imaginar que um hipotético aplicativo AssistantAI foi criado usando o SDK gratuito do banco de dados EtherRelm, a integração era simples, a API era muito conveniente e, como resultado, o aplicativo foi lançado nos mercados.
De repente, o desenvolvedor do SDK EtherRelm decide torná-lo pago, fixando o preço em US$ 100 por ano por usuário do aplicativo.
O que? Sim! O que os desenvolvedores da AssistantAI devem fazer agora, pois já possuem 1 milhão de usuários ativos! Pagar US$ 100 milhões?
Em vez disso, é tomada a decisão de avaliar a transferência da aplicação para o banco de dados RootData nativo da plataforma, segundo os programadores, tal transferência levará cerca de seis meses, isso não leva em consideração a implementação de novos recursos na aplicação; Depois de pensar um pouco, foi tomada a decisão de remover o aplicativo dos mercados, reescrevê-lo em outro framework multiplataforma gratuito com um banco de dados BueMS integrado, isso resolverá o problema com o banco de dados pago + simplificará o desenvolvimento em outras plataformas.
Um ano depois, o aplicativo foi reescrito em BueMS, mas de repente o desenvolvedor do framework decide torná-lo pago. Acontece que a equipe caiu na mesma armadilha duas vezes. Se conseguirão sair na segunda vez é uma história completamente diferente.
Abstração para o resgate
Esses problemas poderiam ter sido evitados se os desenvolvedores tivessem usado uma abstração de interfaces dentro da aplicação. Aos três pilares da OOP – polimorfismo, encapsulamento, herança, não faz muito tempo eles adicionaram outro – abstração.
A abstração de dados permite descrever ideias e modelos em termos gerais, com um mínimo de detalhes, ao mesmo tempo em que é preciso o suficiente para implementar implementações específicas usadas para resolver problemas de negócios.
Como podemos abstrair a operação do banco de dados para que a lógica da aplicação não dependa dela? Usamos a abordagem CRUD!
Um diagrama UML CRUD simplificado tem esta aparência:

Exemplo com um banco de dados EtherRelm fictício:

Exemplo com um banco de dados SQLite real:

Como você já percebeu, ao trocar o banco de dados, apenas ele muda a interface CRUD com a qual a aplicação interage permanece inalterada. CRUD é uma implementação do padrão GoF – Adaptador, porque com ele adaptamos interfaces de aplicações a qualquer banco de dados e combinamos interfaces incompatíveis.
As palavras estão vazias, mostre-me o código
Para implementar abstrações em linguagens de programação, são usadas interfaces/protocolos/classes abstratas. Todos esses são fenômenos da mesma ordem, porém, durante as entrevistas você pode ser solicitado a nomear a diferença entre eles, eu pessoalmente acho que isso não faz muito sentido porque o único propósito de uso é implementar a abstração de dados, caso contrário é para testar a memória do entrevistado.
O CRUD é frequentemente implementado dentro da estrutura do padrão Repository, porém, o repositório pode ou não implementar a interface CRUD, tudo depende da engenhosidade do desenvolvedor.
Considere um código Swift bastante típico para o repositório da estrutura Book, trabalhando diretamente com o banco de dados UserDefaults:
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)
}
O código acima parece simples, mas vamos contar o número de violações do princípio DRY (Do not Repeat Yourself) e a coerência do código:
Conectividade com o banco de dados UserDefaults
Conectividade com codificadores e decodificadores JSON – JSONEncoder, JSONDecoder
Conectado com a estrutura Book, mas precisamos de um repositório abstrato para não criar uma classe de repositório para cada estrutura que iremos armazenar no banco de dados (violação DRY)
Vejo esse tipo de código de repositório CRUD com bastante frequência, ele pode ser usado, mas o alto acoplamento e a duplicação de código levam ao fato de que, com o tempo, seu suporte se tornará muito complicado. Isso será especialmente perceptível ao tentar mudar para outro banco de dados, ou ao alterar a lógica interna de trabalho com o banco de dados em todos os repositórios criados na aplicação.
Em vez de duplicar o código, mantenha o acoplamento alto – Vamos escrever um protocolo para o repositório CRUD, abstraindo assim a interface do banco de dados e a lógica de negócio da aplicação, respeitando o DRY, implementando baixo acoplamento:
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
}
O protocolo CRUDRepository descreve interfaces e tipos de dados associados para implementação adicional de um repositório CRUD específico.
A seguir escreveremos uma implementação específica para o banco de dados 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()
}
}
O código parece longo, mas contém uma implementação concreta completa de um repositório CRUD contendo acoplamento fraco, detalhes abaixo.
typealias foram adicionadas para autodocumentação do código.
Acoplamento fraco e acoplamento forte
O descolamento de uma estrutura específica (struct) é implementado usando o T genérico, que por sua vez deve implementar os protocolos Codable. Codable permite converter estruturas usando classes que implementam os protocolos TopLevelEncoder e TopLevelDecoder, por exemplo JSONEncoder e JSONDecoder, ao usar tipos básicos (Int, String, Float, etc.) não há necessidade de escrever código adicional para converter estruturas. p>
A dissociação de um codificador e decodificador específico ocorre usando abstração no protocolo DataTransformer:
func encode<T: Encodable>(_ object: T) async throws -> Data
func decode<T: Decodable>(data: Data) async throws -> T
}
Usando a implementação de um transformador de dados, implementamos uma abstração das interfaces do codificador e do decodificador, implementando acoplamento fraco para garantir o trabalho com diferentes tipos de formatos de dados.
A seguir está o código para um DataTransformer específico, ou seja, para 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
}
}
Foi possível?
O que mudou? Agora basta inicializar um repositório específico para funcionar com qualquer estrutura que implemente o protocolo Codable, eliminando assim a necessidade de duplicação de código e implementação de acoplamento fraco da aplicação.
Um exemplo de cliente CRUD com um repositório específico, UserDefaults é o banco de dados, formato de dados JSON, estrutura do cliente, também um exemplo de escrita e leitura de um array:
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!)
Durante a primeira verificação CRUD, foi implementado o tratamento de exceções, em que a leitura do item remoto não estará mais disponível.
Trocar bancos de dados
Agora mostrarei como transferir seu código atual para outro banco de dados. Por exemplo, pegarei o código do repositório SQLite que o ChatGPT gerou:
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)
}
}
Ou o código CRUD do repositório do sistema de arquivos, que também foi gerado pelo ChatGPT:
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)
}
}
Substitua o repositório no código do cliente:
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!)
A inicialização de UserDefaultsRepository foi substituída por FileSystemRepository, com os argumentos apropriados.
Após executar a segunda versão do código do cliente, você encontrará um diretório “Banco de Dados de Clientes” na pasta de documentos, que conterá um arquivo de um array serializado em JSON com uma estrutura de Cliente.
Alterar formato de armazenamento de dados
Agora vamos pedir ao ChatGPT para gerar um codificador e decodificador para 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)
}
}
Graças aos tipos integrados no Swift, a tarefa de uma rede neural torna-se elementar.
Substitua JSON por XML no código do cliente:
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!)
O código do cliente mudou para apenas uma expressão JSONDataTransformer -> XMLDataTransformer
Total
Repositórios CRUD são um dos padrões de design que podem ser usados para implementar acoplamento fraco de componentes de arquitetura de aplicativos. Mais uma das soluções – usando ORM (Mapeamento Objeto-Relacional), resumindo, ORM usa uma abordagem em que as estruturas são completamente mapeadas para o banco de dados, e então as alterações com modelos devem ser exibidas (mapeadas(!)) no banco de dados.
Mas essa é uma história completamente diferente.
Uma implementação completa dos repositórios CRUD para Swift está disponível em:
https://gitlab.com/demensdeum/crud-example
A propósito, o Swift tem suporte fora do macOS há muito tempo; o código do artigo foi totalmente escrito e testado no Arch Linux.
Fontes
https://developer.apple.com/documentation/combine/topleveldecoder
https://developer.apple.com/documentation/combine/toplevelencoder
https://en.wikipedia.org/wiki/Create,_read,_update_and_delete