软件架构设计原则和模式之分层架构设计
软件,应该根据其职能分成多个层次。分层架构设计思想,有很多成功的例子。如网络设计上,OSI七层网络模型,就把网络应用软件,按照功能分成了职能各异的七个层次。实际网络中使用的TCP/IP协议,也遵循OSI七层网络模型,只是把OSI的应用层,表示层和会话层全部糅合在应用层内而已。
可以说,按照功能进行分层,是一个经过实践检验、行之有效的软件设计方案。
从大的范围来分,软件可以分为两个层次:前端和后台。这两个概念大家应该都很熟悉。很多程序员招聘广告,都区分前端工程师和后台工程师。
前端,也常称为UI。是用户界面应用程序。用户界面应用程序,是直接和用户进行交互的软件。常见的前端应用有:命令行程序,Web应用,桌面应用(包括移动设备应用)。除了命令行程序外,都是图形化界面。本文只介绍图形化界面的前端程序。
前端程序,负责与用户进行交互。负责接收和校验用户输入,并向用户反馈输出。其业务操作是委托给后台来实现的。
前端程序,必用的设计模式,是20世纪80年代发现的MVC模式。所有成功的前端应用,都使用了MVC模式或者它的一些变体。
MVC模式,MVC全名是Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写。它是Gof设计模式一书中介绍的第一种设计模式,是设计模式之母。
MVC模式,就是用控制器来管理用户输入、输出和图形界面。数据来自模型部分。模型Model实际上集中管理了业务数据。这使对同一种业务数据展现多种图形界面成为了可能。MVC模式,分离了显示逻辑和业务逻辑。
应用MVC模式的前端应用中,模型Model是通向后台系统的通道。
前端应用负责提供与用户交互的软件。后台则向前端提供业务服务。所有业务相关的操作和服务应该由后台来实现。
前端和后台,可以是在同一个进程中的,也可以是分属不同进程的。
一般,较简单或者小型的单机系统,前端和后台都在同一个进程中。也就是都属于图形应用程序(或者命令行应用程序)。这种应用程序,前端部分代码,通过API调用直接调用后台提供的业务服务。如Office软件就是单机系统。
复杂或者分布式系统,前端和后台属于不同的进程,前端通过进程间调用机制来调用后台提供的业务服务接口。目前常用的进程间调用机制有:WebService、REST、基于TCP自己实现的通讯协议,编程语言自己定义的通讯协议,操作系统自己定义的通讯协议等。如,现在很多企业级应用软件,Web应用,互联网应用等。
后台系统本身,也需要分为多个层次,包括:接口层,业务层,助手层。分别对应于大家应该都很熟悉的JavaEE中的表现层,业务层,持久化层。
接口层,在JavaEE中称为表现层。就是后台系统对外的接口层,用于实现跨进程调用。接口层的代码,和你使用的进程间调用机制有关。如,你使用WebService,那么接口层就是用来处理WebService请求的代码;你使用Rest,那么接口层就是处理Http请求和返回Http响应的相关代码;你使用的是RMI这样的语言定义的通讯协议,接口层就是用RMI协议接收和发送数据的相关代码。
接口层,应用了Gof设计模式中的Fa?ade门面模式。它向前端(客户端)提供了一个简洁一致的接口,隐藏了系统的复杂性。只要保证接口不变,那么后台的业务层和助手层代码再怎样变化,对前端(客户端)程序都是透明的。
JavaEE中称之为表现层,意为和Web图形界面有关。这是因为当时JavaEE提出这种分层时,Struts等JavaWeb前端框架正大行其道。Struts等会把html页面返回给浏览器,具备呈现图形界面的功能。
但目前,前端和后台完全分离的应用很多,并已经成为一种趋势。如,前端是桌面应用或者移动应用,显然只需要和后台交换业务数据,不需要后台返回图形界面。Web开发中,目前也流行Html页面上嵌入JavaScript代码通过AJAX向后台交换业务数据,而不是要求后台返回html页面。接口层不再需要返回html页面,只需要返回数据。
因此这里我命名为接口层而不是表现层,更为贴切。
另外,对于前端和后台在一个进程中的软件,无需定义接口层。前端部分直接通过API调用后台部分的业务代码即可。
业务层,负责定义领域对象,完成业务逻辑的处理。它向前台提供后台需要的所有服务。
每一个应用程序,都有其需要解决的问题。针对其问题域,需要划定问题范围,进行数学建模,识别领域对象,并定义各个领域对象之间的关系。
数学建模和识别领域对象的方法有多种。下面分享一下我一直使用的一种方法:
1,请需求方描述清楚问题是什么,想要得到什么结果。
2,描述自己的解决方案,能够做到什么,寻求需求方的确认。
3,对解决方案文字化。找出其中所有名词和动词。名词就是领域对象的候选对象。动词是领域对象的方法的候选对象。寻找名词之间的制约关系。
4,确定领域对象的关系。我使用关系数据库的设计方法,确定对象之间是一对一关系,多对一关系还是多对多关系。
关系数据库实际上和面向对象系统是完全等价的。这可以从很多O-R mapping框架上看出来。
我对关系数据库设计非常熟悉,因此我总是使用这种方法识别和定义领域对象。即使在一些应用场景中,领域对象根本不需要持久化,我还是用这种方式来考察领域对象。
5,根据识别出的领域对象,及其相互之间的关系,需要实现的方法,用面向对象的思维编码。用面向对象的思维编码,不意味着必须使用类。用C这样的过程式编程语言也可以用面向对象的思维编码。只是你心中要放着对象这个概念即可。如,我用Python编码时,很少使用类,经常是直接使用函数。但心中还是存着“对象”的理念的,这样写出的代码才不会乱。
业务层代码,会需要使用一些第三方软件提供的服务或者其他更低层级的自己开发的软件服务,才能实现其业务逻辑。这些不属于接口层,也不属于业务层的代码,我们称其属于助手层。助手层代码是为了协助业务层代码实现功能而存在的。
助手层,在JavaEE中称为持久化层。因为JavaEE是一个企业级软件架构设计方案。其针对的是企业的信息系统。其业务逻辑一般就是操作数据库。
数据库的读写访问,是与具体业务逻辑无关的,是帮助实现业务逻辑的。因此JavaEE称为持久化层。但这个定义也不全面。因为还是存在很多系统,其除了访问数据库外,还需要其他服务才能实现业务逻辑。如,一个视频网站,它可能需要对上传的视频文件进行格式转换,进行文件切分等;OpenStack,除了访问数据库,还需要访问各个Hypervisor的管理软件来管理虚拟机,需要访问网络部分代码,管理虚拟网络,需要访问存储部分代码,访问块设备和文件等。
因此,我认为称之为助手层比持久化层更具普适性。
假设我们的应用是一个类似美团这样的优惠券应用,需要操作地图的功能。对古玩优惠券应用来说,自己的领域对象包括商户、优惠券、用户等。操作地图的功能,不属于领域对象,也就不属于业务层的范围。它是为服务层服务的。因此操作地图的功能,在优惠券应用中就属于助手层。助手层使用的地图API,实际上可能是调用百度地图服务。
对于百度地图服务来说,它相当于一个后端系统,其也由接口层、服务层和助手层构成。地图的位置信息、兴趣点等是它的服务。
因此,服务层和助手层的区别,实际上是逻辑概念上的区别。一个系统的服务层,对于其他系统可能就是助手层。
比如,一个企业信息化应用,需要处理一些时间、字符串、集合等相关的函数。这些函数在这个应用中就属于助手层。因为时间,字符串,集合都属于我的系统的领域对象,它们是为业务服务的。是更低一个层级的。
总之,后台部分中,不属于接口层和业务层的所有代码,都属于助手层。
我们以openstack为例具体说明业务层和助手层的区别。
openstack的业务层,包括其定义的server,也就是虚拟机。
而虚拟机的业务逻辑代码,需要调用hypervisor 管理工具。如果使用KVM这个hypervisor,就需要使用libvirt的客户端库。libvirt客户端在openstack中就是助手层代码。
而libvirt客户端库又会通过网络调用libvirt后台。在libvirt后台系统中,也包括了接口层,业务层,助手层这样的层次。
libvirt内部定义了虚拟机这样的领域对象,对其的操作,需要使用qemu命令行程序。qemu命令行程序在libvirt中就属于助手层。
这就像是剥洋葱,剥开一层里面还有一层。你在剥哪层,哪层就是业务层,其内部的层次就属于助手层。
让我们上升到哲学的高度。这种分层的研究方法,就是形而上学的方法,是用孤立、静止、片面的观点观察和研究世界的思维方式。一次聚焦和处理软件系统的一个层面的逻辑。不采用这种方法,眉毛胡子一把抓是做不好软件的。
分层的软件设计思想已经存在很多个年头了,在计算机科学的各个领域都证明了它的成功。前端(MVC模式)和后端(接口层-业务层-助手层)的分层设计也经过了几十年大量软件的证明。
分层的思想,就是每一个层次专注做一件事情。每一个层次都为上层提供服务。每一个层次对于其上层来说,都是可以复用的。
如,助手层的代码,时间、字符串等函数,换一个应用也可以用上。后端部分,可以为多个前端应用提供服务。前端一开始可能是桌面应用,后面变成Web应用,之后可能再开发移动应用,后端都不需要改变。一个框架的服务层代码,在另一个应用中,可以成为助手层代码。一个应用的整个后台,也可能成为另一个应用的助手层。
分层设计的软件,结构清晰,代码各司其职,能够最大限度地重用代码。