CRUD repository

В этой заметке я опишу основные принципы известного классического паттерна CRUD, реализацию на языке Swift. Swift является открытым, кроссплатформенным языком, доступным для ОС Windows, Linux, macOS, iOS, Android.

Существует множество решений абстрагирования хранилища данных и логики приложения. Одним из таких решений является подход CRUD, это акроним от C – Create, R -Read, U – Update, D – Delete.
Обычно реализация этого принципа обеспечивается с помощью реализации интерфейса к базе данных, в котором работа с элементами происходит с использованием уникального идентификатора, например id. Создается интерфейс по каждой букве CRUD – Create(object, id), Read(id), Update(object, id), Delete(object, id).
Если объект содержит id внутри себя, то аргумент id можно упустить в части методов (Create, Update, Delete), так как туда передается объект целиком вместе со своим полем – id. А вот для – Read требуется id, так как мы хотим получить объект из базы данных по идентификатору.
Все имена вымышлены
Представим что гипотетическое приложение AssistantAI создавалось с использованием бесплатной SDK базы данных EtherRelm, интеграция была простой, API очень удобным, в итоге приложение было выпущено в маркеты.
Внезапно компания-разработчик SDK EtherRelm решает сделать её платной, устанавливая цену в 100$ в год за одного пользователя приложения.
Что? Да! Что же теперь делать разработчикам из AssistantAI, ведь у них уже 1млн активных пользователей! Платить 100 млн долларов?
Вместо этого принимается решение оценить перенос приложения на нативную для платформы базу данных RootData, по оценке программистов такой перенос займет около полугода, это без учета реализации новых фич в приложении. После недолгих раздумий, принимается решение убрать приложение из маркетов, переписать его на другом бесплатном кроссплатформенном фреймворке со встроенной базой данных BueMS, это решит проблему с платностью БД + упростит разработку на другие платформы.
Через год приложение переписано на BueMS, но тут внезапно разработчик фреймворка решает сделать его платным. Получается что команда попала в одну и ту же ловушку дважды, получится ли у них выбраться во второй раз, это уже совершенно другая история.
Абстракция на помощь
Этих проблем удалось бы избежать, если бы разработчики использовали абстракцию интерфейсов внутри приложения. К трем китам ООП – полиморфизму, инкапсуляции, наследованию, не так давно добавили еще одного – абстракцию.
Абстракция данных позволяет описывать идеи, модели в общих чертах, с минимум деталей, при этом достаточно точной для реализации конкретных имплементаций, которые используют для решения бизнес-задач.
Как мы можем абстрагировать работу с базой данных, чтобы логика приложения не зависела от нее? Используем подход CRUD!

Упрощенно UML схема CRUD выглядит так:

Пример с вымышленной базой данных EtherRelm:

Пример с настоящей базой данных SQLite:

Как вы уже заметили, при переключении базы данных, меняется только она, интерфейс CRUD с которым взаимодействует приложение остается неизменным. CRUD является вариантом реализации паттерна GoF – Адаптер, т.к. с помощью него мы адаптируем интерфейсы приложения к любой базе данных, совмещаем несовместимые интерфейсы.
Слова это пустое, покажи мне код
Для реализации абстракций в языках программирования используют интерфейсы/протоколы/абстрактные классы. Все это явления одного порядка, однако на собеседованиях вас могут попросить назвать разницу между ними, я лично считаю что в этом особого смысла нет т.к. единственная цель использования это реализация абстракции данных, в остальном это проверка памяти интервьюируемого.
CRUD часто реализуют в рамках паттерна Репозиторий, репозиторий однако может реализовывать интерфейс CRUD, а может и не реализовывать, всё зависит от изобретательности разработчика.

Рассмотрим достаточно типичный Swift код репозитория структур Book, работающий напрямую с базой данных UserDefaults:


import Foundation

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 not Repeat Yourself) и связанность кода:
Связанность с базой данных UserDefaults
Связанность с энкодерами и декодерами JSON – JSONEncoder, JSONDecoder
Связанность со структурой Book, а нам нужен абстрактный репозиторий чтобы не создавать по классу репозитория для каждой структуры, которую мы будем хранить в базе данных (нарушение DRY)

Такой код CRUD репозитория я встречаю достаточно часто, пользоваться им можно, однако высокая связанность, дублирование кода, приводят к тому что со временем его поддержка очень сильно усложнится. Особенно это будет ощущаться при попытке перейти на другую базу данных, либо при изменении внутренней логики работы с бд во всех созданных в приложении репозиториях.
Вместо того чтобы дублировать код, держать высокую связанность – напишем протокол для CRUD репозитория, таким образом абстрагируя интерфейс базы данных и бизнес-логики приложения, соблюдая DRY, осуществляя низкую связанность:


protocol CRUDRepository {
    typealias Item = Codable
    typealias ItemIdentifier = String
    
    func create(id: CRUDRepository.ItemIdentifier, item: T) async throws
    func read(id: CRUDRepository.ItemIdentifier) async throws -> T
    func update(id: CRUDRepository.ItemIdentifier, item: T) async throws
    func delete(id: CRUDRepository.ItemIdentifier) async throws
}

Протокол CRUDRepository описывает интерфейсы и ассоциированные типы данных для дальнейшей реализации конкретного CRUD репозитория.

Далее напишем конкретную реализацию для базы данных UserDefaults:


class UserDefaultsRepository: CRUDRepository {
    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(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(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(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 и т.д.) нет необходимости писать дополнительный код для преобразования структур.

Отвязка от конкретного энкодера и декодера происходит с помощью абстрагирования в протоколе DataTransformer:


protocol DataTransformer {
	func encode(_ object: T) async throws -> Data
	func decode(data: Data) async throws -> T
}

С помощью реализации дата-трансформера мы реализовали абстракцию интерфейсов энкодера и декодера, реализовав слабую связанность для обеспечения работы с различными типами форматов данных.

Далее приводится код конкретного DataTransformer, а именно для JSON:


class JSONDataTransformer: DataTransformer {
	func encode(_ object: T) async throws -> Data where T : Encodable {
    		let data = try JSONEncoder().encode(object)
    		return data
	}
    
	func decode(data: Data) async throws -> T where T : Decodable {
    		let item: T = try JSONDecoder().decode(T.self, from: data)
    		return item
	}
}

А так можно было?
Что же изменилось? Теперь достаточно проинициализировать конкретный репозиторий для работы с любой структурой которая имплементирует протокол Codable, таким образом исчезает потребность в дублировании кода, реализуется слабая связанность приложения.

Пример клиентский CRUD с конкретным репозиторием, в качестве базы данных выступает UserDefaults, формат данных JSON, структура Client, также пример записи и считывания массива:


import Foundation

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 реализована обработка исключения, при которой чтение удаленного айтема будет уже недоступно.

Переключаем базы данных

Теперь я покажу как перенести текущий код на другую базу данных. Для примера возьму код репозитория SQLite который сгенерил ChatGPT:


import SQLite3

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(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(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(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)
    }
}

Или код CRUD репозитория для файловой системы который тоже сгенерила ChatGPT:


import Foundation

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(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(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(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)
	}
}

Заменяем репозиторий в клиентском коде:


import Foundation

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, с соотетствующими аргументами.
После запуска второго варианта клиентского кода, вы обнаружите в папке документов директорию “Clients Database”, которая будет содержать в себе файл сериализованного в JSON массива с одной структурой Client.

Переключаем формат хранения данных

Теперь попросим ChatGPT сгенерить энкодер и декодер для XML:


class XMLDataTransformer: DataTransformer {
	let formatExtension = "xml"
    
	func encode(_ item: T) async throws -> Data {
    	let encoder = PropertyListEncoder()
    	encoder.outputFormat = .xml
    	return try encoder.encode(item)
	}
    
	func decode(data: Data) async throws -> T {
    	let decoder = PropertyListDecoder()
    	return try decoder.decode(T.self, from: data)
	}
}

Благодаря встроенным типам в Swift, задача для нейросети становится элементарной.

Заменяем JSON на XML в клиентском коде:


import Foundation

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!)

Клиентский код изменился только на одно выражение JSONDataTransformer -> XMLDataTransformer

Итог
CRUD репозитории один из паттернов проектирования, которые можно использовать для реализации слабой связанности компонентов архитектуры приложения. Еще одно из решений – использование ORM (Объектно-реляционный маппинг), если вкратце то в ОРМ используется подход при котором структуры полностью мапятся на базу данных, и затем изменения с моделями должны отображаться (маппиться(!)) на бд.
Но это уже совсем другая история.

Полная реализация репозиториев CRUD для Swift доступна по ссылке:
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

dd input/output error

If you receive an input/output error when copying a normal disk using dd in Linux, here are the steps you should take:

The situation is quite sad, but solvable. Most likely, you are dealing with a faulty disk that contains bad blocks that can no longer be used, written to, or read from.

Be sure to check such a disk using S.M.A.R.T., it will most likely show you disk errors. This was the case for me, the number of bad blocks was so large that I had to say goodbye to the old hard drive and replace it with a new SSD.

The problem was that this disk had a fully functional system with licensed software necessary for work. I attempted to use partimage to quickly copy the data, but found that the utility only copied a third of the disk, then terminated either with a segfault or some other amusing C/C++ quirk.

Then I tried copying the data using dd, and found that dd reached about the same point as partimage, then encountered an input/output error. Various fun flags like conv=noerr, skip, or other such things did not help at all.

However, I was able to copy the data to another disk without any issues using a GNU utility called ddrescue.

ddrescue /dev/sda1 /dev/sdb1

After that, my hair became silky, my wife returned, the kids, and the dog stopped biting the sofa.

A big advantage of ddrescue is the built-in progress bar, so there is no need to hack together some workarounds like pv and other not very pretty dd flags. Also, ddrescue shows the number of attempts to read the data; the wiki also says that the utility has some super algorithm for reading damaged data, we’ll leave that for people who like to dig into the source code, we are not among them, right?

https://en.wikipedia.org/wiki/Ddrescue
https://www.gnu.org/software/ddrescue/ddrescue.html

ChatGPT Overview

Hi all! In this article, I want to talk about ChatGPT, a powerful language modeling tool from OpenAI that can help solve various text-processing tasks. I will show how this tool works and how it can be used in practical situations. Let’s get started!

At the moment, ChatGPT is one of the best neural network-based language models in the world. It was created to help developers create intelligent systems that can generate natural language and communicate with people in it.

One of the key advantages of ChatGPT is its ability for contextual text modeling. This means that the model takes into account the previous dialogue and uses it to better understand the situation and generate a more natural response.

You can use ChatGPT for a variety of tasks such as customer support automation, chatbot creation, text generation, and more.

The neural networks behind ChatGPT have been trained on huge arrays of text to provide highly accurate predictions. This allows the model to generate natural text that can support dialogue and answer questions.

With ChatGPT, you can create your own chatbots and other intelligent systems that can interact with people in natural language. This can be especially useful in industries such as travel, retail, and customer support.

In conclusion, ChatGPT is a powerful tool for solving various language modeling problems. Its ability for contextual modeling makes it especially useful for building chatbots and intelligent systems.


In fact, everything that ChatGPT wrote above was completely written by itself. What? Yes? I’m shocked myself!

The network itself can be tested here:
https://chat.openai.com/chat

How to run Unreal Tournament 99 on MacBook M1

macOS M1 Ventura

If you’re a dedicated Unreal Tournament 99 fan like me, you’ll want to run the game on the latest operating systems and hardware. I have successfully run Unreal Tournament 99 on an M1 Macbook Pro running macOS Ventura 13.0.1.

  1. To run the game under macOS for the M1 processor you need:
  2. Download the version from the repository, https://github.com/OldUnreal/UnrealTournamentPatches/releases for macOS.
  3. Drop UnrealTournament.app to /Applications
  4. Create an Unreal Tournament folder in ~/Library/Application Support/
  5. Copy the Windows versions of the Maps, Sounds, Textures, Music folder to ~/Library/Application Support/Unreal Tournament
  6. Delete the files LadderFonts.utx, UWindowFonts.utx from the folder ~/Library/Application Support/Unreal Tournament/Textures
  7. Run UnrealTournament.app from /Applications, enjoy the frags!

The penultimate step is needed to display the correct fonts, the original ones are displayed too small.
After starting, configure the screen resolution, keyboard, font size in the GUI, and other necessary settings.

Unreal Tournament macOS Ventura M1

Windows 11

Also, for dessert, the launch of Unreal Tournament 99 on Windows 11, the game works immediately after installation, without additional shamanism, but there are problems with displaying the GUI, the performance of an outdated D3D renderer. Therefore, it is better to use the patched version.

  1. The launch process is very similar to that for macOS:
  2. Download version from https://github.com/OldUnreal/UnrealTournamentPatches/releases repository for Windows, for example in zip.
  3. Unpack and replace files over the current Unreal Tournament.
  4. Run the game from [Game folder]/System/UnrealTournament.exe

I am glad that fans continue to support such a masterpiece and there is an opportunity to play even on modern hardware.

Turn on USB keyboard backlight on macOS

I recently bought a very inexpensive Getorix GK-45X USB keyboard with RGB backlight. After connecting it to a Macbook Pro on an M1 processor, it became clear that the RGB backlight was not working. Even by pressing the magic combination Fn + Scroll Lock, it was not possible to turn on the backlight, only the backlight level of the MacBook screen changed.
There are several solutions to this problem, namely OpenRGB (does not work), HID LED Test (does not work). Only the kvmswitch utility worked:
https://github.com/stoutput/OSX-KVM

You need to download it from the github and allow it to run from the terminal in the Security panel of the System Settings.
As I understood from the description, after launching the utility sends pressing Fn + Scroll Lock, thus turning on/off the backlight on the keyboard.

Tree sort

Tree sort – binary search tree sort. Time complexity – O(n²). In such a tree, each node has numbers less than the node on the left, more than the node on the right, when coming from the root and printing the values ​​from left to right, we get a sorted list of numbers. Surprising huh?

Consider the binary search tree schema:

Derrick Coetzee (public domain)

Try to manually read the numbers starting from the penultimate left node of the lower left corner, for each node on the left – a node – on the right.

It will turn out like this:

  1. The penultimate node at the bottom left is 3.
  2. She has a left branch – 1.
  3. Take this number (1)
  4. Next, take the vertex 3 (1, 3) itself
  5. To the right is branch 6, but it contains branches. Therefore, we read it in the same way.
  6. Left branch of node 6 number 4 (1, 3, 4)
  7. Node 6 itself (1, 3, 4, 6)
  8. Right 7 (1, 3, 4, 6, 7)
  9. Go up to the root node – 8 (1,3, 4 ,6, 7, 8)
  10. Print everything on the right by analogy
  11. Get the final list – 1, 3, 4, 6, 7, 8, 10, 13, 14

To implement the algorithm in code, you need two functions:

  1. Building a binary search tree
  2. Printing the binary search tree in the correct order

They assemble a binary search tree in the same way as they read it, a number is attached to each node on the left or right, depending on whether it is less or more.

Lua example:

Node = {value = nil, lhs = nil, rhs = nil}

function Node:new(value, lhs, rhs)
    output = {}
    setmetatable(output, self)
    self.__index = self  
    output.value = value
    output.lhs = lhs
    output.rhs = rhs
    output.counter = 1
    return output  
end

function Node:Increment()
    self.counter = self.counter + 1
end

function Node:Insert(value)
    if self.lhs ~= nil and self.lhs.value > value then
        self.lhs:Insert(value)
        return
    end

    if self.rhs ~= nil and self.rhs.value < value then
        self.rhs:Insert(value)
        return
    end

    if self.value == value then
        self:Increment()
        return
    elseif self.value > value then
        if self.lhs == nil then
            self.lhs = Node:new(value, nil, nil)
        else
            self.lhs:Insert(value)
        end
        return
    else
        if self.rhs == nil then
            self.rhs = Node:new(value, nil, nil)
        else
            self.rhs:Insert(value)
        end
        return
    end
end

function Node:InOrder(output)
    if self.lhs ~= nil then
       output = self.lhs:InOrder(output)
    end
    output = self:printSelf(output)
    if self.rhs ~= nil then
        output = self.rhs:InOrder(output)
    end
    return output
end

function Node:printSelf(output)
    for i=0,self.counter-1 do
        output = output .. tostring(self.value) .. " "
    end
    return output
end

function PrintArray(numbers)
    output = ""
    for i=0,#numbers do
        output = output .. tostring(numbers[i]) .. " "
    end    
    print(output)
end

function Treesort(numbers)
    rootNode = Node:new(numbers[0], nil, nil)
    for i=1,#numbers do
        rootNode:Insert(numbers[i])
    end
    print(rootNode:InOrder(""))
end


numbersCount = 10
maxNumber = 9

numbers = {}

for i=0,numbersCount-1 do
    numbers[i] = math.random(0, maxNumber)
end

PrintArray(numbers)
Treesort(numbers)

An important nuance is that for numbers that are equal to the vertex, a lot of interesting mechanisms for hooking to the node have been invented, but I just added a counter to the vertex class, when printing, the numbers are returned by the counter.

Links

https://gitlab.com/demensdeum /algorithms/-/tree/master/sortAlgorithms/treesort

References

TreeSort Algorithm Explained and Implemented with Examples in Java | Sorting Algorithms | Geekific – YouTube

Tree sort – YouTube

Convert Sorted Array to Binary Search Tree (LeetCode 108. Algorithm Explained) – YouTube

Sorting algorithms/Tree sort on a linked list – Rosetta Code

Tree Sort – GeeksforGeeks

Tree sort – Wikipedia

How to handle duplicates in Binary Search Tree? – GeeksforGeeks

Tree Sort | GeeksforGeeks – YouTube

Bucket Sort

Bucket Sort – bucket sorting. The algorithm is similar to sorting by counting, with the difference that the numbers are collected into “buckets”-ranges, then the buckets are sorted using any other, sufficiently productive, sorting algorithm, and the final chord is the expansion of the “buckets” one by one, resulting in a sorted list.

The time complexity of the algorithm is O(nk). The algorithm runs in linear time for data that obeys a uniform distribution. To put it simply, the elements must be in a certain range, without “splashes”, for example, numbers from 0.0 to 1.0. If among such numbers there are 4 or 999, then such a series, according to the laws of the yard, is no longer considered “even”.

Implementation example in Julia:

function bucketSort(numbers, bucketsCount)
    buckets = Vector{Vector{Int}}()
    
    for i in 0:bucketsCount - 1
        bucket = Vector{Int}()
        push!(buckets, bucket)
    end

    maxNumber = maximum(numbers)

    for i in 0:length(numbers) - 1
        bucketIndex = 1 + Int(floor(bucketsCount * numbers[1 + i] / (maxNumber + 1)))
        push!(buckets[bucketIndex], numbers[1 + i])
    end

    for i in 0:length(buckets) - 1
        bucketIndex = 1 + i
        buckets[bucketIndex] = sort(buckets[bucketIndex])
    end

    flat = [(buckets...)...]
    print(flat, "\n")

end

numbersCount = 10
maxNumber = 10
numbers = rand(1:maxNumber, numbersCount)
print(numbers,"\n")
bucketsCount = 10
bucketSort(numbers, bucketsCount)

The performance of the algorithm is also affected by the number of buckets, for more numbers it is better to take a larger number of buckets (Algorithms in a nutshell by George T. Heineman)

Links

https://gitlab.com/demensdeum/algorithms/-/tree/master/sortAlgorithms/bucketSort

References

https://www.youtube.com/watch?v=VuXbEb5ywrU
https://www.youtube.com/watch?v=ELrhrrCjDOA
https://medium.com/karuna-sehgal/an-introduction-to-bucket-sort-62aa5325d124
https://www.geeksforgeeks.org/bucket-sort-2/
https://ru.wikipedia.org/wiki/%D0%91%D0%BB%D0%BE%D1%87%D0%BD%D0%B0%D1%8F_%D1%81%D0%BE%D1%80%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%BA%D0%B0
https://www.youtube.com/watch?v=LPrF9yEKTks
https://en.wikipedia.org/wiki/Bucket_sort
https://julialang.org/
https://www.oreilly.com/library/view/algorithms-in-a/9780596516246/ch04s08.html

Radix Sort

Radix Sort – radix sort. The algorithm is similar to counting sort in that there is no comparison of elements, instead elements are *character-by-character* grouped into *buckets* (buckets), the bucket is selected by the index of the current number-character. Time complexity – O(nd).

Works like this:

  • The input will be the numbers 6, 12, 44, 9
  • Let’s create 10 buckets of lists (0-9) into which we will add/sort numbers bit by bit.

Further:

  1. Run a loop with counter i up to the maximum number of characters in the number
  2. At index i from right to left we get one character for each number, if there is no character, then we consider it to be zero
  3. The character is converted to a number
  4. Select a bucket by index – number, put the whole number there
  5. After finishing iterating over numbers, convert all buckets back to a list of numbers
  6. Get numbers sorted by digit
  7. Repeat until all digits run out

Radix Sort example in Scala:

import scala.collection.mutable.ListBuffer
import scala.util.Random.nextInt

object RadixSort {
    def main(args: Array[String]) = {
        var maxNumber = 200
        var numbersCount = 30
        var maxLength = maxNumber.toString.length() - 1

        var referenceNumbers = LazyList.continually(nextInt(maxNumber + 1)).take(numbersCount).toList
        var numbers = referenceNumbers
        
        var buckets = List.fill(10)(ListBuffer[Int]())

        for( i <- 0 to maxLength) { numbers.foreach( number => {
                    var numberString = number.toString
                    if (numberString.length() > i) {
                        var index = numberString.length() - i - 1
                        var character = numberString.charAt(index).toString
                        var characterInteger = character.toInt  
                        buckets.apply(characterInteger) += number
                    }
                    else {
                        buckets.apply(0) += number
                    }
                }
            )
            numbers = buckets.flatten
            buckets.foreach(x => x.clear())
        }
        println(referenceNumbers)
        println(numbers)
        println(s"Validation result: ${numbers == referenceNumbers.sorted}")
    }
}

The algorithm also has a version for parallel execution, for example on the GPU; there is also a variant of bit sort, which is probably very interesting and truly breathtaking!

Links

https://gitlab.com/demensdeum/algorithms/-/blob/master/sortAlgorithms/radixSort/radixSort.scala

Sources

https://ru.wikipedia.org/wiki/%D0%9F%D0%BE%D1%80%D0%B0%D0%B7%D1%80% D1%8F%D0%B4%D0%BD%D0%B0%D1%8F_%D1%81%D0%BE%D1%80%D1%82%D0%B8%D1%80%D0%BE%D0% B2%D0%BA%D0%B0
https://www.geeksforgeeks.org/radix-sort/
https://www.youtube.com/watch?v=toAlAJKojos
https://github.com/gyatskov/radix-sort