在《Android开发进阶,从小工到专家》一书的第26页中有这么一段话,说Android之父Andy Rubin在被采访时说过,在设计Android之初他就希望Android能像FaceBook那样可以使用不同的应用中的功能模块儿,通过现有的模块儿像搭积木一样方便地构建一个应用。正式基于这一理念,Android被设计为高度组件化、可复用的系统。
在Android的应用开发中,目前流行MVP和MVVM的软件架构风格,让UI层和业务逻辑层,数据层充分解耦,充分分离。这本来是好事,本文要探讨的本意非是摒弃MVP或MVVM架构,而是意在说明,一切都是根据需要为目的,而不是像做物理题一样的套用公式。意在突出一种模块化和组件化的软件开发思想,助力敏捷开发,大幅度的提高开发效率和稳定性。站在模块化思想的思维角度上是思考和解决问题。而实际的本质上,也是做到了UI和业务逻辑的分离。只是看问题的维度不同。相比之下模块化则是更进一步。因为也能从模块化的思想中找到MVC的影子,显示模块就是V,业务模块就是M。 只是模块化思想来的更细,更容易复用,更实用。M可看做为,又进一步细化的不同的业务逻辑模块组成。至于那个C嘛,算是链接V和M的桥梁。
金庸武侠小说《笑傲江湖》中,独孤老前辈教令狐冲武功时,教他说忘掉之前那种种花枝招展的帅气姿势吧,别忘了你的目的是什么,只有能够打败对手就是好招。再借用一句马列主义的哲学来说,实践是检验真理的唯一标准。用邓大人的话通俗点儿来说,不管黑猫白毛,能抓得住老鼠就是好猫。意思都是说要告诉我们一是思想不要过于固化和死板,二是真理来自于实践中,而非尽信专家或权威,或是人云亦云。所谓的专家都是有专有家。在家里(某一领域)比较专注(专一某个领域且达到一定境界)。说以专家的告诫都是有一定道理的,但也并非都是真理。毕竟都是人嘛,都不是神仙。最明显的例子就比如说在经济领域,有很多的专家,结果谁都不能百分百预测未来经济的走向,预知哪只股票是潜力股。为什么?因为他们各有各的理。真理只能交给时间和实践来检验。
再来说说流行的MVP或MVVM架构模式的本意。只有站在更高的维度上去看问题,才能看透问题的本质,否则就会是“不识庐山真面目,只缘身在此山中”。先说如果没有MVP和MVVM这种架构风格之前是什么样,再说它的出现解决了什么问题,有什么好处,以及为什么要用吧。在Google刚推出android应用开发时,第一批吃螃蟹的人,也就是最早的布道者,大都是看官方文档或历程一路摸爬滚打,总结而来的经验。常规和简单的实现业务应用的办法是直接在activity里直接写代码,因为直观且清晰嘛。但是随着业务越做越大,越做越复杂,发现一个activity(UI层)里好几千的代码,看着都费劲,更别提让别人维护了。还有就是换了一个项目,UI变动了,但原来写好的业务都在UI里,从头再做一遍吗?原来做过的有,就费事儿点儿一点点抠出来吧,累死个人了。你说他复用代码了吗?确实复用了,但是很累很繁琐,把原来的业务抽离出来不亚于从头重新实现了一遍业务,且很容易出错。这样的业务充斥在UI层中到处穿插,不但逻辑不清,复用困难,且容易导致各种各样的问题。于是MVP模式出现了。MVP中的p就像个中间人。把M数据模型层或业务层跟这个V,UI层彻底隔离了。无论你的业务是操作数据库或是获取网络数据再怎么加工处理,要想表达给UI层,只能通过P这个中间人去做。这样的好处,一方面业务逻辑清晰,分工明确。一方面利于更大限度的复用。
但是大道理人都懂,你要是硬是去这么的套公式吧,有时候也很累很纠结。且往往掌握不好度,比如过度的设计,到处是接口,写起来也很累。比如业务逻辑层是放在model层呢还是P层呢,model层是不就是指存取或读取网路或本地数据呀。没人能准确的分清,于是不纠结了咋样顺咋样来,不去较真儿了。
前面说了一堆废话,现在进入正题。
先抛出来一个问题,如何在两周内实现一款Android的POS机应用?有刷卡,有语音,有存储,有界面,有通信,有业务。
是的,没错,是两周的时间功能完整的做出来。
再思考一个问题,平时小朋友们都爱玩一种搭积木的游戏,为什么他们能够仅凭自己的想象力就能创造出那么多的模型?有桥梁、有大门、有城堡、有汽车、有飞机,想到什么就能快速的搭建起来?其实就是模块化的思想。可惜我小时候只玩过泥巴,想来现在的小朋友们应该更聪明吧。不用管这积木是谁造的,怎么造的,用的什么料。只管搭建自己想要的模型即可。那么,回过头来看我们的软件工程项目开发,何尝灵感不是取自于大自然,取自于日常生活。就比如建造房子,你会去想一块儿砖,一扇窗户是怎么造的吗?每个人各有分工,角色不同,分工不同,共同有序的构建起整个大厦。同样的砖头,放在城市里是建筑,放在农村里可以是墙头。放在我桌子下面,可以垫桌子腿。放到美女口袋里,可以当防色狼武器。同样的模块可以用在各个地方去。根据你的需求,哪有需要往哪搬。
到这里真正引申出本文的主题,即采用模块化的思想去考虑我们的Android应用,而非死板的套用MVP或MVVM风格。以模块化和组件化的思想去考虑问题,是站在了更高一层的维度上去思考问题。管你是什么设备,什么型号机器,我的本意就是实现这么一套功能。根据需求,把我们的应用拆分为不同的组件和模块。在Android这种机器中,界面,即activity,即MVP中的V视图层,只是我应用中的一个显示UI显示模块儿而已。既然只是占比很小的一个显示模块儿而已,注定业务不会在activity里去写去实现。
按照模块化的思想来划分的话,一款Android的pos机无非有以下几大模块功能组成而已。
界面UI显示模块,记录存储模块,参数配置文件操作模块,通信模块,语音播报模块,卡操作模块,算法模块(常用加密算法或工具类)等几大模块组成。然后基于这些模块,再在这些模块的上层拆分和建立几个任务去衔接和组织业务。如卡处理任务线程,通信任务线程,定时任务线程等。再利用Android的事件总线通信机制,如rxbus或Eventbus等,把具体业务和UI模块(activity)衔接起来。这不就是完整的应用吗?且结构清晰,分工清晰,全都是模块儿化,一层层的往上搭建应用,是不是很像是搭积木?也完全符合设计模式的几大原则,做到高内聚底耦合。单一职责,接口隔离,开放封闭,迪米特法则。构建的这些基础模块之间,做到相互的独立和无依赖,就最大限度的提高了复用性和稳定性。且结构和逻辑,层次清晰。那么基于这些模块构建的应用之间如何传递数据呢,看下面的图,更清晰直观点儿。
卡操作模块、通信模块、存储模块、语音模块、常用算法或类库等,这些作为底层的基础技术组件用。这些基本都是能够复用的功能独立的模块。
然后在这些可复用的技术组件之上是业务的组织。称之为可复用的业务组件层。模块化的思想有两种,不但是技术组件的模块化,不同的业务之间也可以把独立的业务模块化。目的都是为了结构职责清晰,更大限度的复用,提高稳定性和生产力。比如可以封装出不同的业务模块,交通部卡处理模块,自发卡处理业务模块,二维码业务处理模块,小键盘处理模块,消费记录通信组包和解析等通信业务模块。这些都是可复用的业务组件。那么上层的卡任务和通信任务等可以调用这些业务模块完成整个业务逻辑。
在这些业务模块之上又创建了几个线程任务去组织和衔接业务,各干个的活。跟卡相关的都在卡任务线程里。跟后台通信上传记录和下放参数相关的都在通信线程任务里,需要定时执行的一些其他任务在定时任务线程里。这部分可以看做是业务的大体框架。
最上层就是最接近用户的UI显示层了。需要更新UI时,业务框架层只需要通过事件消息总线技术,把事件通知给UI层处理即可。在UI层可以完成界面展示,页面跳转和切换,以及用户触摸事件等简单的业务逻辑处理。
最后在说下,底层的业务和上层UI,交互的数据从何二来?如何交互,如何传递数据呢,全局的静态类或JavaBean就派上了用场,他们相当于全局变量一样,负责收集和获取数据。至于这数据怎么个显示给UI,何时触发,由业务逻辑层负责触发事件,通过消息总线去通知UI更新或显示,更新或显示的内容在全局的静态类里。
这种模式,如果非要向MVP和MVVM的风格架构上套,它哪个都不是。好像是只有V和M,其他的找不到了。但业务没在V中。总之不要去纠结这究竟是什么路数了。因为一开始考虑问题和看问题的角度就不同,是按照模块儿化思想考虑的。activity只是模块化中的UI模块而已。所以还是不要去往MVP或MVVM上想,不是一回事,忘掉他们吧。
有了这些之后,大体框架已经有了,接来下就可以专注于业务实现啦。岂不是很简单。两周的时间去完成一些基础业务也是绰绰有余的。但是前提是,建立在这些可复用的技术组件之上。比如通信模块,记录存储模块。记录存储模块本来是操作数据库,但是封装后让你看出来任何数据库操作的影子。只有save和read,delet等简单的接口。把数据模块封装的就像是操作快递存储柜,应用的人只需要知道存东西,存在哪。取东西,东西在哪即可。管你内部是操作的oracle还是MySQL还是sqllite,好的封装尽可能做到迪米特法则。最好让使用者不用去关注你的底层是用什么实现的。
所以公司新的Android的pos机,我几乎只用了两周时间就搞出来了。是的,两周,这没有开玩笑。
当然这得益于之前711机器累积的经验,提前实现了记录存储模块,通信模块和配置文件操作模块。同时多亏同事的帮忙,分工合作,同事提供卡操作模块的操作卡片的底层接口,美工提供了UI素材。没有这些,一个月也搞不定啊。但是存储模块,由于涉及到记录安全,显示尤其重要。不经过压测是万万不行的,得确保无一条数据丢失。有的说存储数据多简单啊,操作数据库一个 insert指令就完了的事。但是我想说的存储模块是包含了业务的实现。有哪些业务呢,首先,不用再考虑如何建表结构,数据不会一直往下存储,终端机器里敢一直这么傻瓜式的存下去,机器早晚要存满挂掉。记录上送至后台一条,要删除一条,可不是真正的删除啊, 那么岂不很不安全。只是清掉更新标记,即记录是循环覆盖的模式存储。这里面涉及不少业务逻辑,可不是简单的insert和delte就完事了。那么这样封装后使用起来有多简单呢,可以说是傻瓜式操作,谁都会用。即便不知道SQL是啥玩意儿的都能玩的转。保存数据直接 save( data内容)即可。 data内容为要组织的记录的二进制数据。有的说那要强大的sql有啥用,你当成文件存储来用了吗。sql和数据库的优势木发挥出来。这里我只想说SQL是强大,但是你会用高射炮去打蚊子吗。满足需要才是目的。我在终端上压根用不到负复杂的查询。要保存的记录字段有很多,且终端需求上经常多变,会为了哪天需求要求多传个字段,就去动一动表结构吗?太没必要了。那样就太累了。还是那句话,实践出真知,不信撸起袖子来干,不服来战。可以比一下你用操作SQL快还是我经过封装过之后的使用快。我这读写和操作记录只需要几分钟。但是你要提出来给你个卡号,给我找出来他的所有明细,且金额低于10块的,再统计出总额,再关联查询下是是属于哪个司机消费的,那么不好意思,这就得花点儿时间了。不像你一个SQL语句分分钟钟搞定了。但是终端上没有这种需求。即便有,再封装出个接口就行了。内部实现我也用SQL,它的强大是无疑的。
再举例说下配置文件存储模块的封装,假如有个需求让你存储配置信息如 IP和端口 port到配置文件中。你操作完成这些如何做,需要多久?我给你说我需要30秒足够了。这这取决于我打字的快慢。无论你是操作ini文件或是json文件或是xml文件,再怎么也需要额外好多步吧。就拿Android中的 SharedPreference类来说,那么完成这些操作,你需要:
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); String ip = "218.28.111.121"; int port = 5050; sp.putString("IP",ip); sp.putInt("port",port);
存储完开机要用到的话,还得一个个的加载出来。比如
String ip = sp.getString("ip","");
而我只需要
syscfg.ip = "218.28.133.181"; syscfg.port = 22288;
syscfg.save();即可完成存储。开机后,只需要syscfg.load()即可完成加载。
从这些数据存储量小时,看不到优势多明显。如果有几个个参数要存储和保存呢。优势就出来了。
比如终端要保存票价信息,参数太多了。
dealCfg.ver = 12; dealCfg.time = "201910251551"; dealCfg.discInfo[0].cardType = 5; dealCfg.discInfo[0].purseDisc = 100; dealCfg.discInfo[1].cardType = 6; dealCfg.discInfo[1].purseDisc = 200; dealCfg.saveCfg();即可。
最后再来说下复用性。
假如我有另外一项目,也是Android系统,但卡处理部分硬件变了。那么上述只需要改动卡操作模块。其他完全不变,不受影响。假如有另外一Android项目,不涉及到读卡。那么通信模块和存储模块和工具类模块则是直接可以拿走用的。
如果,想在电脑上实现这么一款POS机咋办?
那么业务部分是不需要动的,卡操作模块改变下,换成操作读卡器模式。界面部分,则可以替换为其他的UI或者网页也可以。比如用前端技术Vue或reaect构建出界面作为UI层。使用websocket和卡处理任务交互起来,卡交易成功后通过websocket通知到前端浏览器,javascirpt操作数据去更新UI。