今天小豆君以第一人称视角,讲讲我做客户端开发的重构历程,并且在文中最后给大家指出我目前认为比较好的一个代码框架。
我把我的经历分为以下6个阶段:
阶段1:界面与业务代码混合
还记得刚毕业时,自己在学校也没怎么学习,代码基础可以说是一片空白,只知道一些基本的语法,凭着面试前死记硬背,勉强算是进入了一家公司,薪资呢也算是刚够吃饱饭的。
此时,我开发的是军工行业的一款桌面应用程序。我的代码都是在单个进程中开发的,而且业务逻辑也混合在界面类中,当另一个界面类想要获取到某个业务数据时,必须将另一个窗口类暴露出来。
随着代码量的增加,我的界面类越来越多,业务数据也越来越多。这就导致所有的业务和界面代码全部混合在一起,它们就像网一样,当需求关联到这些业务数据时,我很难从代码里面把它们清晰的拉出来。一旦发生错误,就像多米诺骨牌一样串联着发生很多个错误。
这使得我很难定位到真正的问题所在,按住葫芦起了瓢,刚刚修复了一个错误,谁想过一会儿测试又发现了新的问题。这让我痛苦不堪,看着其他人下班都早早回家,我却还在苦苦改bug,真是羡慕啊。
记得当时领导跟我讲的最多的是,小伙子的态度非常好,但技术能力就很一般了,还需要我多加把劲儿。为此,我暗下决心,对代码进行重构。
阶段2:界面与业务代码分离
为解决业务代码和界面相互嵌套造成的逻辑混乱,为此,我将业务代码和界面代码进行分离,就是我们平常说的前后端分离
核心点:
- 前端只处理布局、控件、样式、简单计算
- 后端只处理前端请求,为其提供业务数据
我用一个图大概表示如下
界面与业务分离
结合上图
针对阶段1,有以下重要改动
1)前后端彻底分离
前端只负责:数据展示,界面操作,控件布局以及简单计算;
后端只负责:和数据库交互,和窗口类发送来的请求信息交互,返回前端所需数据
2)新增一张全局信号表,当然也可以将该信号表进行详细分类,分别放在不同的文件中
该表主要定义界面与业务的信号
信号和槽的命名方法:
- req(request的前三个字母)开头的为请求信号,以on开头的为对应槽;
- res(response的前三个字母)开头的为响应信号,以on开头的为对应槽。
上图中我为了获取用户信息,在信号表中将get_user分为两个信号req_get_user和res_get_user,业务代码使用槽on_req_get_user来处理请求,子界面A使用槽on_res_get_user来处理响应。
有些同学可能会觉得这是多此一举,在该窗口类A中定义一个获取用户信息的函数,当点击按钮时直接调用该函数岂不方便很多。然而这种做法是错误的,这又回到了我讲的阶段1中,业务代码和界面没有分开。
例如,此时又有一个子窗口B也需要获取用户信息,这时你有两种选择,一是把子窗口A中的代码复制过来再重新实现一遍,显然代码重复不可取;方法二是直接在A中把该函数设为公有,在B中引用A的方法。如果你使用方法二,当时觉得开发很快,但随着需求的不断变化,你会发现代码会越来越难维护,所有的东西都混在一起,你只能不断的往代码中贴补丁,最终整个框架爆掉。
那么,我们如果采用前后端分离的方式,针对以上界面类B想要获取用户信息,只需要在给类中也定义一个同名槽函数on_res_get_user绑定响应信号res_get_user即可获得用户信息。这代码看起来就舒服多了。
但是当我正在为自己的重构而沾沾自喜时,新的问题又出现了,由于客户量的增加,与之对应的数据也在不断攀升,由于我的代码查询大量数据占用了过多时间,从而出现界面出现卡死的情况,为此我又开始思考代码重构。
阶段3:使用多线程
解决界面卡死的一个比较好的办法是使用多线程,多线程大家都清楚,我就不过多解释了。
为此,我继续重构代码
增加线程管理器处理前端请求
在线程管理器类中,新增线程池,每次接收到前端请求任务时,从线程池中取出线程执行任务。
这样,客户就不会因为复杂的sql查询而在界面前苦苦等待了。
阶段4:业务代码进行分层处理
在实现了多线程的处理方式后,我并没有高兴的太久,产品的需求更加变态,虽然前端部分的代码已经分离出来,但业务部分仍是一团糟,这又迫使我不得不再次进行重构,本次主要是对业务代码进行重构。
我的思路是参考java中的分层模型,但java里面分层比较复杂,客户端没必要做的那么做,为此我将其进行了简化。
- 预处理层:类似于网关,主要处理路由转发、请求过滤、请求埋点、业务监控等;
- 服务层(Service):主要处理具体业务请求;
- 管理层(Manager):通用业务处理层,主要封装第三方功能接口;对Service层通用能力的下沉,如缓存方案、中间件通用处理;与DAO层交互,对多个DAO的组合复用;
- 数据访问层(Dao):与数据库进行交互,并返回数据模型类对象。
业务分层
阶段5:双进程or多进程
通过以上代码重构,我渐渐取得了领导的信任,我开始担任起技术组长的职务。随着团队成员的增多,每个人的技术水平参差不齐。C++这门语言只要有个指针引用错误,就会导致整个程序崩溃,有些客户需要保存的数据也会因此而丢失。用户体验实在是太差了,长此以往,我们的客户流失率也必然会升高。为此,我不得不对代码再一次重构。
因为之前一直使用的是单进程,所以为了不让程序彻底死掉,针对我们的业务需求,我采用了双进程模式。前端一个进程,后端一个进程。当然,开发者也可以选择多进程模式,例如谷歌浏览器,一个界面就是一个进程。即使崩溃也只影响的是当前界面而已。具体的方案要根据实际业务需求来制定。
由于前面我已经做了前后端分离,本次重构很顺利的完成了。想想还是很happy的。
阶段6:加入自动化测试
尽管使用了多进程,但并没有真正意义上解决客户问题,产品质量仍然是问题。我们追求的是尽可能少的出现bug更低的缺陷率,我一定要加强测试,此时自动化测试进入了我的视线。
一般的测试方法,就是在界面上根据功能不停的点击,进行人工测试。当我们再次更新程序时,测试人员必须再次进行重复测试,回归测试等,耗时耗力。
下图是一个比较传统的测试分层:
测试分层
测试分层的金字塔模型
- UiTest(塔顶):界面测试,一般测试主要集中在这一层,以人工点击方式为主。这一侧距离用户最近,距离代码最远,这就导致一旦发生错误,我们需要跟踪的链路非常长,定位问题相对比较困难,有时还很难复现问题。但由于其离客户最近,也最能体现完整的业务逻辑。
- UnitTest(塔底):单元测试,这一层相当于白盒测试了,主要以开发为主,自己编写测试用例脚本,对每一个函数进行测试。这一侧距离用户最远,代码最近,所以一旦发生错误,我们能够很容易定位到。如果我们能把这一层做好,客户端代码的稳健性就有了非常大的保证了。但是它需要消耗大量的时间和精力,且随着代码的变更,测试用例也必须实时更新。一般公司都不太具备这样的条件。
- ServiceTest(塔中):服务接口测试,这一层相对其它两层,不会像界面那样多变,也不会像单元测试那样需要巨量的测试用例。接口相对稳定,只要我能保证有足够的测试用例在,就基本能够保证我软件的稳定性。所以我选择在这一层做自动化测试,有了足够的用例后,不管是后面业务变更或是代码重构,我都有非常大的信心去实现,因为自动化测试是我质量的保证。
好了,以上基本就是我的客户端代码的框架设计之路。
欢迎关注:
微信公众号:小豆君编程分享
头条号:小豆君编程分享