ACInteractor

License License

由 Robert C. Martin 和其他人提出的以用例为中心的架构的 Swift 包。

概述

ACInteractor 是一个 Swift 包,它支持以用例为中心的架构以及 Swift 项目中的 TDD(测试驱动开发)。其基本思想是,一个用例,且仅有一个用例,由单个类执行。正如 Robert C. Martin 所提出的,这类类被称为 Interactors(交互器)。

class LoginViewController: UIViewController {
    ...
    func login() {
        let request = LoginUserInteractor.Request()
        
        request.email = "first.last@appcron.com"
        request.password = "1234"
        
        request.onComplete = { response in
            self.userLabel.text = response.username
        }
        
        request.onError = { error in
            self.displayError(error.message)
        }
        
        Logic.executor.execute(request)
    }
}

消费者,例如 ViewControllers(视图控制器),可以在 InteractorExecutor(交互器执行器)的帮助下轻松执行 InteractorRequests(交互器请求)。因此,每个交互器的已初始化实例都必须在 InteractorExecutor 上注册。交互器应该是无状态的,因为给定交互器的所有请求都由该交互器的同一实例处理。

ACInteractor 不对依赖管理添加任何约束。如何初始化您的交互器实例取决于您自己。我推荐使用 使用自定义初始化器的依赖注入。更多详情可以在“依赖注入”部分找到。

ACInteractor 的构建考虑了 TDD。每个交互器都有一个单独的执行函数、已定义的请求和响应、无状态实现和注入的依赖项。这有助于为每个交互器编写隔离的单元测试。有关编写测试的更多详细信息,请参阅“单元测试”部分。

许可证

Apache License, Version 2.0, January 2004

必看

如果您是 Clean Code(代码整洁之道)和 TDD 的新手,强烈推荐观看 Robert C. Martin 在 Ruby Midwest 的演讲。它既有趣又值得花时间观看。围绕 ACInteractor 的整个架构及其目标都基于这次演讲。

内容

ACInteractor 目前包含以下文件

ACInteractor
├── LICENSE
├── Package.swift   // The Swift Package description
├── README.md
├── Sources         // The source files
└── Tests           // The unit test files

设置

由于 Swift Package 尚不支持 iOS Xcode 项目,因此建议将 Sources/ACInteractor 文件夹的文件直接添加到您的项目中。

通过 Git Submodule

您可以将整个 ACInteractor 项目作为 Git Submodule 添加到您的仓库中。

  1. 在终端中导航到您的 Git 仓库的根目录。
  2. 运行以下命令
git submodule add https://github.com/AppCron/ACInteractor.git
  1. Sources/ACInteractor 文件夹的文件添加到您的项目中。

通过下载

或者,您可以直接从 Github 下载文件,并将 Sources 文件夹的文件添加到您的项目中。

作为 Swift Package 依赖项

您也可以将其作为 Swift Package 依赖项添加到另一个 Swift Package 中。

dependencies: [
    .package(url: "https://github.com/AppCron/ACInteractor.git", from: "1.0.0")
]

终端命令

您可以使用 swift test 直接从命令行运行测试。
要创建 Xcode 项目,请使用 swift package generate-xcodeproj

编写交互器

class LoginUserInteractor: Interactor {
    class Request: InteractorRequest<Response> {...}
    class Response {...}
    
    func execute(request: Request) {...}
}

让我们编写我们的第一个交互器。它应该处理用户登录,并接受所有登录,只要用户提供密码即可。

每个交互器都必须实现 Interactor 协议,该协议要求交互器具有 execute() 函数。由于每个交互器只处理一个用例,因此只需要一个 execute 函数。

通常,它包含两个嵌套类。Request(请求)和 Response(响应)。

请求

class LoginUserInteractor: Interactor {
    class Request: InteractorRequest<Response> {
      var email: String?
      var password: String?
    }
    ...
}

请求包含执行它所需的所有参数。在我们的例子中,是电子邮件和密码。

它必须从泛型 InteractorRequest<ResponseType> 派生,并且应该使用交互器的 ResponseType 进行类型化。这将帮助我们稍后执行请求。有关详细信息,请参阅“Execute 函数”部分。

响应

class LoginIntactor: Interactor {
    ...
    class Response {
    	var username: String?
    	var sessionToken: String?
	}
    ...
}

响应是用例的结果。在我们的示例中,它包含用户名和会话令牌。

响应可以是任何类型,只要它与 InteractorRequest 类型化的类型相同即可。它甚至可以是像 StringBool 这样的常见类型。

Execute 函数

class LoginUserInteractor: Interactor {
	...
    func execute(request: Request) {
        if (request.password?.characters.count > 0) {
            
            // Create Response
            let response = Response()
            
            // Set values
            response.username = request.email?.componentsSeparatedByString("@").first
            response.sessionToken = "123456910"
            
            // Call completion handler
            request.onComplete?(response)
            
        } else {
            // Do error handling
        }
    }
}

Execute 函数是实现用例业务逻辑的部分。它接受 Request 作为参数,并将 Response 返回给完成处理程序。

onComplete 闭包已在 InteractorRequest 中定义,并且已经使用 Response 类型化。这就是为什么 Request 必须是 InteractorRequest 的子类。

注册交互器

class Logic {
    
    static let executor = InteractorExecutor()
    
    static func registerInteractors() {
        let loginInteractor = LoginUserInteractor()
        let loginRequest = LoginUserInteractor.Request()
        
        executor.registerInteractor(loginInteractor, request: loginRequest)
    }

}

为了使消费者(例如 ViewControllers)能够轻松执行 InteractorRequests,首先需要注册相应的交互器。

在我们的例子中,我们使用一个名为 Logic 的辅助类。它包含一个静态函数 registerInteractors(),该函数创建一个 LoginInteractor 实例,并在 InteractorExecutor 上使用相应的 Request 注册它。

除此之外,Logic 还包含一个带有全局 Executor 实例的静态属性。这使得消费者更容易访问给定的实例。

执行请求

class LoginViewController: UIViewController {
    ...
    func login() {
        let request = LoginUserInteractor.Request()
        
        request.email = "first.last@appcron.com"
        request.password = "1234"
        
        request.onComplete = { (response: LoginUserInteractor.Response) in
            self.userLabel.text = response.username
        }
        
        Logic.executor.execute(request)
    }
}

要执行 Request,您可以简单地在 InteractorExecutor 上调用 executeMethod()。只需确保您已在同一 InteractorExecutor 实例上使用其 Request 注册了该交互器类。

在我们的示例中,此实例存储在 Logic 类上的静态 executor 属性中,如上所示。

错误处理

基本错误处理已经是 ACInteractor 的一部分。每个 InteractorRequest 都有一个 onError 属性,用于存储错误处理的闭包。

在交互器实现中

class LoginUserInteractor: Interactor {
	...
    func execute(request: Request) {
        if (request.password?.characters.count > 0) {
            ...        
        } else {
            let error = InteractorError(message: "Empty password!")
            request.onError?(error)
        }
    }
}

在交互器中,您只需创建 InteractorError 的实例,提供错误消息,并使用错误对象调用错误处理程序。

在 Execute 调用中

class LoginViewController: UIViewController {
    ...
    func login() {
        let request = LoginUserInteractor.Request()
        
		...
        
        request.onError = { (error: InteractorError) in
            self.displayError(error.message)
        }
        
        Logic.executor.execute(request)
    }
}

在执行请求时,请确保在其 onError 属性上为错误处理设置一个闭包。

扩展的完成处理程序

没有必要使用默认的完成和错误处理程序。您可以添加自定义完成闭包,例如 onUpdate(UpdateResponse),或自定义错误闭包,例如 onExtendedError(ExtendedError)。这可以通过将它们作为属性添加到特定请求或通过子类化 InteractorRequest 来完成。

添加自定义错误处理程序时请注意。 ACInteractor 使用默认的 onError 闭包来报告内部错误,例如找不到给定请求的交互器。有关详细信息,请参阅“故障排除”部分。

异步请求

由于 ACInteractor 使用闭包进行结果处理,因此您可以轻松地在同步和异步行为之间切换,而无需调整交互器的接口。

从您的交互器进行异步回调时,建议将您的 onCompletiononError 闭包调用分派到调用交互器的 execute() 方法的线程。是否使用后台线程以及何时或如何调度回调用者的线程是您的交互器的实现细节,应该对调用者隐藏。将您的异步内容调度回主线程不是 ViewController 的责任。也许可以使用 dispatch_async 完成,也许需要 dispatch_sync,ViewController 无法知道。

依赖注入

在一个真实的示例中,LoginInteractor 将调用 Web 服务来验证登录凭据,并将会话令牌存储在本地数据库中。由于我们不希望在我们的交互器中包含所有这些技术细节,因此我们将它们封装在两个单独的类中。

protocol WebservicePlugin
class HttpWebservicePlugin: WebservicePlugin { }

protocol UserEntityGateway
class UserCoreDataGateway: UserEntityGateway { }

WebservicePlugin 处理 Web 服务调用,并在完成后调用 onCompletion 闭包。

UserEntityGateway 具有创建新用户和保存用户的功能。它负责创建和保存新实体。因此,我们的 LoginInteractor 不需要知道我们如何持久化数据。它可以是 CoreData、Realm 或仅仅是内存数据库。

此外,为每个依赖项定义一个协议很有用。这使我们可以在编写单元测试时轻松地用模拟对象替换它们。

在交互器实现中

class LoginUserInteractor: Interactor {
	let webservicePlugin: WebservicePlugin
	let userGateway: UserEntityGateway

	init(webservicePlugin: WebservicePlugin, userGateway: UserEntityGateway) {
		self.webservicePlugin = webservicePlugin
		self.userGateway = userGateway
	}

    func execute(request: Request) {
    	...
    	self.webservicePlugin.doStuff()
    	self.userEntityGateway.doStudd()
    	...
    }
}

在交互器中,我们需要一个自定义的 init 函数,它将依赖项作为参数并将其存储在属性中。然后我们可以在 execute 函数中使用它们。

注册依赖项

class Logic {
    
    static let executor = InteractorExecutor()
    
    static func registerInteractors() {
    	let webservicePlugin = HttpWebservicePlugin()
    	let userGateway = UserCoreDataGateway()

        let loginInteractor = LoginUserInteractor(webservicePlugin: webservicePlugin, userGateway: userGateway)
        let loginRequest = LoginUserInteractor.Request()
        
        executor.registerInteractor(loginInteractor, request: loginRequest)
    }
}

在注册 LoginInteractor 时,我们使用自定义的 init 方法来提供匹配的实现。此时,您可以轻松地将 Plugin 和 Gateway 的具体实现替换为其他实现,只要它们符合指定的协议即可。例如,UserCoreDataEntityGateway 可以替换为 UserInMemoryGateway

在 Execute 调用中

class LoginViewController: UIViewController {
    ...
    func login() {
        let request = LoginUserInteractor.Request()
       	...
        Logic.executor.execute(request)
    }
}

没有任何改变 :) 交互器仍然在 InteractorExecutor 上使用相同的 Request 执行。这意味着您可以在幕后轻松地重构交互器,并将技术细节提取到 EntityGateways 和 Plugins 中,而不会破坏调用者使用的 API。

单元测试

ACInteractor 的构建考虑了 TDD。每个交互器都有一个单独的执行函数、已定义的请求和响应、无状态实现和注入的依赖项。这有助于为每个交互器编写隔离的单元测试。

如果您使用上面描述的依赖注入方法,您可以轻松地在单元测试中模拟依赖项。例如,这意味着您不需要执行真实的 Web 服务调用或设置真实的数据库来运行测试。模拟对象可以只返回支持您当前测试的值。模拟对象还可以帮助您测试原本难以模拟的边缘情况,例如长时间运行的 Web 服务请求或已满的数据库。

故障排除

  1. 我无法在 InteractorExecutor 上注册我的交互器。我收到编译器错误。

    • 确保 interactor 实现了 Interactor 协议
    • 确保 requestInteractorRequest<Response> 的子类并且类型正确。
    • 确保 request 是已初始化的对象实例。
  2. 在 InteractorExecutor 上调用 execute 不会调用我的交互器的 execute 方法。

    • 确保您已通过在 InteractorExecutor 上调用 registerInteractor() 函数来使用相应的 InteractoRequest 注册了交互器。
    • 确保您已在 相同InteractorExecutor 实例上调用了 registerInteractor()execute() 函数。
    • 确保您已在 request 上设置了 onError 闭包。它可能会在错误消息中提供有关出错原因的更多详细信息。

致谢

Florian Rieger 特别感谢帮助组装 ACInteractor 的人们。

编码愉快 🐳