Чистая архитектура: источники данных

Введение

В контексте чистой архитектуры источник данных — это модуль, обеспечивающий доступ к данным из внешних систем, таких как базы данных, веб-службы или файловые системы. Он отвечает за реализацию низкоуровневых деталей доступа к данным, таких как открытие и закрытие соединений с базой данных, выполнение запросов, выполнение 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.

Обертки

Использование оболочек базы данных в наших источниках данных может быть полезно по нескольким причинам:

  1. Инкапсуляция. Оболочки обеспечивают дополнительный уровень инкапсуляции между приложением и инфраструктурой. Это может помочь защитить приложение от изменений деталей реализации, а также упростить переключение на другую библиотеку, если это необходимо.
  2. Абстракция: Оболочки могут обеспечить абстракцию функциональности более высокого уровня, что может упростить ее использование и работу. Например, оболочка HTTP может предоставлять методы для часто используемых операций, таких как (GET, PUT, POST, DELETE).
  3. Тестирование. Оболочки могут упростить написание тестов для кода, связанного с инфраструктурой. Например, оболочка может предоставить способ имитировать инфраструктуру или стороннюю библиотеку во время тестирования, что может сделать тесты более быстрыми и надежными.
  4. Безопасность. Оболочки также могут помочь повысить безопасность приложения, предоставив уровень защиты от таких вещей, как атаки путем внедрения кода 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 для отправки данных.

Заключение

В заключение, источник данных важен для организованного хранения и извлечения данных. Чтобы убедиться, что источник данных прост в использовании, надежен и прост в обслуживании, необходимо помнить о некоторых важных вещах:

  1. Держите его отдельно от остальной части приложения, чтобы его можно было легко изменить при необходимости.
  2. Убедитесь, что он работает с разными инструментами и программным обеспечением, чтобы его можно было использовать в разных средах.
  3. Протестируйте его, чтобы убедиться, что он работает правильно, и при необходимости внесите изменения, чтобы сделать его лучше.
  4. Убедитесь, что хранящиеся в нем данные соответствуют правилам и требованиям приложения.
  5. Защитите его от несанкционированного доступа, используя такие меры безопасности, как пароли и шифрование.

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