职责单一的重要性


最近在做 iPad 改版功能,由于 iPad 的功能和 iPhone 功能上基本保持一致,除了 UI 要适配下 iPad 的屏幕以及分屏的情况外。在这个开发过程中,和同事讨论了一个很有意思的功能扩展的问题。虽然简单但是感觉很有必要记录下来,对于在软件开发中设计业务逻辑时很有启发。

故事的开始

iPad 的UI适配的代码写在 XXXiPadViewController 中,主要目的是不改变原有代码的逻辑,只需要添加适配代码即可。其实也是软件设计中经常用的一个特点开闭原则。

但是遇到的问题是,原来的XXXComponent的代码,但是业务代码都是继承在ViewController中的,了解iOS开发的都清楚,ViewController主要负责页面UI的添加和布局。由于适配 iPad 的视图,尤其是分屏的逻辑,一定需要更改布局,如果想使用原来的 XXXComponent 的代码逻辑,但是不想改动代码的布局,似乎不太可能。

解决方法

如果熟悉软件设计的装饰器模式的话,这其实有点像装饰器。不改变原有对象的情况下动态地给一个对象扩展功能,即插即用。XXXiPadComponent,就是装饰器,用来装饰XXXComponent,让他具备iPad的布局能力,但是要把分屏布局都放到装饰器中,就需要父视图布局的能力暴露给 XXXiPadComponent。

下面就是解决这个问题的思路,通过写一个 iPad 的适配器,通过把 XXXiPadViewController 的布局能力暴露给 XXXiPadViewController,达到不改动原来 XXXViewController 的代码逻辑,同时增加了 iPad 布局的能力。要达到上面的目的 ,在 OC 里可以通过代理方法,把布局的动作交给 XXXiPadComponent,就使用了如下的设计,下面用伪代码演示下。


// 布局的回调方法
@protocol layoutDelegate

- layoutViewInContainer(View *)

// Component 增加如下能力
- addPlugin(Component)

然后调用者使用的方式就很简单如下


let iPadComponet = XXXiPadComponent()
XXXComponent.addPlugin(iPadComponet)

通过这种方式就是把布局代理给了 XXXiPadComponent,由于 XXXiPadComponent 基于于UIViewController,自然就想起来给UIViewController 加上能力扩展就行了。最终版本如下。这样父视图就不用关系childViewController的布局了,只要一行 ` addPlugin `调用就可以了,代码也可以节省很多。

- (void)addPlugin:(DTKViewController<ADTViewControllerPlugin>*)pluginVc {
    pluginVc.overrideInterfaceOrientation = self.overrideInterfaceOrientation;
    [self addChildViewController:pluginVc];
    [self.view addSubview:[pluginVc pluginView]];
    
    if ([pluginVc respondsToSelector:@selector(layoutPluginInContainer:)]) {
        [pluginVc layoutPluginInContainer:self.view];
    }
    
    [pluginVc didMoveToParentViewController:self];
}

- (void)removePlugin:(DTKViewController<ADTViewControllerPlugin>*)pluginVc {
    [pluginVc removeFromParentViewController];
    [[pluginVc pluginView] removeFromSuperview];
}

问题的出现

从上面的过程来看,装饰器模式代码结构清晰,但是带来的问题是会增加程序复杂性,下面就说出现的问题。

对于iPad适配,这种方式似乎挺好基本不用改变原来的代码,只需要适配 iPadViewController 的代码就可以。但是有同事开始质疑这种方式。

最大的质疑就是为什么不用addChildViewController,不就是childViewController么? 自己仔细想想确实是,为啥当时不用哪?很重要的一点就是 addChildViewController 的布局是需要父视图来写逻辑代码的,这样就和自己想要把iPad布局的适配代码都写在XXXiPadComponent中矛盾了。

尤其同事说的这句话让我记忆犹新,为什么你想要把SubViewController的布局交给本身那?SubViewController 就应该是父视图来布局,这样才符合UIViewController逻辑,别人看代码才看的懂。

如果从UIKit框架来看,确实这样写才是容易理解的代码。但是假如说一个从来没学习过苹果UIKit布局的程序员,想设计一个组件,然后父组件添加子组件他会怎么写哪?最便捷的方法就是直接通过 addPlugin 完成就行,然后子组件布局的代码也很可能写在组件中,因为这样封装性更好,其实很多VUE的组件很多都是一行代码,不用关心布局,一行代码就可以加载插件,除非调用者需要动态改变布局,否则默认就不再暴露出来。

所以矛盾就出现了 苹果 UIKit 框架设计就是给了UIViewController太多的权利,既可以添加各种视图,又要负责各种视图的布局。当然和刚才说的组件思想有很大的差别,所以自己如果给 UIViewController 添加plugin就会让其他开发者感到困惑。

解决方式

既然UIKi 框架的设计理念和业务插件的理念有冲突,怎么解决?其实本质上就是我们定义的Plugin暴露出了UIViewController,让这个plugin具备了过多的无用的能力,所以我们的plugin完全可以用下面的方式。


id<ADTPluginProtocol>

- registerPlugin:(id<ADTPluginProtocol>)plugin

plugin定义的协议接口不要和UIViewController关联,自然调用者就不会想到这个是UIViewController。所以我们设计某些功能时,发现和系统框架的理念不太一样的话,尽量还是要区分开自己的业务逻辑的功能。说白了就是要职责单一,不要把各种职责都给与一个类,这样就会变成看似功能强大,但是代码无比臃肿。

思考总结

软件设计中其实比较忌讳的是鱼与熊掌兼得,既想要代码可复用性强,又想要代码功能多扩展强,就很容易陷入到上面说的困境中。其实上面plugin的设计完全没有必要基于 UIViewController,看起来这个plugin功能很强大,既有UIViewController布局子视图的能力,又具备了子视图可以在父视图上布局的能,这样改变了UIKit框架原本定义的UIViewController能力,会让使用开发者非常困惑。 解决方案就是可以自己定义一个Plugin的对象,这样在使用plugin时,开发者就不会联想到这个就是ViewController,而只会知道Plugin可以让自己获得布局到父视图的能力。 这也提醒了平台框架层和业务逻辑的能力尽量分离清晰,不要混合在一起,否则开发者理解起来也很容易歧义。说白了还是软件设计的重要原则职责单一,不要给设计的功能分配太多没用的权利,仅仅给需要完成的权利就可以。

如果你喜欢这篇文章,谢谢你的赞赏

图3

如有疑问请联系我