模块化的发展或方式,没有回头


我们如何找到一种在RaiffeisenBank iOS应用程序中使用模块的新方法。

问题


在Raiffeisenbank应用程序中,每个屏幕都包含几个彼此尽可能独立的模块。 我们将“模块”称为具有自己想法的视觉组件。 在设计应用程序时,编写逻辑非常重要,这样模块才是独立的,并且可以轻松地添加或删除它们而无需借助重构。

我们面临的困难是:


突出显示建筑模式上的抽象
显然,在开发的第一阶段,我们不想被束缚于特定的架构模式。 如果您需要显示包含一些信息的页面,则MVC很好。 同时,与用户的交互很少或根本没有。 例如:“关于公司”或“用户协议”页面。 对于复杂的模块,VIPER是一个很好的工具,这些模块具有使用服务,路由和许多其他功能的逻辑。

相互作用和封装问题
每个架构模式都有其自己的构造结构和协议,这对使用该模块施加了限制。 要抽象该模块,您需要突出显示主要的输入/输出交互接口。

突出显示路由逻辑
作为可视单元的模块不应该也不知道在何处以及如何显示。 一个模块和一个模块应该并且可以被实现为任何屏幕上的独立单元或组合。 对此的责任不能归咎于模块本身。

先前的解决方案://生意不佳


我们在Objective-C中编写的第一个解决方案是基于NSProxy。 架构模式的封装问题是通过防御解决的,防御是由给定条件(即模块的输入/输出)确定的 ,它可以将对模块的任何调用代理到其输入,并通过输出 (如果有的话)接收消息。

这是前进的一步,但出现了新的困难:

  • 代理接口不保证输入协议的实现;
  • 即使不需要,也必须描述输出
  • 有必要将输出属性添加到输入接口。

除了NSProxy之外我们还通过查看ViperMcFlurry的思想来实现路由:我们在ViewController创建了一个类别,随着出现了用于在屏幕上显示模块的不同选项,该类别开始增长。 当然,我们对类别进行了划分,但是它仍然不是一个好的解决方案。

通常,第一个煎饼是块状的,很明显,您需要以不同的方式解决问题。

解决方案://最终


意识到NSProxy没有其他东西 ,我们拿起了标记并开始绘画。 结果,我们隔离了RFModule协议:

@objc protocol RFModule { var view: ViewController { get } var input: AnyObject? { get } var output: AnyObject? { get set } var transition: Transitioning { get set } } 

我们有意在协议级别放弃了相关的类型,这是有充分的理由的:当时,90%的代码在Objective-C中。 ObjC模块之间的互操作性←→Swift将无法实现。

为了仍然使用泛型并确保按类型使用模块,我们引入了满足协议的Module
RFModule

 final class Module<I: Any, O: Any>: RFModule { public typealias Input = I public typealias Output = O public var setOutput: ((O?) -> Void)? //... public var input: I? { get { return inputObjc as? I} set { inputObjc = newValue as AnyObject } } public var output: O? { get { return outputObjc as? O} set { outputObjc = newValue as AnyObject } } @objc(input) public weak var inputObjc: AnyObject? @objc(moduleOutput) public weak var outputObjc: AnyObject? { didSet{ setOutput?(output) } } } @objc protocol RFModule { var view: ViewController { get } @objc(input) var inputObjc: AnyObject? { get } @objc(moduleOutput) var outputObjc: AnyObject? { get set } var transition: Transitioning { get set } } public extension RFModule { public var input: AnyObject? { return inputObjc } public var output: AnyObject? { get { return outputObjc } set { outputObjc = newValue} } } 

这样我们得到了一个类型化的模块。 实际上,Swift使用类Module和Objective-C RFModule 。 另外,事实证明,它是在需要创建数组的地方混搭类型的便捷工具:例如TabContainer

由于用于创建模块的DI在UserStory 范围内 ,并且在将使用输出的位置分配输出的值无法描述简单的设置程序。 本质上, “ SetOutput”是一种定义,在分配输出的阶段它将根据模块的逻辑将其传递给负责人。

 class SomeViewController: UIViewController, ModuleInput { weak var delegate: ModuleOutput } class Assembly { func someModule() -> Module<ModuleInput, ModuleOutput> { let view = SomeViewController() let module = Module<ModuleInput, ModuleOutput>(view: view, input: view) { [weak view] output in view?.delegate = output } return module } } ... let assembly: Assembly let module = assembly.someModule() module.output = self 

过渡是一种协议,其名称顾名思义,其实现负责显示和隐藏模块的逻辑。

 protocol Transitioning { var destination: ViewController? { get } // should be weak func perform(_ completion: (()->())?) // present func reverse(_ completion: (()->())?) // dissmiss } 

对于显示,它是原因执行的 ,对于隐藏是反向的 。 尽管协议中有目的地 ,但一开始似乎应该有来源 。 实际上, source可能不是,并且它的类型并不总是ViewController 。 例如,如果需要在新窗口中打开模块,则为Window ,如果需要嵌入 ,则需要AND父级: ViewController AND容器: UIView

 class PresentTransition: Transitioning { weak var source: ViewController? weak var destination: ViewController? ... func perform(_ completion: (()->())?) { source.present(viewController: self.destinaton) } } 

因此,我们摆脱了在ViewController上编写扩展的想法,并描述了如何在各种对象中显示模块的逻辑。 这为我们提供了路由的灵活性,即 现在,我们可以独立或复杂地显示任何模块,并且它们在屏幕上的显示方式也有所不同:在窗口(Window),当前,在导航(按入导航),嵌入,在窗帘(遮盖) 。

这就是全部吗?


到目前为止,还有另外一件事困扰。 为了有机会轻松选择模块的显示方式并从中删除该逻辑,我们为失去外观属性的设置付出了代价。 例如,如果在“导航”中显示它,则需要指定barTintColor应该是什么颜色。 或者,如果我们在窗帘中展示模块,则需要设置处理程序的颜色。

到目前为止,我们已经用无类型的外观解决了这个问题:任何属性,并且在打开模块时进行转换会导致其工作的类型,如果成功,它将删除必要的属性。

Source: https://habr.com/ru/post/zh-CN448206/


All Articles