Какой правильный метод для создания многоразовых конвейеров с Combine? - PullRequest
0 голосов
/ 30 января 2020

Я относительно новичок в мире программирования Functional Reactive, и все еще пытаюсь обдумать концепции. Я использую SDK для выполнения сетевых запросов, в частности, для запросов к удаленной базе данных. SDK возвращает издателя, и у меня есть рабочий конвейер, который преобразует этот результат в объекты модели. Вот этот рабочий конвейер:

let existingClaimQuery = "SELECT Id, Subject, CaseNumber FROM Case WHERE Status != 'Closed' ORDER BY CaseNumber DESC"
let requestForOpenCases = RestClient.shared.request(forQuery: existingClaimQuery, apiVersion: RestClient.apiVersion)
caseCancellable = RestClient.shared
  .publisher(for: requestForOpenCases)
  .receive(on: RunLoop.main)
  .tryMap({restresponse -> [String:Any] in
    let json = try restresponse.asJson() as? [String:Any]
    return json ?? RestClient.JSONKeyValuePairs()
  })
  .map({json -> [[String:Any]] in
    let records = json["records"] as? [[String:Any]]
    return records ?? [[:]]
  })
  .map({
    $0.map{(item) -> Claim in
      return Claim(
        id: item["Id"] as? String ?? "None Listed",
        subject: item["Subject"] as? String ?? "None Listed",
        caseNumber: item["CaseNumber"] as? String ?? "0"
      )
    }
  })
  .mapError{error -> Error in
    print(error)
    return error
  }
  .catch{ error in
    return Just([])
  }
.assign(to: \.claims, on: self)

Я приступил к работе над другим разделом кода и понял, что часто необходимо выполнить этот же процесс - написать запрос, создать запрос на этот запрос, и обработать его через конвейер, который в конечном итоге возвращает [[String: Any]].

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

class QueryStream: ObservableObject {

  var query: String = ""
  private var queryCancellable: AnyCancellable?

  @Published var records: [[String:Any]] = [[String:Any]]()

  func execute(){
    let queryRequest = RestClient.shared.request(forQuery: query, apiVersion: RestClient.apiVersion)

    queryCancellable = RestClient.shared.publisher(for: queryRequest)
      .receive(on: RunLoop.main)
      .tryMap({restresponse -> [String:Any] in
        let json = try restresponse.asJson() as? [String:Any]
        return json ?? [String:Any]()
      })
      .map({json -> [[String:Any]] in
        let records = json["records"] as? [[String:Any]]
        return records ?? [[:]]
      })
      .mapError{error -> Error in
        print(error)
        return error
      }
      .catch{ error in
        return Just([])
      }
    .assign(to: \.records, on: self)
  }

}

Для этого по-прежнему требуется писать конвейер для каждого использования. Я чувствую, что должен быть какой-то способ получить одноразовое обещание, такое как конвейер, который позволил бы

let SomeRecords = QueryStream("Query here").execute()

Я тоже n00b? продумывать это? Какова мудрость стека?

Ответы [ 2 ]

2 голосов
/ 30 января 2020

Целые трубопроводы не подлежат повторному использованию. Издатели можно использовать повторно. Когда я говорю «издатель», я имею в виду первоначального издателя и подключенных к нему операторов. (Помните, что оператор сам по себе является издателем.) Издатель может существовать как свойство чего-либо, поэтому вы можете подписаться на него или его можно сгенерировать для конкретного случая (например, для конкретного запроса) с помощью функции.

Для иллюстрации приведем разовый конвейер:

let s = "https://photojournal.jpl.nasa.gov/tiff/PIA23172.tif"
let url = URL(string:s)!
let eph = URLSessionConfiguration.ephemeral
let session = URLSession(configuration: eph)
session.dataTaskPublisher(for: url)
    .map {$0.data}
    .replaceError(with: Data())
    .compactMap { UIImage(data:$0) }
    .receive(on: DispatchQueue.main)
    .assign(to: \.image, on: self.iv)
    .store(in:&self.storage)

Этот конвейер пытается загрузить данные с URL-адреса, проверяет, являются ли они данными изображения, и, если это так, поворачивает изображение. данные в изображение и отображает его в виде изображения в интерфейсе.

Допустим, я хочу сделать это для различных различных удаленных изображений. Очевидно, было бы смешно повторять весь трубопровод повсюду. Что отличается в первую очередь, так это URL, поэтому давайте инкапсулируем первую часть конвейера как издатель, который может быть сгенерирован по требованию на основе URL:

func image(fromURL url:URL) -> AnyPublisher<UIImage,Never> {
    let eph = URLSessionConfiguration.ephemeral
    let session = URLSession(configuration: eph)
    return session.dataTaskPublisher(for: url)
        .map {$0.data}
        .replaceError(with: Data())
        .compactMap { UIImage(data:$0) }
        .receive(on: DispatchQueue.main)
        .eraseToAnyPublisher()
}

Теперь единственное, что нужно повторить в различные места в нашем коде подписчик для этого издателя:

let s = "https://photojournal.jpl.nasa.gov/tiff/PIA23172.tif"
let url = URL(string:s)!
image(fromURL:url)
    .map{Optional($0)}
    .assign(to: \.image, on: self.iv)
    .store(in:&self.storage)

Вы видите? В другом месте у нас может быть другой URL, и мы можем сделать что-то другое с UIImage, который появляется из-за вызова image(fromURL:), и это просто прекрасно; основная часть конвейера была инкапсулирована и не нуждается в повторении.

Издатель вашего примера конвейера подвержен такой же инкапсуляции и повторному использованию.

0 голосов
/ 30 января 2020

Позвольте мне сначала отметить, что я думаю, что вы отправляете на main в начале вашего конвейера. Насколько я могу судить, все ваши map преобразования являются чистыми функциями (без побочных эффектов или ссылок на изменяемое состояние), поэтому они также могут запускаться в фоновом потоке и, следовательно, не блокировать пользовательский интерфейс.

Во-вторых, как сказал Мэтт, Publisher обычно можно использовать повторно. Ваш конвейер создает большой комплекс Publisher, а затем подписывается на него, что приводит к AnyCancellable. Поэтому выделите большой комплекс Publisher, но не подписку.

Вы можете выделить его в метод расширения для вашего RestClient для удобства:

extension RestClient {
    func records<Record>(
        forQuery query: String,
        makeRecord: @escaping ([String: Any]) throws -> Record)
        -> AnyPublisher<[Record], Never>
    {
        let request = self.request(forQuery: query, apiVersion: RestClient.apiVersion)

        return self.publisher(for: request)
            .tryMap { try $0.asJson() as? [String: Any] ?? [:] }
            .map { $0["records"] as? [[String: Any]] ?? [] }
            .tryMap { try $0.map { try makeRecord($0) } }
            .mapError { dump($0) } // dump is a Swift standard function
            .replaceError(with: []) // simpler than .catch
            .eraseToAnyPublisher()
    }
}

Тогда вы можете использовать это выглядит так:

struct Claim {
    var id: String
    var subject: String
    var caseNumber: String
}

extension Claim {
    static func from(json: [String: Any]) -> Claim {
        return .init(
            id: json["Id"] as? String ?? "None Listed",
            subject: json["Subject"] as? String ?? "None Listed",
            caseNumber: json["CaseNumber"] as? String ?? "0")
    }
}

class MyController {
    var claims: [Claim] = []
    var caseCancellable: AnyCancellable?

    func run() {
        let existingClaimQuery = "SELECT Id, Subject, CaseNumber FROM Case WHERE Status != 'Closed' ORDER BY CaseNumber DESC"
        caseCancellable = RestClient.shared.records(forQuery: existingClaimQuery, makeRecord: Claim.from(json:))
            .receive(on: RunLoop.main)
            .assign(to: \.claims, on: self)
    }
}

Обратите внимание, что я добавил оператор receive(on: RunLoop.main) в метод, который подписывается на издателя, а не встраивает его в издателя. Это позволяет легко добавлять дополнительные операторы, которые выполняются в фоновом планировщике, перед отправкой в ​​основной поток.


ОБНОВЛЕНИЕ

Из вашего комментария:

В синтаксисе обещания я мог бы сказать execute run (), как определено выше, и .then (doSomethingWithThatData ()), зная, что doSomethingWithThatData не запустится, пока начальная работа не завершится успешно. Я пытаюсь разработать настройку, в которой мне нужно использовать этот метод записей (fromQuery :), а затем (и только потом) делать что-то с этими данными. Я изо всех сил пытаюсь понять, как это сделать до конца.

Я не знаю, какую реализацию обещаний вы используете, поэтому трудно понять, что делает ваш .then(doSomethingWithThatData()). То, что вы написали, не имеет большого смысла в Swift. Возможно, вы имели в виду:

.then { data in doSomething(with: data) }

В этом случае метод doSomething(with:) не может быть вызван до тех пор, пока не станет доступен data, поскольку doSomething(with:) принимает data в качестве аргумента!

...