架构设计,到底是什么呢?基于这段时间的学习和自己的一些思考,我认为架构是基于产品和技术所达成的一种共识。我不是专业的架构师,也不是经验老道的开发者。本文目的有三,一是整理这段时间的架构学习和思考以及总结这一年的开发经验教训,二是希望能够与各位朋友探讨移动端App的架构设计,三是希望我们每一个应用开发者能够拥有架构的意识。个人的水平有限,诸多不对的地方,恳请批评指正。
提示:文中链接需要点击文章末尾处阅读原文才能点击。
前面啰嗦了很多,终于写到这里了。对于一个开发人员来说,怎么做才是我们的关键问题所在。只会Android开发,所以以下只讨论Android。我主要从以下几个方面来谈一谈怎么做这个问题。
1、技术选型
(1)、 开发平台
移动端的开发目前主要是两大阵营Android、IOS,其他的就不多说了。
(2)、 开发工具
编译工具:Eclipse&Ant、AndroidStudio&Gradle,作为Android开发者,目前毫无疑问应该选择AndroidStudio&Gradle;
代码仓库:Git 、SVN ,工具有海龟、AndroidStudio也集成了VCS;
Maven仓库:可以使用nexus创建自己的maven私服;
持续集成:Jinkens、Buildbot、Travis CI、Strider、Integrity;
(3)、 开发语言
Java、Kotin、Grovvy、SQL等等;
(4)、 开发模式
MVC、MVP、MVVM、clean等,各有优缺点,在此不做详细说明;
(5)、 开源框架
都说了不要重复造轮子,因为你造的轮子不一定比人家的好用,对于我们开发者而言,有一件非常好的事情就是我们有太多的开源免费的第三方库供我们使用,这样给我们省去了大量的工作,做到更加高效的开发。但是,如何选择,是否引入使我们需要考虑的一个问题。下面列出一些常用的第三方库,更多请点击。
网络:okhttp、 android-asyn-http、 volley、 Retrofit
事件总线:otto、 EventBus
依赖注入:Dagger、 RoboGuice、 ButterKnife
图片:Fresco、Glide、 picasso
数据库:GreenDao、 Ormlite、LitePal
Json解析: Gson、JackSon、 FastJson
响应式编程: RxJava、 RxAndroid
异常统计平台:腾讯Bugly、Crashlytics
性能优化: blockcanary、 leakcanary
(6)、 新兴技术
软件开发而言,新技术的发展相当迅速,然而我们实际落地到项目中却需要很长的时间,因为新的技术刚出来一是需要学习成本,二是需要承担新技术不够成熟,存在缺陷带来的一些风险。当然,我们应该积极的引入好的新的东西,跟得上时代的步伐才好。下面列举的一些也许都算不上新的东西,但是也是近年来大家所追捧的新技术。
AndroidSupport:DataBinding、MaterialDesign等;
混合开发:ReactNative、Hybrid、Weex等;
编程语言:Java8、Kotlin;
热修复:AndFix、HotFix、Tinker等;
构建:InstantRun、Freeline
2、业务拆分
我们在进行业务拆分的时候,我认为可以将业务分成三类:
(1)、常用基础业务
基础业务主要是我们的app的一些基础功能,像我们公司有BFC团队给我们开发了文件上传下载、网络请求、行为采集、账号系统等SDK,免除我们一些重复的劳动工作。怎么去定义什么业务才是基础业务呢?我觉得可以这么去区分。如果你的业务在行业普通的应用app都有需要,那么这些这些就是具有普遍适用性的基础业务。我们根据不同的功能进行拆分。
(2)、通用技术业务
通用技术业务我觉得是和自己app相关并且有技术性很强的业务,可能是你应用的核心技术部分,比如美颜这一类软件的图片处理,小猿搜题这类的图片识别等就是一项通用技术型业务。通用技术业务的特点就是在和你同一类的app都会有需要使用的技术,我们可以根据不同的技术领域进行拆分。
(3)、特定功能业务
特定功能业务就是属于你自己app的特定功能了,一般可以按照功能进行拆分成不同模块。比如说我目前的一键搜(类似于小猿搜题)主要有搜题、查单词、翻译三大功能。那么就可以分拆为三大块。搜题要经过拍照、框题、图片处理、网络请求等步骤,每个步骤都可以看成一块小业务,以此进行拆分。特定功能业务大部分仅适用于你自身的APP。
以上的说法仅从自身的经验出发来进行描述,在我们实际的开发中可能会有一些特殊情况,或者有不同的拆分分方法。总之,业务的拆分还需要根据实际情况来。
3、架构设计(关注点分离、抽象)
(1)、核心概念
关注点分离
世上本没有架构,关注点一分离就有了架构,我们将一个软件系统的开发从多个维度将我们的工作进行拆分,对于每个领域进行设计,将各个领域有系统的组织起来,这种组织结构就是架构。然而如何将一个复杂的系统将关注点进行合理的分离,这个是非常有挑战的。
抽象
抽象,这是在请教一位前辈时最后给我强调的一点。如果你对app是跟着交互走、一个页面一个页面写的,那么很显然,你没有对你的业务进行抽象,而只是在实现。作为java的设计思想也很强调抽象的概念。那抽象到底是什么呢?抽象就是你要做什么!更简单的理解就是,写interface而不是class。不知道大家有没有这样的经历,在我们的MVP的开发当中,我们有个Model,也有一个IModel,但是我们写完了Model才知道怎么写IModel,最后成了粘贴复制的体力劳动。如果你是这么做的,你可以自己思考下,假如我们先写是IModel,而不是Model,那就是怎么样的体验呢?这就是将你的业务进行抽象。在架构的设计当中,你只需要知道你要做些什么?而不需要去过多的关注你具体怎么去实现它,这才是设计。
(2)、设计思维
面向过程(Procedure Oriented)
众所周知,在C语言的开发中,我们的逻辑大多是根据任务的流程走。这是面向过程的典型例子。面向过程关注的是工作的流程、一步一步的完成任务。
面向对象(Object Oriented)
Java语言作为面向对象开发的典型代表,这是我们所熟知的。我们将计算机按照人的思维来进行设计,每一个对象都持有自己的属性,并且持有自己的操作方法。对象之间有继承,组合等关系,通过组织这些关系来完成我们的程序。就像社会的人和物一样,人与人之间各种复杂的关系组合完成了社会各项活动的运转。
面向切面 (Aspect-Oriented)
面向切面是为弥补面向对象中的一些缺陷而生的,我们将某些功能封装到一起,提供对外的接口,方便在任何地方调用。就如SharedPreferences, Json, Xml, File, Device, System, Log, 格式转换等,这些通常会在until包里边。它就相当于一个横截面,我们可以随时面向这个横截面完成操作,而自己的逻辑里边不再需要重复的设计。
面向服务(Service-Oriented)
面向服务是将系统进行拆分,分成一个个独立的程序或组件,并对外提供某一项服务。每项服务之间通过某种协议进行通信,并进行分开部署,如HTTP,从而达到松耦合的目的。
以上四种思维重点在于看待问题的角度不同,不同的角度解决问题的方案就不一样,当然各种角度各有优劣。那么对于在android开发中是否都只是按照OOP原则来设计呢?很显然不是。面对不同的需求,不同的场景,我们需要及时调整自己的思维,灵活运用,寻找最适合的角度,拿出最优的设计方案,这才是我们所追求的。
(3)、设计原则
高内聚
怎么理解高内聚?我认为我们在拆分时某一细分领域只完成单一的功能,其内部的事情自己处理。从表面来看比如一个model的class,对外提供了一个接口,那么他有一个输入,一个输出。单独看这个接口而言,它是高内聚的。当然,其内部的组织结构有可能千差万别,所以内聚的形式又各有不同。所以我们将他们分类为功能内聚、顺序内聚、时间内聚等等。
低耦合
耦合指的是模块之间存在依赖关系,关系相互依赖就会相互制衡,这是必然的。所以,如果耦合度太高的话,将会导致牵一发而动全身的后果,这个使我们不想看到的,也极大的影响的程序版本的迭代以及bug的修复。根据依赖关系的不同,我们分为了非直接耦合、数据耦合、内容耦合、开关耦合、控制耦合、外部耦合等等。我们要完成一个系统的开发,必须要将各个模块有效的组织起来,这种组织关系便无法避免存在了耦合,我们要做的是尽量减少这些依赖关系,尤其避免交叉依赖,将耦合度降低到最低,把我们的程序设计的更加的灵活。
适度设计
我们在设计的时候如果考虑不周,那么设计不够,不能满足现有或者可预知的需求,从发展的眼光来看,会导致后期的开发中出现很多的问题。如果想的太多,很容易进行过度的设计,从而将一个简单的系统设计的很复杂,那么就给当前的开发将增加了许多无意义的工作,降低了开发效率。那么怎样的设计才是合理的设计呢?我认为能够同时满足现有的需求和可预知的需求,并且面对架构的调整时能够很方便的进行扩展。这样的设计,是非常好的设计。如何才能达到这样的效果呢?我个人觉得在对系统进行设计时,关注点分离的颗粒度需要把握好,系统不过就是将不同单一小模块进行组织而已,那么这些细小的模块就是架构设计的基础,这就好比建房的那些砖头。这些砖头是什么呢?他么可以是是一个对外提供接口的公共方法,也可以是私有的内部方法,也可以是某某持用的成员变量。当然往大里看,他可以是某一个功能模块。在上述行业内各个app的架构演进中,都很强调进行模块化的改造。所以,分离好你的系统,才能够灵活的组织起来,以不变应万变。
(4)、设计方案
指导模型
下图在文中已经提到,这里再次引入,因为这张图对我的启发真的很大,也表达出了我心之所想。面对一个复杂的系统,我们怎么样去分离,怎么样去组织,我认为这张图已经传达出了其中的精髓,所以我认为这是架构设计的指导模型,无论你是什么MVC、MVP、MVVM之类的,都可以从中去理解。
模型分解
根据实践开发中个人的理解,我将此图再次进行了简化如下:
横向分块
根据上图的简化模型,我们可以这么理解,在横向我们根据业务功能进行模块划分。比如主题商店,我们可以分为壁纸、铃声等等模块,每个模块间解耦。同时,在每一层的业务间再次进行分块,比如壁纸在数据层就有图片的请求、加载、缓存、裁剪处理等等。
纵向分层
接下来我们在对每个模块的业务根据职责分为展现层、业务层、数据层。数据层主要负责数据的获取、封装等工作,业务层主要更加上层的需要调配各数据层最终将数据返回给展现层,展现层的工作就是将数据展现在UI界面上,并且响应人的各种指令切换UI,操作新的数据。
理想模型
在横向来看,我们将业务进行了分块,保证块与块之间相互之间没有任何依赖,保证了绝对的解耦。从纵向来看,每个层级之间的依赖很明显是无法避免的,所以我们可以保证上层仅依赖下层的接口,从而达到降低其耦合度的目的。
如上图所示,通过上述横向分块、纵向分层、接口依赖这三大步骤之后,我们可以将一个系统进行了很好的分解,并得到一个理想模型。当然,这是一个理想的模型。在我们的实际开发中可能无法避免一些交叉等特殊情况,我们还需要从实际情况出发。但是有一点,我们可以保证接口的分离,已达到更低的耦合度的目的。
统一管理
统一管理,是对于我们的设计中有一些东西是需要统一管理起来的。通过上述原则,我们将一个复杂的系统进行了拆解,已达到架构设计中将关注点分离的目的。然而在实际的开发中,我们除了要进行业务的分拆,也需要对某些业务进行统一的管理。比如说一些模式的开关管理,比如说我们在进行网络请求时需要在测试环境和正式环境之间的切换,我们可以将这些模式切换的开关放到一个地方,方便我们进行管理,而不要去到各个地方去修改。再比如说我们的请求url地址,是否可以写到一起进行统一的管理。还有在某些应用中会通过一个中间人来进行统一管理数据的流通、页面的跳转,这也是一个可以尝试的方案,详细请看苏宁易购移动端的架构优化实践文中提到的模块管理器、Url跳转管理器。统一管理的意思就是将分拆的某一类小的模块某一些特性放到某一处进行统一的管理。但是这样会存在一个问题,比如前面举例说到的统一开关管理,这造成了开关耦合,如何去避免呢?我觉得可以将开关默认写到自己的模块里边,并公开出修改的接口,方便上层进行统一的修改,以达到统一管理的目的。这样的话,即使这个模块拆离出来,也不会受到影响。但是,这样的话,其安全性受到了一定的影响。架构设计总是这样,你总需要选择一个折中适合自己的方案。
我们通过上述横向分块、纵向分层的方法将一个系统切成不同的小块,这些小块负责某一单一的职责,然后通过接口将块与块之间进行了间接性的连接,依赖的是接口而不是实例,以弱化这种模块间通信造成的耦合。当然,上述模型仅仅只是一个理想状态的模型,如果是一个非常复杂的系统,那么层级之间也能拆分出更多的层级。比如,在数据层,我们在MVP模式的开发下使用Model来完成,当Model层的业务变得非常复杂时,有部分人会考虑拆分出Data层放在最底层,最为最基础的数据操作等。最后,为了方便我们对模块进行组合并进行管理,我们可以考虑在小模块中开放出接口,供上层进行统一的控制管理。最后,我想说的时,我们在进行业务分离拆解时可以考虑按照上述的方案来做,最终还得根据实际情况来进行设计。
4、开发实现
当完成我们的设计工作后,我们进入了开发编码阶段,在这个阶段主要表达我们的设计,并最终取得实实在在的成果。当进入这个阶段之前,我们的设计不能仅仅是一份文档,而应该是开发人员和架构设计者达成的某种共识。再好的设计,也需要获得良好的表达和实现。下面主要谈一谈在实现过程中需要考虑的问题。
(1)、项目分包
项目的分包结构体现一个软件的架构,我们在进行分包的时候总有一种困惑。因为我们存在多种分法,比如我们可以分为根据类的功能分为activity、fragment、adapter、util等,有的时候,我们又根据功能模块分,比如一键搜中有查单词模块、有搜题模块,同时又存在网络请求,软件升级等小的外围通用功能模块。存在的问题就是模块之间又存在一些可以复用的东西,那么我们进行拆分明显出现了代码的冗余。如果按照两种方案同时分,那就肯定存在了架构的混乱。我们该如何达到这两种的平衡?我认为,这个也需要更加项目的大小而来,如果是非常小的项目,也不存业务扩展的可能,我们就可以采用上述的第一种方案,简单的分类就好。但是,对于较大的项目,我建议使用第二种方案。下面,我简单列一个模型仅供参考:
+ app
+main
+com.jfg
+common //常用基础业务
+util
+wedget
+base
+function //通用技术业务
+camera
+sensor
+moudule //特定功能业务
+mouduleA
+model
+presenter
+view
+mouduleB
+model
+presenter
+view
+mouduleC
+demo //主程序
+app
+activity
如上所示,我们根据开始的项目业务拆分分包如上,将常用的基础业务放到common包里边,这个包在大多数情况是不变的,并且为app提供基础性的服务,不过我们尽量不要放到这个common包里边,如果这个common包变得足够大的时候,就一定要思考是不是该拆分了。因为common给人的感觉就是什么都是,那就让我们无法快速认知这个包所担当的职责。我们可以这样理解,common包是面向切面而设计的一些业务,但也不是绝对的。接下来我们先聊module这个包,实际这里是将业务进行了模块化的分拆,如上我们拆分出了moudleA和moudleB,这两者之间要求没有任何的联系。但是,我们会存在一个问题,那就是moudleA和moudleB某些业务是一样的,我们拆开显得重复了许多体力活。这应该是大多数开发者面对的困扰,这种该怎么去平衡呢?我是这么考虑的。如果,moudle和moudleB存在重叠的业务,我们将这些业务提取到function包或者common包中,这样降低了业务的层级。我们允许moudle包的各模块业务依赖于function和common为我们提供的基础服务。为了更好的区分模块A和模块B虽然重叠但在逻辑上是各自属于各自的,我们有两种方法来做。第一种是将两种业务进行一定的抽象,实现的过程还是放到各个moudle业务中。第二种方案定义两个接口类,各自定义各自的接口。在具体的实现类中实现了这两个接口类的方法,内部在进行相同的逻辑操作。这样,对外看来,逻辑上moudleA和moduleB是分离的。总之,如何分包还得权衡利弊,尽量以一种思维来进行划分,以避免设计混乱。
(2)、抽象接口
如果说在架构设计中抽象很重要,你可能有些迷糊,但是如果要你先写interface或者abstract class 而不是class时,你就可能感觉得到抽象的意义。我们将一个系统分解成几个大的模块,一个模块查分成不同的层级,每个层级再次拆分成不同的细节业务。最后,我们很清晰的知道我们要完成某一项功能需要做哪些事?对的,做哪些事就就是一个个接口,我们在编码时先写接口再写实现有利于帮助我们对业务进行拆分和抽象。我们都知道做一件事情一般情况都需要提供一些条件,做完了会有返回结果。这些都可以在接口的设计中完成。我们需要注意是一个接口只做一件事情,如果有两件事非常相似也要尽量拆分而不是合并。在接口命名方面做到见名知意,怎么去评判,就是如果你的接口没有注释也同样能让人知道你的接口是做什么的就好。
(3)、数据存储
数据存储常用的有SQLite、SharedPreference、文件等,缓存是否也可以算是一种。这里想强调的就是要注意数据存储的规范性以及安全性,如果是数据库还有必要考虑其扩展性,如果不满足需求将会需要进行升级。
(4)、性能管理
这里源自于对性能优化的一点体会,对于服务端的开发我们很珍惜服务器资源,应该是看的见的需要银子买的。然而,对于客户端的开发我们常常忽略了这一点。虽然手机设备现在拥有大内存,但是如何写出一个优秀的程序,性能也是一个非常重要的指标。性能优化处理,那是我们在更正错误,那么之后应该是少犯错误。性能体验不够好,无非就是对机器设备的内存、CPU、GPU资源无节制的使用,造成资源的浪费,当机器设备无法承受时就会应用就会出现卡段、死机、异常等不良反应,严重影响了应用的体验。我们要做的就是要有很强的性能管理意识,对于内存、CPU、GPU等资源按需借用,并做到有借有还,即用完后记得释放资源。
(5)、特殊处理
我们在开发的过程中,总有那么多问题并不是按照正常思路出牌的,这些得归功于我们强大的测试团队。不同的手法,就能得到不同的结果,然后就给了我们一堆的bug。所以,我们在软件的开发中需要特别注意一些特殊情况的处理,这些最终往往还是逻辑上的死角。以下简单总结了一些:
功能冲突
功能冲突可以分为两种,一个是应用内部的功能冲突,二是应用之间的功能冲突。应用内冲突比如A功能和B功能都使用了某资源文件,如果在同时使用就会出现问题,我们通常加同步锁来防止这种冲突。应用外的冲突有很多,比如多媒体、闹钟、日历、铃声、电话等都肯能引起这些冲突,比如你正在播放一段视频,这个时候来了一个电话,那我该优先哪一个呢?还有当闹钟响起的时候,弹出一个界面是竖屏的,那么他就会强制将当前的界面变为竖屏,而如果你这个时候如果是横屏的话该怎么办呢?类似于这类还有很多,以后再细细总结。
极限操作
我们的测试人员喜欢对着某一个按钮狂点、或者在机器上安装无数的应用使内存爆满,或者在磁盘里边塞满各种文件。这些场景虽然并不是理性的用户所出现的,但实际也是程序的缺陷。所以,我们要注意对这些问题进行处理。
网络问题
不可用的网络,信号很弱的网络,网路在wifi和流量之间切换,2G网络和4G网络,网络请求超时等都需要我们针对实际情况进行处理,比如切回到流量的时候进行下载是否有提醒用户。这些处理也算是各个应用的标配了,就不再多说了。
为null处理
这应该是最常见的问题了,我们平时改bug或者从后台异常抓取的大多数都是空指针异常。首先,我们得搞清楚为null的原因是什么?然后我们需要进行为null的判断,并警告。
(6)、Log打印
这里把Log打印单独拿出来是应为我觉得很需要重视。Log是用来干嘛的?很显然是用来帮助我们查找问题的,然后我们大多数的情况下是问题来了再去加打印,并且TAG五花八门的,是有错误的地方用Log.d,而只是查看信息却是用的Log.e,我们查问题的时候要去阅读很多的代码逻辑,最后再定位到位置。我们在修复bug的时候花了大量的时候再阅读代码,这个太影响工作效率了。如果我们在编码之初就对Log有了很好的规范设计,有异常的地方就用Log.e,可能出现问题的地方就用Log.w等等,关键点的信息用Log.i,临时调试的用Log.d这样区分不是很好吗?我们在控制台一样就能够分辨自己需要的信息。我们应该充分应用这个工具,帮助我们快速定位问题。
(7)、软件重构
重构是因为业务的需要,也是因为对代码更高的质量要求,重构无处不在。我们不要为了重构而重构,也不要一直停滞不前,该重构时不重构,欠下太多的技术债务。重构有小到一个方法的重构,也有大到整个系统架构的重构。重构是软件迭代升级的一个必要过程,也是我们能够满足当前需要,并且能够适应未来的发展,获得良好的扩展性的必要手段。重构并不难,也不是什么大事,重点在于重构背后的目的、思想、设计是否清晰。
(8)、兼容适配
兼容适配的问题是我们开发一个头疼的问题,Android设备无法八门的屏幕尺寸、层次不齐的Android系统版本。除了进行针对性的处理,还得提醒产品设计人员在设计之初就得考虑兼容性问题。
5、软件测试
软件测试是我们开发中非常重要的一到工序,除了能够客观的感应我们所开发的软件质量水平,最终目的还是在于帮助开发人员修复软件缺陷,提高软件的质量。除了开发人员提交测试之前的自测,我们需要专业的测试人员来进行测试。人工测试的效率相对较低,所以我们应该考虑通过技术手段完成自动化的测试,如单元测试等。这样有利于软件的稳定,也同样的有助于开发人员提高代码质量。
6、开发规范
(1)、设计一致
什么样的设计才是有规范的设计呢?我个人认为一个项目保持一致的设计思想就是有规范的设计。我们可以这样去理解,在某一类的情况下,我们按照某一类似的方案来解决这一类问题。面对复杂的系统,我们一种设计思想也许无法满足架构的需求,但是我们只需要明白,这种事情这么干,那种事情那么干,相互之间灵活的组合却也不存在交叉,给人设计思路清晰的感觉。
(2)、编码清晰
什么样的代码才是高质量的代码?我觉得结构简单,逻辑清晰,一看就懂的代码就是一份高质量的代码。所以,我们可以抛开那些编码规范、命名规范等等,重新去审视自己的代码,看看读起来是不是很舒服。如果来了一个新同事,对你对项目一无所知,是否能够很快速的理解你的思维?最后几点有必要提出,一是慎用设计模式,二是尽量少写文档注释,三是遵循Java的面向对象六大设计原则。我想这三点就是编码的原则,遵守这些原则,形成一种习惯,自然而然就是一种规范。
(3)、文档有效
什么样的文档才是有效的文档,我认为能说明核心问题的文档就是有效的文档。作为软件开发人员,我们没有耐心去阅读一份复杂啰嗦的文档。文档只需要给我们呈现代码无法说明的问题,帮助我们快速理解项目结构,备忘重点问题,追溯历史记录。文档的形式又很多,比如我们git的提交记录、tag,项目结构图、核心功能流程图等。总之,文档是给人看的,以尽量少的文档来说明核心问题就好。
7、日常工作