软件框架 转

( 本文源自《.NET通信框架的设计、实现与应用》书稿第一章内容,未经许可,不得转载。) 
转自zhuweisky


   框架和类库等概念的出现都是源于人们对复用的渴望。“不要重复发明轮子”,成了软件界的一句经典名言。从最初的单个函数源代码的复用,到面向对象中类的复用(通常以类库的形式体现),再到基于组件编程中二进制组件(.NET中是以IL程序集形式存在的)的复用,人们复用软件的抽象层次越来越高。现在,框架复用是抽象层次的又一提升,框架的复用不仅仅是功能的复用,更是设计的复用。

11 框架与类库的区别

   我们先来简单说说什么是类库(Class Library)?望文生义,类库就是一些类的集合,只要我们将一些可以复用的类集中放到一个Library中,我们就可以称其为一个类库。类库中的许多元素(如类、结构、接口、枚举、委托等)之间可能有一些关联,但这些关联通常用于支持一个类概念或接口概念的完整表达。如果我们从一个更高的视角来审视类库,可以发现类库中的一个个“完整的概念”之间是无关的或是关系松散的。

   再来说框架,框架的第一含义是一个骨架,它封装了某领域内处理流程的控制逻辑,所以我们经常说框架是一个半成品的应用。由于领域的种类是如此众多,所以框架必须具有针对性,比如,专门用于解决底层通信的框架,或专门用于医疗领域的框架。框架中也包含了很多元素,但是这些元素之间关系的紧密程度要远远大于类库中元素之间的关系。框架中的所有元素都为了实现一个共同的目标而相互协作。

   没有一个万能的框架可以应用于所有种类的领域和应用,框架的目标性非常强,它专注于解决某一特定领域的问题,并致力于为这一特定领域提供通用的解决方案。

   框架与类库的区别主要表现在以下几个方面:
(1)从结构上说,框架内部是高内聚的,而类库内部则是相对松散的。
(2)框架封装了处理流程的控制逻辑,而类库几乎不涉及任何处理流程和控制逻辑。
   正是由于框架对处理流程的控制逻辑进行了封装,才使得框架成为一个应用的骨架。框架中的处理流程和控制逻辑需要经过精心的设计,因为所有使用了该框架的应用程序都会复用该设计。
(3)框架具有IOC(控制反转)能力,而类库没有。   
   IOC,即俗称的好莱坞模式(Don’t call us, we will call you)。对于类库中的元素来说,通常都是由我们的应用来调用它;而框架具有这种能力――在适当的时候调用我们应用中的逻辑。这种能力是通过框架扩展点(或称为“插槽”)来做到的――具体的应用通过扩展点注入自己的逻辑,而在适当的时候,框架会调用这个扩展点中已注册的逻辑。实际上,.NET中的事件(event)发布、预定机制就是IOC的一个代表性例子。
(4)框架专注于特定领域,而类库却是更通用的。
   框架着力于一个特定领域的解决方案的完整表达,而类库几乎不针对任何特定领域。比如,本书中提到的通信框架只适用于需要在TCP/UDP基础上直接构建通信的应用程序,而像正则表达式这样的类库却可以使用在各种不同的应用中。
(5)框架通常建立在众多类库的基础之上,而类库一般不会依赖于某框架。

12 通用框架与应用框架

      如果要对框架进行进一步分类,则可以根据框架针对的领域是否具有通用性而将它们分为通用框架(General Framework)和应用框架(Application Framework)。通用框架可以在不同类型的应用中使用,而应用框架只被使用于某一特定类型的应用中。
      比如,ORM框架NHibernate就是一个通用框架,该框架可以用于所有需要解决O/R映射的各种类型的应用中。而某个金融框架则是一个应用框架,它仅仅被用于金融类型的应用中。
      可以这么说,通用框架所解决的是所有类型的应用都关心的“普遍”问题,而应用框架解决的是某一特定类型的应用关心的问题。所以,如果我们需要将某种类型的应用的核心业务逻辑流程提升到一个框架中,所得到的这个框架就是一个应用框架。与通用框架相比,应用框架需要了解更多目标业务领域内的领域知识。
      在实现具体的应用程序时,可以采用一个应用框架与多个通用框架相结合的方式,这样有利于快速、高质量的应用程序开发。比如,某个金融领域的一个应用,可以采用金融框架作为应用框架来解决与金融业务逻辑相关的问题,采用Nhibernate解决数据访问,采用ESFramework解决应用中各分布式系统之间的通信。 

      下图描述了类库、框架和应用之间的层次关系。

   当然,一个应用也可以完全不采用任何框架,而是直接从最基础的底层API(如.NET Framework)开始构建。对于微型的系统,这种方式或许可行。但对于复杂大型的应用,困难度就可想而知了。

13 框架之于应用

   当一个应用系统选定了框架之后,我们需要做的就是在框架提供扩展点的地方添加应用的具体逻辑,也就是使用“血”和“肉”来填充这个骨架从而得到一个“有机体”。
   由于框架通常都是在实践中经过反复使用和检验的,所以质量有一定的保证,这使得我们用更少的时间、更少的编码来实现一个更稳定的系统成为可能。当然,框架也不是“银弹”,它不能解决软件复杂性的根本问题,但是我们却通过它向这个终极的理想目标又迈进了一步。
   有一点需要注意,框架使得我们的系统在有所支撑的同时,它也给出了限制。因为通常当我们确定采用了某一个框架之后,我们就必须在这个框架限制的“框框”之内来构建我们的应用。大多数时候,这不是一个问题,但是如果因为框架的限制而严重影响了我们系统目标的实现的时候,我们就需要考虑是否应该放弃这个框架,或者换一个其它的同类型的框架。

14 框架设计

   框架使得我们开发应用的速度更快、质量更高、成本更低,这些好处是不言而喻的。然而,面对万千变化日趋复杂的软件需求,设计和实现一个高度灵活可复用的框架又谈何容易!
   框架源于应用,却又高于应用。
   框架往往是这样产生的:我们拥有了开发某种类型应用的大量经验,我们总结这种类型的应用中共性的东西,将其提炼到一个高的层次中,以备复用。这个“高层次”的东西便是框架的原型。随着我们经验的不断积累,框架也会不断地完善、发展。
   框架是一个实践的产物,而不是在实验室中理论研究出来的。所以设计一个框架最好的方法就是从一个具体的应用开始,以提供同一类型应用的通用解决方案为目标,不断地从具体应用中提炼、萃取框架!然后在应用中使用这个框架,并在使用的过程中不断地修正和完善。
   有一点需要特别注意,正如所有的软件架构设计的要点在于权衡(在这方面有点像艺术),框架的设计也不例外,正如前面提到,框架在为应用提供了一个骨架的同时,也给我们的应用圈定了一个框框,我们只能在这个有限的天地内来发挥。所以,一个好的框架设计应当采用了一个非常恰当的权衡决策,以使框架在为我们应用提供强大支持的同时,而又对我们的应用作更少的限制。权衡,从来就不是一件简单的事情,但是有很多框架设计的经验可以供我们参考。

1.4.1 框架设计经验、原则

(1)框架不要为应用做过多的假设!     

   关于框架为应用做过多的假设,一个非常具体的现象就是,框架越俎代庖,把本来是应用要做的事情揽过来自己做。这是一种典型的吃力不讨好的做法。框架越俎代庖,也许会使得某一个具体应用的开发变得简单,却会给其它更多想使用该框架的应用增加了本没有必要的束缚和负担。

(2)使用接口,保证框架提供的所有重要实现都是可以被替换的。

框架终究不是应用,所以框架无法考虑所有应用的具体情况,保证所有重要的组件的实现都是可以被替换的,这一点非常重要,它使得应用可以根据当前的实际情况来替换掉框架提供的部分组件的默认实现。使用接口来定义框架中各个组件及组件间的联系,将提高框架的可复用性。

(3)框架应当简洁、一致、且目标集中。

框架应当简洁,不要包含那些对框架目标来说无关紧要的东西,保证框架中的每个组件的存在都是为了支持框架目标的实现。包含过多无谓的元素(类、接口、枚举等),会使框架变得难以理解,尝试将这些对于框架核心目标不太重要的元素转移到类库中,可以使得框架更清晰、目标更集中。

(4)提供一个常用的骨架,但是不要固定骨架的结构,使骨架也是可以组装的。

比如说,如果是针对某种业务处理的框架,那么框架不应该只提供一套不可变更的业务处理流程,而是应该将处理流程“单步”化,使得各个步骤是可以重新组装的,如此一来,应用便可以根据实际情况来改变框架默认的处理流程。这种框架的可定制化能力可以极大地提高框架的可复用性。

(5)不断地重构框架。

   如果说设计和实现一个高质量的框架有什么秘诀?答案只有一个,重构、不断地重构。重构框架的实现代码、甚至重构框架的设计。重构的驱动力源于几个方面,比如对要解决的本质问题有了更清晰准备的认识,在使用框架的时候发现某些组件职责不明确、难以使用,框架的层次结构不够清晰等。

1.4.2 如何称得上一个优秀的框架?

      一个优秀框架的最主要的特点是:简单。这种简单性不是轻而易举就可以获得的,正如优秀的框架不是一蹴而就的,达到这种简单性需要对框架不断地抽丝、不断地提炼和完善。简单的真正原因在于它抓住了要解决的问题的本质。一个优秀的框架通常都具有如下特点:

(1)清晰的、简洁的、一致的。    

      “清晰”指的是框架的结构是清晰的、框架的层次是清晰明朗的、框架中各个类和组件的职责是清晰明确的。

“简洁”指的是框架中没有无关紧要多余的元素,而且各个类和组件的职责目标是非常集中的,这正是“高内聚、低耦合”设计原则的体现。

“一致”通常会带来这样的好处,框架的使用者在熟悉了框架的一部分后,会非常容易地理解框架的另一部分。“一致”通常体现在命名的规则一致、命名的含义一致、组件的装配方式和使用方式一致等。

(2)易于使用的

      只有易于使用的框架才会走得更远。

   正是因为易于使用,框架使用者们才有可能试用这个框架,在试用满意后才有可能决定采用这个框架。一个框架功能即使再强大,如果难以使用,那么框架使用者们很可能根本就不会有试用这个框架的念头。

框架的生命力源于框架一直在不断地完善和发展,如果没有人使用这个框架,这个框架便没有了发展和完善的源动力。正如友好的用户界面是优秀应用程序不可或缺的重要部分,易于使用也是优秀框架的一个重要特性。

(3)高度可扩展的、灵活的

      框架通过高度可扩展性来应对应用程序的万千变化。

      没有任何一个框架可以预料所有应用的需求,万能的框架是不存在的。企图设计、实现一个万能框架的想法是荒诞的。框架必须具有“以不变应万变”的能力,框架可以通过为应用预留恰当的、足够的扩展点来做到这一点。

      框架的灵活体现在框架可以根据不同的应用进行不同的组装和配置,就像框架是专门为当前的应用所订制的一样。

(4)轻量的

      “轻量”,说的通俗点,就是只为自己需要使用的服务付费,而不需要为自己不需要的服务买单。一个重量级的框架有一个很明显的特征就是,如果你需要一套完整的套餐服务,那是没有问题的,框架可以很好的满足你;但是,如果你只需要这份套餐中的一小块点心,对不起,框架仍然会强加一个完整的套餐给你,你必须付一整份套餐的费用。
   优秀的框架应当支持使用者“按需所取”的原则,框架使用者可以随意“点菜”进行组装来满足自己的需求。

(5)弱侵入性的

      所谓“弱侵入性”,采用了框架的应用程序可以尽可能的以普通的方式来编写应用逻辑,而不必为了适应框架不得不使用一些特殊的手法。

   这可能有点难以理解,我们可以举个例子来简单说明。在.NET中,实现AOP(面向方面编程)机制的两种主要方式是使用Proxy和动态代理。使用Proxy实现的AOP框架通常要求那些需要使用AOP截获功能的类必须继承自ContexBoundObject;而采用动态代理实现的AOP框架则没有任何如此侵入性的要求,我们仍可以以最普通的方式来编写应用逻辑类,这类框架会在运行时根据配置动态地生成目标对象的代理对象来实现AOP截获。所以我们可以说,采用动态代理方式实现的AOP框架相比采用Proxy实现的AOP框架,具有更弱的侵入性。

   弱侵入性意味着框架对应用逻辑的干扰更少,由于应用逻辑类都是普通的类,这非常方便应用逻辑在另外一个程序中复用,而另外的程序可能采用了一个完全不同的框架。

我的架构经验小结(一)-- 常用的架构模型 经过这几年的积累,在系统架构方面逐渐积累了一些自己的经验,到今天有必要对这些经验作个小结。在我的架构思维中,主要可以归类为三种架构模型:3/N层架构、“框架+插件”架构、地域分布式架构。

一.三种架构模型

1.3/N层架构

       这是经典的多层架构模型,对于稍微复杂一点或特别复杂的系统,不使用分层架构是很难想象的。下图是经典的3层架构:
 

   如今,凡是个程序员都能侃侃而谈3/N层架构,这确实是解决系统复杂性的一种主流模式,但是,只要采用了3/N层架构是不是就一定能解决系统的复杂性了?不一定,关键在于你在你的系统中如何实作你的3/N层结构。
   在采用了3/N层架构后,我们还是要解决以下非常重要的问题:系统的可扩展性(能从容地应对变化)、系统的可维护性(因为系统并不是使用一次就被抛弃)、方便部署(在需求变化时,方便部署新的业务功能)、还有等等其它系统质量属性。然而系统的可扩展性和可维护性是大多数软件系统必须解决的重中之重,这是由于当前需求复杂多变的软件环境决定的。就像实现功能需求是最基本的,采用3/N层架构也只是万里长征的第一步
   我采用“框架+插件”架构来解决与系统的可扩展性、可维护性和部署相关的难题。 

2. “框架+插件”架构

       经典的3/N层架构是对系统进行“纵向”分层,而“框架+插件”架构对系统进行“横向”分解。3/N层架构和“框架+插件”架构处于一个平等的位置,它们没有任何依赖关系。
 

   但是我经常将它们结合在一起使用,我们的系统在经过3/N层架构的纵向分层和“框架+插件”架构的横向分层后,可以被看作一个“网格”结构,其中的某些网格可以看作是“扩展点”,我们可以在这些扩展点处挂接“插件”。也就是说我们可以在3/N层架构的每一层都挂接适当的插件来完成该层的一些功能。如:
 

   插件最主要的特点是可以实现“热插拔”,也就是说可以在不停止服务的情况下,动态加载/移除/更新插件。所以,采用插件技术可以实现以下功能:
(1)在UI层,我们可以在运行时,替换掉某些用户界面、或加载与新的业务相关的用户界面。在业务逻辑层,我们可以在运行时加载、替换或者删除某项业务服务。在数据访问层,通过使用插件技术我们可以动态地添加对新的数据库类型(如MySQL)的支持。
   插件的“热插拔”功能使得我们的系统有非常好的可扩展性。
(2)如果我们需要升级系统,很多情况下,只要升级我们的插件(比如业务插件)就可以了,我们可以做到在服务运行的时候进行插件的自动升级。
(3)要想将系统做成“框架+插件”的结构,要求我们需要在系统的各层进行“松耦合”设计,只有松耦合的组件才可以被做成“插件”。
   
   在3/N层架构中融合“框架+插件”架构,最难的是对业务逻辑层的松耦合处理,这需要我们细致分析业务需求之间的关联,将耦合度紧密的业务封装在一个组件中,如此得到的相互独立的业务组件便可以有机会成为插件。这个过程可能需要不断的重构、设计的重构。
   我们知道,相比于那些紧密耦合的组件,松耦合的组件更加清晰明确、更加容易维护。另外,在该架构模型中引入了AOP框架进行Aspect焦点的集中编程(比如处理日志记录、权限管理等方面),使得Aspect代码不会掺杂在正常的业务逻辑代码中,使得代码的的清晰性、可维护性进一步增强。
   从上述介绍可以看出,采用3/N层架构和“框架+插件”架构相结合,我们可以增强系统的可扩展性、可维护性和简单部署升级的能力。

3.地域分布式架构

       我无意中发明了“地域分布式架构”这个词,呵呵,不知道意思是否表达得准确。地域分布式架构主要针对类似LBS(基于位置的服务)的需要进行地域分布的应用。      地域分布式架构基于上述的3/N层架构和“框架+插件”架构,它们的关系如下:
 

   现在我对地域分布式架构作个简单的介绍。假设我们需要为全国的各个大城市提供我们的业务功能服务,假设每个城市的客户量很大,而且每个城市访问的数据可能是不一样的(如每个城市的地图数据)、访问的功能也不尽相同(如有的城市提供天气查询服务,而另一些城市不提供)。客户除了跟我们的系统请求服务之外,可能还想通过我们的系统与他的好朋友进行即时通信,而它们好朋友可能与他在同一个城市,也可能位于另外一个城市。
   好了,我们看地域分布式架构是如何解决类似上述的需求的。
   首先,地域分布式架构将用户管理和业务功能服务分开,分别由应用服务器(AS)和功能服务器(FS)负责,然后将它们部署到不同的节点上。AS和FS都采用了3/N层架构和“框架+插件”架构相结合的架构,比如,FS通过功能插件提供功能服务。
   比如,对于武汉这个地域,我们部署了一台AS和一台FS,客户端通过连接到AS进行服务请求。假设有一天,我们在武汉的客户急剧增加,这是压力最大的是FS,因为所有的业务计算都是在FS上完成的。
   这时,地域分布式架构将允许我们在不停止任何服务的情况下,动态的添加FS服务器,新添加的FS服务器会自动注册到AS。
 
   AS可以监控每个FS的负载(如CPU消耗、内存消耗),再有客户端请求到来时,AS会将请求交给负载最低的FS处理,这就实现了FS的负载均衡。
   如果Client A需要与Client B进行即时通信,那么这些通信消息将通过AS中转。
   上面看到的是我们的系统在武汉的部署,而在其他城市部署情况也一样。

   在这种情况下,AS和AS之间是相互独立的,但是经常会发生AS之间需要相互通信的情况,比如:Client A需要与Client E进行即时通信,或者Client A需要请求上海地区独有的服务,等等。
   地域分布式架构使用跨区域的应用服务器(IRAS)来解决AS之间的通信问题。所有AS在启动的时候,将自动向IRAS注册。
   如果,我们想在长沙市也提供我们的服务,那么我们只需要在长沙部署我们的AS和FS,这样就可以融入到上图表示的整个地域分布式架构中。
   关于地域分布式架构,就简单的介绍这么多,更多的内容,读者可以自己去分析挖掘。

二.对架构模型的支持

       如果没有自己的一套工具对上述的架构模型作支持,那么你可能会认为我是在这里胡扯、夸夸其谈。在这几年的开发中,我积累了几套框架和类库用于对上述架构模型提供支持。
(1)    DataRabbit 提供了基于关系和基于ORM(轻量)的数据访问,通过插件的方式来支持新的数据库类型。

(2)    ESFramework 解决了分布式系统(如上述的地域分布式架构)之间的底层通信(直接基于TCP和UDP)。

(3)    AddinsFramework 为“框架+插件”架构模型提供支持。

(4)    ESAspect 通过Proxy方式实现的AOP框架,对方面编程提供支持。

(5)    EsfDRArchitecture 为地域分布式架构模型提供支持。比如支持,FS的动态添加/移除;FS的负载均衡;AS与FS、AS与IRAS之间的通信;跨区域的服务请求等等。   可以参见http://zhuweisky.cnblogs.com/archive/2006/03/15/350408.html了解更多。

    上面介绍的很多内容在我以往的blog文章中都有提及,读者可以针对我早期的blog来进一步了解这些内容。

我的架构经验小结(一)-- 常用的架构模型 一文中简单介绍了我常采用的几种架构模型,本文将稍微深入地介绍其中的一种 -- 三层架构模型。

一.三层架构图
 
二.系统各层次职责
1.UI(User Interface)层的职责是数据的展现和采集,数据采集的结果通常以Entity object提交给BL层处理。与UI平行的Service Interface层用于将业务发布为服务(如WebServices)。

2.BL(Business Logic)层的职责是按预定的业务逻辑处理UI层提交的请求。
(1)Business class 子层负责基本业务功能的实现。
(2)Business Flow 子层负责将Business class子层提供的多个基本业务功能组织成一个完整的业务流。(Transaction通常在Business Flow 子层开启。)

3.DataAccess层的职责是提供全面的数据访问功能支持,并向上层屏蔽所有的SQL语句以及数据库类型差异。
(1)DB Adapter子层负责屏蔽数据库类型的差异。
(2)ORM子层负责提供对象-关系映射的功能。
(3)Relation子层提供ORM无法完成的基于关系(Relation)的数据访问功能。
(4)BEM(Business Entity Manager)子层采用ORM子层和Relation子层来提供业务需要的基础数据访问能力。

三.Aspect
 Aspect贯穿于系统各层,是系统的横切关注点。通常采用AOP技术来对横切关注点进行建模和实现。
1.Securtiy Aspect:用于对整个系统的Security提供支持。
2.ErrorHandling Aspect:整个系统采用一致的错误/异常处理方式。
3.Log Aspect:用于系统异常、日志记录、业务操作记录等。

四.规则
1.系统各层次及层内部子层次之间都不得跨层调用。
2.Entity object 在各个层之间传递数据。
3.需要在UI层绑定到列表的数据采用基于关系的DataSet传递,除此之外,应该使用Entity object传递数据。
4.对于每一个数据库表(Table)都有一个Entity class与之对应,针对每一个Entity class都会有一个BEM Class与之对应。
5.在数量上,BEM Class比Entity class要多,这是因为有些跨数据库或跨表的操作(如复杂的联合查询)也需要由相应的BEM Class来提供支持。
6.对于相对简单的系统,可以考虑将Business class 子层和Business Flow 子层合并为一个。
7.UI层和BL层禁止出现任何SQL语句。

五.错误与异常
        异常可以分为系统异常(如网络突然断开)和业务异常(如用户的输入值超出最大范围),业务异常必须被转化为业务执行的结果。
1.DataAccess层不得向上层隐藏任何异常(该层抛出的异常几乎都是系统异常)。
2.要明确区分业务执行的结果和系统异常。比如验证用户的合法性,如果对应的用户ID不存在,不应该抛出异常,而是返回(或通过out参数)一个表示验证结果的枚举值,这属于业务执行的结果。但是,如果在从数据库中提取用户信息时,数据库连接突然断开,则应该抛出系统异常。
3.在有些情况下,BL层应根据业务的需要捕获某些系统异常,并将其转化为业务执行的结果。比如,某个业务要求试探指定的数据库是否可连接,这时BL就需要将数据库连接失败的系统异常转换为业务执行的结果。
4.UI层除了从调用BL层的API获取的返回值来查看业务的执行结果外,还需要截获所有的系统异常,并将其解释为友好的错误信息呈现给用户。

 六.项目目录结构
1.目录结构:以EAS系统为例。

2.命名空间命名:每个dll的根命名空间即是该dll的名字,如EAS.BL.dll的根命名空间就是EAS.BL。每个根命名空间下面可以根据需求的分类而增加子命名空间,比如,EAS.BL的子空间EAS.BL.Order与EAS.BL.Permission分别处理不同的业务逻辑。

七.发布服务与服务回调
   以EAS系统为例。
1.如果EAS系统提供了WebService(Remoting)服务,则EAS必须提供EAS.Entrance.dll。EAS.Entrance.dll封装了与EAS服务交换信息的通信机制,客户系统只要通过EAS.Entrance.dll就可以非常简便地访问EAS提供的服务。
2.如果EAS需要通过WebService(Remoting)回调客户系统,则必须提供仅仅定义了接口的EAS.CallBack.dll,客户系统将引用该dll,实现其中的接口,并将其发布为服务,供EAS回调。
3.当WebService的参数或返回值需要是复杂类型,则该复杂类型应该在对应的EAS.Entrance.dll或EAS.CallBack.dll中定义,WebService定义的方法中的复杂类型应该使用Xml字符串代替,而Xml字符串和复杂类型对象之间的转换应当在EAS.Entrance.dll或EAS.CallBack.dll中实现。

我的架构经验小结(三)-- 深入三层架构

原文地址:https://www.cnblogs.com/anorthwolf/p/1634243.html