软件架构及几种典型框架

    什么是软件架构么是软件框架多时候,我们常常会混用架构和框架这两个词。实际上,广义上的架构和框架在概念上有很大的不同,架构给人的感觉,包容上更大,所以实际上架构是包含了框架的概念的。广义的架构应为一个系统的架构,不仅仅涉及软件中的技巧,更有系统的观念与视角在其中;不仅要考虑代码的因素,更要布局非代码的因素;不仅仅有技术的内容,更有管理的内容在其中。而为了便于讨论,这里的架构很多时候是指狭义上的纯技术上的架构,我们根据表达需要,将其与框架混用。

   就单纯的框架而言,其核心是抽象。抽象就是统一建模的过程。语言的最高境界应该就是高度的抽象。就比如,单说树木我们认为是一种抽象,而具体言及柏树、杨树、松树、银杏等,则是一种具体化。这种抽象会剥离出这类事物共性的东西。比如对树木而言,有树根、树干、树枝、树叶这些特征。对一个从来没有接受过训练的小孩子,画一个具有上述特征的树,他也能告诉你,这是一棵树。这就是抽象的威力。对语言、对程序设计,也需要这种对现实的、非现实的事物进行抽象,找到共同点,构建出统一完成体。这里,我们提到了非现实事物,举个例子,Android应用程序组件就是对APP的一种抽象,并统一抽象为Activity来与用户交互;而对Apple的IOS而言,则有另一种抽象。二者都是对非现实事物的抽象描述,虽有不同,但都是探索与人类交互的APP的共同特征。抽象带来的最大好处就是对变化的应对能力,达到以不变应万变的效果。

   一,框架本身很复杂,但是却是用来将复杂的东西简单化

   软件设计,到了变大,变复杂的时候,框架或者架构,就是重要的一块了。一个小测试程序,谈不上郑重其事的架构。但是,一旦成为一个大系统,那么架构或者框架就是重中之重了。这时架构设计上的差异,可能会导致截然不同的结果。好的架构,系统稳定灵活,可扩展性强,开发、运行效率高,生命周期长。相反,不好的架构,可能系统还没有设计出来,就因为中途暴露的各种坑而提前夭折。即便磕磕绊绊走到交付,也可能因为难以维护而使得生命周期大打折扣。

   理解大型软件的设计思想:

   不过这个类比,或者说隐喻,还是有点问题的。因为软件设计跟建筑大厦还是有些不一样。盖楼,是一个实物,实实在在能够看到,很多东西做成什么样,通过图纸就能完全确定。软件设计则不太一样,是一个抽象的东西,纯粹存在于我们大脑的一个系统。而且软件设计面临的不仅是系统本身的问题,还有解决问题领域的限制,很多东西无法跟建筑施工那样复用。不过这里重点是要突出一个强大的软件系统需要精心的设计,在这一点上,将二者进行隐喻是恰当的。

   作为设计者而言,定然要有深厚功力。俗话说,也许每一个人都可以做一个好的欣赏家,评判家,但是,并非每一个人都能做一个好的创造者。也就是说,好东西容易看出来,但是不容易做出来。就像大楼,我可以住,可以看,可以评判,但是,要设计,就是另外一回事了。

   作为一个软件开发者,一个有一定经验的软件开发者,笔者常常在想,那些好用的、庞大的系统的设计者,是如何认识系统架构的如何想出来设计方案的操作系统,Office办公软件,集成开发环境Eclipse,Android手机等等,哪一个不是投入了大量的人力物力开发出来的一个不是人类智慧的结晶,像艺术品一样的存在许世上诸多人,这辈子都注定只是望码兴叹的命。在本章最后,附有笔者对架构认识有感而发的思考过程记录。

   在 络上,也有以学武为例,类比程序员境界的例子。第一阶段是学习各种语言,类似学习武功招数套路;第二阶段是学以致用,类似可以在实践中对所学武功进行简单应用;第三阶段对特定语言平台有了一定深入了解,具备了初级能力,也就是武学中的手中有剑,心中无剑;第四阶段进入了高级阶段,不在受具体语言的限制,语言只是工具,此时手中为何剑已不重要;第五阶段就进入了系统架构层次,此时可以说是达到了手中无剑,心中有剑的境界,系统开发已不再话下;第六阶段是最高境界,此时眼中所见问题已于软件无关,达到了手中无剑,心中亦无剑的境界,即所谓的无招胜有招。就像笑傲江湖中的风清扬,手中有无剑已不重要。

   我想框架与架构的设计者也应该是经历了如上所述的种种“修炼”过程,才到达高峰的。而我们的所不理解,亦可简单解释为不攀高峰无以阅天地。

   但是与武学中上乘武功存于秘籍中所不同,软件系统开发似乎就缺少这种秘籍。框架设计之人,应该是达到了无剑胜有剑的境界。可是,即使是心中无剑,框架的设计也不是一蹴而就的。框架一般是一类场景开发的基础,需要充分完备的考虑之后才可以进行设计。因为一旦成形,小改则小痛,大改则大痛。即使如此,框架实现过程中,修修改改、拆拆补补也不可避免,需要不断的完善,不断的重构,不断的迭代,才逐渐完备成形。

   如此可见,软件框架的设计,相对于武学修炼,似乎还要难一点呢。不过,在计算机的世界里,还有一个奇特之处,就是但凡是语言,都可以再设计,从而提升到框架的角度。不论是编译型语言还是解释性的语言,无一例外。比如,像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这一层,整个平台基本架子就出来了。留给软件开发者的,就是怎么使用框架,开发各种好玩的应用了。

   整个层次,如下图(之前在总体目录中已经展示过该图片了,但是没有做具体说明):

   从以上描述来看,可以将整个Android平台理解为硬件加系统加框架。硬件和系统是基础,针对应用领域,添加相关的扩展——基础扩展,然后将其管理封装到框架层,来方便应用开发者。至此,借助框架,构建完成了一个生态平台。

   以上这些框架,主要都是围绕终端层面展开的。其实,互联 生态中,框架的使用更加丰富多样,也有很多值得学习的好例子。但是,不论是针对何种业务采用何种技术为了何种目的而开发的框架,其总的目标还是抽象化,将复杂繁琐的东西简化明晰化,为整个系统“积木”的搭建提供便利。

   

   三,关于框架的进一步描述

   第一部分我们已经提到,框架的存在,是为了将复杂的软件开发,组件化,模块化,简单化,并在第二部分展示了几个例子。这里再进一步展开。比如像VC++里面提供的MFC类库,这就是一个框架。使用MFC开发Windows的应用程序,要比直接使用Windows操作系统的API接口简单。但是MFC本身也是基于Windows的API接口完成的,不过它实现了一些框框块块,这些框框块块,在实际开发中更加易用,因此,被开发者拿来,用于简化Windows程序的开发。框架本身并不是想实现应用的部分功能,而是为应用提供基础,将一些繁琐的东西,隐藏起来,提供简易的接口,从而将应用开发者解放出来。简单底下隐藏着复杂,就像iPhone,用起来简单,但是这种简单的代价就是背后复杂的实现了。

   从最底层的机器语言到汇编语言,程序开发接近CPU的物理工作原理,也受限于这一机制,需要考虑太多机器底层的东西,从而影响逻辑功能的开发。高级语言的出现,很好的解决了这一点,这本身,就是从物理到逻辑的抽象。而框架,则是在高级语言的基础上,再次的抽象。将软件的开发,变为模块化,组件化的开发。举个简单的例子,用汇编语言,按照面向对象的思想去开发程序,这个过程就显得很别扭,而用C++,则就顺利成章了,但是最终的程序经过编译后,还是一条一条的汇编。

   框架,给人一种硬件软化和软件硬化的感觉。所谓硬件软化,就是用软件的思想去管理硬件。就好比Linux里对驱动、总线、设备的抽象。将它们通过软件思想,抽象出来,达到灵活管理的目的。而所谓的软件硬化,就是将软件开发中,一些繁琐的,共性的东西,固定下来,不用去动它,也不用去管它。比如,设计好图形界面的各个元素,点击什么控件,调用什么接口,都固定下来,开发者只需要专心的在接口中完成逻辑即可。

   四,抽象与实际的结合

   这部分,我们继续通过非常简单的例子,来感受框架背后复杂的原理。

   首先,从编译链接角度来看。框架一般没有main函数入口,这里的没有是指开发人员看不到main入口函数。流程由框架提供,main入口亦被框架封装起来。开发人员开发的模块,对于整个软件模块来讲,相当于是机器的一个零部件,或者说是建筑物的一个个性化的嵌入房间,而部件和房间的模型早已经设计好了。如果将软件比做一个拼图板,那么整个系统就被分成了几块,一些块在框架或者SDK中,一些块需要框架的使用者来实现,还有一些块甚至在编译器中。这些块整体合并起来,才构成了一个实际可运行的程序。

   具体来看,对编译和链接程序而言,开发人员的代码是和框架的代码整合在一起编译链接的,比如最终的编译链接可能是这样的:

   g++  framework1.cpp  framework2.cpp  userfile1.cpp  userfile2.cpp  -l support.so …

   userfile不仅实现了自己需要的逻辑,还可能实现了框架要求的逻辑和接口。比如,框架中定义了某个虚函数,开发者需要重新实现该虚函数等。

   当然,实际情况可能多种多样,这里只是举个简单例子,便于理解。

   其次,从流程和生命周期角度考虑。以C++和Android的Activity为例。框架实现的main接口的伪代码逻辑应该类似下面

   上例中,Activity是框架实现的一个基类,封装了活动的一般特性。开发人员继承该基类,实现基类定义的一般动作,也就是虚函数接口。利用C++的虚函数的动态执行特性,代码得以可先实现调用流程,也就是什么时候调用那个接口,但是接口的具体实现,由开发人员完成具体任务时再去实现。这样编译时,框架代码和开发人员代码编译在一起;运行时,框架执行基类的虚函数,实际上最终执行的是开发人员继承实现的代码,从而达到框架设计机制流程,开发者实现具体动作的要求。

   就如上图中所示,框架定义了大流程,使用者在特定的点实现UserPoint需要做的事情即可,至于何时调用如何调用,就不用管了。(当然,这里只是说明示例而已,实际中也没有这么绝对)

   再次,从消息交互角度来看。通常情况下,一个应用程序就是一个进程。进程完成一个具体的任务,实现具体的功能,从而解决客户需要解决的具体问题。但是一个具体功能任务的实现,可能需要多个子任务协助完成(分而治之)。如果实现方式为多进程,则子任务间相互交互通信多有不便,而且效率也不高。如使用线程来作为子任务的载体,一个应用为一个进程多个线程模型,能够解决大部分实际中遇到的问题。基于这一点,可以考虑在框架中实现该模块。多子任务模式,需要解决的最重要的问题就是通信和同步,这一般可使用消息队列模型来解决。能够实现方便的消息通信机制,可以说是基本问题就解决了一半。(这里,我们举线程间的例子,也只是为了方便理解。实现时,可以做到通用而不区分进程和线程。像互联 应用中的消息队列框架,不只是跨进程,甚至跨主机,虽然不同进程跟主机之间的差异在实现者看来本质上差异不大,但是具体实现时,细节问题还是不少。)

   消息队列机制可以很容易的通过C++的虚函数机制来解决。框架定义一个处理消息队列的基类,完成下面的逻辑

   子任务获取各自的handler,并调用其h.send_msg()发送消息到消息队列中,main能够遍历所有任务的handler,并调用handler通用的虚函数进行消息的处理。

   这里handler_msg和send_msg都是handler基类的虚函数,由开发人员继承handler类时覆盖实现。实际执行时同前面的任务流程类似,通过虚函数表,框架实际执行开发人员写的实现逻辑,并能决定消息流经路径。整体来看,框架完成调用流程支架,而YOU完成具体节点上的处理。

   有了该消息驱动模型,结合前面的编译和活动生命周期管理部分,一个小框架的雏形就初步具备了。开发人员开发应用时,继承Activity类,完成具体动作的响应处理;继承Handler类,完成消息事件的分发;利用编译组织,完成整个应用的构建。

   额外提一点,语言对框架设计也是有一定影响的。可以明显感受到,基于C++对象技术和多态特性,能够在一定程度上为框架设计带来便利。

   

   五,架构研究方法

   对任何一门学问的掌握,都是学习、模仿、超越这三个过程,古今中外,概莫能外。所谓的超越,不见得是狭隘的超过前人,能自成一派,就是超越。比如诗歌,先是学习韵律等框框,然后模仿名作,最后再自我发挥。其他艺术形式、科学研究都是如此。

   这里,我们要强调的就是上述过程中的模仿。这是一个很重要的过程。通过模仿,才能体会优秀的经典的内涵,才能内化自己的东西。著名哲学家、数学家罗素就讲过,生活中不是缺少美,而是缺少发现。

   回到软件架构,对于框架的研究,也不例外。通过对优秀框架的研究学习,不仅能够加深对框架的理解,也能够提高自己的设计能力。

   书中对MFC的类层次、运行时识别、动态创建、消息映射、消息传递等重点技术进行了详细的讲解并提供了仿真程序。感兴趣的读者可以查找相关资料。如果有机会,笔者再补充这部分内容。

   其次,以Android为例。Android作为一个开源的,经过市场检验的,成熟的操作系统,也是一个绝佳的学习对象。前面我们已经对其整体结构做了简单介绍。市场上有很多Android相关的书籍,虽然不少是讲述应用开发的,但也不乏讲述Android核心实现的。只是这些书籍要么只偏向其中一个层次(比如驱动),要么代码堆得太多,讲的太细,有些就纯粹成了代码的翻译。这类讲解存在四个问题:一是变成了工具书性质,除非因为工作原因,对某个细节或者有相关模块的问题需要解决,才可能需要去查阅;二是很容易湮没在细节的汪洋大海中,就好比每一个字都认识,放在一起不知道啥意思,只见树木不见森林,缺乏对整体的掌握;三是Android版本更新很快,细节过多讲解的内容很容易过时;四是看过即忘记,太多的细枝末节无法搭建完整的系统视图。

   其实,对Android的学习研究,更多的是需要道而非术。东西是永远学不完的,中国在科技领域长期处于学的状态,有一部分原因我觉得就是这种学习的教育方式影响的。所以我也考虑仿照MFC那样,做个Android仿真。骨架的东西是不容易变化的,那就是Android之所以为Android的基因所在。通过骨架,更能够看清设计的本源,也才谈得上模仿突破创新。

   六,多视角

   任何复杂的事物,都是多面派,需要多角度来看,软件的架构就更是如此了。横看成岭侧成峰,远近高低各不同。对于架构来讲,常常需要既深入细节又总揽大局。树木与森林都要看到。在软件架构领域,为了更好的、更全面的、更清晰地展现系统架构,人们已经总结了一些通用的视角来描述架构,通常我们把它也称作视图。常用的有4+1视图,4是逻辑视图、进程视图、开发视图、物理视图等,1是用例视图。实际中我们可以有意识的用上述4视图分析软件架构。

声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

上一篇 2022年6月20日
下一篇 2022年6月20日

相关推荐