「戦略」を使用したネットワーク層の抽象化

ネットワーク層の私の以前の実装はすべて、まだ成長の余地があるという印象を残しました。 この出版物は、アプリケーションのネットワーク層を構築するためのアーキテクチャソリューションの1つを提供することを目的としています。 これは、次のネットワークフレームワークを使用する次の方法ではありません。







パート1.既存のアプローチの検討



まず、 Artsyアプリは、 Swiftで作成された21個のすばらしいオープンソースiOSアプリから取得されます。 ネットワーク層全体の構築に基づいて、人気のあるMoyaフレームワークを使用します。 このプロジェクトで出会った多くの主要な欠点に注意し、他のアプリケーションや出版物でよく会います。







応答変換チェーンの繰り返し



let endpoint: ArtsyAPI = ArtsyAPI.activeAuctions provider.request(endpoint) .filterSuccessfulStatusCodes() .mapJSON() .mapTo(arrayOf: Sale.self)
      
      





このコードを持つ開発者は、 activeAuctionsリクエストへの応答がSaleオブジェクトの配列に変換される特定の論理チェーンを指定しています。 このリクエストを他のViewModelまたはViewControllerで再利用する場合開発者はリクエストをレスポンス変換チェーンとともにコピーする必要があります。 反復的な変換ロジックのコピーを避けるために、リクエストとレスポンスは、一度だけ記述される特定のコントラクトに接続できます。







多数の依存関係



多くの場合、 AlamofireMoya、およびその他のフレームワークがネットワークとの連携に使用されますが、理想的には、アプリケーションはこれらのフレームワークに最小限に依存する必要があります。 Artsyリポジトリ検索でimport Moyaと入力すると、何十もの一致が表示されます。 突然プロジェクトがMoyaの使用を放棄することを決定した場合、多くのコードをリファクタリングする必要があります。







この依存関係を削除し、アプリケーションを動作状態に変更しようとすると、各プロジェクトがネットワークフレームワークにどれだけ依存しているかを評価することは難しくありません。







ジェネラルリクエストマネージャークラス



依存関係の状況から抜け出す可能性のある方法は、フレームワークと、ネットワークからデータを取得するすべての可能な方法について知る唯一の特別なクラスを作成することです。 これらのメソッドは、厳密に型指定された着信および発信パラメーターを持つ関数によって記述されます。これは、上記の契約となり、 応答変換チェーンの繰り返しの問題に対処するのに役立ちます 。 このアプローチも非常に一般的です。 その実用的なアプリケーションは、Swiftで書かれた21のすばらしいオープンソースiOSアプリのリストにあるアプリケーションにもあります。 たとえば、 DesignerNewsAppで 。 このようなクラスは次のようになります。







 struct DesignerNewsService { static func storiesForSection(..., response: ([Story]) -> ()) { // parameters Alamofire.request(...).response { _ in // parsing } } static func loginWithEmail(..., response: (token: String?) -> ()) { // parameters Alamofire.request(...).response { _ in // parsing } } }
      
      





このアプローチには欠点もあります。 このクラスに割り当てられる職務の数は、単独責任の原則によって要求されるものよりも多くなります。 リクエストの実行方法を変更するとき( Alamofireを置き換える)、解析のフレームワークを変更するとき、リクエストパラメータを変更するときに変更する必要があります。 さらに、そのようなクラスは、 神のオブジェクトに成長するか、またはすべての結果を伴うシングルトンとして使用できます。







プロジェクトを次のRESTful APIと統合する必要があるときの悲観を知っていますか? これは、ある種のAPIManagerを作成し、Alamofireリクエストで埋める必要がある場合です... (リンク)


パート2.戦略ベースのアプローチ



出版物の最初の部分で説明されているすべての欠点を考慮して、私は自分自身のために、ネットワークと連携する将来のレイヤーのための多くの要件を策定しました。









結果として何が起こったのか:







基本的なネットワーク層プロトコル



ApiTargetプロトコルは、リクエストの作成に必要なすべてのデータ(パラメーター、パス、メソッドなど)を定義します







 protocol ApiTarget { var parameters: [String : String] { get } }
      
      





汎用のApiResponseConvertibleプロトコルは、結果のオブジェクト(この場合はData )を関連するタイプのオブジェクトに変換する方法を定義します。







 protocol ApiResponseConvertible { associatedtype ResultType func map(data: Data) throws -> ResultType }
      
      





ApiServiceプロトコルは、リクエストの送信方法を定義します。 通常、プロトコルで宣言された関数は、応答オブジェクトと考えられるエラーを含むクロージャーを受け入れます。 現在の実装では、関数はRxSwiftリアクティブフレームワークのオブジェクトであるObservableを返します。







 protocol ApiService: class { func request<T>(with target: T) -> Observable<T.ResultType> where T: ApiResponseConvertible, T: ApiTarget }
      
      





戦略



私は戦略を発行の最初に述べた契約と呼びます。これはいくつかのタイプのデータをリンクします。 戦略はプロトコルであり、最も単純なケースでは次のようになります。







 protocol Strategy { associatedtype ObjectType associatedtype ResultType }
      
      





ネットワーク層のニーズに合わせて、戦略は、 ApiServiceプロトコルに準拠するクラスのインスタンスに渡すことができるオブジェクトを作成できる必要があります。 オブジェクト作成関数をApiStrategyプロトコルに追加します。







 protocol ApiStrategy { associatedtype ObjectType associatedtype ResultType static func target(with object: ObjectType) -> AnyTarget<ResultType> }
      
      





新しいユニバーサルAnyTarget構造の導入は、プロトコルに関連するtypeがあるため、汎用のApiResponseConvertibleプロトコルを関数によって返されるオブジェクトのタイプとして使用できないという事実によるものです。







 struct AnyTarget<T>: ApiResponseConvertible, ApiTarget { private let _map: (Data) throws -> T let parameters: [String : String] init<U>(with target: U) where U: ApiResponseConvertible, U: ApiTarget, U.ResultType == T { _map = target.map parameters = target.parameters } func map(data: Data) throws -> T { return try _map(data) } }
      
      





これは、戦略の最も原始的な実装のようです。







 struct SimpleStrategy: ApiStrategy { typealias ObjectType = Int typealias ResultType = String static func target(with object: Int) -> AnyTarget<String> { let target = Target(value: object) return AnyTarget(with: target) } } private struct Target { let value: Int } extension Target: ApiTarget { var parameters: [String : String] { return [:] } } extension Target: ApiResponseConvertible { public func map(data: Data) throws -> String { return "\(value)" // map value from data } }
      
      





ターゲット構造はプライベートであることに注意してください。 ファイルの外部では使用されません。 ユニバーサルAnyTarget構造を初期化するためにのみ必要です。







オブジェクトの変換もファイル内で行われるため、 ApiServiceは解析に使用されるツールについては何も知りません。







戦略とサービスの使用



 let service: ApiService = ... let target = SimpleStrategy.target(with: ...) let request = service.request(with: target)
      
      





この戦略により、リクエストを実装するために必要なオブジェクトと出力されるオブジェクトがわかります。 すべてが戦略によって厳密に型指定されており、ユニバーサル関数の場合のように型を指定する必要はありません。







ApiServiceの実装



ご覧のとおり、このアプローチでは、ネットワークフレームワークはサービスを構築する基本的なロジックを超えたままです。 最初は、まったく使用できません。 たとえば、 ApiResponseConvertibleプロトコルのマップ関数の実装でモックオブジェクトが返される場合、サービスは非常にプリミティブなクラスになります。







 class MockService: ApiService { func request<T>(with target: T) -> Observable<T.ResultType> where T : ApiResponseConvertible, T : ApiTarget { return Observable .just(Data()) .map({ [map = target.map] (data) -> T.ResultType in return try map(data) }) } }
      
      





実際のMoyaネットワークフレームワークに基づくApiServiceプロトコルのテスト実装とアプリケーションは、スポイラーで見ることができます。







ApiService + Moya +実装
 public extension Api { public class Service { public enum Kind { case failing(Api.Error) case normal case test } let kind: Api.Service.Kind let logs: Bool fileprivate lazy var provider: MoyaProvider<Target> = self.getProvider() public init(kind: Api.Service.Kind, logs: Bool) { self.kind = kind self.logs = logs } fileprivate func getProvider() -> MoyaProvider<Target> { return MoyaProvider<Target>( stubClosure: stubClosure, plugins: plugins ) } private var plugins: [PluginType] { return logs ? [RequestPluginType()] : [] } private func stubClosure(_ target: Target) -> Moya.StubBehavior { switch kind { case .failing, .normal: return Moya.StubBehavior.never case .test: return Moya.StubBehavior.immediate } } } } extension Api.Service: ApiService { public func dispose() { // } public func request<T>(headers: [Api.Header: String], scheduler: ImmediateSchedulerType, target: T) -> Observable<T.ResultType> where T: ApiResponseConvertible, T: ApiTarget { switch kind { case .failing(let error): return Observable.error(error) default: return Observable .just((), scheduler: scheduler) .map({ [weak self] _ -> MoyaProvider<Target>? in return self?.provider }) .filterNil() .flatMap({ [headers, target] provider -> Observable<Moya.Response> in let api = Target(headers: headers, target: target) return provider.rx .request(api) .asObservable() }) .map({ [map = target.map] (response: Moya.Response) -> T.ResultType in switch response.statusCode { case 200: return try map(response.data) case 401: throw Api.Error.invalidToken case 404: do { let json: JSON = try response.data.materialize() let message: String = try json["ErrorMessage"].materialize() throw Api.Error.failedWithMessage(message) } catch let error { if case .some(let error) = error as? Api.Error, case .failedWithMessage = error { throw error } else { throw Api.Error.failedWithMessage(nil) } } case 500: throw Api.Error.serverInteralError case 501: throw Api.Error.appUpdateRequired default: throw Api.Error.unknown(nil) } }) .catchError({ (error) -> Observable<T.ResultType> in switch error as? Api.Error { case .some(let error): return Observable.error(error) default: let error = Api.Error.unknown(error) return Observable.error(error) } }) } } }
      
      





ApiService + Moya +使用法
 func observableRequest(_ observableCancel: Observable<Void>, _ observableTextPrepared: Observable<String>) -> Observable<Result<Objects, Api.Error>> { let factoryApiService = base.factoryApiService let factoryIndicator = base.factoryIndicator let factorySchedulerConcurrent = base.factorySchedulerConcurrent return observableTextPrepared .observeOn(base.factorySchedulerConcurrent()) .flatMapLatest(observableCancel: observableCancel, observableFactory: { (text) -> Observable<Result<Objects, Api.Error>> in return Observable .using(factoryApiService) { (service: Api.Service) -> Observable<Result<Objects, Api.Error>> in let object = Api.Request.Categories.Name(text: text) let target = Api.Strategy.Categories.Auto.target(with: object) let headers = [Api.Header.authorization: ""] let request = service .request(headers: headers, scheduler: factorySchedulerConcurrent(), target: target) .map({ Objects(text: text, manual: true, objects: $0) }) .map({ Result<Objects, Api.Error>(value: $0) }) .shareReplayLatestWhileConnected() switch factoryIndicator() { case .some(let activityIndicator): return request.trackActivity(activityIndicator) default: return request } } .catchError({ (error) -> Observable<Result<Objects, Api.Error>> in switch error as? Api.Error { case .some(let error): return Observable.just(Result<Objects, Api.Error>(error: error)) default: return Observable.just(Result<Objects, Api.Error>(error: Api.Error.unknown(nil))) } }) }) .observeOn(base.factorySchedulerConcurrent()) .shareReplayLatestWhileConnected() }
      
      





おわりに



結果のネットワーク層は、戦略なしで正常に存在できます。 同様に、戦略は他の目標や目的に適用できます。 それらを組み合わせて使用​​することで、ネットワーク層が理解しやすく使いやすくなりました。








All Articles