如何节省额外的费用



大家好! 我叫Ilya,我是Tinkoff.ru的iOS开发人员。 在本文中,我想谈谈如何使用协议减少表示层中的代码重复。

怎么了


随着项目的发展,代码重复的数量也随之增长。 这不会立即变得明显,并且变得难以纠正过去的错误。 我们在项目中注意到了这个问题,并使用一种方法(有条件地将其称为特征)解决了该问题。

生活例子


该方法可以与各种不同的体系结构解决方案一起使用,但是我将以VIPER为例来考虑。

考虑路由器中最常见的方法-关闭屏幕的方法:

func close() { self.transitionHandler.dismiss(animated: true, completion: nil) } 

它存在于许多路由器中,最好只编写一次。

继承可以帮助我们,但是将来,当我们的应用程序中有越来越多的带有不必要方法的类,或者由于所需的方法位于不同的基类中而无法创建所需的类时,则会出现较大的类问题。
结果,该项目将通过多余的方法成长为许多基本类和子类。 继承不会帮助我们。

有什么比继承更好的? 当然是组成。

您可以为关闭屏幕的方法创建一个单独的类,并将其添加到需要它的每个路由器中:

 struct CloseRouter { let transitionHandler: UIViewController func close() { self.transitionHandler.dismiss(animated: true, completion: nil) } } 

我们仍然必须在路由器的输入协议中声明此方法,并在路由器本身中实现它:

 protocol SomeRouterInput { func close() } class SomeRouter: SomeRouterInput { var transitionHandler: UIViewController! lazy var closeRouter = { CloseRouter(transitionHandler: self. transitionHandler) }() func close() { self.closeRouter.close() } } 

事实证明,有太多代码只是代理对close方法的调用。 懒惰的好程序员不会欣赏。

协议解决方案


协议可以解救。 这是一个非常强大的工具,可让您实现合成,并且可能包含扩展中的实现方法。 因此,我们可以创建一个包含close方法的协议,并在扩展中实现它。

它是这样的:

 protocol CloseRouterTrait { var transitionHandler: UIViewController! { get } func close() } extension CloseRouterTrait { func close() { self.transitionHandler.dismiss(animated: true, completion: nil) } } 

问题是,为什么在协议名称中出现“特征”一词? 很简单-您可以指定此协议以扩展方式实现其方法,并且应将其用作另一种类型的混合物以扩展其功能。

现在,让我们看一下这种协议的使用情况:

 class SomeRouter: CloseRouterTrait { var transitionHandler: UIViewController! } 

是的,仅此而已。 看起来很棒:)。 通过将协议添加到路由器的类中,我们得到了组成,没有写多余的一行,并且有机会重用了代码。

这种方法有什么不寻常之处?


您可能已经问过这个问题。 使用协议作为特征非常普遍。 主要区别在于将这种方法用作表示层内的体系结构解决方案。 像任何体系结构解决方案一样,应该有自己的规则和建议。

这是我的清单:

  • 特性不应该存储和更改状态。 它们只能具有服务等形式的依赖项,它们是仅获得的属性。
  • 特性不应具有未在扩展中实现的方法,因为这违反了它们的概念
  • trait中方法的名称应明确反映它们的作用,而不必与协议名称绑定。 这将有助于避免名称冲突,并使代码更清晰。

从VIPER到MVP


如果您完全转换为对协议使用此方法,则路由器和交互器类将如下所示:

 class SomeRouter: CloseRouterTrait, OtherRouterTrait { var transitionHandler: UIViewController! } class SomeInteractor: SomeInteractorTrait { var someService: SomeServiceInput! } 

这并不适用于所有类;在大多数情况下,项目将只具有空的路由器和交互器。 在这种情况下,您可以通过向演示者添加杂质协议来破坏VIPER模块的结构并平稳地切换到MVP。

像这样:

 class SomePresenter: CloseRouterTrait, OtherRouterTrait, SomeInteractorTrait, OtherInteractorTrait { var transitionHandler: UIViewController! var someService: SomeSericeInput! } 

是的,失去了将路由器和交互器实现为依赖关系的能力,但是在某些情况下是这种情况。

唯一的缺点是transitionHandler = UIViewController。 而且根据VIPER Presenter的规则,对于View层及其使用何种技术的实现方式一无所知。 在这种情况下,这很容易解决-协议“关闭”了UIViewController的转换方法,例如TransitionHandler。 因此Presenter将与抽象进行交互。

改变特质行为


让我们看看如何更改此类协议中的行为。 这将是模块某些部分替代的模拟,例如,用于测试或临时存根。

例如,以一个简单的交互器与执行网络请求的方法为例:

 protocol SomeInteractorTrait { var someService: SomeServiceInput! { get } func performRequest(completion: @escaping (Response) -> Void) } extension SomeInteractorTrait { func performRequest(completion: @escaping (Response) -> Void) { someService.performRequest(completion) } } 

例如,这是一个抽象代码。 假设我们不需要发送请求,而只需返回某种存根即可。 在这里,我们开始窍门-创建一个名为Mock的空协议,然后执行以下操作:

 protocol Mock {} extension SomeInteractorTrait where Self: Mock { func performRequest(completion: @escaping (Response) -> Void) { completion(MockResponse()) } } 

在这里,对于执行Mock协议的类型,performRequest方法的实现已更改。 现在,您需要为将实现SomeInteractor的类实现Mock协议:

 class SomePresenter: SomeInteractorTrait, Mock { // Implementation } 

对于SomePresenter类,将在扩展中调用self满足Mock协议的performRequest方法的实现。 删除Mock协议是值得的,performRequest方法的实现将取自SomeInteractor的通常扩展。

如果仅将其用于测试,则最好将与实现的替换相关联的所有代码都放在测试目标中。

总结一下


总之,值得指出此方法的利弊,在我看来,在这种情况下值得使用。

让我们从缺点开始:

  • 如示例所示,如果您放弃了路由器和交互器,那么实现这些依赖项的能力就会丧失。
  • 另一个缺点是协议数量急剧增加。
  • 有时,代码看起来可能不像使用常规方法那样清晰。

这种方法的积极方面如下:

  • 最重要和最明显的是,复制大大减少了。
  • 静态绑定应用于协议方法。 这意味着将在编译阶段确定方法的实现。 因此,在程序执行期间,将不会在搜索实现上花费额外的时间(尽管这段时间不是特别重要)。
  • 由于协议是小的“实体”,因此可以轻松地从中组合任何内容。 加上业力,可以灵活使用。
  • 易于重构,此处无评论。
  • 您可以在项目的任何阶段开始使用此方法,因为它不会影响整个项目。

对于每个人来说,决定这个决定的好坏是一个私事。 我们在这种方法上的经验是积极而已解决的问题。

仅此而已!

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


All Articles