1.1 我是“牛”类,我可以担任多职吗

单一职责原则的英文名称是Single Responsibility Principle,简称是SRP。这个设计原则备受争议,只要你想和别人争执、怄气或者是吵架,这个原则是屡试不爽的。如果你是老大, 看到一个接口或类是这样或那样设计的,你就问一句:“你设计的类符合SRP原则吗?”保准对方立马“萎缩”掉,而且还一脸崇拜地看着你,心想:“老大确实英明”。这个原则存在争议之处在哪里呢?就是对职责的定义,什么是类的职责,以及怎么划分类的职责。我们先举个例子来说明什么是单一职责原则。

只要做过项目,肯定要接触到用户、机构、角色管理这些模块,基本上使用的都是RBAC模型(Role-Based Access Control,基于角色的访问控制,通过分配和取消角色来完成用户权限的授予和取消,使动作主体(用户)与资源的行为(权限)分离),确实是一个很好的解决办法。我们这里要讲的是用户管理、修改用户的信息、增加机构(一个人属于多个机构)、增加角色等,用户有这么多的信息和行为要维护,我们就把这些写到一个接口中, 都是用户管理类嘛,我们先来看它的类图,如图1-1所示。

image-20210926110405399

图1-1 用户信息维护类图

太Easy的类图了,我相信,即使是一个初级的程序员也可以看出这个接口设计得有问题,用户的属性和用户的行为没有分开,这是一个严重的错误!这个接口确实设计得一团糟,应该把用户的信息抽取成一个BO(Business Object,业务对象),把行为抽取成一个Biz(Business Logic,业务逻辑),按照这个思路对类图进行修正,如图1-2所示。

image-20210926110541704

图1-2 职责划分后的类图

重新拆封成两个接口,IUserBO负责用户的属性,简单地说,IUserBO的职责就是收集和反馈用户的属性信息;IUserBiz负责用户的行为,完成用户信息的维护和变更。各位可能要说了,这个与我实际工作中用到的User类还是有差别的呀!别着急,我们先来看一看分拆成两个接口怎么使用。OK,我们现在是面向接口编程,所以产生了这个UserInfo对象之后,当然可以把它当IUserBO接口使用。也可以当IUserBiz接口使用,这要看你在什么地方使用了。 要获得用户信息,就当是IUserBO的实现类;要是希望维护用户的信息,就把它当作IUserBiz 的实现类就成了,如代码清单1-1所示。

代码清单1-1 分清职责后的代码示例

1
2
3
4
5
IUserInfo userInfo = new UserInfo(); 
//我要赋值了,我就认为它是一个纯粹的BO
IUserBO userBO = (IUserBO)userInfo; userBO.setPassword("abc");
//我要执行动作了,我就认为是一个业务逻辑类
IUserBiz userBiz = (IUserBiz)userInfo; userBiz.deleteUser();

确实可以如此,问题也解决了,但是我们来分析一下刚才的动作,为什么要把一个接口拆分成两个呢?其实,在实际的使用中,我们更倾向于使用两个不同的类或接口:一个是IUserBO,一个是IUserBiz,类图如图1-3所示。

image-20210926110726201

图1-3 项目中经常采用的SRP类图

以上我们把一个接口拆分成两个接口的动作,就是依赖了单一职责原则,那什么是单一职责原则呢?单一职责原则的定义是:应该有且仅有一个原因引起类的变更

为什么写这本书

2009年5月份,我在JavaEye上发了一个帖子,其中提到自己已经工作9年了,总觉得这9 年不应该就这么荒废了,应该给自己这9年的工作写一个总结,总结的初稿就是这本书。

在谈为什么写这本书之前,先抖抖自己前9年的职业生涯吧。大学时我是学习机械的, 当时计算机刚刚热起来,自己也喜欢玩一些新奇的东西,记得最清楚的是用VB写了一个自由落体的小程序,模拟小球从桌面掉到地板上,然后计算反弹趋势,很有成就感。于是2000 年毕业时,我削尖了脑袋进入了IT行业,成为了一名真正的IT男,干着起得比鸡早、睡得比狗晚的程序员工作,IT男的辛酸有谁知晓!

坦白地说,我的性格比较沉闷,属于典型的程序员型闷骚,比较适合做技术研究。在这9年里,项目管理做过,系统分析做过,小兵当过,团队领导人也当过,但至今还是一个做技术的。要总结这9年技术生涯,总得写点什么吧,最好是还能对其他人有点儿用的。那写什么好呢?Spring、Struts等工具框架类的书太多太多,很难再写出花样来,经过一番思考, 最后选择了一个每一位技术人员都需要掌握的、但普及程度还不是非常高的、又稍微有点难度的主题——设计模式(Design Pattern,DP)。

中国人有不破不立的思维,远的如秦始皇焚书坑儒、项羽火烧阿房宫,近的如破“四旧”。正是由于有了这样的思想,于是乎能改的就改,不能改的就推翻重写,没有一个持续开发蓝图。为什么要破才能立呢?为什么不能持续地发展?你说这是谁的错呢?是你架构师的错,你不能持续地拥抱变化,这是一个系统最失败的地方。那怎么才能实现拥抱变化的理想呢?设计模式!

设计模式是什么?它是一套理论,由软件界的先辈们(The Gang of Four:包括Erich Gamma、Richard Helm、Ralph Johnson、John Vlissides)总结出的一套可以反复使用的经验,它可以提高代码的可重用性,增强系统的可维护性,以及解决一系列的复杂问题。做软件的人都知道需求是最难把握的,我们可以分析现有的需求,预测可能发生的变更,但是我们不能控制需求的变更。问题来了,既然需求的变更是不可控的,那如何拥抱变化呢?幸运的是,设计模式给了我们指导,专家们首先提出了6大设计原则,但这6大设计原则仅仅是一系列“口号”,真正付诸实施还需要有详尽的指导方法,于是23种设计模式出现了。

设计模式已经诞近20年了,其间出版了很多关于它的经典著作,相信大家都能如数家珍。尽管有这么多书,工作5年了还不知道什么是策略模式、状态模式、责任链模式的程序员大有人在。不信?你找个机会去“虚心”地请教一下你的同事,看看他对设计模式有多少了解。不要告诉我要翻书才明白!设计模式不是工具,它是软件开发的哲学,它能指导你如何去设计一个优秀的架构、编写一段健壮的代码、解决一个复杂的需求。

因为它是软件行业的经验总结,因此它具有更广泛的适应性,不管你使用什么编程语言,不管你遇到什么业务类型,设计模式都可以自由地“侵入”。

因为它不是工具,所以它没有一个可以具体测量的标尺,完全以你自己的理解为准,你认为自己多了解它,你就有可能产生多少的优秀代码和设计。

因为它是软件行业的经验总结,因此它具有更广泛的适应性,不管你使用什么编程语言,不管你遇到什么业务类型,设计模式都可以自由地“侵入”。

9.3.9 观察者模式

观察者模式定义了对象间的一对多依赖关系,让一个或多个观察者对象观察一个主题对象。当主题对象的状态发生变化时,系统能通知所有的依赖于此对象的观察者对象,从而使得观察者对象能够自动更新。
在观察者模式中,被观察的对象常常也被称为目标或主题(Subject),依赖的对象被称为观察者(Observer)。

程序示例

下面以一个简单的示例来示范观察者模式,程序先提供一个观察者接口。

上面Observer接口是一个观察者接口,程序中所有观察者都应该实现该接口。在该接口的update方法中包含了一个Observable类型的参数,该参数代表被观察对象,也就是前面介绍的目标或主题。此处的Observable是一个抽象基类,程序中被观察者应该继承该抽象基类。 Observable类的代码如下。

Observable抽象类是所有被观察者的基类,它主要提供了registObserver()方法用于注册一个新的观察者:并提供了一个removeObserver()方法用于删除一个已注册的观察者;当具体被观察对象的状态发生改变时,具体被观察对象会调用notifyObservers()方法来通知所有观察者。
下面提供一个具体的被观察者类:Product,该产品有两个属性,它继承了Observable抽象基类

正如程序中两行粗体字代码所示,当程序调用Product对象的setName()setPrice()方法来改变Productnameprice成员变量时,这两个方法将自动触发Observable基类的notifyObservers方法。
接着主程序创建一个Product对象(被观察的目标对象),然后向该被观察对象上注册两个观察者对象,当主程序调用Product对象的sete方法来改变该对象的状态时,注册在Product对象上的两个观察者将被触发。主程序代码如下。
运行上面的程序,可以看到当Product的成员变量值发生改变时,注册在该Product上的Name ObserverPriceObserver将被触发。
纵观上面介绍的观察者模式,发现观察者模式通常包含如下4个角色。

9.3.8 桥接模式

桥接模式是一种结构型模式,它主要应对的是:由于实际的需要,某个类具有两个或两个以上的维度变化,如果只是使用继承将无法实现这种需要,或者使得设计变得相当臃肿。
举例来说,假设现在需要为某个餐厅制造菜单,餐厅供应牛肉面、猪肉面……而且顾客可根据自己的口味选择是否添加辣椒。此时就产生了一个问题,如何应对这种变化:是否需要定义辣椒牛肉面、无辣牛肉面、辣椒猪肉面、无辣猪肉面4个子类?如果餐厅还供应羊肉面、韭菜面……·呢?如果添加辣椒时可选择无辣、微辣、中辣、重辣……风味呢?那程序岂非一直忙于定义子类?
为了解决这个问题,可以使用桥接模式,桥接模式的做法是把变化部分抽象出来,使变化部分与主类分离开来,从而将多个维度的变化彻底分离。最后提供一个管理类来组合不同维度上的变化,通过这种组合来满足业务的需要

程序示例

1
2
3
4
5
6
7
8
9
E:\workspace_QingLiangJiJavaEEQiYeYingYongShiZhang5\Bridge
└─src\
├─AbstractNoodle.java
├─BeefNoodle.java
├─Peppery.java
├─PepperySytle.java
├─PlainStyle.java
├─PorkyNoodle.java
└─Test.java

下面以一个简单的示例来示范桥接模式的使用。程序首先提供了一个Peppery接口,该接口代表了面条是否添加辣椒。

1
2
3
4
public interface Peppery
{
String style();
}

接着程序为该接口提供两个实现类,第一个实现类代表辣椒的风格。

1
2
3
4
5
6
7
8
public class PepperySytle implements Peppery
{
// 实现"辣味"风格的方法
public String style()
{
return "辣味很重,很过瘾...";
}
}

下面一个实现类代表不添加辣椒的风格。

1
2
3
4
5
6
7
8
public class PlainStyle implements Peppery
{
// 实现"不辣"风格的方法
public String style()
{
return "味道清淡,很养胃...";
}
}

从上面的程序可以看出,该Peppery接口代表了面条在辣味风格这个维度上的变化,不论面条在该维度上有多少种变化,程序只需要为这几种变化分别提供实现类即可。对于系统而言,辣味风格这个维度上的变化是固定的,是程序必须面对的,程序使用桥接模式将辣味风格这个维度的变化分离出来了,避免与牛肉、猪肉材料风格这个维度的变化耦合在一起。
接着程序提供了一个AbstractNoodle抽象类,该抽象类将会持有一个Peppery属性,该属性代表该面条的辣味风格。程序通过AbstractNoodle组合一个Peppery对象,从而运行了面条在辣味风格这个维度上的变化;而AbstractNoodle本身可以包含很多实现类,不同实现类则代表了面条在材料风格这个维度上的变化。下面是AbstractNoodle类的代码。

1
2
3
4
5
6
7
8
9
10
11
public abstract class AbstractNoodle
{
// 组合一个Peppery变量,用于将该维度的变化独立出来
protected Peppery style;
// 每份Noodle必须组合一个Peppery对象
public AbstractNoodle(Peppery style)
{
this.style = style;
}
public abstract void eat();
}

正如上面的代码所示,上面的AbstractNoodle实例将会与一个Peppery实例组合,不同的AbstractNoodle实例与不同的Peppery实例组合,就可完成辣味风格、材料风格两个维度上变化的组合了。
由此可见, AbstractNoodle抽象类可以看做是一个桥梁,它被用来”桥接”面条的材料风格的改变与辣味风格的改变,使面条的特殊属性得到无绑定的扩充。
接下来为AbstractNoodle提供一个PorkyNoodle子类,该子类代表猪肉面。

1
2
3
4
5
6
7
8
9
10
11
12
public class PorkyNoodle extends AbstractNoodle
{
public PorkyNoodle(Peppery style)
{
super(style);
}
// 实现eat()抽象方法
public void eat()
{
System.out.println("这是一碗稍嫌油腻的猪肉面条。" + super.style.style());
}
}

再提供一个BeefMoodle子类,该子类代表牛肉面。

1
2
3
4
5
6
7
8
9
10
11
12
public class BeefNoodle extends AbstractNoodle
{
public BeefNoodle(Peppery style)
{
super(style);
}
// 实现eat()抽象方法
public void eat()
{
System.out.println("这是一碗美味的牛肉面条。" + super.style.style());
}
}

PorkyNoodle.javaBeefMoodle.java中可以看出:AbstractNoodle的两个具体类实现eat()方法时,既组合了材料风格的变化,也组合了辣味风格的变化,从而可表现出两个维度上的变化。在桥接模式下这些接口和类之间的结构关系如图9.11所示。
这里有一张图片
下面提供一个主程序,可以分别产生辣椒牛肉面、无辣牛肉面、辣椒猪肉面、无辣猪肉面4种风格的面条。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Test
{
public static void main(String[] args)
{
// 下面将得到“辣味”的牛肉面
AbstractNoodle noodle1 = new BeefNoodle(new PepperySytle());
noodle1.eat();
// 下面将得到“不辣”的牛肉面
AbstractNoodle noodle2 = new BeefNoodle(new PlainStyle());
noodle2.eat();
// 下面将得到“辣味”的猪肉面
AbstractNoodle noodle3 = new PorkyNoodle(new PepperySytle());
noodle3.eat();
// 下面将得到“不辣”的猪肉面
AbstractNoodle noodle4 = new PorkyNoodle(new PlainStyle());
noodle4.eat();
}
}

上面程序的main()方法中得到了4种面条,这4种面条就满足了面条在两个维度上的变化,但程序结构又比较简洁。
桥接模式在Java EE架构中有非常广泛的用途,由于Java EE应用需要实现跨数据库的功能,程序为了在不同数据库之间迁移,因此系统需要在持久化技术这个维度上存在改变;除此之外,系统也需要在不同业务逻辑实现之间迁移,因此也需要在逻辑实现这个维度上存在改变,这正好符合桥接模式的使用场景。因此, Java EE应用都会推荐使用业务逻辑组件和DAO组件分离的结构,让DAO组件负责持久化技术这个维度上的改变,让业务逻辑组件负责业务逻辑实现这个维度上的改变。由此可见, Java EE应用中常见的DAO模式正是桥接模式的应用。

可能有读者会感到奇怪,刚才还提到用业务逻辑组件来包装DAO组件是门面模式,怎么现在又说这种方式是桥接模式呢?其实这两种说法都没有问题,称这种方式为门面模式,是从每个业务逻辑组件底层包装了多个DAO组件这个角度来看的,从这个角度来看,业务逻辑组件就是DAO组件的门面;如果从DAO组件的设计初衷来看,设计DAO组件是为了让应用在不同持久化技术之间自由切换,也就是分离系统在持久化技术这个维度上的变化,从这个角度来看, Java EE应用中分离出DAO组件本身就是遵循桥接模式的

不要以为每段代码、每个应用只能使用一种设计模式!实际上,一个设计优良的项目,本身就是设计模式最好的教科书,例如Spring框架,当你深入阅读其源代码时,你会发现这个框架处处充满了设计模式的应用场景。

9.3.7 门面模式

随着系统的不断改进和开发,它们会变得越来越复杂,系统会生成大量的类,这使得程序流程更难被理解。门面模式可为这些类提供一个简化的接口,从而简化访问这些类的复杂性,有时这种简化可能降低访问这些底层类的灵活性,但除了要求特别苛刻的客户端之外,它通常都可以提供所需的全部功能,当然,那些苛刻的用户仍然可以直接访问底层的类和方法。
门面模式( Facade)也被称为正面模式、外观模式,这种模式用于将一组复杂的类包装到一个简单的外部接口中。
现在考虑这样的场景:有一个顾客需要到饭店用餐,这就需要定义一个Customer类,并为该类定义一个haveDinner()方法。考虑该饭店有三个部门:收银部、厨师部和服务生部,用户就餐需要这三个部门协调才能完成。

程序示例

本示例程序先定义一个收银部,用户需要调用该部门的pay()方法来支付用餐费

1
2
3
4
5
6
7
8
9
10
public class PaymentImpl implements Payment
{
// 实现模拟顾客支付费用的方法
public String pay()
{
String food = "快餐";
System.out.println("你已经向收银员支付了费用,您购买的食物是:" + food);
return food;
}
}

程序接下来要定义一个厨师部门,用户需要调用该部门的cook()方法来烹调食物。

1
2
3
4
5
6
7
8
9
public class CookImpl implements Cook
{
// 实现模拟烹调食物的方法
public String cook(String food)
{
System.out.println("厨师正在烹调:" + food);
return food;
}
}

程序还要定义一个服务生部门,用户需要调用该部门的serve()方法来得到食物

1
2
3
4
5
6
7
8
public class WaiterImpl implements Waiter
{
// 模拟服务生上菜的方法
public void serve(String food)
{
System.out.println("服务生已将" + food + "端过来了,请慢用...");
}
}

接下来实现Customer类的haveDinner()方法时,系统将有如下代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Customer
{
public void haveDinner()
{
// // 依次创建三个部门实例
Payment pay = new PaymentImpl();
Cook cook = new CookImpl();
Waiter waiter = new WaiterImpl();
// 依次调用三个部门实例的方法来实现用餐功能
String food = pay.pay();
food = cook.cook(food);
waiter.serve(food);
}
public static void main(String[] args)
{
new Customer().haveDinner();
}
}

正如上面的代码所示, Customer需要依次调用三个部门的方法才可实现这个havaDinner()方法。实际上,如果这个饭店有更多的部门,那么程序就需要调用更多部门的方法来实现这个haveDinner()方法—这就会增加haveDinner()方法的实现难度了
为了解决这个问题,可以为PaymentCookWaiter三个部门提供一个门面(Facade),使用该Facade来包装这些类,对外提供一个简单的访问方法。下面是该Facade类的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Facade
{
// 定义被Facade封装的三个部门
Payment pay;
Cook cook;
Waiter waiter;
// 构造器
public Facade()
{
this.pay = new PaymentImpl();
this.cook = new CookImpl();
this.waiter = new WaiterImpl();
}
public void serveFood()
{
// 依次调用三个部门的方法,封装成一个serveFood()方法
String food = pay.pay();
food = cook.cook(food);
waiter.serve(food);
}
}

Facade代码可以看出,该门面类保证了PaymentCookWaiter三个部门,程序的粗体字代码对外提供了一个简单的serveFood方法,该方法对外提供了一个用餐的方法,而底层则依赖于三个部门的pay()cook()serve()三个方法。
一旦程序提供了这个门面类Facade之后, Cutsomer类实现haveDinner()方法就变得更加简单了下面是通过Facade类实现haveDinney()方法的代码。

1
2
3
4
5
6
public void haveDinner()
{
// 直接依赖于Facade类来实现用餐方法
Facade f = new Facade();
f.serveFood();
}

从上面的程序可以看出,如果不采用门面模式,客户端需要自行决定需要调用哪些类、哪些方法,并需要按合理的顺序来调用它们才可实现所需的功能。不采用门面模式时,程序有如图9.8所示的结构。
这里有一张图片
从图9.8中可以看出,两个客户端需要和底层各对象形成错综复杂的网络调用,无疑增加了客户端编程的复杂度。使用门面模式后的程序结构如图9.9所示。
这里有一张图片
从图9.9可以看出,当程序使用了门面模式之后,客户端代码只需要和门面类进行交互,客户端代码变得极为简单。
阅读到此处相信读者对SpringHibernateTemplate类有点感觉了,当程序使用HibernateTemplatefindo方法时,程序只要此一行代码即可得到查询返回的List。但实际上该find()方法后隐藏了如下代码:

1
2
3
4
5
6
7
Session session=sf.openSession();
Query query=session.createQuery(hql);
for(int i=0 ;i<args.length;i++)
{
query.setParameter(i +"",args[i]);
}
query.list();

因此可以认为Hibernate TemplateSessionFactorySessionQuery等类的门面,当客户端程序需要进行持久化査询时,程序无须调用这些类,而是直接调用HibernateTemplate门面类的方法即可。
除此之外, Java EE应用里使用业务逻辑组件来封装DAO组件也是典型的门面模式,每个业务逻辑组件都是众多DAO组件的门面,系统的控制器类无须直接访问DAO组件,而是由业务逻辑方法来组合多个DAO方法以完成所需功能,而Action只需与业务逻辑组件交互即可。在这种设计方式下, Java EE应用的各组件有如图9.10所示的结构。
这里有一张图片

9.3.6 策略模式

策略模式用于封装系列的算法,这些算法通常被封装在一个被称为Context的类中,客户端程序可以自由选择其中一种算法,或让Context为客户端选择一个最佳的算法—使用策略模式的优势是为了支持算法的自由切换
考虑如下场景:假如正在开发一个网上书店,该书店为了更好地促销,经常需要对图书进行打折促销,程序需要考虑各种打折促销的计算方法。
为了实现书店现在所提供的各种打折需求,程序考虑使用如下方式来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//一段实现`discount()`方法的代码
public double discount(double price)
{
//针对不同情况采用不同的打折算法
switch(getDiscountType())
{
case VIP_DISCOUNT:
return vipDiscount(price);
break;
case OlD_DISCOUNT:
return oldDiscount(price);
break;
case SAlE_DISCOUNT:
return saleDiscount(price);
break;
}
}

上面的粗体字代码会根据打折类型来决定使用不同的打折算法,从而满足该书店促销打折的要求。从功能实现的角度来看,这段代码没有太大的问题。但这段代码有一个明显的不足,程序中各种打折方法都被直接写入了discount(double price)方法中。
如有一天,该书店需要新增一种打折类型呢?那开发人员必须修改至少三处代码:

  • 首先需要增加一个常量,该常量代表新增的打折类型;
  • 其次需要在switch语句中增加一个case语句;
  • 最后开发人员需要实现xxxDiscount()方法,用于实现新增的打折算法

为了改变这种不好的设计,下面将会选择使用策略模式来实现该功能。

程序示例

1
2
3
4
5
6
7
E:\workspace_QingLiangJiJavaEEQiYeYingYongShiZhang5\Strategy
└─src\
├─DiscountContext.java
├─DiscountStrategy.java
├─OldDiscount.java
├─StrategyTest.java
└─VipDiscount.java

下面先提供一个打折算法的接口,该接口里包含一个getDiscount()方法,该接口的代码如下。

1
2
3
4
5
public interface DiscountStrategy
{
// 定义一个用于计算打折价的方法
double getDiscount(double originPrice);
}

下面为该打折接口提供两个策略类,它们分别实现了不同的打折算法。

1
2
3
4
5
6
7
8
9
10
// 实现DiscountStrategy接口,实现对VIP打折的算法
public class VipDiscount implements DiscountStrategy
{
// 重写getDiscount()方法,提供VIP打折算法
public double getDiscount(double originPrice)
{
System.out.println("使用VIP折扣...");
return originPrice * 0.5;
}
}
1
2
3
4
5
6
7
8
9
public class OldDiscount implements DiscountStrategy
{
// 重写getDiscount()方法,提供旧书打折算法
public double getDiscount(double originPrice)
{
System.out.println("老用户折扣...");
return originPrice * 0.7;
}
}

提供了如上两个折扣策略类之后,程序还应该提供一个DiscountContext类,该类用于为客户端代码选择合适折扣策略,当然也允许用户自由选择折扣策略。下面是该DiscountContext类的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class DiscountContext
{
// 组合一个DiscountStrategy对象
private DiscountStrategy strategy;
// 构造器,传入一个DiscountStrategy对象
public DiscountContext(DiscountStrategy strategy)
{
this.strategy = strategy;
}
// 根据实际所使用的DiscountStrategy对象得到折扣价
public double getDiscountPrice(double price)
{
// 如果strategy为null,系统自动选择OldDiscount类
if (strategy == null)
{
strategy = new OldDiscount();
}
return this.strategy.getDiscount(price);
}
// 提供切换算法的方法
public void changeDiscount(DiscountStrategy strategy)
{
this.strategy = strategy;
}
}

从上面的程序的粗体字代码可以看出,该Context类扮演了决策者的角色,它决定调用哪个折扣策略来处理图书打折。当客户端代码没有选择合适的折扣时,该Context会自动选择OldDiscount折扣策略:用户也可根据需要选择合适的折扣策略。
下面的程序示范了使用该Context类来处理图书打折的任何情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class StrategyTest
{
public static void main(String[] args)
{
// 客户端没有选择打折策略类
DiscountContext dc = new DiscountContext(null);
double price = 79;
// 使用默认的打折策略
System.out
.println(price + "元的书默认打折后的价格是:" + dc.getDiscountPrice(price));
// 客户端选择合适的VIP打折策略
dc.changeDiscount(new VipDiscount());
price = 89;
// 使用VIP打折得到打折价格
System.out
.println(price + "元的书对VIP用户的价格是:" + dc.getDiscountPrice(price));
}
}

上面程序的第一行粗体字代码创建了一个Discountcontext对象,客户端并未指定实际所需的打折策略类,故程序将使用默认的打折策略类;程序第二行粗体字代码指定使用VipDiscount策略类,故程序将改为使用VP打折策略。
从上面的介绍中可以看岀,使用策略模式可以让客户端代码在不同的打折策略之间切换,但也有个小小的遗憾:客户端代码需要和不同的策略类耦合。为了弥补这个不足,可以考虑使用配置文件来指定DiscountContext使用哪种打折策略——这就彻底分离客户端代码和具体打折策略类
介绍到这里,相信读者对HibernateDialect会有一点感觉了,这个Dialect类代表各数据库方言的抽象父类,但不同数据库的持久化访问可能存在一些差别,尤其在分页算法上存在较大的差异, Dialect不同子类就代表了一种特定的数据库访问策略。为了让客户端代码与具体的数据库、具体的Dialect实现类分离, Hibernate需要在hibernate.cfg.xml文件中指定应用所使用的Dialect子类。
与此类似的是, SpringResource接口也是一个典型的策略接口,不同的实现类代表了不同的资源访问策略。当然Spring可以非常”智能”地选择合适的Resource实现类,通常来说, Spring可以根据前缀来决定使用合适的Resource实现类;还可根据Application Context的实现类来决定使用合适的Resource实现类。具体请参考本书8.3节的介绍。

12.9 测试Spring5整合MyBatis3

里目,在浏览器中输入如来测试应用:
http://localhost:8080/MyBookApp/loginForm

会看到如图12.1所示的界面,表示Spring mvc访问成功。

image-20210828170611808

输入登录名"Donald Trump",密码"123456",单击”登录”按钮,请求将会被提交到UserController类的login方法进行登录验证。验证成功,请求将会被转发到BookControllermain方法,获取所有书籍信息,之后跳转到main.jsp页面,如图12.2所示

image-20210828172642235

如果登录名和密码输入错误,会重新跳转到登录页面提示用户重新输入,如图12.3所示。

image-20210828172702676

12.8 JSP页面

loginForm.jsp

/MyBookApp/WebContent/WEB-INF/jsp/loginForm.jsp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>登录页面</title>
</head>
<body>
<h3>登录页面</h3>
<form action="login" method="post">
<font color="red">${requestScope.message }</font>
<table>
<tr>
<td><label>登录名: </label></td>
<td><input type="text" id="loginname" name="loginname"></td>
</tr>
<tr>
<td><label>密码: </label></td>
<td><input type="password" id="password" name="password"></td>
</tr>
<tr>
<td><input type="submit" value="登录"></td>
</tr>
</table>
</form>
</body>
</html>

main.jsp

/MyBookApp/WebContent/WEB-INF/jsp/main.jsp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>书籍列表</title>
<style type="text/css">
table {
border-collapse: collapse;
border-spacing: 0;
border-left: 1px solid #888;
border-top: 1px solid #888;
background: #efefef;
}

th, td {
border-right: 1px solid #888;
border-bottom: 1px solid #888;
padding: 5px 15px;
}

th {
font-weight: bold;
background: #ccc;
}
</style>
</head>
<body>
<span>欢迎[${sessionScope.user.username}]访问.</span>
<table>
<tr>
<th>封面</th>
<th>书名</th>
<th>作者</th>
<th>价格</th>
</tr>
<tr>
<c:forEach items="${requestScope.book_list}" var="book">
<td><img alt="此处有一张图片" src="${book.image}" height="60"></td>
<td>${book.name}</td>
<td>${book.author}</td>
<td>${book.price}</td>
</c:forEach>
</tr>
</table>
</body>
</html>

loginForm.jsp是一个登录页面,可以在此输入登录名和密码进行登录。登录完成后main.jsp显示从数据库tb_book表读取出的书籍信息。

12.7 控制层功能实现

FormController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
/**
* 表单控制器
*/
public class FormController {
@RequestMapping(value = "/loginForm")
public String loginForm() {
// 返回表单的地址
return "/loginForm";
}
}

UserController.java

/MyBookApp/src/com/controller/UserController.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.controller;

import javax.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.view.RedirectView;
import com.domain.User;
import com.service.UserService;

@Controller
public class UserController{
/**
* 自动注入UserService
*/
@Autowired
@Qualifier("userService")
private UserService userService;

/**
* 处理/login请求
*/
@RequestMapping("/login")
public ModelAndView login(String loginname,String password,ModelAndView mv,HttpSession session){
System.out.println("com.controller.UserController.login(String, String, ModelAndView, HttpSession)");
// 指定登录业务
User user = userService.login(loginname, password);
if(user != null){
System.out.println("登录成功:" + user);
// 登录成功,将User对象 放置到HttpSession作用域中
session.setAttribute("user", user);
//
mv.setView(new RedirectView("/MyBookApp/main"));
// mv.setViewName("loginForm");
// mv.setViewName("redirect:/main");
}else{
mv.addObject("message", "登录名或密码错误,请重新输入!");
mv.setViewName("loginForm");
}
return mv;
}
}

BookController.java

/MyBookApp/src/com/controller/BookController.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.controller;

import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import com.domain.Book;
import com.service.BookService;

@Controller
public class BookController {
/**
* 自动注入BookService.
*/
@Autowired
@Qualifier("bookService")
private BookService bookService;

@RequestMapping(value = "/main")
public String main(Model model) {
System.out.println("com.controller.BookController.main(Model)");
List<Book> book_list = bookService.getAllBooks();
model.addAttribute("book_list", book_list);
return "main";
}
}

控制层使用了Spring@Autowired注解自动注入服务层的Service对象,@Qualifier注解用于指明需要注入的具体类型,并且使用@Controller注解将类注释成为SpringController