Aftermath 1.1.0

Aftermath 1.1.0

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

Vadym Markov维护。



Aftermath 1.1.0

Aftermath

描述

Aftermath是一个基于Swift的无状态消息驱动的微框架,它基于单向数据流架构的概念。

乍一看,Aftermath可能只是发布-订阅消息模式的类型安全实现,但实际上它可以被认为是应用程序设计中的一种独特的思维模型,与熟悉的MVC、MVVM或MVP方法不同。通过利用事件溯源Flux模式背后的想法,它有助于隔离关注点,减少代码依赖性,并使数据流动更加可预测。

以下图表详细说明了Aftermath架构中的数据流


目录

核心组件

命令

命令是一个消息,包含了一系列描述执行相应行为意图的指令。命令可能导致数据检索、数据更改以及任何类型的同步或异步操作,这些操作会产生更新应用程序/视图状态的所需输出。

每个命令只能产生一个唯一的输出类型。

命令处理器

命令处理器层负责应用程序中的业务逻辑。命令提交给命令处理器,它通常执行短期或长期操作,例如网络请求、数据库查询、缓存读/写处理等。命令处理器可以同步并立即发布结果。另一方面,这是应用程序中编写异步代码的最佳位置。

限制是针对每个命令只创建一个命令处理器。

事件

命令处理器负责发布将由反应器消耗的事件。有三种类型的事件

  • progress 事件指示由命令触发的操作已开始,目前处于挂起状态。
  • data 事件保存由命令执行产生的输出。
  • error 事件表示命令执行过程中发生了错误。

反应

反应 响应命令处理器发布的事件。它应该通过在每个场景中描述所需行为来处理 3 种可能的事件类型

  • wait 函数响应事件的 progress 类型。
  • consume 函数响应事件的 data 类型。
  • rescue 函数是在接收到 error 事件时的回退选项。

通常,反应执行 UI 更新,但也可以用于其他类型的输出处理。

流程

通过对前面描述的 4 个核心组件,我们可以构建一个简化的数据流版本


命令执行

第一步是声明一个命令。您的命令类型必须符合 Aftermath.Command 协议,而 Output 类型必须隐式指定。

假设我们想要从某个不可信的资源中获取书籍列表,并更正标题和作者姓名中的拼写错误🤓.

// This is our model we are going to work with.
struct Book {
  let id: Int
  let title: String
  let author: String
}

struct BookListCommand: Command {
  // Result of this command will be a list of books.
  typealias Output = [Book]
}

struct BookUpdateCommand: Command {
  // Result of this command will be an updated book.
  typealias Output = Book

  // Let's pass the entire model to the command to simplify this example.
  // Ideally we wouldn't do that because a command is supposed to be as simple
  // as possible, only with attributes that are needed for handler.
  let book: Book
}

注意 任何类型都可以扮演 Output 的角色,因此如果我们想给我们的 BookUpdateCommand 添加日期,它可能看起来像以下这样

typealias Output = (Book, Date)

为了执行命令,您必须符合 CommandProducer 协议

class ViewController: UITableViewController, CommandProducer {

  // Fetch a list of books.
  func load() {
    execute(command: BookListCommand())
  }

  // Update a single book with corrected title and/or author name.
  func update(book: Book) {
    execute(command: BookUpdateCommand(book: book))
  }
}

命令处理

命令是一种意图,需要由处理器将其转换为动作。命令处理器负责发布事件以通知该操作的执行结果。命令处理器类型必须符合 Aftermath.CommandHandler 协议,该协议需要了解它将要处理的命令类型

struct BookListCommandHandler: CommandHandler {

  func handle(command: BookListCommand) throws -> Event<BooksCommand> {
    // Start network request to fetch data.
    fetchBooks { books, error in
      if let error = error {
        // Publish error.
        self.publish(error: error)
        return
      }

      // Publish fetched data.
      self.publish(data: books)
    }

    // Load data from local database/cache.
    let localBooks = loadLocalBooks()

    // If the list is empty let the listeners know that operation is in the process.
    return Book.list.isEmpty ? Event.progress : Event.data(localBooks)
  }
}

注意 每个命令处理器都需要在 Aftermath Engine 上注册。

Engine.shared.use(handler: BookListCommandHandler())

响应事件

最后一步但并非最不重要的是,响应命令处理器发布的事件。只需符合 ReactionProducer 协议,实现反应行为,然后您就可以开始了

class ViewController: UITableViewController, CommandProducer, ReactionProducer {

  var books = [Book]()

  deinit {
    // Don't forget to dispose all reaction tokens.
    disposeAll()
  }

  override func viewDidLoad() {
    super.viewDidLoad()

    // React to events.
    react(to: BookListCommand.self, with: Reaction(
      wait: { [weak self] in
        // Wait for results to come.
        self?.refreshControl?.beginRefreshing()
      },
      consume: { [weak self] books in
        // We're lucky, there are some books to display.
        self?.books = books
        self?.refreshControl?.endRefreshing()
        self?.tableView.reloadData()
      },
      rescue: { [weak self] error in
        // Well, seems like something went wrong.
        self?.refreshControl?.endRefreshing()
        print(error)
      }))
  }

  // ...
}

重要的是ReactionProducer 实例即将被删除或需要取消订阅事件时,销毁所有反应令牌。

// Disposes all reaction tokens for the current `ReactionProducer`.
disposeAll()

// Disposes a specified reaction token.
let token = react(to: BookListCommand.self, with: reaction)
dispose(token: token)

额外信息

动作

动作 是命令的一种变体,可以自行处理。这在命令本身或业务逻辑很小的时候可以简化代码。不需要注册动作,它将自动添加到正在运行的命令处理器列表中,当它作为一个命令执行时。

import Sugar

struct WelcomeAction: Action {
  typealias Output = String

  let userId: String

  func handle(command: WelcomeAction) throws -> Event<WelcomeAction> {
    fetchUser(id: userId) { user in
      self.publish(data: "Hello \(user.name)")
    }
    return Event.progress
  }
}

// Execute action

struct WelcomeManager: CommandProducer {

  func salute() {
    execute(action: WelcomeAction(userId: 11))
  }
}

事实

事实类似于通知,不涉及异步操作。当不需要处理程序生成输出时可以使用它。事实本身就是输出,所以你想要的只是通知所有订阅者系统中发生了什么,他们会相应地做出反应。从这个意义上讲,它更接近于Notification类型安全的替代品。

struct LoginFact: Fact {
  let username: String
}

class ProfileController: UIViewController, ReactionProducer {

  override func viewDidLoad() {
    super.viewDidLoad()

    // React
    next { (fact: LoginFact) in
      title = fact.username
    }
  }
}

struct AuthService: FactProducer {

  func login() {
    let fact = LoginFact(username: "John Doe")
    // Publish
    post(fact: fact)  
  }
}

中间件

中间件是一个层,可以在命令和事件到达其听者之前拦截它们。

这意味着你可以在命令处理程序处理之前,在命令中间件中修改/取消/扩展要执行的命令


或者,你可以在发布的窃听者接收到事件之前在事件中间件中执行适当的操作。


它适用于日志记录、崩溃报告、终止特定命令或事件等。

// Command middleware
struct ErrorCommandMiddleware: CommandMiddleware {

  func intercept(command: AnyCommand, execute: Execute, next: Execute) throws {
    do {
      // Don't forget to call `next` to invoke the next function in the chain.
      try next(command)
    } catch {
      print("Command failed with error -> \(command)")
      throw error
    }
  }
}

Engine.shared.pipeCommands(through: [ErrorCommandMiddleware()])

// Event middleware
struct LogEventMiddleware: EventMiddleware {

  // Don't forget to call `next` to invoke the next function in the chain.
  func intercept(event: AnyEvent, publish: Publish, next: Publish) throws {
    print("Event published -> \(event)")
    try next(event)
  }
}

Engine.shared.pipeEvents(through: [LogEventMiddleware()])

注意在构建自定义中间件时,有必要调用next以调用链中的下一个函数。

AnyCommandAnyEvent是特殊协议,每个CommandEvent都符合这些协议。它们主要用于middleware中,以克服使用具有associatedtype的Swift泛型协议的限制。

引擎

引擎Aftermath配置的主入口点。

  • 注册命令处理程序
Engine.shared.use(handler: BookListCommandHandler())
  • 添加命令和事件中间件
// Commands
Engine.shared.pipeCommands(through: [LogCommandMiddleware(), ErrorCommandMiddleware()])
// Events
Engine.shared.pipeEvents(through: [LogEventMiddleware(), ErrorEventMiddleware()])
  • 设置全局错误处理程序以捕获所有意外错误和框架警告
struct EngineErrorHandler: ErrorHandler {

  func handleError(error: Error) {
    if let error = error as? Failure {
      print("Engine error -> \(error)")
    } else if let warning = error as? Warning {
      print("Engine warning -> \(warning)")
    } else {
      print("Unknown error -> \(error)")
    }
  }
}

Engine.shared.errorHandler = EngineErrorHandler()
  • 处置所有已注册的命令处理程序和事件监听器(反应)
Engine.shared.invalidate()

秘诀

故事

命名很困难。拥有像BookListCommandBookListCommandHandlerBookListWhatever这样的名字感觉不太对,对吧?如果你同意,那么你可以通过引入一个新想法来解决这个问题。你可以将所有相关类型组合成故事,使流程更加具体。

struct BookListStory {

  struct Command: Aftermath.Command {
    // ...
  }

  struct Handler: Aftermath.CommandHandler {
    // ...
  }
}

从这个意义上讲,它与在敏捷软件开发方法中使用的用户故事非常相似。

你可以在AftermathNotes演示项目中找到更多详细示例。

特性

某些故事可能看起来非常相似。那么根据DRY原则使它们更具通用性和可重用性是有意义的。例如,假设我们有通过id获取单个资源的流程。

import Aftermath
import Malibu

// Generic feature
protocol DetailFeature {
  associatedtype Model: Entity
  var resource: String { get }
}

// Command
struct DetailCommand<Feature: DetailFeature>: Aftermath.Command {
  typealias Output = Feature.Model
  let id: Int
}

// Command handler
struct DetailCommandHandler<Feature: DetailFeature>: Aftermath.CommandHandler {
  typealias Command = DetailCommand<Feature>

  let feature: Feature

  func handle(command: Command) throws -> Event<Command> {
    fetchDetail("\(feature.resource)/\(command.id)") { json, error in
      if let error = error {
        self.publish(error: error)
        return
      }

      do {
        self.publish(data: try Feature.Model(json))
      } catch {
        self.publish(error: error)
      }
    }

    return Event.progress
  }
}

// Concrete feature
struct BookFeature: ListFeature, DeleteFeature, CommandProducer {
  typealias Model = Todo
  var resource = "books"
}

// Execute command to load a single resource.
execute(command: DetailCommand<BookFeature>(id: id))

// Register reaction listener.
react(to: DetailCommand<BookFeature>.self, with: reaction)

你可以在AftermathNotesPlus演示项目中找到更多详细示例。

总结

我们认为,在iOS应用程序中,大多数情况下,没有真正需要单个全局状态(单一真相来源)或分布在各个商店之间的多个子状态。数据存储在本地持久化层,如数据库和缓存中,或者从网络上获取。然后,这个内容,从不同的来源组装成一个一个的部分,被转化为“视图状态”,它可以被视图读取以在屏幕上渲染。这个“视图状态”保持内存有效,直到我们切换上下文并且当前视图被释放。

记住这一点,与保留在任何形式的全局状态中不再使用的复制相比,丢弃属于视图的“视图状态”更有意义。

通过重新播放历史中的先前事件来恢复状态应该足以

Aftermath的优势

  • 关注点分离
  • 代码可重用性
  • 单向数据流
  • 类型安全

**Aftermath** 的缺点

  • 无状态 (?)
  • 关注命令输出而非实际数据
  • 异步命令处理可能会混乱流程

备注 即使目前 Aftermath 是一个无状态框架,我们也有计划引入某种可选的存储以更好地管理状态。这可能将是 v2 中的新功能,请保持关注。

工具

  • Aftermath 包含一系列开发工具,如表辅助函数、用于记录、错误处理等功能的有用命令和事件中间件。
// Commands
Engine.sharedInstance.pipeCommands(through: [LogCommandMiddleware(), ErrorCommandMiddleware()])
// Events
Engine.sharedInstance.pipeEvents(through: [LogEventMiddleware(), ErrorEventMiddleware()])

安装

Aftermath 通过 CocoaPods 提供。要安装它,只需将以下行添加到您的 Podfile 中

pod 'Aftermath'

Aftermath 也可以通过 Carthage 安装。为了安装,只需将其写入您的 Cartfile

github "hyperoslo/Aftermath"

Aftermath 也可以手动安装。只需下载并将在项目中放置 Sources 文件夹。

示例

  • iOS Playground 利用交互式 Playgrounds 的实时视图来展示如何从网络获取数据并在 UITableView 中显示。

  • AftermathNotes 是一个简单应用,演示了如何使用 Aftermath 设置网络堆栈和缓存层。它使用 故事 概念将相关类型分组,使 命令 -> 事件 流更容易阅读。

  • AftermathNotesPlus 是一个更高级的示例,它扩展了 AftermathNotes 的演示。它玩弄泛型,并引入了 功能 概念,以重用视图控制器和 RESTful 网络请求。

扩展

此存储库旨在成为框架的核心实现,但也有一系列扩展,它们将 Aftermath 与其他库集成并增加更多功能

  • AftermathCompass 是一个基于 AftermathCompass 的消息驱动路由系统。

  • AftermathSpots 是为了改善使用 Spots 跨平台视图控制器框架构建组件化 UI 时的开发流程。它提供了自定义反应和可注入行为,将代码重用性提升到新的水平,并使您的应用更加解耦和灵活。

替代方案

仍不确定如何管理状态?涵盖所有场景并找到所有场合的银弹并不容易。但如果您认为现在是时候打破常规并在您的下一个应用程序中尝试新的架构,这里有一些进一步的阅读和研究链接。

作者

Hyper Interaktiv AS,[邮箱地址被隐藏]

影响

后果 受益于Flux中单向数据流的概念,并利用了事件溯源中的命令序列和事件等一些概念。

贡献

我们非常欢迎您为 后果 贡献,有关更多信息,请查阅贡献指南文件。

许可证

后果 提供 MIT 许可。有关更多信息,请参阅许可证文件