什么是软件架构?什么是软件框架?很多时候,我们常常会混用架构和框架这两个词。实际上,广义上的架构和框架在概念上有很大的不同,架构给人的感觉,包容上更大,所以实际上架构是包含了框架的概念的。广义的架构应为一个系统的架构,不仅仅涉及软件中的技巧,更有系统的观念与视角在其中;不仅要考虑代码的因素,更要布局非代码的因素;不仅仅有技术的内容,更有管理的内容在其中。而为了便于讨论,这里的架构很多时候是指狭义上的纯技术上的架构,我们根据表达需要,将其与框架混用。
就单纯的框架而言,其核心是抽象。抽象就是统一建模的过程。语言的最高境界应该就是高度的抽象。就比如,单说树木我们认为是一种抽象,而具体言及柏树、杨树、松树、银杏等,则是一种具体化。这种抽象会剥离出这类事物共性的东西。比如对树木而言,有树根、树干、树枝、树叶这些特征。对一个从来没有接受过训练的小孩子,画一个具有上述特征的树,他也能告诉你,这是一棵树。这就是抽象的威力。对语言、对程序设计,也需要这种对现实的、非现实的事物进行抽象,找到共同点,构建出统一完成体。这里,我们提到了非现实事物,举个例子,Android应用程序组件就是对APP的一种抽象,并统一抽象为Activity来与用户交互;而对Apple的IOS而言,则有另一种抽象。二者都是对非现实事物的抽象描述,虽有不同,但都是探索与人类交互的APP的共同特征。抽象带来的最大好处就是对变化的应对能力,达到以不变应万变的效果。
一,框架本身很复杂,但是却是用来将复杂的东西简单化
软件设计,到了变大,变复杂的时候,框架或者架构,就是重要的一块了。一个小测试程序,谈不上郑重其事的架构。但是,一旦成为一个大系统,那么架构或者框架就是重中之重了。这时架构设计上的差异,可能会导致截然不同的结果。好的架构,系统稳定灵活,可扩展性强,开发、运行效率高,生命周期长。相反,不好的架构,可能系统还没有设计出来,就因为中途暴露的各种坑而提前夭折。即便磕磕绊绊走到交付,也可能因为难以维护而使得生命周期大打折扣。
理解大型软件的设计思想:
软件的设计为软件的核心。写一个小程序跟设计一个大型系统区别在于何处?我们可以通过类比来说明。比如构建房屋,最简单的搭建一个犬舍,只需要几块木板加图钉(例子来源于代码大全)就可以完成。如果搭建的不好看,完全可以拆了重来,如果选的位置不好,也可以拆了重来。建一座房子,就有一点不同了,需要提前想一想该怎么弄。搭建犬舍时不需要图纸,只需要埋头干就行了。而建房子就不能这样,不但要仔细想一想,而且想好了可能还需要简单设计一下,画个图,实地丈量丈量,等等如此一番。完了还需要准备材料,石头沙子,水泥,砖块,钢筋等等,准备好材料后,就可以施工了。需要打个地基,一米深就够了吧,需要一些测量水平和垂直的工具。盖房屋已经不允许拆了重来了。再进一步,我们来看看设计高楼大厦。这就不是一个人可以完成的了,需要多人协作,进行精心完整的设计,并输出规范的图纸作为大家形成共识的基础。地基也不是简单的弄一下就行,要几米,怎么布局,都需要严格的计算和测量。各种施工工具需要具备,才能开始建造。软件设计也是类似,如果只是写一个测试小程序,完全不必要讲究命名,书写,功能有就可以了,当然可以写的好看点,有一个好看的犬舍还是给人不一样的感觉。再往后,大点的设计一个模块,或者一个具有一定功能的程序,有完整的操作界面,那么就需要简单的设计一下了。另外可能还需要一些脚本等其他语言协助。也许一个人也是可以完全完成的。再大点,一个大型的软件或者系统,情况就大不一样了。跟建设高楼大厦类似,需要进行详细的设计。接口要求,代码风格等等,都需要要求。每一个连接结构都必须是一致的,大家都可以拿来使用。一个功能模块,一个组件做什么用,需要具有什么样的要求,特点,接口等,都必须进行严格的设计,就像大楼的钢筋要用什么型号,做成什么形状等等,都需要按设计来。所有这些整合到一起,才能形成一个功能强大而又稳定的系统。此时的系统,也许就无法靠一个人来完成了。
不过这个类比,或者说隐喻,还是有点问题的。因为软件设计跟建筑大厦还是有些不一样。盖楼,是一个实物,实实在在能够看到,很多东西做成什么样,通过图纸就能完全确定。软件设计则不太一样,是一个抽象的东西,纯粹存在于我们大脑的一个系统。而且软件设计面临的不仅是系统本身的问题,还有解决问题领域的限制,很多东西无法跟建筑施工那样复用。不过这里重点是要突出一个强大的软件系统需要精心的设计,在这一点上,将二者进行隐喻是恰当的。
作为设计者而言,定然要有深厚功力。俗话说,也许每一个人都可以做一个好的欣赏家,评判家,但是,并非每一个人都能做一个好的创造者。也就是说,好东西容易看出来,但是不容易做出来。就像大楼,我可以住,可以看,可以评判,但是,要设计,就是另外一回事了。
作为一个软件开发者,一个有一定经验的软件开发者,笔者常常在想,那些好用的、庞大的系统的设计者,是如何认识系统架构的?是如何想出来设计方案的?像操作系统,Office办公软件,集成开发环境Eclipse,Android手机等等,哪一个不是投入了大量的人力物力开发出来的?哪一个不是人类智慧的结晶,像艺术品一样的存在?也许世上诸多人,这辈子都注定只是望码兴叹的命。在本章最后,附有笔者对架构认识有感而发的思考过程记录。
在《程序员的自我修养》这本书里,作者在序言部分提到了这么一段话:CPU体系结构、汇编、C语言(C++)和操作系统,永远都是编程大师们的护身法宝,就如同少林寺的《易筋经》,是最为上乘的武功;学会了《易筋经》,你将无所不能,任你创造武功;学会了编程“易筋经”,大师们可以任意开发操作系统、编译器,甚至是开发一种新的程序设计语言!
在网络上,也有以学武为例,类比程序员境界的例子。第一阶段是学习各种语言,类似学习武功招数套路;第二阶段是学以致用,类似可以在实践中对所学武功进行简单应用;第三阶段对特定语言平台有了一定深入了解,具备了初级能力,也就是武学中的手中有剑,心中无剑;第四阶段进入了高级阶段,不在受具体语言的限制,语言只是工具,此时手中为何剑已不重要;第五阶段就进入了系统架构层次,此时可以说是达到了手中无剑,心中有剑的境界,系统开发已不再话下;第六阶段是最高境界,此时眼中所见问题已于软件无关,达到了手中无剑,心中亦无剑的境界,即所谓的无招胜有招。就像笑傲江湖中的风清扬,手中有无剑已不重要。
我想框架与架构的设计者也应该是经历了如上所述的种种“修炼”过程,才到达高峰的。而我们的所不理解,亦可简单解释为不攀高峰无以阅天地。
但是与武学中上乘武功存于秘籍中所不同,软件系统开发似乎就缺少这种秘籍。框架设计之人,应该是达到了无剑胜有剑的境界。可是,即使是心中无剑,框架的设计也不是一蹴而就的。框架一般是一类场景开发的基础,需要充分完备的考虑之后才可以进行设计。因为一旦成形,小改则小痛,大改则大痛。即使如此,框架实现过程中,修修改改、拆拆补补也不可避免,需要不断的完善,不断的重构,不断的迭代,才逐渐完备成形。
如此可见,软件框架的设计,相对于武学修炼,似乎还要难一点呢。不过,在计算机的世界里,还有一个奇特之处,就是但凡是语言,都可以再设计,从而提升到框架的角度。不论是编译型语言还是解释性的语言,无一例外。比如,像Javascript这类语言,也可以通过设计变为框架,展现出强大的能力。此处只能弱弱的说一句,没有做不到,只有想不到。
说了这么多,那么这里所说的框架,到底是什么呢,而且到底有什么用呢?前面已述,大型软件开发十分复杂,框架设计也非常复杂,而且框架的核心是抽象,提取事物的核心本质的东西,自然是避免不了复杂化的。那么,我们为什么要费经心思,来提取事物的共性特质,来抽象呢,其实最终目标却是为了简单化。总结起来就是,一切复杂都是为了简单一切。比如,操作系统就可以看做一个框架,这个框架简化了开发者对系统各种资源的使用,包括CPU、内存、磁盘以及各种各样的外设等;数据库也可以看做一个框架,简化了开发者对各种各样大量数据的管理;Android中Framework的设计就更像是一个框架了,提供了应用开发的各种组件,简化了各种APP的开发。除了这些大系统外,还有其他一些应用内的框架,像jQuery是一个JavaScript框架,便于我们做Web相关的设计开发;ZeroMQ是一个消息框架,便于我们进行消息通信相关的开发;除此之外,还有很多各种各样针对特定应用的、特定语言的、特定开发方向的框架,方便我们构建更加可靠、更加复杂、更加庞大的系统。这一类的框架,也是更贴近开发者的框架。
二,几种不同类型的框架例子
在这一部分,笔者取几个自己开发中常用的框架,简单做个实例说明。其实软件的发展日新月异,框架的种类和数量也在飞速增加中,想挑选几个典型的例子放在这里,实在是有心无力。而且就软件的发展来看,典型二字似乎也难有立足之地。索性,倒不如就选择自己熟悉的,简单介绍,权当为了通篇结构的完整性。
例子一,基于事件的框架libuv。Libuv封装了操作系统的IO,支持异步操作,支持网络接口,定时器,任务等许多特性,完全可以作为一个底层框架。基于该框架,可以简化跟系统打交道的方式,专注于业务相关功能的开发,并且异步特性也不会拖住整个软件的性能。经典的NodeJS底层就是基于libuv的。
例子二,基于中间件的框架。中间件通常充当一个桥梁的作用,但是本身又提供一类复杂功能的封装。作为桥梁,中间件通常需要对上对下都提供接口。对下,面向操作系统,封装操作系统的相关资源接口。比如任务、内存、磁盘、网络等。向下接口多是在跨操作系统或者平台时使用。对上,则是为实际业务应用提供调用接口。作为功能体,中间件自身实现特定的复杂功能,比如对某一类领域的功能实现。比如ERP中间件,3D建模中间件,浏览器中间件等等。
例子三,基于多进程多线程的应用框架。是在实际开发工作中团队独创的小框架。满足特定应用的需求。不同模块间通过进程实现,模块内部如果简单,就是单线程,否则就是多线程实现。这一机制实现了两种消息机制,进程间和进程内线程间。
另外一篇博文专门描述了该框架:
基于多进程架构的嵌入式软件框架研究与实现_龙城赤子的博客-CSDN博客_嵌入式多进程
例子四,基于Binder的Android平台中的Framework框架。
关于Android框架,这里要多说一些,我们看看Android中框架对平台构建的作用。
首先,在最底层是硬件,这无需多言。
硬件之上是操作系统,包含很多驱动,这也很好理解。
操作系统之上呢?操作系统提供了硬件封装,那么再之上,就是软件功能了。但是在Android里面,这一层是硬件抽象层。这就是Google的高明之处了。我们对硬件资源的使用都是通过操作系统提供的系统调用完成的,但是,Google在这里却单独提供一个HAL层,用于隔离操作系统和上层软件对硬件的使用。之所以采用这种方案,个人认为有以下几种原因:
1 Linux毕竟不是Google自己的。如果觉得哪些接口不好,向修改或增加,不是那么容易的一件事;
2 Google定位自己做软件,不做硬件,做方案,不做产品。这里是不做硬件产品,这一点是放开的,否则,就可能不会有那么多企业投向自己的怀抱。但是,硬件厂商不见得愿意将自己硬件的驱动放到开源的Linux内核中。本质上,就是硬件厂商不愿意开源自己的驱动,那么怎么办呢?一种办法就是采用硬件抽象层,在Linux内核中只提供读写设备的接口,比如寄存器读写接口,而具体的使用逻辑,则在操作系统之外提供,比如提供动态库放在硬件抽象层。这时,Linux内核只是充当了一个访问硬件设备的通道。上层应用对设备的使用是通过硬件抽象层中的动态库来完成的。因为这一点,Google和Linux社区闹了矛盾。
3 Google通过HAL层,隔离了硬件和软件。在HAL层,Google告诉硬件厂商,自己对各种硬件资源的使用接口是什么,至于这些接口如何实现,那是你们自己的事,愿意公开也罢,不愿意公开也罢,反正方案都提供了,请自便。而且,通过这一层,也约束了所有的设备提供商,保证上层软件的演进不用过多考虑底层硬件的差异。比如A厂商的摄像头和B厂商的摄像头硬件接口不一样,但是你们都得提供摄像头该有的功能,我Google只抽象摄像头的共性作为接口,给我上层应用使用。你们只要满足我的接口即可,同样,如果C厂商也想让Android支持其摄像头,则同样提供满足上述接口要求的动态库即可。最终,通过抽象接口层,隔离了变化,底层硬件更新换代和上层应用迭代演进都可以无负担的独立进行了。
硬件抽象层之上是运行库层。这一块就是模块集合,各种支撑模块,存在于这里,而且,很多是开源组件。像浏览器内核,音视频编解码、加密体系,无线WIFI管理等等。这一层就可以叫做支撑层。这一层的内容,多以动态库的方式提供,可以供所有应用程序来使用;另外,一个应用,也可能会用到多个模块。所以,这些模块,都是基础性的模块。我们知道,Android应用早期是基于Java语言开发的,所以这一层还有一个类似Java运行时模块,用于处理Java字节码,将其转换为机器码。
再往上,就是框架了。它是一个衔接模块,向上,为应用编写的简单化,提供组件和接口;向下,将支撑层的各个模块管理起来。这一层是整个Android中最有设计感的一层,大量使用了设计模式。作为一个衔接层,将支撑层的功能封装为Java组件,供上层应用使用,特点是简单,许多复杂的状态处理等,都被这一层封装隐藏了。整个这一层的设计,都是类似Client-Server模式的。Activity有ActivityManager,Package有PackageManager,Window有WindowManager,还有ServiceManager,MediaServer等等。所有这些Java的或者C++的manager或者server提供服务功能的具体实现管理,而调用者通过进程间或者线程间通信,完成RPC调用。为了提供高效的通信接口,Android基于共享内存实现了Binder机制,这就是整个Framework的基础框架:Binder+RPC。服务调用方和实现方依赖接口描述语言提供的经过抽象的接口,完成各自的任务。接口是共享的,实现架子是统一模板式的,具体填充内容是纯功能逻辑的,这就是一个框架该有的样子。不断地封装隐藏,你只需要关注自己该关注的,跟具体业务逻辑功能高度相关的,而其他系统相关能统一提出来的,都给你作为框架一部分在隐蔽处实现好。
完成Framework这一层,整个平台基本架子就出来了。留给软件开发者的,就是怎么使用框架,开发各种好玩的应用了。
整个层次,如下图(之前在总体目录中已经展示过该图片了,但是没有做具体说明):
就如上图中所示,框架定义了大流程,使用者在特定的点实现UserPoint需要做的事情即可,至于何时调用如何调用,就不用管了。(当然,这里只是说明示例而已,实际中也没有这么绝对)
再次,从消息交互角度来看。通常情况下,一个应用程序就是一个进程。进程完成一个具体的任务,实现具体的功能,从而解决客户需要解决的具体问题。但是一个具体功能任务的实现,可能需要多个子任务协助完成(分而治之)。如果实现方式为多进程,则子任务间相互交互通信多有不便,而且效率也不高。如使用线程来作为子任务的载体,一个应用为一个进程多个线程模型,能够解决大部分实际中遇到的问题。基于这一点,可以考虑在框架中实现该模块。多子任务模式,需要解决的最重要的问题就是通信和同步,这一般可使用消息队列模型来解决。能够实现方便的消息通信机制,可以说是基本问题就解决了一半。(这里,我们举线程间的例子,也只是为了方便理解。实现时,可以做到通用而不区分进程和线程。像互联网应用中的消息队列框架,不只是跨进程,甚至跨主机,虽然不同进程跟主机之间的差异在实现者看来本质上差异不大,但是具体实现时,细节问题还是不少。)
消息队列机制可以很容易的通过C++的虚函数机制来解决。框架定义一个处理消息队列的基类,完成下面的逻辑
While(1){ aH = new Handle(); aH.get_msg_from_subtask();//从消息队列获取消息 aH.handler_msg(); //处理消息}
子任务获取各自的handler,并调用其h.send_msg()发送消息到消息队列中,main能够遍历所有任务的handler,并调用handler通用的虚函数进行消息的处理。
这里handler_msg和send_msg都是handler基类的虚函数,由开发人员继承handler类时覆盖实现。实际执行时同前面的任务流程类似,通过虚函数表,框架实际执行开发人员写的实现逻辑,并能决定消息流经路径。整体来看,框架完成调用流程支架,而YOU完成具体节点上的处理。
有了该消息驱动模型,结合前面的编译和活动生命周期管理部分,一个小框架的雏形就初步具备了。开发人员开发应用时,继承Activity类,完成具体动作的响应处理;继承Handler类,完成消息事件的分发;利用编译组织,完成整个应用的构建。
额外提一点,语言对框架设计也是有一定影响的。可以明显感受到,基于C++对象技术和多态特性,能够在一定程度上为框架设计带来便利。
五,架构研究方法
对任何一门学问的掌握,都是学习、模仿、超越这三个过程,古今中外,概莫能外。所谓的超越,不见得是狭隘的超过前人,能自成一派,就是超越。比如诗歌,先是学习韵律等框框,然后模仿名作,最后再自我发挥。其他艺术形式、科学研究都是如此。
这里,我们要强调的就是上述过程中的模仿。这是一个很重要的过程。通过模仿,才能体会优秀的经典的内涵,才能内化自己的东西。著名哲学家、数学家罗素就讲过,生活中不是缺少美,而是缺少发现。
回到软件架构,对于框架的研究,也不例外。通过对优秀框架的研究学习,不仅能够加深对框架的理解,也能够提高自己的设计能力。
首先,以MFC框架为例。笔者最早对MFC的了解,还是通过侯捷的深入浅出MFC那本书。作者通过仿真的方式,对MFC背后的机制原理抽丝剥茧,将其核心骨架展示给读者,就好比让读者戴了副隐形眼镜,透过华丽的外表看到内部的本质。通过这个过程,省却了可能被细节干扰迷惑的大量宝贵时间,同时也减少了记忆负担,反而看得更清楚,记得更牢固。
书中对MFC的类层次、运行时识别、动态创建、消息映射、消息传递等重点技术进行了详细的讲解并提供了仿真程序。感兴趣的读者可以查找相关资料。如果有机会,笔者再补充这部分内容。
其次,以Android为例。Android作为一个开源的,经过市场检验的,成熟的操作系统,也是一个绝佳的学习对象。前面我们已经对其整体结构做了简单介绍。市场上有很多Android相关的书籍,虽然不少是讲述应用开发的,但也不乏讲述Android核心实现的。只是这些书籍要么只偏向其中一个层次(比如驱动),要么代码堆得太多,讲的太细,有些就纯粹成了代码的翻译。这类讲解存在四个问题:一是变成了工具书性质,除非因为工作原因,对某个细节或者有相关模块的问题需要解决,才可能需要去查阅;二是很容易湮没在细节的汪洋大海中,就好比每一个字都认识,放在一起不知道啥意思,只见树木不见森林,缺乏对整体的掌握;三是Android版本更新很快,细节过多讲解的内容很容易过时;四是看过即忘记,太多的细枝末节无法搭建完整的系统视图。
其实,对Android的学习研究,更多的是需要道而非术。东西是永远学不完的,中国在科技领域长期处于学的状态,有一部分原因我觉得就是这种学习的教育方式影响的。所以我也考虑仿照MFC那样,做个Android仿真。骨架的东西是不容易变化的,那就是Android之所以为Android的基因所在。通过骨架,更能够看清设计的本源,也才谈得上模仿突破创新。
六,多视角
任何复杂的事物,都是多面派,需要多角度来看,软件的架构就更是如此了。横看成岭侧成峰,远近高低各不同。对于架构来讲,常常需要既深入细节又总揽大局。树木与森林都要看到。在软件架构领域,为了更好的、更全面的、更清晰地展现系统架构,人们已经总结了一些通用的视角来描述架构,通常我们把它也称作视图。常用的有4+1视图,4是逻辑视图、进程视图、开发视图、物理视图等,1是用例视图。实际中我们可以有意识的用上述4视图分析软件架构。