Flow-iOS 1.0.1

Flow-iOS 1.0.1

测试已测试
语言语言 SwiftSwift
许可证 MIT
发布上次发布Aug 2017
SwiftSwift 版本3.0
SPM支持 SPM

Roy Ng维护。



Flow-iOS 1.0.1

  • Roy Ng

Flow

什么是 Flow

Flow 是一个工具/设计模式,帮助开发者编写简洁易读的代码。有两个主要关注点:操作流和数据流

使用 Flow,我们应该能够实现以下功能

  • 逻辑/操作可以轻松复用
  • 逻辑流程是任何人(包括代码审查人员)可读的
  • 每一行代码都富含意义并避免了含糊其辞的关键词
  • 不再有复杂的异步操作中的回调地狱
  • 开发和生产阶段都可调试

Flow 引用了组合模式(https://en.wikipedia.org/wiki/Composite_pattern)和责任链模式(包括命令模式)(https://en.wikipedia.org/wiki/Chain-of-responsibility_pattern

因此,我们将操作封装成对象,这些对象可以以树结构进行链接。每个操作都是独立的,但如果存在操作所需的数据,也能够与其他操作一起使用。

以下是一个简单使用的示例

@IBAction func simpleChainedFlow() {
    Flow()
      .add(operation: SimplePrintOp(message: "hello world"))
      .add(operation: SimplePrintOp(message: "good bye"))
      .setWillStartBlock(block: commonWillStartBlock())
      .setDidFinishBlock(block: commonDidFinishBlock())
      .start()
  }

在这 5 行代码中,我们可以知道,两个操作将依次执行,并能在操作前后执行某些操作。

命名

为了使逻辑可读,重要的是使操作名称具有意义。为操作命名是开发者的责任。名称还决定了代码的可复用程度。例如:如果您创建了一个名为 SimplePrintOp 的操作,它应只包含与它关联的消息输出版本。您不应在名称的上下文之外做任何事情。例如:将消息发送到服务器/写入文件。

此外,所有为 Flow 创建的操作应共享一个共同的后缀(例如 Op),以便所有开发者都能知道存在可复用的操作。

分组操作

您可以使用 FlowArrayGroupDispatcher 运行一批操作。

Flow()
      .setDataBucket(dataBucket: ["images": ["a", "b", "c", "d"]])
      .addGrouped(operationCreator: UploadSingleImageOp.self, dispatcher: FlowArrayGroupDispatcher(inputKey: "images", outputKey: "imageURLs", maxConcurrentOperationCount: 3, allowFailure: false))
      .start()

FlowArrayGroupDispatcher 将将目标数组数据桶中的数组发送到创建的操作,并传递必要的参数,并在之后收集结果。

import Flow_iOS
class UploadSingleImageOp: FlowOperation, FlowOperationCreator {
  
  static func create() -> FlowOperation {
    return UploadSingleImageOp()
  }
  
  override var primaryInputParamKey: String { return "targetImageForUpload" }
  override var primaryOutputParamKey: String { return "uploadedImageUrl" }
  
  override func mainLogic() {
    guard let image: String = getData(name: primaryInputParamKey) else { return }
    DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(1)) {
      self.log(message: "simulation of upload single image callback")
      self.setData(name: self.primaryOutputParamKey, value: "url_of_" + image)
      self.finishSuccessfully()
    }
    startWithAsynchronous()
  }
}

对于上述示例,FlowArrayGroupDispatcher 会根据数据 bucket 中 images 数据的数组大小创建一组 UploadSingleImageOp。由于 UploadSingleImageOp 声明 targetImageForUpload 作为输入键,uploadedImageUrl 作为输出键。FlowArrayGroupDispatcher 将为每个 UploadSingleImageOp 创建临时数据 bucket 并在其中包含 targetImageForUpload。如果操作成功,FlowArrayGroupDispatcher 将收集键为 uploadedImageUrl 的对象并放入结果数组 imageURLs 中。

在如此设计中,UploadSingleImageOp 可以 作为单一操作或分组操作重用

您也可以设置 maxConcurrentOperationCount(可选,默认=3) 以控制操作是逐个执行还是批量执行。如果将 allowFailure(可选,默认=false) 设置为 true,即使组中的某些/所有操作失败,Flow 也将继续运行。因此,输出数组可能比输入数组更短甚至是空的。

案例

Flow 允许简单的案例处理。例如

@IBAction func demoCases() {
    Flow()
      .setDataBucket(dataBucket: ["images": ["a", "b", "c", "d", 1], "target": "A"])
      .add(operation: SimplePrintOp(message: "Step1"))
      .add(operation: SimplePrintOp(message: "Step2A1"), flowCase: FlowCase(key: "target", value: "A"))
      .add(operation: SimplePrintOp(message: "Step2A2"))
      .add(operation: SimplePrintOp(message: "Step2B1"), flowCase: FlowCase(key: "target", value: "B"))
      .add(operation: SimplePrintOp(message: "Step2B2"))
      .combine()
      .add(operation: SimplePrintOp(message: "Step3"))
      .setWillStartBlock(block: commonWillStartBlock())
      .setDidFinishBlock(block: commonDidFinishBlock())
      .start()
  }

Step1 完成后,Flow 将根据数据 bucket中的 target 的值运行 Step2A1 分支或 Step2B1 分支。且使用 combine 将所有案例合并回单个节点 Step3

为了使蓝图可读,不支持的嵌套案例。同时,案例值类型必须是 String

数据处理

import Flow_iOS

class MockAsyncLoginOp: FlowOperation {
  override func mainLogic() {
    guard let email: String = getData(name: "email") else { return }
    guard let password: String = getData(name: "password") else { return }
    
    DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(1)) {
      self.log(message: "simulation of login callback")
      if email == "[email protected]" && password == "123456" {
        let mockAccessToken = "sdaftadagasg"
        self.setData(name: "accessToken", value: mockAccessToken)
        self.finishSuccessfully()
      } else {
        let error = NSError(domain: "No such account", code: 404, userInfo: nil)
        self.finishWithError(error: error)
      }
    }
    startWithAsynchronous()
  }
}

在示例中的 MockAsyncLoginOp 中,它需要从数据 bucket 中获取两个输入数据(emailpassword)。Flow 将检查数据是否存在并且数据类型是否正确(即它们在此案例中必须是 String)。如果没有找到具有正确类型的任何数据,操作将被标记为失败,Flow 将停止。您可以请求任何类型。例如,您在项目中有一个名为“LoginData”的类。

guard let loginData: LoginData = getData(name: "loginData") else { return }

创建操作

创建操作很简单

  1. 选择一个好的名称
  2. 继承 FlowOperation
  3. mainLogic 中放置您的逻辑
  4. 对于同步操作:根据结果调用 finishSuccessfullyfinishWithError
  5. 对于异步操作:在 mainLogic 中的异步调用之后调用 startWithAsynchronous
  6. 使用 log 记录调试日志
  7. 扩展 FlowOperationCreator 以使操作可在 Group 中使用。并重写 primaryInputParamKeyprimaryOutputParamKey

一些示例

class SimplePrintOp: FlowOperation {
  var message: String!
  
  init(message: String) {
    self.message = message
  }
  
  override func mainLogic() {
    log(message: message)
    finishSuccessfully()
  }
}

class MockAsyncLoadProfileOp: FlowOperation {
  override func mainLogic() {
    guard let accessToken: String = getData(name: "accessToken") else { return }
    
    DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(1)) {
      self.log(message: "simulation of success load profile callback")
      self.setData(name: "profileRefreshDate", value: Date())
      self.finishSuccessfully()
    }
    startWithAsynchronous()
  }
}

class UploadSingleImageOp: FlowOperation, FlowOperationCreator {
  
  static func create() -> FlowOperation {
    return UploadSingleImageOp()
  }
  
  override var primaryInputParamKey: String { return "targetImageForUpload" }
  override var primaryOutputParamKey: String { return "uploadedImageUrl" }
  
  override func mainLogic() {
    guard let image: String = getData(name: primaryInputParamKey) else { return }
    DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(1)) {
      self.log(message: "simulation of upload single image callback")
      self.setData(name: self.primaryOutputParamKey, value: "url_of_" + image)
      self.finishSuccessfully()
    }
    startWithAsynchronous()
  }
}

回调

您可以设置 WillStartBlockDidFinishBlock 来在 Flow 运行之前或之后被通知。它们在 主线程 中调用,因此您可以进行 UI 变更。在块中使用 Flow 实例,您可以访问 dataBucket: [String: Any]isSuccess: Boolerror: Error?,并执行您的处理。

建议创建通用的处理块,以进一步简化您的蓝图。

  func commonWillStartBlock(block: FlowWillStartBlock? = nil) -> FlowWillStartBlock {
    let result: FlowWillStartBlock = {
      flow in
      block?(flow)
      self.summaryTextView.text = "Flow Starting..."
    }
    return result
  }
  
  func commonDidFinishBlock(block: FlowDidFinishBlock? = nil) -> FlowDidFinishBlock {
    let result: FlowDidFinishBlock = {
      flow in
      block?(flow)
      self.summaryTextView.text = flow.generateSummary()
    }
    return result
  }

日志记录

这是我创建 Flow 时的最喜爱功能之一。开发者总是很难跟踪控制台日志,因为控制台中有很多不受欢迎的日志。更糟的是,在一系列异步操作中。Flow 将捕获操作中发送的所有日志,并在最后为您生成摘要。在完成块中调用 flow.generateSummary()

例如

====== Flow Summary ======
4:17:18 PM [DataBucket] start with:
password: 123456
email: [email protected]
4:17:18 PM [MockAsyncLoginOp] WillStart
4:17:19 PM [MockAsyncLoginOp] simulation of login callback
4:17:19 PM [DataBucket] add for accessToken: sdaftadagasg
4:17:19 PM [MockAsyncLoginOp] DidFinish: successfully
4:17:19 PM [MockAsyncLoadProfileOp] WillStart
4:17:20 PM [MockAsyncLoadProfileOp] simulation of success load profile callback
4:17:20 PM [DataBucket] add for profileRefreshDate: 2017-07-13 08:17:20 +0000
4:17:20 PM [MockAsyncLoadProfileOp] DidFinish: successfully
4:17:20 PM [DataBucket] end with:
profileRefreshDate: 2017-07-13 08:17:20 +0000
email: [email protected]
accessToken: sdaftadagasg
password: 123456
Flow isSuccess: true
======    Ending    ======

====== Flow Summary ======
4:17:06 PM [DataBucket] start with:
password: 123456
email_address: [email protected]
4:17:06 PM [MockAsyncLoginOp] WillStart
4:17:06 PM [MockAsyncLoginOp] DidFinish: withInsufficientInputData: can't find <email> in data bucket
4:17:06 PM [DataBucket] end with:
password: 123456
email_address: [email protected]
Flow isSuccess: false
======    Ending    ======

您可以在一个地方追踪数据变化,查看操作如何执行。如果需要,您可以将总结字符串发送到您的服务器。

为什么不是 RxSwift?

当然,在某个方面 RxSwift 更强大。但我认为,如果我们能使我们的代码:简单易懂,使用 流程,即使是代码审核人员和非程序员也能理解蓝图中您的逻辑。

要求

Swift 3.2 iOS 8.0

安装

Flow 可以通过 CocoaPods 获取。

pod "Flow-iOS"

导入

import Flow_iOS

作者

Roy Ng,[email protected] @ Redso,https://www.redso.com.hk/

领英: https://www.linkedin.com/in/roy-ng-19427735/

许可证

Flow 在 MIT 许可证下可用。有关更多信息,请参阅 LICENSE 文件。