1分层
1.1 三个基本层次
三层架构:表现层:表现逻辑处理用户与软件间的交互。主要职责是:
ü 向用户显示信息
ü 把从用户那里获得的信息解释成领域层或数据源层上的各种动作。
数据源层:数据源逻辑主要关注与其他系统的交互,这些系统将代表应邀完成相关的任务。主要的数据源逻辑就是数据库,它的主要职责是存储持久数据。
领域层:领域逻辑(业务逻辑),它就是应用必须做的所有领域相关的工作:
ü 根据输入数据或已有数据进行计算
ü 对从表现层输入的数据进行验证
ü 根据从表现层接收的命令来确定应该调度哪些数据源逻辑。
三层的关系:领域层是核心!表现层是系统对外提供服务的外部接口;数据源层是系统使用外部服务的接口。
分层结构依赖原则:领域层和数据源层绝对不要依赖于表现层。也就是说,在领域层和数据源层的代码中,不要出现调用表现层代码的情况。
区分逻辑层次的是否划分正确的简单测试原则:层次替换修改原则(注:我自己给起的名字)——假想向系统中增加一个完全不同的新层,例如为Web应用增加一个命令行界面层。如果这个过程中,发现需要重复实现某些功能,则说明可能有一些本应该在领域层实现的逻辑,现在在表现层实现了。类似的,你可以假想一下,将后台数据库更换成XML文件格式,看看情况又如何?
2 组织领域逻辑
事务脚本模式与领域模型模式。
范例:
核心问题是:对于同一给定的合同,不同种类的产品有不同的收入确认算法。计算收入确认的方法中必须先确定给定合同中产品的种类,然后应用正确的算法,之后再创建相应的收入确认对象U1(RevenueRecognition)来保存计算结果。
使用事务脚本计算收入确认
a Recognition Service:确认服务
calculateRecognitions(contractID):根据合同(contract)ID计算确认
findContract(contractID):向数据入口发送“查找合同”消息,根据合同ID查找合同,返回合同结果集
insert revenue recognitions:插入收入确认。
使用领域模型计算收入确认
利用了典型的策略模式(GoF),将对于不同产品的确认策略分离出来,以实现对于不同产品采取不同的计算确认策略。
class Contract{
private Product product;
public void setProduct(Product product){
this. product= product;
}
public Product getProduct(){
return this. product;
}
public void calculateRecogntions(){
getProduct().calculateRecogntions(this);
}
}
class Product{
private RecogntionStrategy recogntionStrategy;
public void set RecogntionStrategy (RecogntionStrategy recogntionStrategy){
this. recogntionStrategy = recogntionStrategy;
}
public RecogntionStrategy getRecogntionStrategy (){
return this. recogntionStrategy;
}
public void calculateRecogntions(Contract contract){
getRecogntionStrategy ().calculateRecogntions (contract);
}
}
2.1 服务层
概念:处理领域逻辑的常见方法是将领域层再细分成两层。服务层独立出来,置于底层的领域模型或表模块之上。
职责:表现逻辑与领域层的交互完全通过服务层,就好像应用程序的API一样。服务层是放置事务控制和安全等功能的好场所。
组成:最小化情况下,服务层只是一个外观,所有实际的行为都在下层的对象中,服务层所做的只是将上层调用传递到更低层。此时,服务层提供一个更易于使用的API,因为它的方法通常根据用例来组织。该方式是Fowler最为推崇的方式。另外还有将服务层实现为一个事务脚本或使用控制器-实体模式。
3 映射到关系数据库
3.1 架构模式
讲了几种入口(Gateway)模式后,Fowler指出:一种更好的方法是把领域模型和数据库完全独立,可以让间接层完成领域对象和数据库表之间的映射。这个数据映射器处理数据库和领域模型之间所有的存取操作,并且允许双方都能独立变化。
注解:目前采用Hiberante之类的O/RM框架就是实现该模式。
另外,Fowler不推荐把入口(Gateway)用作领域模型的首选持久化机制。
即使使用数据映射器作为首选持久化机制,还是可以使用数据入口来封装被视为外部接口的表或者服务。
注解:目前采用DAO模式是更好的表述。
1.1.1 关系的映射
对象和关系处理连接的方式不同,会造成两大问题:
1、表现方法不同:
a、对象是通过在运行时(内存管理环境或内存地址)中保存引用的方式来处理连接;
public class A{
private B b;
}
A和B的连接,通过在A的实例a中保存的B的实例b建立。
b、关系数据库则通过创建到另外一个表的键值来处理连接。
表A中保存表B的ID,作为A的外键,来建立A和B的连接。
2、对象可以很容易的通过集合来表示多个引用。
规范化则要求所有的关系连接都必须是单值。
解决办法:通过对象中的一个标识域来保持每个对象的关系特性,并且通过查找这些值来保持对象引用和关系键之间的互相映射。
1.1.2 继承
三种模式:单表继承,具体表继承,类表继承。
优点 | 缺点 | |
单表继承 (为继承体系中所有类建立一个统一的表) | 把所有内容放到一起 a、这样修改起来容易 b、并且避免了join操作 | 1、浪费空间,因为每一行都必须为每种可能的子类保留一些类,导致很多空类。 2、它的大小将成为访问的瓶颈。 |
具体表继承 (为继承体系中每个具体类建立一张表) | 避免join操作,效率高 | 1、改变困难 a、对超类的任何改变都不得不改变所有表(还有映射代码) b、改变层次结构自身会带来更大的改变 2、缺乏超类表 a、使主键管理十分可怕 b、引用完整性也有问题 |
类表继承 (为继承体系中每个类创建一张表) | 简单 | 使用多个join操作载入一个对象,会损失性能 |
Fowler倾向于使用单表继承:因为易于实现和重构。
1.1.3 建立映射
Fowler建议:首先使用领域建模,然后在不超过六个月的迭代中建立数据库模型。
如果领域设计和数据库表同构有意义,则考虑使用活动记录代替。(原文:If a database design isomorphic to the Domain Model (116) makes sense, you might consider an Active Record (160) instead.)
注解:isomorphic,同构指的应该是结构相同,比如Employee类和Employee表拥有相同的属性/字段。
2 Web表现层
MVC中M是指领域模型。
注解:当然,看Larman的UML和模式应用中介绍是,原来模型是指数据模型,后来成立领域模型。
2.1 输入控制器模式
MVC中的控制器:
一种理解为“输入控制器”,主要作用为:
1、从请求消息流中读取数据
2、将业务逻辑传递给一个合适的领域对象
3、将返回的数据放到HTTP会话(Session)对象中,与视图共享。
第二种理解为“应用控制器”,主要作用为:
处理应用程序流,决定视图应该按什么顺序出现。
Fowler对输入控制器的结论,更为准确地说法是为每个动作准备一个页面控制器(Page Controller)。
注解:应用控制器负责处理页面流程。Struts的Aciton是页面控制器(除了包含输入控制器功能外,还负责创建视图对象FormBean),ActionServlet是前端控制器,负责解释URL来判断调用哪个页面控制器。配置文件起到应用控制器的作用。
2.2 视图模式
A two-stage view (Figure 4.3) breaks this process into two stages, producing a logical screen from the domain data and then rendering it in HTML. There's one first-stage view for each screen but only one second-stage view for the whole application.
注解:两步视图中的第一步逻辑屏幕应该是指View Object或者是Struts中FormBean这种东西,第二步应该是使用相应的自动化程序将其变成HTML或者是Swing客户端,在Sturts中应该就是Struts自己的类,它可以将数据渲染(rendering)成HTML页面。
3 并发
并发造成的问题:
1、更新丢失。A读取了一张表中的数据,然后对其修改。这时B也读取了该表中的数据,修改后将其保存。这时A才修改完,将其保存。这样A就覆盖了B的修改。
2、不一致读。
并发涉及到两个层面:
1、客户端访问应用服务器,使用线程或进程,一个进程中可包含多个线程,线程使用共享内存,这样就会造成并发问题。
2、访问数据库,这会涉及到事务,并发访问数据库。
系统事务:发生在应用程序到数据库之间
业务事务:发生在用户到应用程序之间
解决并发的两种方式:
1、隔离
2、不变性
预防死锁!
3.1 事务
在企业应用中处理并发最主要的工具是事务。
事务:1、事务是一个有边界的工作序列,开始和结束都有明确定义
2、所有相关资源在事务开始和结束时都保持一致。
3、每个事务都必须保证要么全部完成,要么什么都不做。
ACID:
原子性:
一致性:在事务开始和完成的时候,系统地资源都必须处于一致的,没有被破坏的状态。比如一个人买啤酒,虽然少了钱,但是得到了等价值的啤酒,其拥有的总资产不变。
隔离性:一个事务,直到它被成功提交之后,它的结果对于任何其他的事务才是可见的。
持久性:一个已提交的事务的任何结果都必须是永久性的,即“在任何系统崩溃的情况下都能保存下来”。
事务资源:在技术讨论时,使用事务资源表示进行事务处理的任何事物——即使用事务来控制并发过程。
长事务:跨越多个请求的事务叫长事务
几种不一致读的情况:
脏读:脏读是指事务A访问并修改了一个数据,但还没有提交回表中,这时事务B访问并使用了该数据,则事务B读到的可能就是一个“脏”的数据。依据脏数据所作的操作就很可能是错误的。
不可重复读:不可重复读是指事务A对某数据进行一次读取后,数据被事务B访问并修改。当事务A再一次访问数据时,会发现跟前一次读到的数据不一致。
幻读:幻读是指当事务不是独立执行时发生的一种现象。例如事务A对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。同时,事务B也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,操作事务A的用户就会发现表中还有未修改的数据行,就好象发生了幻觉一样。
不可重复读的重点是修改:
同样的条件,你读取过的数据,再次读取出来发现值不一样了
幻读的重点在于新增或者删除
同样的条件,第1次和第2次读出来的记录数不一样
几个隔离级别:
读未提交:意思就是说,允许用户读取其他用户未提交的事务中修改的数据。比如B修改了数据D(比如从2改成了1,此时D被修改了,成了脏数据),但是还没有提交,在这种隔离级别下,数据库允许此时A读取数据D(即允许脏读), 读完显示给A后,B做修改的事务回滚了,也就是修改实际上没有完成,那么A读到的数据D(值为1)就不是真实的情况(应该还是为2)。这时A对数据D进行 操作,就可能出现错误,比如将D(商品价格)*0.8(折扣)然后保存到另一张表(比如是用户订单)中,那么就保存了一个错误的结果。
读已提交:意思就是说,数据库允许用户读取其他用户已经提交的事务中修改的数据。允许不可重复读这种不一致的情况发生。这样允许事务A未提交时,事务B对A已读到的数据进行修改。但是事务A读到的数据一定是已经提交的数据(因此不可能出现脏读),而在读未提交隔离级别下,可以读未提交的数据。
可重复读:锁定查询中使用的所有数据以防止修改(避免脏读和不可重复读),但是不防止插入数据,这时重新读取时,就会读到之前没读到的数据,出现了幻读。此隔离级别允许出现幻读。Fowler:这种幻读出现在你向一个集合添加一部分元素而读的人只能读到其中一部分的时候。
可序列化:实际上是独占方式读取数据。
隔离级别与所允许的不一致读:
隔离级别 | 脏读 | 不可重复读 | 幻读 |
读未提交 | 是 | 是 | 是 |
读已提交 | 否 | 是 | 是 |
可重复读 | 否 | 否 | 是 |
可序列化 | 否 | 否 | 否 |
3.2 离线并发控制的模式
处理业务事务的模式
乐观离线锁
悲观离线锁
3.3 应用服务器并发
应用服务器自身的进程并发,这里不涉及事务。
最简单的处理办法是使用每会话一进程,就是每个会话都在自己的进程中运行。问题是:大量资源的耗费,因为进程是昂贵的。可以通过进程池来提高利用率,这时每个进程在一个时刻只处理单个请求,但可以在时间序列上处理来自不同会话的多个请求。问题是:必须保证每个请求结束时都释放其占用的所有资源。
每会话一线程,即在一个进程中运行多个线程来进一步提高吞吐率。每次请求由进程中的某个线程处理。
4 会话状态
Fowler建议:
1、倾向于使用服务器会话状态模式,特别是在以下情况下:
a、备忘文件被远程存储以备系统在服务器崩溃之后仍能恢复
2、使用会话状态模式来存放会话标识号和数据量较小的会话。
3、我个人并不喜欢数据库会话状态模式,建议只在以下三种情况使用
a、需要故障恢复和集群时
b、无法存储远程备忘文件时
c、不关心会话间数据隔离时
5 分布策略
分布式对象第一定律:不要分布使用对象。(大多数情况下使用集群)
6 通盘考虑
Core J2EE分层模型
Core J2EE | Fowler |
客户层层 | 运行于客户端的表现层(如Java客户端) |
表现层 | 运行于服务器端的表现层(如HTTP处理程序,服务器页面(Java Server Page)) |
业务层 | 领域层 |
集成层 | 数据源层) |
资源层 | 需要与数据源层通信的外部资源 |
Marinescu分层模型
Marinescu | Fowler |
表现层 | 表现层 |
应用层 | 表现层(应用控制器) |
服务层 | 领域层(服务层) |
领域层 | 领域层(领域模型) |
持久层 | 数据源层 |
服务层将工作流逻辑从纯粹领域逻辑中剥离出来。服务层所包含的逻辑一般都特定于某个用例,并与其他一些基础设施相互通信。(Fowler个人认为这种分离偶尔有用,但一般情况下没用)
Mircosoft DNA分层模型
Mircosoft DNA分层模型 | Fowler |
表现层 | 表现层 |
业务层 | 领域层 |
数据访问层 | 数据源层 |
DNA中的记录集实际上充当一种各层的数据传输对象(DTO)。业务层能够根据自己的方式修改记录集,甚至可以自己创建一个新的记录集(当然,这是极少数情况)。好处是允许表现层使用一些数据敏感的GUI控件,这些控件甚至可以操作由业务层修改了的数据。领域层通常组织成表模块形式,而数据层使用表数据入口。
第二部分 模式
7 领域逻辑模式
7.1 领域模型模式
在应用程序中使用领域模型需要建立一个完整的对象组成的层。有的对象模拟业务活动中数据,有的对象捕捉业务使用的规则。数据和处理一般整合在一起,从而使得数据和数据之上的操作紧密聚合。
使用领域逻辑的一个常见问题是领域对象过于臃肿。这使人们开始考虑哪些职责是通用的,应当放到领域对象(例如订单)中,哪些职责是特殊的,应当放到针对具体使用的类中,例如事务脚本或表现层本身。但是Fowler建议,不要这样做。
使用领域模型时,首选的数据库交互方式为数据映射器。
使用领域模型时,你可能会考虑设立一个服务层,以便给领域模型一个更清晰的API。
7.2 服务层
服务层定义了应用的边界[Cockburn PloP]和从接口客户层角度所能看到的可用操作集。它封装了应有的业务逻辑、事务控制及其操作实现中的响应协调。
设计动机:
通过职责的细分来避免冗余代码和提高可重用性。
业务逻辑:
1、领域逻辑:只与问题领域有关,如计算合同收入确认的策略等
2、应用逻辑:与应用的职责有关[Cockburn UC],如关于收入确认计算的相关事宜,通知合同管理者,集成系统等。应用逻辑有时被称为“工作流逻辑”,但不同的人对“工作流”有不同的解释。
注解:应用逻辑层应该负责的是跟具体应用有关的责任,那具体应用是指什么呢?就是指用例中描述的业务操作功能。除了完成业务操作(如计算工资)之外,还可能包括将工资单通过发送Email的方式寄给职员,或者通知财务人员进行帐务处理。
实现方法:
1、领域外观:服务层以领域层模型之上的瘦外观稽核方式实现。负责实现外观的类不包含任何业务逻辑,所有业务逻辑均由领域模型实现。
2、操作脚本:服务层由一组相对复杂的类组成,这些类直接实现应用逻辑,但将领域逻辑委托给封装好的领域对象类。服务层客户所能使用的操作(比如Java类中的方法)以脚本方式实现,数个脚本组成一个类,一个类定义与某一个主题相关的逻辑。每一个类组成一个应用程序“服务”,通常服务类型的名字为“XXXService”。服务层由这些应用程序服务类组成,在它们之上应当扩展出一个抽象了职责和公共行为的层超类型。
领域对象类应当只对问题域中与应用有关的部分建模,而不是应用的全部用例职责。
将应用逻辑封装在较高的层中可以使得变更该层的实现更为容易——你可能用一个工作流引擎就做到这一点。
注解:我的理解是可以直接将服务配置为工作流引擎的节点。
8 数据源架构模式
8.1 活动记录
一个对象,它包含数据库表或者视图中某一行,封装数据库访问,并在这些数据上增加领域逻辑。