本书先介绍了一些企业应用开发的基础知识,比如分层架构、WEB表现、业务逻辑、数据库映射、并发、会话、分布策略等等。通过使用场景、解决方案、UML等手段详细介绍了设计模式(包括一些常用的设计模式GOF23和本书上新创的设计模式)。了解书中这些模式是干什么的、它们解决什么问题、它们是如何解决问题的。这样,如果你碰到类似的问题,就可以从书中找到相应的模式。可以为你节约成本、缩短项目周期时间、避免风险,以确保项目能够完美的完成。
一、三个基本层次:表现层、领域层、数据源层
层次 |
职责 |
表现层 |
提供服务,显示信息(例如在Windows或HTML页面中,处理用户请求(鼠标点击、键盘敲击等),HTTP请求,命令行调用,批处理API) |
领域层 |
逻辑,系统中真正的核心 |
数据源层 |
与数据库,消息系统、事务管理器及其他软件包通信 |
关于依赖性的普遍性原则:领域层和数据源层绝对不要依赖于表现层。
一旦选择了处理节点,接下来就应该尽可能使所有代码保持在单一进程内完成(可能是在同一个节点上,也可能拷贝在集群中的多个节点上)。除非不得已,否则不要把层次放在多个进程中完成。因为那样做不但损失性能,而且增加复杂性,因为必须增加类似下面的模式,如远程外观和数据传输对象。
复杂性增压器:分布、显式多线程、范型差异、多平台开发以及极限性能要求(如每秒100个事务以上)。
二、领域逻辑
领域逻辑的组织可以分成三种主要模式:事务脚本、领域模型、表模块。
三者之间的区别:
事务脚本是一个过程来控制一系列动作逻辑的执行。
领域模型不再是由一个过程来控制用户某动作的逻辑,而是由每一个对象都承担一部分相关逻辑。这些对象可以看成是领域的不同组成部分。
表模块只有一个公共的合同类实例,而领域模型对数据库中每一个合同都有一个相应的合同类的实例。
三、映射到关系数据库
在使用领域模型的时候,它的读取应该把相关联的对象也一块读出来。例如,读取一个合同,应该把合同涉及到的产品和定购厂商的对象加载到内存中。由时候为了避免这些没有必要的连带读取,我们可以使用【延迟加载】模型。
读取数据的时候,性能问题可能回变得比较突出。这就导致了几条经验法则。
1)、尽可能一次查询多个记录,不要一次查询一个记录,然后进行多次查询。可以一次查询多条相关的记录,例如使用联合查询。或者使用多条SQL语句。
2)避免多次进入数据库的方法是使用连接(Join),这样就可以通过一次返回多个表。可以制作一个入口,让入口完成相关数据的一次性读取。
3)数据库中进行优化。DBA来优化数据库。
映射到关系数据库的时候,一般会遇到三种情况:
1) 自己选择数据库方案。
2) 不得不映射到一个现有数据库方案,这个方案不能改变。
3) 不得不映射到一个现有数据库方案,但这个方案是可以考虑改变的。
最简单的情况是自己选择数据库方案,并且不用迁就领域逻辑的复杂性。当已经存在一个数据库方案的时候,应该逐步建立领域模型并包括数据映射器,把数据保存到现有的数据库中。
四、并发
并发问题:更新丢失和不一致读。
并发问题,人们提出了各种不同的解决方案。对于企业应用来说,有两个非常重要的解决方案:一个是隔离,一个是不变性。
隔离是划分数据,使得每一片数据都可能被一个执行单元访问。比如文件锁。
不变性是识别那些不变的数据,不用总考虑这些数据的并发问题而是广泛地共享它们。
当有一些可变数据无法隔离的时候,会发生什么样的情况呢?总的来说,我们可以使用两种形式的并发控制策略:乐观并发控制和悲观并发控制。
如果把乐观锁看作是关于冲突检测的,那么悲观锁就是关于冲突避免的。
假如Martin和David同时都要编辑Customer文件。如果使用乐观锁策略,他们两个人都能得到一份文件的拷贝,并且可以自由编辑文件。假设David第一个完成了工作,那么他可以毫无困难地更新他的修改。但是,当Martin想要提交他的修改时,并发控制策略就会开始起作用。源代码控制系统会检测到在Martin的修改和David的修改之间存在着冲突,因而拒绝Martin的提交,并由Martin负责指出怎样处理这种情况。如果使用悲观锁策略,只要有人先取出文件,其他人就不能对该文件进行编辑。因此,假如是Martin先取出了文件,那么David就只能在Martin完成任务并提交之后才能对该文件进行操作。
多种技术处理死锁:一种是使用软件来检测死锁的发生。另一种是给每一个锁都加上时间限制,一旦到达时间限制,所加的所就会失效,工作就会丢失。
软件事务经常使用ACID的属性来描述。
原子性(Atomicity):在一个事务里,动作序列的每一个步骤都必须是要么全部成功,要么所有的工作都将回滚。部分完成不是一个事务概念。
一致性(Consistency):在事务开始和完成的时候,系统的资源都必须处于一致的、没有被破坏的状态。
隔离性(Isolation):一个事务,直到它被成功提交之后,它的结果才对其他所有的事务是可见的。
持久性(Durability):一个已提交事务的任何结果都必须是永久性的,即“在任何崩溃的情况的能保存下来”。
大多数企业应用是在数据库方面涉及到事务的,但还有很多情况要进行事务控制,比如说哦消息队列、打印机和ATM等。为了处理最大的吞吐率,现代的事务处理系统被设计成保证事务尽可能短,尽可能不让事务跨越多个请求;尽可能晚打开事务。
五、分布策略
按类模型进行分布的方法不可行的主要原因与计算机的基本特点有关。进程内的过程调用非常快。两个对立进程间的过程调用就慢了一个数量级。在不同机器间运行过程又要慢一两个数量级,取决于网络拓扑。
本地接口最好是细粒度接口。但细粒度不能很好地用在远程调用中。分布对象设计第一定律:不要分布使用对象,大多数情况下是使用集群系统。
六、一些关于具体技术的建议
1、 Java和J2EE
企业级Java Bean(EJB)的价值有多大,这个是JAVA世界中最大的争论。但是要构建良好的J2EE引用,其实并不需要EJB。使用POJO(普通Java对象)和JDBC同样能够完成这一任务。J2EE的设计选择随使用的模式不同而不同,同样,也受到领域逻辑的制约。
2、.NET
.NET、Visual Studio以及微软世界应用中,其中起决定作用的模式是表模块。.NET大力宣传Web Services,但是我不会在一个应用程序内部使用Web Services,而只会像Java中一样,使用它们作为一种允许应用集成的表现层。
2、 存储过程
3、 Web Services
Web Services使得重用成为现实,并最终导致系统集成商的消失。
4、 其他分层
七、模式
领域逻辑模式
1、事务脚本:使用过程来组织业务逻辑,每个过程处理来自表现层的单个请求。
事务脚本置于何处将取决于你如何组织你的软件层次,它可能会位于服务器页面、CGI脚本和分布式会话对象中。我喜欢尽可能的分离事务脚本。至少应当将它们放在不同的子程序中,而更好的方法则是将它们置于与其他处理表现层和数据源层的类相独立的类中。此外,绝不要让事务脚本调用任何表现层逻辑;这样会使我们容易修改代码和测试事务脚本。
可以用两种方法来把事务脚本组织成类。一种是把多个事务脚本放在一个类中,每个类围绕一个主题将相关的事务脚本组织在一起。另一种方法则是每个事务脚本对应一个类,通过命令模式来完成消息传递。
2、领域模型:合并了行为和数据的领域的对象模式。
领域模型按照它的复杂程度分,可以分为:简单领域模型和复杂领域模型。简单的领域模型如【活动记录】,可以应对领域逻辑比较简单的系统。复杂的领域逻辑需要复杂的领域模型。
3、表模块:处理某一数据库表或视图中所有行为的业务逻辑的一个实例。
表模块以一个类对应数据库中的一个表来组织领域逻辑,而且使用单一的类实例来包含将对数据进行的各种操作程序。它与领域逻辑的主要区别在于,如果你有许多订单,领域模型对每一个订单都有一个对象,而表模块则只用一个对象来处理所有的订单。
4、服务层:通过一个服务层来定义应用程序边界,在服务层中建立一组可用的操作集合,并在每个操作内部协调应用程序的响应。
数据源架构模式
1、表数据入口:充当数据库表访问入口的对象。一个实例处理表中所有的行。
表数据入口包含了用于单个表或视图的所有SQL,如选择、插入、更新、删除等。其他代码调用它的方法来实现所有与数据库的交互。
2、行数据入口:充当数据源中单条记录入口的对象,每行一个实例。
3、活动记录:一个对象,它包装数据库表或视图中某一行,封装数据库访问,并在这些数据上增加了领域逻辑。
4、数据映射器:在保持对象和数据库彼此独立的情况下在二者之间移动数据的一个映射器层。
数据映射器是分离内存对象与数据库的一个软件层。其职责是在内存对象与数据库之间的传递数据并保持它们彼此独立。有了数据映射器,内存对象甚至不需知道数据库的存在;它们也不需要SQL接口代码,当然也不需要知道数据库方案。由于数据映射器是映射器的一种形式,因此数据映射器自身根本不为领域层所察觉。
对象—关系行为模式
1、工作单元:维护受业务影响的对象列表,并协调变化的写入和并发问题的解决。
工作单元是一个记录这些变化的对象。只要开始做一些可能会对数据库有影响的操作,就创建一个工作单元去记录这些变化。每当创建、改变或者删除一个对象时,就通知此工作单元。
工作单元的关键是在提交的时候,它决定要做什么。它打开一个事务,做所有的并发检查(使用悲观离线锁和乐观离线锁)并向数据库写入所做的修改。开发人员根本不用显式调用数据库更新方法。这样,他们就不必记录所修改的内容或者不必担心的引用完整性如何影响他们的操作顺序。
2、标识映射:通过在映射中保存每个已经加载的对象,确保每个对象只加载一次。当要访问对象的时候,通过映射来查找他们。
3、延迟加载:一个对象,它虽然不包含所需要的所有数据,但是知道怎么获取这些数据。
实现延迟加载的四种方法:延迟初始化、虚代理、值保持器、重影。
对象—关系结构模式
1、标识域:为了在内存对象和数据库行之间维护标识而在对象内存的一个数据库标识域。
数据库中通过主键来区分数据行,然而,内存对象不需要这样一个键,因此为对象系统能够保证正确的身份确认(在C++中是直接用原始内存位置)。
2、外键映射:把对象间的关联映射到表间的外键引用。
3、关联表映射:把关联保存为一个表,带有指向(有关联所连接的)表的外键。
4、依赖映射:让一个类的部分类执行数据库映射。
5、嵌入值:把一个对象映射成另一个对象表的若干字段。
6、序列化LOB:通过将多个对象序列化到一个大对象(LOB)中保存一个对象图,并存储在一个数据库字段中。
7、单表继承:将类的继承层次表示为一个单表,表中的各列代表不同类中的所有域。
8、类表继承:用每个类对应一个表来表示类的继承层次。
9、具体表继承:用每个具体类对应一个表来表示类的继承层次。
对象—关系元数据映射模式
1、元数据映射:在元数据中保持关系—对象映射的详细信息。
元数据映射使开发者可以以一种简单的表格形式来定义映射,这些映射可由通用代码来处理,从而实现读取、插入和更新数据的细节。使用元数据映射最主要的决策是如何根据运行代码来表示元数据中的信息。有两种主要的途径:代码生成和反射编程。
2、查询对象:描述一次数据库查询的对象。
3、资源库:协调领域和数据映射层,利用类似于集合的接口来访问领域对象。
WEB表现模式
1、模型—视图—控制器(MVC):把用户界面交互分到不同的三种角色中。
2、页面控制器:在WEB站点上为特定页面或者动作处理请求的对象。
3、前端控制器:为一个WEB站点处理所有请求的控制器。
4、模板视图:通过在HTML也面中嵌入标记向HTML发送消息。
5、转换视图:一个视图,它一项一项地处理领域数据,并且把它们转换成HTML。
6、两步视图:用两个步骤来把领域数据转换成HTML。第一步,形成某种逻辑页面;第二步,把这些逻辑页面转换成HTML页面。
7、应用控制器:一个用来处理屏幕导航和应用程序流的集中控制点。
分布模式
1、远程外观:为细粒度对象提供粗粒度的外观来改进网络上的效率。
2、数据传输对象:一个为了减少方法调用次数而在进程间传输数据的对象。
数据序列化
离线并发模式
1、乐观离线锁:通过冲突检测和事务回滚来防止并发业务事务中的冲突。
2、悲观离线锁:每次只允许一个业务事务访问数据以防止并发业务事务中的冲突。
3、粗粒度锁:用一个锁锁住一组相关的对象。
4、隐含锁:允许框架或层超类型代码来获取离线锁。
会话状态模式
包括客户会话状态、服务器会话状态、数据库会话状态。
通常使用数据传输对象来传输数据。数据传输对象可以在网络上序列化,因此,即使是很复杂的数据也可以传输。
客户会话状态:将会话状态保持在客户端。客户会话状态有一定的优点。特别是支持无状态服务器对象,从而提高最大的集群性能和容错恢复。当然,如果客户崩溃了,它所有的会话数据就丢失了,但用户通常认为这合乎情理。对于安全问题,就要对传输的数据进行加密。
客户数据传输:客户也需要存储数据。大客户的应用可以用自己的数据结构来存储。如果使用HTML,情况就复杂一些。一般有三种方法:URL参数、表单的隐藏域和Cookie。
服务器会话状态:将会话状态以序列化形式存放在服务器端。
服务器会话里面最简单的一种方法是把会话数据放在应用服务器的内存中。可以将会话数据以会话标识号作为键标识存放在内存映射表中。只需要客户给出会话标识号,服务器就可以从映射表中取出会话数据。这种方法假设应用服务器有足够的内存处理,而且是只能有一个应用服务器,没有集群——如果这个应用服务器崩溃了,所有的会话数据就丢失得无影无踪。
解决方案是将服务器会话状态序列化存放到数据库中,用一个以会话标识号为键值的表,表中需要有一个序列化LOB存放序列化后的会话状态。还要对作废的会话进行处理,尤其在一个面向顾客的应用中。一种方法是用一个监督进程检查并删除过期的会话,但这样会造成很多与会话表的连接。Kai Yu提供了他使用的一个好方法:将会话表分成12段,每两个小时轮换一次,轮换时先删除时间最旧的段中所有的数据,并把所有新的数据放到该段中。虽然这样会把那些超过24个小时的会话强制删除,但实际上不用去担心这样极少数的情况。
实现举例:
Java实现 最常用的服务器会话状态技术是使用HTTP会话或者使用有状态会话bean。
HTTP会话是让WEB服务器保存会话状态的一种简单方法。
使用有状态会话bean,它需要一个EJB服务器。EJB容器负责持久和钝化处理,因此很容易实现。
。NET实现 服务器会话状态可以很容易地使用内建的会话状态功能实现。默认情况下,.NET将会话状态放在服务器进程内。也可以换成通过状态服务存取,可以在本地也可以在远程。如果使用远程的状态服务,可以在重启WEB服务器后依然使用原来的会话数据。通过在配置文件中指定是使用进程内方式还是使用状态服务,因此不必要修改应用程序。
数据库会话状态是将会话数据作为已提交的数据保存到数据库中。
基本模式
1、 入口:一个封装外部或资源访问的对象。
实际上这是一个十分简单的包装器模式。封装外部资源,创建一个简单的API,并用入口将对该API的调用转移到外部资源上。
2、 映射器:在两个独立的对象之间建立通信的对象。
映射器通常需要在层与层之间进行数据交换。这种数据交换一旦被激活,工作方式就是显而易见的。映射器的使用难点在于如何激活,因此你无法在被映射子系统中的任何一方直接调用它。有时可以使用一个第三方的子系统来完成映射并调用映射器。另一个可选的方案是让映射器成为某个子系统的观察者。通过监视子系统中发生的事件,映射器就可以调用了。
3、层超类型:某一类型充当一层中所有类型的超类型。
某一层中所有的对象都具有某些方法,但你并不希望这些方法在系统内被多次复制而产生冗余代码。此时你可以将这些行为移到一个通用的层超类型中。
4、分离接口
在一个包中定义接口,而在另一个与这个包分离的包中实现这个接口。
5、 注册表
一个众所周知的对象,其他对象可以通过该对象找到公共的对象和服务。
6、 值对象:一个如货币或日期范围这样的小而简单的对象,判断时并不根据标识ID。
在由多种类型对象组成的对象中,区分引用对象和值对象是有用的。二者之中值对象通常规模小一些;它类似于许多非纯粹面向对象程序设计语言中的原始类型。通常来说,我们倾向于认为值对象是小对象,如货币对象或日期对象;而引用对象是大的对象,如订单或顾客。
7、 货币:表示一个货币值
面向对象的程序设计使你可以通过创建一个处理货币的货币类来解决货币与金额(小数点)这类问题。令人感到奇怪的是,居然没有任何主流的类库提供这个类。
创建一个货币类,在其中保存金额和币种,可以以整数或定点小数的方式来保存金额。
8、 特殊情况:针对特殊情况提供特殊行为的子类。
9、 插件:在配置时而非编译时的连接类。
10、服务桩:在测试时移除对有问题服务的依赖。
企业级系统通常要依赖于第三方服务(例如信用评分、税率查询和价格引擎)的访问。有这类系统开发经验的开发人员都知道:如果依赖于完全不受自己控制的外部资源,通常会使软件项目受挫。第三方服务的特性是不可预知的,而且这些服务通常是远程的,因而软件性能和可靠性也会受到损害。
这种依赖关系可能会导致测试无法进行,从而使得开发周期成倍增长。在测试时,用运行在本地的、快速的、位于内存中的服务桩来代替服务将会改善你的开发经验。
当你发现对某一特定服务的依赖妨碍你的开发和测试时,就应当使用服务桩。许多XP的实践者使用术语“模拟对象”来代替服务桩。
11、记录集:表格数据在内存中的表现方式。
记录集模式的思想是通过一个内存中数据结构来提供解决这一问题的完整方案,该数据结构看起来与SQL查询的结果极为类似,但是它可以由系统中其他部件来生成和操控。
记录集通常无需你亲自创建,而是由所有软件平台的销售商提供。ADO.NET中的data set和JDBC2.0中的row set都是记录集的例子。
目前有一种内存数据库sqlite,读取访问速度很快,有兴趣的可以研究,是开源