模块化与解耦

简述

本文主要讲述了在iOS开发过程中,模块化工程架构的一种组织方式,本文主要讲述基于cocoapods来做模块化的方案,详细讲述了iOS开发怎么进行模块划分的内容,主要会在以下方面做阐述:

  • 为什么要做模块化
  • 模块设计原则
  • 模块化开发有哪些优点和缺点
  • 解耦与通信

1.为什么要做模块化?

我们都知道最基本的代码设计原则:“Don’t repeat yourself!”,每一个工程都会有自己的架构,即使你是刚入门的开发者,写几天代码也会发现要把一些常用到的重复代码单独拿出来放在一个叫common的地方,实现代码复用。这样看来每个开发者其实都或多或少的做过架构方面的事情,每个团队至少有1~2个人在做这样的事情。

说道app代码架构,记得Samurai的开发者郭虹宇在群里说过这段精辟的话,引用一下:

一派是说app开发并不需要什么狗P架构,第二派说我们有自己NB的架构,第三派说只要模块化够好,每个模块应该有自己的架构。

这三个观点的出发点,我觉得也比较好理解,第一种应该是一些个人开发者,个人能力很强,经常一个人很快搞出来一个app,他的映像中不需要弄太多的框框框住自己,但是其实他也是有一套自己的架构的。第二派应该是一些公司或者大公司,有一套NB的架构对于团队的意义就比较大了,可以保证稳定迭代,保证规范和持久可维护性。第三派应该是BAT这样的有很多BU的超级公司,或者一些先进的开源开发者们,模块化能够更好的实现跨app的代码和功能的复用, 能够更好的共享资源,避免重复造轮子。

那么为什么要做模块化?已经很明显了,模块化的代码框架最屌,不信,看看苹果的框架怎么做的,你就明白了。

2. 模块设计原则

既然模块化最屌,那怎么才能做好project的模块化拆分呢,哪些代码应该被放到一个模块?这里分享一些我的经验。

越底层的模块,应该越稳定,越抽象,越具有高复用度。

这一点,目测大家应该比较认同,越是底层的SDK,就应该越稳定,稳定的最直观表现就是API很久都不用变化,所有的变化因子不要暴露出来,避免传递给依赖它的模块。但是要做到设计一套API很久都不用改变,那么就需要设计的时候能越抽象, 即需要我们抽象总结的能力。

稳定性 还有一个特点就是会传递,比如 B 模块依赖了 A 模块,如果 B 模块很稳定,但是 A 模块不稳定,那么B模块也会变的不稳定了,因此下一个原则:

不要让稳定的模块依赖不稳定的模块, 减少依赖

既然上面说最好不要依赖,但是我发现我的 B 模块的确依赖了 A 模块里面不可或缺的代码怎么办? 假设依赖的代码段为 x , 现在来看x的特性, 如果X是一个可能高复用的代码段,那么无妨把x从 A 模块里面拿出来,单做成一个模块 X, 那么 B 模块依赖 X 模块就好了;灵一种情况,x是一个方法或函数,而且不太适合单做成一个模块,所以那就在B模块里面拷贝一份 x 代码就ok了,因为这样可以保证模块的 稳定性自完备性.

如果上面两种方法都不太合适,我们会在后面解耦里面讲到如何解耦

提升模块的复用度,自完备性有时候要优于代码复用

什么是自完备性,就是尽可能的依赖少的模块来达到代码可复用。

举个例子,我有个模块 Utils 里面放了大量的category工具方法等,在日常UI产品开发中,依赖这个Utils会很方便,但是我现在要写一个比较基础的模块,应该就要求复用度更高一些,这个时候需要用到Utils里面的几个方法,那这个时候还适合直接依赖Utils吗,当然不合适了,这与我们上面的设计原则相悖了啊,因此我们这时候为了这个模块的自完备性,就可以重新实现下这几个方法,而不是依赖Utils模块

每个模块只做好一件事情,不要让Common出现

模块化结构是让工程结构更清晰,每个模块都只做一件事情,都有自己的一个命名,这样这个模块才能良性发展, 但是这个名字千万不要再叫Common了,试想下你有没有做过这样的事情:“哎呀,这块代码放哪都不太合适,放Common吧”, 日久以后,这个Common就变成了毒瘤,大家都依赖它,还一堆不相关的代码,这个Common模块就是我们设计原则第一点的反面教材: “非常不稳定,大量依赖,全是耦合,整个模块无法复用到其他app”, 所以删掉工程里面的Common吧,再遇到不知道放哪的代码,就要好好思考模块的设计,再不行如果具有可复用性就单建一个模块吧,为什么不可以呢?

按照你架构的层数从上到下依赖,不要出现下层模块依赖上层模块的现象

业务模块之间也尽量不要耦合

3. 模块化开发有哪些优点和缺点

优点: 1、不只提高了代码的复用度,还可以实现真正的功能复用,比如同样的功能模块如果实现了自完备性,可以在多个app中复用 2、业务隔离,跨团队开发代码控制和版本风险控制的实现 3、模块化对代码的封装性、合理性都有一定的要求,提升开发同学的设计能力。

缺点,模块化当然也有它的缺点: 1、入门门槛较高,新手入门需要的成本也更高 2、工具的使用成本,团队间和模块间的配合成本升高,开发效率短期会降低。

但是从长期的影响来说,带来的好处远大于坏处的,因此模块化仍然是最佳的架构选择。

4. 解耦与通信

我先说说为什么要解耦吧,模块化并不是说你把工程的代码拆分成 50 个 pod 或者framework就算完事了,要实现模块之间真正的解耦才算真正的模块化,否则如果模块之间还都是互相调用代码,循环依赖,那么和原本放文件夹里面没啥两样。那么什么是模块间的解耦呢?

模块解耦的目标就是, 在基于模块设计原则上, 让模块之间没有循环依赖, 让业务模块之间解除依赖。

4.1 公共模块下沉

这块其实还是讲的模块设计,一个工程的架构可能会分为很多层,然而在开发的过程中,很容易有人不注意让应该处于较底层的模块依赖了上层的模块,这种情况下应该对模块的设计进行改造实现单向依赖。

比如一个常见的普遍的例子: 一个公共的WebView模块,里面可能有WebViewController的基类,然后还有JSBridge的服务,如果设计的时候没有注意,很容易在开发过程中,这个模块被塞入大量的其他业务代码,依赖了一大堆业务模块,因为经常注册JSBridge服务需要跟业务耦合。

这个时候怎么做呢,首先我们要思考WebView模块的定位,从更全局的角度思考,每个app的架构应该都需要这样一个模块,那么我们完全可以把这个模块单独拎出来下沉为基础模块,这个时候的解耦就需要你对WebView模块做出一些设计,添加一些注册型Api,修改JSBridge的服务为可以通过注册的方式添加逻辑,这样来实现与业务解耦,业务完全可以把与自己业务相关的代码放在自己的模块里面,然后通过你设计的Api注册到WebView模块中。

4.2 面向接口调用

虽然说公共模块可以通过架构设计来避免耦合业务,但是业务模块之间还是会有耦合的啊,而且这种情况是最多的,比如页面跳转啊,数据传递啊,这些情况前面的方法已经不够用了。那如何解耦不同业务模块之间的代码调用呢?

那就是面向接口调用,我们知道只要直接引用代码,就会有依赖,比如:

1
2
3
4
5
6
7
8
9
// A 模块
- (void)getSomeDataFromB {
    B.getSomeData();
}

// B 模块
- (void)getSomeData {
    return self.data;
}

那么我们可以实现一个 getSomeDataFromB 的接口,让 A 只依赖这个接口,而 B 来实现这个接口,这样就实现了 A 与 B 的解耦。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 接口
@protocol BService <NSObject>
- (void)getSomeData;
@end

// A 模块, 只依赖接口
- (void)getSomeDataFromB {
    id  b = findService(@protocol(BService));
    b.getSomeData;
}

// B 模块,实现BService接口
@interface B : NSObject <BService>

- (void)getSomeData {
    return self.data;
}

@end

这样就可以实现了即满足了模块之间调用,也实现了解耦

优点:

1、接口类似代码,可以非常灵活的定义函数和回调等。

缺点:

1、接口定义文件需要放在一个模块以供依赖,但是这个模块不回贡献代码,所以还好。
2、使用较为麻烦,每各调用都需要定义一个service,并实现, 对于一些具有普适性规律的场景不太合适,比如页面统一跳转

4.3 面向协议调用

面向接口调用的缺点导致并不能满足所有的需求,也解耦的不够彻底,那么终极手段就是通过定义一套协议来实现模块间的通信,协议现成的,那就是URL跳转协议,基本满足需要,简单易上手,基本上现在很多的App架构里面都会有“统一跳转” 这一套东西的,这个不光是对模块解耦有帮助,对于统一化运营都是有极好的帮助的,比如app里面的任何页面,或者任何操作都是通过一个URL来唤起的话,这样是不是就把各个复杂的业务之间解耦了呢,通信都使用URL.

5. 源码推荐

说了这么多,也要放点干货吧,下面给出2个库的介绍,对你模块化的进程希望有帮助。

1、 JLRoutes 是一个URL跳转协议支持的库,跟我想要的简直很契合,强烈推荐。

2、 我自己写的一个解耦框架 AppLord. 简单介绍一下几个概念

* Module 是负责管理启动模块的工具,可以帮助你把AppDelegate里面一坨初始化的代码分别放到不同的module里面去
* Service 就是对上面 4.2 中面向接口解耦方式的一种封装
* Task, 全局的后台任务管理器,有时候一些不知道放哪的任务执行可以塞进去