Чистая архитектура: источники данных
Введение
В контексте чистой архитектуры источник данных — это модуль, обеспечивающий доступ к данным из внешних систем, таких как базы данных, веб-службы или файловые системы. Он отвечает за реализацию низкоуровневых деталей доступа к данным, таких как открытие и закрытие соединений с базой данных, выполнение запросов, выполнение HTTP-запросов и обработку сериализации и десериализации данных.
Вот пример источника данных:
import Foundation struct Todo: Codable, Identifiable { var id: Int? let userId: Int let title: String let completed: Bool } class TodoDataSource { private let baseURL: URL init(baseURL: URL) { self.baseURL = baseURL } func getTodos() async throws -> [Todo] { let url = baseURL.appendingPathComponent("/todos") let (data, response) = try await URLSession.shared.data(from: url) guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { throw NSError(domain: "Invalid response", code: 0, userInfo: nil) } let todos = try JSONDecoder().decode([Todo].self, from: data) return todos } func saveTodo(_ todo: Todo) async throws { let url = baseURL.appendingPathComponent("/todos") var request = URLRequest(url: url) request.httpMethod = "POST" request.addValue("application/json", forHTTPHeaderField: "Content-Type") let data = try JSONEncoder().encode(todo) request.httpBody = data let (_, response) = try await URLSession.shared.data(for: request) guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 201 else { throw NSError(domain: "Invalid response", code: 0, userInfo: nil) } } }
Пример написан на Swift, но вы можете использовать ту же базовую концепцию и на других языках программирования
Источник данных — это деталь реализации «уровня данных» или «уровня инфраструктуры», который отвечает за связь с внешними системами. Уровень инфраструктуры изолирован от остальной системы, и его компонентам не разрешается знать или зависеть от уровней более высокого уровня, таких как «уровень домена».
Интерфейсы источников данных
Как правило, рекомендуется иметь источники данных в интерфейсах реализации уровня инфраструктуры. Причина этого в том, что, определяя интерфейс для источника данных, вы можете абстрагироваться от деталей реализации и создать четкое разделение задач между источником данных и другими модулями.
Потребителей источника данных не должны волновать особенности того, как данные хранятся или извлекаются. Определив интерфейс для источника данных, вы можете гарантировать, что потребители взаимодействуют с данными только с помощью набора четко определенных методов, что упрощает тестирование и поддержку кода.
Еще одно преимущество использования интерфейсов заключается в том, что упрощается замена одной реализации источника данных на другую. Например, если вы хотите переключиться с использования реляционной базы данных на базу данных документов, вы можете создать новую реализацию источника данных, которая реализует тот же интерфейс, и заменить его, не затрагивая другие модули.
Обновим источник данных
import Foundation // Define the interface for the TodoDataSource protocol TodoDataSource { func getTodos() async throws -> [Todo] func saveTodo(_ todo: Todo) async throws -> Void } // Define the Todo struct struct Todo { let id: Int? let title: String let completed: Bool } class TodoDataSourceImpl: TodoDataSource { private let baseURL: URL init(baseURL: URL) { self.baseURL = baseURL } func getTodos() async throws -> [Todo] { let url = baseURL.appendingPathComponent("/todos") let (data, response) = try await URLSession.shared.data(from: url) guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { throw NSError(domain: "Invalid response", code: 0, userInfo: nil) } let todos = try JSONDecoder().decode([Todo].self, from: data) return todos } func saveTodo(_ todo: Todo) async throws { let url = baseURL.appendingPathComponent("/todos") var request = URLRequest(url: url) request.httpMethod = "POST" request.addValue("application/json", forHTTPHeaderField: "Content-Type") let data = try JSONEncoder().encode(todo) request.httpBody = data let (_, response) = try await URLSession.shared.data(for: request) guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 201 else { throw NSError(domain: "Invalid response", code: 0, userInfo: nil) } } } // Usage example do { let BASE_URL = URL(string: "https://jsonplaceholder.typicode.com")! let todoDataSource: TodoDataSource = TodoDataSourceImpl(baseURL: BASE_URL) // Fetch todos let todos = try await todoDataSource.getTodos() print(todos) // Save a todo let todo = Todo(title: "Buy milk", completed: false) try await todoDataSource.saveTodo(todo) } catch { print("An error occurred: \(error)") }
В этом примере мы сначала определяем структуру Todo, которая представляет один элемент todo. Затем мы определяем интерфейс TodoDataSource, который имеет два метода: getTodos и saveTodo. Оба этих метода используют async/await для выполнения своих операций и выдают ошибки, если что-то пойдет не так.
Далее мы определяем класс TodoDataSourceImpl, реализующий интерфейс TodoDataSource. Метод getTodos извлекает задачи, а метод saveTodo создает новую задачу.
Наконец, мы покажем пример использования класса TodoDataSourceImpl, создав его экземпляр и вызвав его методы getTodos и saveTodo.
Обертки
Использование оболочек базы данных в наших источниках данных может быть полезно по нескольким причинам:
- Инкапсуляция. Оболочки обеспечивают дополнительный уровень инкапсуляции между приложением и инфраструктурой. Это может помочь защитить приложение от изменений деталей реализации, а также упростить переключение на другую библиотеку, если это необходимо.
- Абстракция: Оболочки могут обеспечить абстракцию функциональности более высокого уровня, что может упростить ее использование и работу. Например, оболочка HTTP может предоставлять методы для часто используемых операций, таких как (GET, PUT, POST, DELETE).
- Тестирование. Оболочки могут упростить написание тестов для кода, связанного с инфраструктурой. Например, оболочка может предоставить способ имитировать инфраструктуру или стороннюю библиотеку во время тестирования, что может сделать тесты более быстрыми и надежными.
- Безопасность. Оболочки также могут помочь повысить безопасность приложения, предоставив уровень защиты от таких вещей, как атаки путем внедрения кода SQL, которые могут возникнуть, когда пользовательский ввод не подвергается надлежащей санации.
Давайте обновим код еще раз, чтобы включить обертку
import Foundation protocol HttpWrapper { func get(_ url: URL) async throws -> Data func post(_ url: URL, data: Data) async throws } class URLSessionWrapper: HttpWrapper { func get(_ url: URL) async throws -> Data { let (data, response) = try await URLSession.shared.data(from: url) guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { throw NSError(domain: "Invalid response", code: 0, userInfo: nil) } return data } func post(_ url: URL, data: Data) async throws { var request = URLRequest(url: url) request.httpMethod = "POST" request.addValue("application/json", forHTTPHeaderField: "Content-Type") request.httpBody = data let (_, response) = try await URLSession.shared.data(for: request) guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 201 else { throw NSError(domain: "Invalid response", code: 0, userInfo: nil) } } } protocol TodoDataSource { func getTodos() async throws -> [Todo] func saveTodo(_ todo: Todo) async throws -> Void } class HTTPDataSource: TodoDataSource { private let httpWrapper: HttpWrapper private let baseURL: URL init(httpWrapper: HttpWrapper, baseURL: URL) { self.httpWrapper = httpWrapper self.baseURL = baseURL } func getTodos() async throws -> [Todo] { let url = baseURL.appendingPathComponent("/todos") let data = try await httpWrapper.get(url) let todos = try JSONDecoder().decode([Todo].self, from: data) return todos } func saveTodo(_ todo: Todo) async throws { let url = baseURL.appendingPathComponent("/todos") let data = try JSONEncoder().encode(todo) try await httpWrapper.post(url, data: data) } } // Usage example do { let BASE_URL = URL(string: "https://jsonplaceholder.typicode.com")! let todoDataSource = HTTPDataSource(httpWrapper: URLSessionWrapper(), baseURL: BASE_URL) // Fetch todos let todos = try await todoDataSource.getTodos() print(todos) // Save a todo let todo = Todo(title: "Buy milk", completed: false) try await todoDataSource.saveTodo(todo) } catch { print("An error occurred: \(error)") }
В этом примере мы используем оболочку URLSessionWrapper
в нашем источнике данных HTTPDataSource
. Метод getTodos
из HTTPDataSource
использует метод get
из HTTPWrapper
для получения данных. Метод saveTodos
HTTPDataSource
использует метод post
HTTPWrapper
для отправки данных.
Заключение
В заключение, источник данных важен для организованного хранения и извлечения данных. Чтобы убедиться, что источник данных прост в использовании, надежен и прост в обслуживании, необходимо помнить о некоторых важных вещах:
- Держите его отдельно от остальной части приложения, чтобы его можно было легко изменить при необходимости.
- Убедитесь, что он работает с разными инструментами и программным обеспечением, чтобы его можно было использовать в разных средах.
- Протестируйте его, чтобы убедиться, что он работает правильно, и при необходимости внесите изменения, чтобы сделать его лучше.
- Убедитесь, что хранящиеся в нем данные соответствуют правилам и требованиям приложения.
- Защитите его от несанкционированного доступа, используя такие меры безопасности, как пароли и шифрование.
Следуя этим принципам, источник данных в чистой архитектуре может помочь сделать программное обеспечение более надежным, гибким и безопасным.