首先明确学习设计模式的目的,面向对象设计和实现要做的事情,就是把合适的代码放到合适的类中。至于到底选择哪种划分方法,判定的标准是让代码尽量地满足各种设计原则,尽量地做到代码可复用、易读、易扩展、易维护。
上段说的合适的代码需要使用到类的6种关系以及面向对象语言的多态特性,要熟练掌握类的6种关系和实现多态特性3种方式。
目录:
- 理解类的6种关系,学会画UML类图,以及代码实现。
- 掌握多态的3种实现方式,类的方法重写、抽象类和抽象方法、接口
- 掌握7个面向对象设计原则
- 掌握23种设计模式的定义,每个模式举一个例子,画出UML图,然后写出实现代码。
- 用足球运动这个场景串联记忆23种设计模式
笔记内容来自下面文章:
- 第1章 统一建模语言基础知识 --本文类的6种关系引用了它
- 设计模式Java版 --本文设计原则和设计模式引用了它
- 设计模式之美 --包括大量设计实战经验,熟能生巧就靠它了
一、理解类的6种关系,学会画UML类图,以及代码实现。
1、关联关系(Association),用于表示一类对象与另一类对象之间有关系。在UML类图中,用实线连接有关联的类。代码实现关联关系时,通常将一个类的对象作为另一个类的属性。类有以下三种关联关系:
1)双向关联
public class Customer {
private Product[] products;
......
}
public class Product {
private Customer customer;
......
}2)单项关联,用带箭头的实线表示。
public class Customer {
private Address address;
......
}
public class Address {
......
}3)自关联,一些类的属性对象类型为该类本身。
public class Node {
private Node subNode;
......
}2、聚合关系(Aggregation),表示整体和成员之间的关系。成员对象是整体的一部分,但是成员对象可以脱离整体独立存在。在UML中,用带空心菱形的直线表示。
public class Car {
private Engine engine;
public Car(Engine engine) {
this.engine = engine;
}
public void setEngine(Engine engine) {
this.engine = engine;
}
......
}
public class Engine {
......
}3、组合关系(Composition),表示整体和部分之间的关系。部分和整体有统一的生存期,整体类控制部分类的生命周期。在UML中,用带实心菱形的直线表示。
public class Head {
private Mouth mouth;
public Head() {
mouth = new Mouth();
}
......
}
public class Mouth {
......
}4、依赖关系(Dependency),是一种使用关系。在需要表示一个事物使用另一个事物时使用依赖关系。在UML中,依赖关系使用带箭头的虚线表示。编程语言实现时,依赖关系体现在某个类的方法使用另一个类的对象作为参数。
public class Driver {
public void drive(Car car) {
car.move();
}
......
}
public class Car {
public void move() {
......
}
}5、泛化关系(Generalization),也就是继承关系,也称为“is-a-kind-of"关系。在UML中用,用带空心三角形的直线来表示。
public class Person {
protected String name;
protected int age;
public void move() {
......
}
public void say() {
......
}
}
public class Student extends Person {
private String studentNo;
public void study() {
......
}
}6、接口与实现关系(Realization),在这种关系中,类实现了接口。在UML中,用带空心三角形的虚线来表示。
public interface Vehicle {
public void move();
}
public class Ship implements Vehicle {
public void move() {
......
}
}
public class Car implements Vehicle {
public void move() {
......
}
}二、掌握多态的3种实现方式
设计模式是利用语言的多态特性实现的,多态是指同一个行为具有多个不同表现形式或形态的能力。
多态分为编译时多态和运行时多态。其中编译时多态是静态的,主要是指方法的重载,它是根据参数列表的不同来区分不同的方法。通过编译之后会变成两个不同的方法,在运行时谈不上多态。而运行时多态是动态的,它是通过动态绑定来实现的,也就是大家通常所说的多态性。
实现多态有 3 个必要条件:继承、重写和向上转型。只有满足这 3 个条件,开发人员才能够在同一个继承结构中使用统一的逻辑实现代码处理不同的对象,从而执行不同的行为。
- 继承:在多态中必须存在有继承关系的子类和父类。
- 重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。
- 向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才既能调用父类的方法,又能调用子类的方法。
当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;如果有,再去调用子类的同名方法。
1、用类的方法重写实现多态
public class Animal {
public void move() {
System.out.println("动物可以移动");
}
}
public class Dog extends Animal {
public void move() {
System.out.println("狗可以跑和走");
}
}
public class Test {
public static void main(String args[]) {
Animal a = new Animal(); // Animal 对象
Animal b = new Dog(); // Dog 对象
a.move();// 执行 Animal 类的方法,输出:动物可以移动
b.move();//执行 Dog 类的方法,输出:狗可以跑和走
}
}在上面的例子中可以看到,尽管 b 属于 Animal 类型,但是它运行的是 Dog 类的 move方法。
这是由于在编译阶段,只是检查参数的引用类型。
然而在运行时,Java 虚拟机(JVM)指定对象的类型并且运行该对象的方法。
因此在上面的例子中,之所以能编译成功,是因为 Animal 类中存在 move 方法,然而运行时,运行的是特定对象的方法。
2、用抽象类和抽象方法实现多态
如果要表示一种 is-a 的关系,并且是为了解决代码复用问题,就用抽象类。
public abstract class Animal {
public void move() {
System.out.println("动物可以移动");
}
}
public class Dog extends Animal {
public void move() {
System.out.println("狗可以跑和走");
}
}
public class Test {
public static void main(String args[]) {
Animal b = new Dog(); // Dog 对象
b.move();//执行 Dog 类的方法,输出:狗可以跑和走
}
}3、用接口实现多态
如果要表示一种 has-a 关系,并且是为了解决抽象而非代码复用问题,就用接口。
public interface Vehicle {
public void move();
}
public class Ship implements Vehicle {
public void move() {
......
}
}
public class Car implements Vehicle {
public void move() {
......
}
}
public class Test {
public static void main(String args[]) {
Vehicle a = new Ship();
Vehicle b = new Car();
a.move();// 执行 Ship 类的方法
b.move();//执行 Car 类的方法
}
}三、掌握7个面向对象设计原则
1、单一职责原则(Single responsibility principle, SRP)
一个类只负责一个功能领域中的相应职责,或者可以定义为:就一个类而言,应该只有一个引起它变化的原因。单一职责原则是实现高内聚、低耦合的指导方针。
2、开闭原则(Open-Closed Principle, OCP)
一个软件实体应当对扩展开放,对修改关闭。即软件实体应尽量在不修改原有代码的情况下进行扩展。
为了满足开闭原则,需要对系统进行抽象化设计,抽象化是开闭原则的关键。
面向对象编程语言中都提供了接口、抽象类等机制,可以通过它们定义系统的抽象层,再通过具体类来进行扩展。
3、里氏代换原则(Liskov Substitution Principle, LSP)
所有引用基类 (父类)的地方必须能透明地使用其子类的对象。
里氏代换原则是实现开闭原则的重要方式之一,由于使用基类对象的地方都可以使用子类对 象,因此在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。
4、接口隔离原则(Interface Segregation Principle, ISP)
使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口。
在使用接口隔离原则时,我们需要注意控制接口的粒度,接口不能太小,如果太小会导致系 统中接口泛滥,不利于维护;接口也不能太大,太大的接口将违背接口隔离原则,灵活性较差,使用起来很不方便。一般而言,接口中仅包含为某一类用户定制的方法即可,不应该强 迫客户依赖于那些它们不用的方法。
5、依赖倒转原则(Dependency Inversion Principle, DIP)
抽象不应该依赖于细节,细节应当依赖于抽象。换言之,要针对接口编程,而不是针对实现编程。
在实现依赖倒转原则时,我们需要针对抽象层编程,而将具体类的对象通过依赖注入 (DependencyInjection, DI)的方式注入到其他对象中,依赖注入是指当一个对象要与其他对象发生依赖关系时,通过抽象来注入所依赖的对象。常用的注入方式有三种,分别是:构造注入,设值注入(Setter注入)和接口注入。构造注入是指通过构造函数来传入具体类的对象,设值注入是指通过Setter方法来传入具体类的对象,而接口注入是指通过在接口中声明的业务方法来传入具体类的对象。这些方法在定义时使用的是抽象类型,在运行时再传入具体类型的对象,由子类对象来覆盖父类对象。
在重构过程中,经常会使用到开闭原则、里氏代换原则和依赖倒转原则。开闭原则是目标,里氏代换原则是基础,依赖倒转原则是手段,它们相辅相成,相互补充,目标一致,只是分析问题时所站角度不同而已。
6、迪米特法则,又称最少知道原则(Law of Demeter, LoD)
一个软件实体应当尽可能少地与其他实体发生相互作用。
迪米特法则还有几种定义形式,包括:不要和“陌生人”说话、只与你的直接朋友通信等,在迪米特法则中,对于一个对象,其朋友包括以下几类:
(1) 当前对象本身(this);
(2) 以参数形式传入到当前对象方法中的对象;
(3) 当前对象的成员对象;
(4) 如果当前对象的成员对象是一个集合,那么集合中的元素也都是朋友;
(5) 当前对象所创建的对象。
7、合成复用原则(Composite Reuse Principle, CRP)
合成复用原则又称为组合/聚合复用原则(Composition/Aggregate Reuse Principle, CARP),是指尽量使用对象组合,而不是继承来达到复用的目的。
一般而言,如果两个类之间是“Has-A”的关系应使用组合或聚合,如果是“Is-A”关系可使用继 承。
四、掌握23种设计模式
学习设计模式要为每个设计模式举一个例子,画出UML图,然后写出实现代码。设计模式是熟能生巧的活,需要多看多练多想。
0、简单工厂模式(Simple Factory Pattern)
简单工厂模式是工厂方法模式的“小弟”,它不属于 GoF 23种设计模式,但在软件开发中应用较为频繁。此外,工厂方法模式还有一位“大哥”抽象工厂模式。
定义:设计一个工厂类,它可以根据参数的不同返回不同类的实例,被创建的实例通常都具有共同的父类。因为在简单工厂模式中用于创建实例的方法是静态(static)方法,因此简单工厂模式又被称为静态工厂方法(Static Factory Method)模式,它属于类创建型模式。
在使用简单工厂模式时,首先需要对产品类进行重构,不能设计一个包罗万象的产品类,而需根据实际情况设计一个产品层次结构,将所有产品类公共的代码移至抽象产品类,并在抽象产品类中声明一些抽象方法,以供不同的具体产品类来实现。
例子:Sunny软件公司欲基于Java语言开发一套图表库,该图表库可以为应用系统提供各种不同外观 的图表,例如柱状图、饼状图、折线图等。
UML类图:
代码如下:
//抽象图表接口:抽象产品类
interface Chart {
public void display();
}
//柱状图类:具体产品类
class HistogramChart implements Chart {
public HistogramChart() {
System.out.println("创建柱状图!");
}
public void display() {
System.out.println("显示柱状图!");
}
}
//饼状图类:具体产品类
class PieChart implements Chart {
public PieChart() {
System.out.println("创建饼状图!");
}
public void display() {
System.out.println("显示饼状图!");
}
}
//折线图类:具体产品类
class LineChart implements Chart {
public LineChart() {
System.out.println("创建折线图!");
}
public void display() {
System.out.println("显示折线图!");
}
}
//图表工厂类:工厂类
class ChartFactory {
//静态工厂方法
public static Chart getChart(String type) {
Chart chart = null;
if (type.equalsIgnoreCase("histogram")) {
chart = new HistogramChart();
System.out.println("初始化设置柱状图!");
}
else if (type.equalsIgnoreCase("pie")) {
chart = new PieChart();
System.out.println("初始化设置饼状图!");
}
else if (type.equalsIgnoreCase("line")) {
chart = new LineChart();
System.out.println("初始化设置折线图!");
}
return chart;
}
}
class Client {
public static void main(String args[]) {
Chart chart;
chart = ChartFactory.getChart("histogram"); //通过静态工厂方法创建产品
chart.display();
}
}有时候,为了简化简单工厂模式,可以将抽象产品类和工厂类合并,将静态工厂方法移至抽象产品类中,客户端可以通过产品父类的静态工厂方法,根据参数的不同创建不同类型的产品子类对象,这种做法在JDK等类库和框架中也广泛存在。
1、工厂方法模式(Factory Method Pattern)
简单工厂模式最大的缺点是当有新产品要加入到系统中时,必须修改工厂类,需要在其中加入必要的业务逻辑, 这违背了“开闭原则”。此外,在简单工厂模式中,所有的产品都由同一个工厂创建,工厂类职责较重,业务逻辑较为复杂,具体产品与工厂类之间的耦合度高,严重影响了系统的灵活性和扩展性。
在工厂方法模式中,不再提供一个统一的工厂类来创建所有的产品对象,而是针对不同的产品提供不同的工厂,系统提供一个与产品等级结构对应的工厂等级结构。
与简单工厂模式相比,工厂方法模式最重要的区别是引入了抽象工厂角色,抽象工厂可以是接口,也可以是抽象类或者具体类。
定义:设计一个用于创建对象的接口,让子类决定将哪一个类实例化。工厂方法模式让一个类的实例化延迟到其子类。工厂方法模式又简称为工厂模式 (Factory Pattern),又可称作虚拟构造器模式(Virtual Constructor Pattern)或多态工厂模式 (Polymorphic Factory Pattern)。工厂方法模式是一种类创建型模式。
例子:Sunny软件公司欲开发一个系统运行日志记录器(Logger),该记录器可以通过多种途径保存系 统的运行日志,如通过文件记录或数据库记录,用户可以通过修改配置文件灵活地更换日志记录方式。
UML类图:
代码如下:
//日志记录器接口:抽象产品
interface Logger {
public void writeLog();
}
//数据库日志记录器:具体产品
class DatabaseLogger implements Logger {
public void writeLog() {
System.out.println("数据库日志记录。");
}
}
//文件日志记录器:具体产品
class FileLogger implements Logger {
public void writeLog() {
System.out.println("文件日志记录。");
}
}
//日志记录器工厂接口:抽象工厂
interface LoggerFactory {
public Logger createLogger();
}
//数据库日志记录器工厂类:具体工厂
class DatabaseLoggerFactory implements LoggerFactory {
public Logger createLogger() {
//连接数据库,代码省略
//创建数据库日志记录器对象
Logger logger = new DatabaseLogger();
//初始化数据库日志记录器,代码省略
return logger;
}
}
//文件日志记录器工厂类:具体工厂
class FileLoggerFactory implements LoggerFactory {
public Logger createLogger() {
//创建文件日志记录器对象
Logger logger = new FileLogger();
//创建文件,代码省略
return logger;
}
}
class Client {
public static void main(String args[]) {
LoggerFactory factory;
Logger logger;
factory = new FileLoggerFactory(); //可引入配置文件实现
logger = factory.createLogger();
logger.writeLog();
}
}2、抽象工厂模式(Abstract Factory Pattern)
工厂方法模式通过引入工厂等级结构,解决了简单工厂模式中工厂类职责太重的问题,但由于工厂方法模式中的每个工厂只生产一类产品,可能会导致系统中存在大量的工厂类,势必会增加系统的开销。此时,可以考虑将一些相关的产品组成一个“产品族”,由同一个工厂来统一生产,这就是抽象工厂模式的基本思想。
抽象工厂模式与工厂方法模式最大的区别在于, 工厂方法模式针对的是一个产品等级结构,而抽象工厂模式需要面对多个产品等级结构,一个工厂等级结构可以负责多个不同产品等级结构中的产品对象的创建。
产品等级结构:产品等级结构即产品的继承结构,如一个抽象类是电视机,其子类有海尔 电视机、海信电视机、TCL电视机。
产品族:在抽象工厂模式中,产品族是指由同一个工厂生产的,位于不同产品等级结构中 的一组产品,如海尔电器工厂生产的海尔电视机、海尔电冰箱。
定义:提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。抽象工厂模式又称为Kit模式,它是一种对象创建型模式。
例子:Sunny软件公司欲开发一套界面皮肤库,可以对Java桌面软件进行界面美化。为了保护版权, 该皮肤库源代码不打算公开,而只向用户提供已打包为jar文件的class字节码文件。用户在使用时可以通过菜单来选择皮肤,不同的皮肤将提供视觉效果不同的按钮、文本框、组合框等界面元素。
UML类图:
代码如下:
//按钮接口:抽象产品
interface Button {
public void display();
}
//Spring按钮类:具体产品
class SpringButton implements Button {
public void display() {
System.out.println("显示浅绿色按钮。");
}
}
//Summer按钮类:具体产品
class SummerButton implements Button {
public void display() {
System.out.println("显示浅蓝色按钮。");
}
}
//文本框接口:抽象产品
interface TextField {
public void display();
}
//Spring文本框类:具体产品
class SpringTextField implements TextField {
public void display() {
System.out.println("显示绿色边框文本框。");
}
}
//Summer文本框类:具体产品
class SummerTextField implements TextField {
public void display() {
System.out.println("显示蓝色边框文本框。");
}
}
//组合框接口:抽象产品
interface ComboBox {
public void display();
}
//Spring组合框类:具体产品
class SpringComboBox implements ComboBox {
public void display() {
System.out.println("显示绿色边框组合框。");
}
}
//Summer组合框类:具体产品
class SummerComboBox implements ComboBox {
public void display() {
System.out.println("显示蓝色边框组合框。");
}
}
//界面皮肤工厂接口:抽象工厂
interface SkinFactory {
public Button createButton();
public TextField createTextField();
public ComboBox createComboBox();
}
//Spring皮肤工厂:具体工厂
class SpringSkinFactory implements SkinFactory {
public Button createButton() {
return new SpringButton();
}
public TextField createTextField() {
return new SpringTextField();
}
public ComboBox createComboBox() {
return new SpringComboBox();
}
}
//Summer皮肤工厂:具体工厂
class SummerSkinFactory implements SkinFactory {
public Button createButton() {
return new SummerButton();
}
public TextField createTextField() {
return new SummerTextField();
}
public ComboBox createComboBox() {
return new SummerComboBox();
}
}
class Client {
public static void main(String args[]) {
//使用抽象层定义
SkinFactory factory;
Button bt;
TextField tf;
ComboBox cb;
factory = (SkinFactory)XMLUtil.getBean();
bt = factory.createButton();
tf = factory.createTextField();
cb = factory.createComboBox();
bt.display();
tf.display();
cb.display();
}
}通过以上代码的实现,可以看到在抽象工厂模式中,增加新的产品族很方便,不用修改原有的类,符合开闭原则。但是增加新的产品等级结构很麻烦,需要修改原有的类,不符合开闭原则。抽象工厂模式的这种性质称为“开闭原则”的倾斜性。
3、确保对象的唯一性——单例模式(Singleton Pattern)
定义:确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方 法。单例模式是一种对象创建型模式。
例子:Sunny软件公司承接了一个服务器负载均衡(Load Balance)软件的开发工作,该软件运行在一台负载均衡服务器上,可以将并发访问和数据流量分发到服务器集群中的多台设备上进行并发处理,提高系统的整体处理能力,缩短响应时间。由于集群中的服务器需要动态删减,且客户端请求需要统一分发,因此需要确保负载均衡器的唯一性,只能有一个负载均衡器来负责服务器的管理和请求的分发,否则将会带来服务器状态的不一致以及请求分配冲突等问题。
UML类图:
代码如下:
import java.util.*;
//负载均衡器LoadBalancer:单例类,真实环境下该类将非常复杂,包括大量初始化的工作和业务方法,考虑到代码的可读性和易理解性,只列出部分与模式相关的核心代码
class LoadBalancer {
//私有静态成员变量,存储唯一实例
private static LoadBalancer instance = null;
//服务器集合
private List serverList = null;
//私有构造函数
private LoadBalancer() {
serverList = new ArrayList();
}
//公有静态成员方法,返回唯一实例
public static LoadBalancer getLoadBalancer() {
if (instance == null) {
instance = new LoadBalancer();
}
return instance;
}
//增加服务器
public void addServer(String server) {
serverList.add(server);
}
//删除服务器
public void removeServer(String server) {
serverList.remove(server);
}
//使用Random类随机获取服务器
public String getServer() {
Random random = new Random();
int i = random.nextInt(serverList.size());
return (String)serverList.get(i);
}
}
class Client {
public static void main(String args[]) {
//创建四个LoadBalancer对象
LoadBalancer balancer1,balancer2,balancer3,balancer4;
balancer1 = LoadBalancer.getLoadBalancer();
balancer2 = LoadBalancer.getLoadBalancer();
balancer3 = LoadBalancer.getLoadBalancer();
balancer4 = LoadBalancer.getLoadBalancer();
//判断服务器负载均衡器是否相同
if (balancer1 == balancer2 && balancer2 == balancer3 && balancer3 == balancer4) {
System.out.println("服务器负载均衡器具有唯一性!");
}
//增加服务器
balancer1.addServer("Server 1");
balancer1.addServer("Server 2");
balancer1.addServer("Server 3");
balancer1.addServer("Server 4");
//模拟客户端请求的分发
for (int i = 0; i < 10; i++) {
String server = balancer1.getServer();
System.out.println("分发请求至服务器: " + server);
}
}
}分析上面的getLoadBalancer()方法,当在高并发或多线程环境中时,仍然会创建多个instance对象,可以用饿汉式单例类或懒汉式单例类解决这个问题。
饿汉式单例类:
从图中可以看出,由于在定义静态变量的时候实例化单例类,因此在类加载的时候,静态变量instance会被初始化,此时类的私有构造函数会被调用,单例类的唯一实例将被创建。
class EagerSingleton {
private static final EagerSingleton instance = new EagerSingleton();
private EagerSingleton() { }
public static EagerSingleton getInstance() {
return instance;
}
}懒汉式单例类与线程锁定:
从图中可以看出,懒汉式单例在第一次调用getInstance()方法时实例化,在类加载时并不自行实例化,这种技术又称为延迟加载(Lazy Load)技术,即需要的时候再加载实例,为了避免多个线程同时调用getInstance()方法,我们可以使用关键字synchronized。synchronized 关键字声明的方法同一时间只能被一个线程访问。
class LazySingleton {
private static LazySingleton instance = null;
private LazySingleton() { }
synchronized public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}上述代码虽然解决了线程安全问题,但是每次调用getInstance()时都需要进行线程锁定判断,在多线程高并发访问环境中,将会导致系统性能大大降低。如何既解决线程安全问题又不影响系统性能呢?事实上,我们无须对整个getInstance()方法进行锁定,只需对其中的代码“instance = new LazySingleton();”进行锁定即可。因此getInstance()方法可以进行如下改进:
public static LazySingleton getInstance() {
if (instance == null) {
synchronized (LazySingleton.class) {
instance = new LazySingleton();
}
}
return instance;
}使用以上代码来实现单例,还是会存在单例对象不唯一的情况。假如在某一瞬间线程A和线程B都在调用getInstance()方法,此时instance对象为null值,均能通过instance == null的判断。由于实现了synchronized加锁机制,线程A进入synchronized锁定的代码中执行实例创建代码,线程B处于排队等待状态,必须等待线程A执行完毕后才可以进入 synchronized锁定代码。但当A执行完毕时,线程B并不知道实例已经创建,将继续创建新的实例,导致产生多个单例对象。
进一步改进,在 synchronized 中再进行一次 (instance == null) 判断,这种方式称为双重检查锁定(Double-Check Locking)。
class LazySingleton {
private volatile static LazySingleton instance = null;
private LazySingleton() { }
public static LazySingleton getInstance() {
//第一重判断
if (instance == null) {
//锁定代码块
synchronized (LazySingleton.class) {
//第二重判断
if (instance == null) {
instance = new LazySingleton(); //创建单例实例
}
}
}
return instance;
}
}需要注意的是,如果使用双重检查锁定来实现懒汉式单例类,需要在静态成员变量instance之前增加修饰符volatile,被volatile修饰的成员变量可以确保多个线程都能够正确处理,且该代码只能在JDK 1.5及以上版本中才能正确执行。volatile 修饰的成员变量在每次被线程访问时,都强制从共享内存中重新读取该成员变量的值。而且,当成员变量发生变化时,会强制线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。
由于volatile关键字会屏蔽Java虚拟机所做的一些代码优化,可能会导致系统运行效率降低,因此即使使用双重检查锁定来实现单例模式也不是一种完美的实现方式。
一种更好的单例实现方法被称之为Initialization Demand Holder (IoDH)的技术:
在IoDH中,我们在单例类中增加一个静态(static)内部类,在该内部类中创建单例对象,再将该单例对象通过getInstance()方法返回给外部使用。
//Initialization on Demand Holder
class Singleton {
private Singleton() {
}
private static class HolderClass {
private final static Singleton instance = new Singleton();
}
public static Singleton getInstance() {
return HolderClass.instance;
}
public static void main(String args[]) {
Singleton s1, s2;
s1 = Singleton.getInstance();
s2 = Singleton.getInstance();
System.out.println(s1==s2);
}
}编译并运行上述代码,运行结果为:true,即创建的单例对象s1和s2为同一对象。由于静态单例对象没有作为Singleton的成员变量直接实例化,因此类加载时不会实例化Singleton,第一次调用getInstance()时将加载内部类HolderClass,在该内部类中定义了一个static类型的变量instance,此时会首先初始化这个成员变量,由Java虚拟机来保证其线程安全性,确保该成员变量只能初始化一次。由于getInstance()方法没有任何线程锁定,因此其性能不会造成任何影响。
通过使用IoDH,我们既可以实现延迟加载,又可以保证线程安全,不影响系统性能,不失为一种最好的Java语言单例模式实现方式(其缺点是与编程语言本身的特性相关,很多面向对象语言不支持IoDH)。
单例模式的主要缺点是单例类的职责过重,在一定程度上违背了“单一职责原则”。因为单例类既充当了工厂角 色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起。
4、对象的克隆——原型模式(Prototype Pattern)
定义:使用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。原型模式是一种对象创建型模式。
原型模式是一种“另类”的创建型模式,创建克隆对象的工厂就是原型类自身,工厂方法由克隆 方法来实现。
例子:Sunny软件公司一直使用自行开发的一套OA (Office Automatic,办公自动化)系统进行日常工作办理,但在使用过程中,越来越多的人对工作周报的创建和编写模块产生了抱怨。追其原因,Sunny软件公司的OA管理员发现,由于某些岗位每周工作存在重复性,工作周报内容都大同小异。这些周报只有一些小地方存在差异,但是现行系统每周默认创建的周报都是空白报表,用户只能通过重新输入或不断复制粘贴来填写重复的周报内容,极大降低了工作效率,浪费宝贵的时间。
UML类图:
代码如下:
//工作周报WeeklyLog:具体原型类,考虑到代码的可读性和易理解性,只列出部分与模式相关的核心代码
class WeeklyLog implements Cloneable
{
private String name;
private String date;
private String content;
public void setName(String name) {
this.name = name;
}
public void setDate(String date) {
this.date = date;
}
public void setContent(String content) {
this.content = content;
}
public String getName() {
return (this.name);
}
public String getDate() {
return (this.date);
}
public String getContent() {
return (this.content);
}
//克隆方法clone(),此处使用Java语言提供的克隆机制
public WeeklyLog clone()
{
Object obj = null;
try
{
obj = super.clone();
return (WeeklyLog)obj;
}
catch(CloneNotSupportedException e)
{
System.out.println("不支持复制!");
return null;
}
}
}
class Client
{
public static void main(String args[])
{
WeeklyLog log_previous = new WeeklyLog(); //创建原型对象
log_previous.setName("张无忌");
log_previous.setDate("第12周");
log_previous.setContent("这周工作很忙,每天加班!");
System.out.println("****周报****");
System.out.println("周次:" + log_previous.getDate());
System.out.println("姓名:" + log_previous.getName());
System.out.println("内容:" + log_previous.getContent());
System.out.println("--------------------------------");
WeeklyLog log_new;
log_new = log_previous.clone(); //调用克隆方法创建克隆对象
log_new.setDate("第13周");
System.out.println("****周报****");
System.out.println("周次:" + log_new.getDate());
System.out.println("姓名:" + log_new.getName());
System.out.println("内容:" + log_new.getContent());
}
}两种不同的克隆方法,浅克隆(ShallowClone)和深克隆 (DeepClone),浅克隆和深克隆的主要区别在于是否支持引用类型的成员变量的复制。在Java语言中,通过覆盖Object类的clone()方法可以实现浅克隆。如果需要实现深克隆,可以通过序列化(Serialization)等方式来实现。序列化就是将对象写到流的过程,写到流中的对象是原有对象的一个拷贝,而原对象仍然存在于内存中。
5、复杂对象的组装与创建——建造者模式(Builder Pattern)
定义:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。建造者模式是一种对象创建型模式。
建造者模式一步一步创建一个复杂的对象,它允许用户只通过指定复杂对象的类型和内容就 可以构建它们,用户不需要知道内部的具体构建细节。
在建造者模式的结构中还引入了一个指挥者类Director,该类主要有两个作用:一方面它隔离了客户与创建过程;另一方面它控制产品的创建过程。在实际生活中也存在类似指挥者一样的角色,如一个客户去购买电脑,电脑销售人员相当于指挥者,只要客户确定电脑的类型,电脑销售人员可以通知电脑组装人员给客户组装一台电脑。
建造者模式与抽象工厂模式有点相似,但是建造者模式返回一个完整的复杂产品,而抽象工厂模式返回一系列相关的产品。在抽象工厂模式中,客户端通过选择具体工厂来生成所需对象;而在建造者模式中,客户端通过指定具体建造者类型并指导Director类如何去生成对象, 侧重于一步步构造一个复杂对象。如果将抽象工厂模式看成一个汽车配件生产厂,生成不同类型的汽车配件,那么建造者模式就是一个汽车组装厂,通过对配件进行组装返回一辆完整的汽车。
例子:Sunny软件公司游戏开发小组决定开发一款名为《Sunny群侠传》的网络游戏,该游戏采用主 流的RPG(Role Playing Game,角色扮演游戏)模式,玩家可以在游戏中扮演虚拟世界中的一个特定角色,角色根据不同的游戏情节和统计数据(如力量、魔法、技能等)具有不同的能力, 角色也会随着不断升级而拥有更加强大的能力。
UML类图:
代码如下:
class Actor
{
private String type; //角色类型
private String sex; //性别
private String face; //脸型
private String costume; //服装
private String hairstyle; //发型
public void setType(String type) {
this.type = type;
}
public void setSex(String sex) {
this.sex = sex;
}
public void setFace(String face) {
this.face = face;
}
public void setCostume(String costume) {
this.costume = costume;
}
public void setHairstyle(String hairstyle) {
this.hairstyle = hairstyle;
}
public String getType() {
return (this.type);
}
public String getSex() {
return (this.sex);
}
public String getFace() {
return (this.face);
}
public String getCostume() {
return (this.costume);
}
public String getHairstyle() {
return (this.hairstyle);
}
}
//角色建造器:抽象建造者
abstract class ActorBuilder
{
protected Actor actor = new Actor();
public abstract void buildType();
public abstract void buildSex();
public abstract void buildFace();
public abstract void buildCostume();
public abstract void buildHairstyle();
//工厂方法,返回一个完整的游戏角色对象
public Actor createActor()
{
return actor;
}
}
//英雄角色建造器:具体建造者
class HeroBuilder extends ActorBuilder
{
public void buildType()
{
actor.setType("英雄");
}
public void buildSex()
{
actor.setSex("男");
}
public void buildFace()
{
actor.setFace("英俊");
}
public void buildCostume()
{
actor.setCostume("盔甲");
}
public void buildHairstyle()
{
actor.setHairstyle("飘逸");
}
}
//天使角色建造器:具体建造者
class AngelBuilder extends ActorBuilder
{
public void buildType()
{
actor.setType("天使");
}
public void buildSex()
{
actor.setSex("女");
}
public void buildFace()
{
actor.setFace("漂亮");
}
public void buildCostume()
{
actor.setCostume("白裙");
}
public void buildHairstyle()
{
actor.setHairstyle("披肩长发");
}
}
//恶魔角色建造器:具体建造者
class DevilBuilder extends ActorBuilder
{
public void buildType()
{
actor.setType("恶魔");
}
public void buildSex()
{
actor.setSex("妖");
}
public void buildFace()
{
actor.setFace("丑陋");
}
public void buildCostume()
{
actor.setCostume("黑衣");
}
public void buildHairstyle()
{
actor.setHairstyle("光头");
}
}
//游戏角色创建控制器:指挥者
class ActorController
{
//逐步构建复杂产品对象
public Actor construct(ActorBuilder ab)
{
Actor actor;
ab.buildType();
ab.buildSex();
ab.buildFace();
ab.buildCostume();
ab.buildHairstyle();
actor=ab.createActor();
return actor;
}
}
class Client
{
public static void main(String args[])
{
ActorBuilder ab; //针对抽象建造者编程
ab = (ActorBuilder)XMLUtil.getBean(); //反射生成具体建造者对象
ActorController ac = new ActorController();
Actor actor;
actor = ac.construct(ab); //通过指挥者创建完整的建造者对象
String type = actor.getType();
System.out.println(type + "的外观:");
System.out.println("性别:" + actor.getSex());
System.out.println("面容:" + actor.getFace());
System.out.println("服装:" + actor.getCostume());
System.out.println("发型:" + actor.getHairstyle());
}
}结构型模式
6、不兼容结构的协调——适配器模式(Adapter Pattern)
定义:将一个接口转换成客户希望的另一个接口,使接口不兼容的那 些类可以一起工作,其别名为包装器(Wrapper)。适配器模式既可以作为类结构型模式,也可以作为对象结构型模式。
在适配器模式中,我们通过增加一个新的适配器类来解决接口不兼容的问题,使得原本没有任何关系的类可以协同工作。根据适配器类与适配者类的关系不同,适配器模式可分为对象适配器和类适配器两种,在对象适配器模式中,适配器与适配者之间是关联关系;在类适配器模式中,适配器与适配者之间是继承(或实现)关系。
例子:Sunny软件公司在很久以前曾开发了一个算法库,里面包含了一些常用的算法,例如排序算法和查找算法,在进行各类软件开发时经常需要重用该算法库中的算法。
对象适配器模式UML类图:
类适配器模式UML类图:
双向适配器:
在对象适配器的使用过程中,如果在适配器中同时包含对目标类和适配者类的引用,适配者 可以通过它调用目标类中的方法,目标类也可以通过它调用适配者类中的方法,那么该适配 器就是一个双向适配器。
7、处理多维度变化——桥接模式(Bridge Pattern)
将两个独立变化的维度设计为两个独立的继承等级结构,并且在抽象层建立一个抽象关联,该关联关系类似一条连接两个独立继承结构的桥,故名桥接模式。
桥接模式用一种巧妙的方式处理多层继承存在的问题,用抽象关联取代了传统的多层继承,将类之间的静态继承关系转换为动态的对象组合关系,使得系统更加灵活,并易于扩展,同时有效控制了系统中类的个数。
定义:将抽象部分与它的实现部分分离,使它们都可以独立地变化。它是 一种对象结构型模式,又称为柄体(Handle and Body)模式或接口(Interface)模式。
例子:为了减少所需生成的子类数目,实现将操作系统和图像文件格式两个维度分离,使它们可以 独立改变,Sunny公司开发人员使用桥接模式来重构跨平台图像浏览系统的设计。
UML类图:
8、树形结构的处理——组合模式(Composite Pattern)
定义:组合多个对象形成树形结构以表示具有“整体—部分”关系的层 次结构。组合模式对单个对象(即叶子对象)和组合对象(即容器对象)的使用具有一致 性,组合模式又可以称为“整体—部分”(Part-Whole)模式,它是一种对象结构型模式。
组合模式的关键是定义了一个抽象构件类,它既可以代表叶子,又可以代表容器,而客户端 针对该抽象构件类进行编程,无须知道它到底表示的是叶子还是容器,可以对其进行统一处 理。同时容器对象与抽象构件类之间还建立一个聚合关联关系,在容器对象中既可以包含叶 子,也可以包含容器,以此实现递归组合,形成一个树形结构。
例子:为了让系统具有更好的灵活性和可扩展性,客户端可以一致地对待文件和文件夹,Sunny公司开发人员使用组合模式来进行杀毒软件的框架设计。
UML类图:
透明组合模式与安全组合模式:
透明组合模式中,抽象构件Component中声明了所有用于管理成员对象的方法,包括add()、 remove()以及getChild()等方法,这样做的好处是确保所有的构件类都有相同的接口。在客户端看来,叶子对象与容器对象所提供的方法是一致的,客户端可以相同地对待所有的对象。透明组合模式也是组合模式的标准形式。
安全组合模式中,在抽象构件Component中没有声明任何用于管理成员对象的方法,而是在 Composite类中声明并实现这些方法。这种做法是安全的,因为根本不向叶子对象提供这些管 理成员对象的方法,对于叶子对象,客户端不可能调用到这些方法。
9、扩展系统功能——装饰模式(Decorator Pattern)
装饰模式是一种用于替代继承的技术,它通过一种无须定义子类的方式来给对象动态增加职责,使用对象之间的关联关系取代类之间的继承关系。在装饰模式中引入了装饰类,在装饰类中既可以调用待装饰的原有类的方法,还可以增加新的方法,以扩充原有类的功能。
定义:动态地给一个对象增加一些额外的职责,就增加对象功能来说, 装饰模式比生成子类实现更为灵活。装饰模式是一种对象结构型模式。
例子:为了让系统具有更好的灵活性和可扩展性,克服继承复用所带来的问题,Sunny公司开发人员使用装饰模式来重构图形界面构件库的设计。
UML类图:
透明装饰模式与半透明装饰模式:
在实际使用过程中,由于新增行为可能需要单独调用,因此这种形式的装饰模式也经常出现,这种装饰模式被称为半透明(Semi-transparent)装饰模式,而标准的装饰模式是透明(Transparent)装饰模式。
在透明装饰模式中,要求客户端完全针对抽象编程,装饰模式的透明性要求客户端程序不应该将对象声明为具体构件类型或具体装饰类型,而应该全部声明为抽象构件类型。
10、外观模式(Facade Pattern)
定义:为子系统中的一组接口提供一个统一的入口。外观模式又称为门面模式,它是一种对象结构型模式。外观模式是迪米特法则的一种具体实现。
外观模式中所指的子系统是一个广义的概念,它可以是一个类、一个功能模块、系统的一个组成部分或者一个完整的系统。子系统类通常是一些业务类,实现了一些具体的、独立的业务功能。
例子:某软件公司欲开发一个可应用于多个软件的文件加密模块,该模块可以对文件中的数据进行 加密并将加密之后的数据存储在一个新文件中,具体的流程包括三个部分,分别是读取源文 件、加密、保存加密之后的文件,其中,读取文件和保存文件使用流来实现,加密操作通过 求模运算实现。这三个操作相对独立,为了实现代码的独立重用,让设计更符合单一职责原 则,这三个操作的业务代码封装在三个不同的类中。
UML类图:
抽象外观类:
在标准的外观模式结构图中,如果需要增加、删除或更换与外观类交互的子系统类,必须修改外观类或客户端的源代码,这将违背开闭原则,因此可以通过引入抽象外观类来对系统进行改进,在一定程度上可以解决该问题。在引入抽象外观类之后,客户端可以针对抽象外观类进行编程,对于新的业务需求,不需要修改原有外观类,而对应增加一个新的具体外观类,由新的具体外观类来关联新的子系统对象,同时通过修改配置文件来达到不修改任何源代码并更换外观类的目的。
11、实现对象的复用——享元模式(Flyweight Pattern)
定义:运用共享技术有效地支持大量细粒度对象的复用。系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用。由于享元模式要求能够共享的对象必须是细粒度对象,因此它又称为轻量级模式,它是一种对象结构型模式。
享元模式结构较为复杂,一般结合工厂模式一起使用。
例子:为了节约存储空间,提高系统性能,Sunny公司开发人员使用享元模式来设计围棋软件中的棋子。
UML类图:
单纯享元模式和复合享元模式:
在单纯享元模式中,所有的具体享元类都是可以共享的,不存在非共享具体享元类。
将一些单纯享元对象使用组合模式加以组合,还可以形成复合享元对象,这样的复合享元对 象本身不能共享,但是它们可以分解成单纯享元对象,而后者则可以共享。
12、代理模式(Proxy Pattern)
定义:给某一个对象提供一个代理或占位符,并由代理对象来控制对原对象的访问。代理模式是一种对象结构型模式。在代理模式中引入了一个新的代理对象,代理对象在客户 端对象和目标对象之间起到中介的作用,它去掉客户不能看到的内容和服务或者增添客户需 要的额外的新服务。
例子:代理模式的结构比较简单,其核心是代理类,为了让客户端能够一致性地对待真实对象和代理对象,在代理模式中引入了抽象层。
UML类图:
行为型模式
13、请求的链式处理——职责链模式(Chain of Responsibility Pattern)
定义:避免请求发送者与接收者 耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传 递请求,直到有对象处理它为止。职责链模式是一种对象行为型模式。
例子:为了让采购单的审批流程更加灵活,并实现采购单的链式传递和处理, Sunny公司开发人员使用职责链模式来实现采购单的分级审批
UML类图:
14、请求发送者与接收者解耦——命令模式(Command Pattern)
定义:将一个请求封装为一个对象,从而让我们可用不同的请求对客户 进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。命令模式是一种对象 行为型模式,其别名为动作(Action)模式或事务(Transaction)模式。
例子:为了降低功能键与功能处理类之间的耦合度,让用户可以自定义每一个功能键的功能,Sunny软件公司开发人员使用命令模式来设计“自定义功能键”模块。
UML类图:
在图中,FBSettingWindow是“功能键设置”界面类,FunctionButton充当请求调用者, Command充当抽象命令类,MinimizeCommand和HelpCommand充当具体命令类, WindowHanlder和HelpHandler充当请求接收者。
15、自定义语言的实现——解释器模式(Interpreter Pattern)
定义:定义一个语言的文法,并且建立一个 解释器来解释该语言中的句子,这里的“语言”是指使用规定格式和语法的代码。解释器模式是 一种类行为型模式。
例子:为了能够解释机器人控制指令,Sunny软件公司开发人员使用解释器模式来设计和实现机器人 控制程序。针对五条文法规则,分别提供五个类来实现,其中终结符表达式direction、action 和distance对应DirectionNode类、ActionNode类和DistanceNode类,非终结符表达式expression 和composite对应SentenceNode类和AndNode类。
UML类图:
在图中,AbstractNode充当抽象表达式角色,DirectionNode、ActionNode和DistanceNode充当终结符表达式角色,AndNode和SentenceNode充当非终结符表达式角色。
16、遍历聚合对象中的元素——迭代器模式(Iterator Pattern)
定义:提供一种方法来访问聚合对象,而不用暴露这个对象的内部表示,其别名为游标(Cursor)。迭代器模式是一种对象行为型模式。
例子:为了简化AbstractObjectList类的结构,并给不同的具体数据集合类提供不同的遍历方式, Sunny软件公司开发人员使用迭代器模式来重构AbstractObjectList类的设计。
UML类图:
在图中,AbstractObjectList充当抽象聚合类,ProductList充当具体聚合类,AbstractIterator充当抽象迭代器,ProductIterator充当具体迭代器。
17、协调多个对象之间的交互——中介者模式(Mediator Pattern)
中介者模式可以使对象之间的关系数量急剧减少,通过引入中介者对象,可以将系统的网状 结构变成以中介者为中心的星形结构。在这个星形结构中,同事对象不再直接 与另一个对象联系,它通过中介者对象与另一个对象发生相互作用。
如果在一个系统中对象之间存在多对多的相互关系,我们可以将对象之间的一些交互行为从 各个对象中分离出来,并集中封装在一个中介者对象中,并由该中介者进行统一协调,这样 对象之间多对多的复杂关系就转化为相对简单的一对多关系。通过引入中介者来简化对象之 间的复杂交互,中介者模式是“迪米特法则”的一个典型应用。
定义:用一个中介对象(中介者)来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。中介者模式又称为调停者模式,它是一种对象行为型模式。
中介者模式的核心在于中介者类的引入,在中介者模式中,中介者类承担了两方面的职责:
(1) 中转作用(结构性):通过中介者提供的中转作用,各个同事对象就不再需要显式引用其 他同事,当需要和其他同事进行通信时,可通过中介者来实现间接调用。该中转作用属于中介者在结构上的支持。
(2) 协调作用(行为性):中介者可以更进一步的对同事之间的关系进行封装,同事可以一致的和中介者进行交互,而不需要指明中介者需要具体怎么做,中介者根据封装在自身内部的协调逻辑,对同事的请求进行进一步处理,将同事成员之间的关系行为进行分离和封装。该协调作用属于中介者在行为上的支持。
例子:为了协调界面组件对象之间的复杂交互关系,Sunny公司开发人员使用中介者模式来设计客户信息管理窗口。
UML类图:
Component充当抽象同事类,Button、List、ComboBox和TextBox充当具体同事类,Mediator充当抽象中介者类,ConcreteMediator充当具体中介者类,ConcreteMediator维持 了对具体同事类的引用,为了简化ConcreteMediator类的代码,我们在其中只定义了一个 Button对象和一个TextBox对象。
18、撤销功能的实现——备忘录模式(Memento Pattern)
定义:在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态。它是一种对象行为型模式,其别名为Token。
例子:为了实现撤销功能,Sunny公司开发人员决定使用备忘录模式来设计中国象棋软件。
UML类图:
Chessman充当原发器,ChessmanMemento充当备忘录,MementoCaretaker充当负责人,在MementoCaretaker中定义了一个ChessmanMemento类型的对象,用于存储备忘录。
19、对象间的联动——观察者模式(Observer Pattern)
定义:定义对象之间的一种一对多依赖关系, 使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。观察者模式 的别名包括发布-订阅(Publish/Subscribe)模式、模型-视图(Model/View)模式、源-监听器 (Source/Listener)模式或从属者(Dependents)模式。观察者模式是一种对象行为型模式。
观察者模式描述了如何建立对象与对象之间的依赖关系,以及如何构造满足这种需求的系 统。观察者模式包含观察目标和观察者两类对象,一个目标可以有任意数目的与之相依赖的观察者,一旦观察目标的状态发生改变,所有的观察者都将得到通知。
作为对这个通知的响应,每个观察者都将监视观察目标的状态以使其状态与目标状态同步,这种交互也称为发布- 订阅(Publish-Subscribe)。观察目标是通知的发布者,它发出通知时并不需要知道谁是它的观 察者,可以有任意数目的观察者订阅它并接收通知。
例子:为了实现对象之间的联动,Sunny软件公司开发人员决定使用观察者模式来进行多人联机对战游戏的设计。
UML类图:
在图中,AllyControlCenter充当目标类, ConcreteAllyControlCenter充当具体目标类,Observer充当抽象观察者,Player充当具体观察者。
20、处理对象的多种状态及其相互转换——状态模式(State Pattern)
定义:允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修
改了它的类。其别名为状态对象(Objects for States),状态模式是一种对象行为型模式。
例子:Sunny软件公司开发人员使用状态模式来解决账户状态的转换问题,客户端只需要执行简单的存款和取款操作,系统根据余额将自动转换到相应的状态。
UML类图:
在图中,Account充当环境类角色,AccountState充当抽象状态角色,NormalState、 OverdraftState和RestrictedState充当具体状态角色。
21、算法的封装与切换——策略模式(Strategy Pattern)
定义:定义一系列算 法类,将每一个算法封装起来,并让它们可以相互替换,策略模式让算法独立于使用它的客 户而变化,也称为政策模式(Policy)。策略模式是一种对象行为型模式。
例子:为了实现打折算法的复用,并能够灵活地向系统中增加新的打折方式,Sunny软件公司开发人员使用策略模式对电影院打折方案进行重构。
UML类图:
22、模板方法模式(Template Method Pattern)
定义:定义一个操作中算法的框架,而将一些步骤延迟到子类中。模板方法模式使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。模板方法模式是一种基于继承的代码复用技术,它是一种类行为型模式。
例子:某软件公司欲为某银行的业务支撑系统开发一个利息计算模块,利息计算流程如下:
(1) 系统根据账号和密码验证用户信息,如果用户信息错误,系统显示出错提示;
(2) 如果用户信息正确,则根据用户类型的不同使用不同的利息计算公式计算利息(如活期账 户和定期账户具有不同的利息计算公式);
(3) 系统显示利息。
UML类图:
23、操作复杂对象结构——访问者模式(Visitor Pattern)
访问者模式是一种较为复杂的行为型设计模式,它包含访问者和被访问元素两个主要组成部分。
定义:提供一个作用于某对象结构中的各元素的操作表示,它使我们可以在不改变各元素的类的前提下定义作用于这些元素的新操作。访问者模式是一种对象行为型模式。
例子:Sunny软件公司开发人员使用访问者模式对OA系统中员工数据汇总模块进行重构,使得系统 可以很方便地增加新类型的访问者,更加符合“单一职责原则”和“开闭原则”。
UML类图:
在图中,FADepartment表示财务部,HRDepartment表示人力资源部,它们充当具体访问者角色,其抽象父类Department充当抽象访问者角色;EmployeeList充当对象结构,用于存储员工列表;FulltimeEmployee表示正式员工,ParttimeEmployee表示临时工,它们充当具体元素角色,其父接口Employee充当抽象元素角色。
五、用足球运动这个场景串联记忆23种设计模式
1、创建型模式
(1) 工厂方法模式
足球生产商是工厂,足球是产品。增加一 种新的足球品牌,对应需要增加一个新的生产商。
(2) 抽象工厂模式
Adidas工厂除了生产Adidas足球外,还生产Adidas球鞋、球服、球袜 (adidas is all in),一个工厂可以生产一族产品,而不只是一种产品。
(3) 单例模式
在比赛过程中每个球队的守门员有且仅有一个,肯定不会有两个穿相同球衣的守门员同时上场,这不是单例吗?
(4) 建造者模式
如果我要一套完整的意大利国家队足球装备包括球衣、球裤、球袜,只需跟某专卖店销售人员说一下返回给你的是一套经典的蓝色意大利国家队队服。在此,销售人员相当于建造者模式中的指挥者 (Director),他向用户返回一个复杂产品(足球装备),该复杂产品由多个部件组成(球衣、球 裤、球袜等),用户无须关心具体组装过程即可得到一个完整的复杂产品。
(5) 原型模式
无论是足球还是球服,都是批量生产的,先做一个原型(模板),然后照着生产就好了,想要多少就生产多少。
2、结构型模式
(6) 适配器模式
很多足球队都喜欢请外国教练,外国教练请回来通常很难跟队员直接交流,因此需要配翻译,此时,翻译充当了教练和队员之间的适配器,负责协调教练和队员之间的交流。
(7) 桥接模式
在足球比赛中,有人踢前锋、有人踢中场(前腰、中卫)、有人踢后卫;有人习惯踢左边、有人习惯踢右边、也有人喜欢站在中间,因此诞生了左中卫、右前 锋、中后卫、右后卫等名词,难道这不是两个变化维度的组合吗?
(8) 组合模式
欧洲杯一共分为四个组,每个组四个队,每个队有23名球员,如果要用一个图来表示欧洲杯全体球员及各国分组情况,不用说,一定是个树状图,组里有队,队里有人。
(9) 装饰模式:
现在足球服上的广告越来越多了,队服胸前右边一个奔驰,左边一个阿迪,当然还可以继续增加,广告既没有改变球衣的用途和性能,还能起到装饰效果,增加收入,何乐而不为呢?
(10) 外观模式
为了给记者和球队提供一个交流的平台,欧洲杯组委会在每场足球比赛前后都安排了新闻发布会,记者可以通过新闻发布会来与球队进行沟通交流,在此,新闻发布会充当了记者(客户端)和队员、教练(子系统)之间的外观角色。
(11) 享元模式
同一个国家队的队员,他们都共享着一个伟大的称谓,即"XXX国家队队员"。但是每个队员的球衣号码是不同的,球衣号码就是不能共享的外部状态。在享元模式中区分了对象的内部状态和外部状态。
(12) 代理模式
球员转会离不开球员的经纪人,经纪人将球员的想法传递给另一家俱乐部。经纪人就是球员的代理,球员是目标对象,而经纪人是代理对象。
3、行为型模式(上)
(13) 职责链模式
在传球的过程中足球就是一个请求,而球员就是请求的处理者,足球在球员间不断进行传递,构成了 一条传递链。
(14) 命令模式
主教练用手势指挥球员,这个过程主教练就是命令的发送者,手势就是命令对象,所有队员都是命令的接收者。不同的命令对象将对应不同的执行动作。
(15) 解释器模式
在足球场上,教练的手势就是一门语言,有的表示“传球”,有的表示“全线压上”,有的表示“全线防守”。每个队员都需要在比赛中阅读教练的手势并将其转换成执行指令,按照教练的意图来展开攻守。
(16) 迭代器模式
球队队员是按照一定的顺序出场的,球队是一个包含多个队员的聚合对象,可以提供一个迭代器来遍历其中的队员。
(17) 中介者模式
裁判经常是足球赛场的主角,当两队队员发生冲突时,他充当了球员之间的中介者(调停者),一切需服从裁判。
4、行为型模式(下)
(18) 备忘录模式
要是有后悔药的话,如果能回到昨晚2012 年欧洲杯决赛的中场休息,我相信普兰德利一定不会用莫塔换下蒙托利沃。软件系统中可以通过备忘录模式来实现对象的 状态恢复,备忘录就是软件中的后悔药,它就是软件中的月光宝盒。Ctrl + Z,撤销随你!
(19) 观察者模式
教练大手一挥,全线压上。此时,教练是观察目标,球员是观察者,观察目标与观察者之间有一对多的联动。
(20) 状态模式
随着比赛的进行,球员的状态会发生转换,在不同状态下球员的行为也不同。在梦游状态下基本上没有射门,在亢奋状态下基本上没有传球,在体力透支状态下基本上没有抢断。如果将每一种状态封装在一个状态类中,那么球员就是拥有状态的环境类了。
(21) 策略模式
据说1863年足球刚开始的时候流行1-0-9阵型,1个后卫,9个前锋,木有越 位,惊讶。随着足球的发展,现代足球阵型的变化越来越多,面对防守型球队,可以选择3-5- 3阵型,面对攻击性强的球队,可以选择5-4-1阵型,当然还有经典的4-4-2。每一种阵型都是一种策略,面对不同的对手可以选择不同的策略。
(22) 模板方法模式
如果将踢战术角球设计为一个模板方法,那么每一个步骤就是其中要调用的基本方法了,而且在不同战术中某些具体步骤的实施还各不相同。
(23) 访问者模式
有些从事足球比赛研究的人很喜欢数据。有的专门研究一场比赛中每个球员的跑动距离,有的研究每个球员的抢断次数,有的研究每个球员的射门次数,有的研究球员传球次数,有的研究球员传球成功率......如果将每一种研究类型看成一个访问者,那么球队就是一个包含多个队员元素的对象结构,以供不同访问者来研究。当然,我们还可以很 方便地增加新的访问者(研究者),例如,研究每个球员在比赛中吐口水的次数,研究每个 球员在比赛中与对方球员“亲密接触”次数......
详细参考:
- 第1章 统一建模语言基础知识 --本文类的6种关系引用了它
- 设计模式Java版 --本文设计原则和设计模式引用了它
- 设计模式之美 --包括大量设计实战经验,熟能生巧就靠它了