设计模式速览
- 前言:资料来源吉大设计模式课程,自用
- 只提取应试回忆关键部分,省略优缺点说明,详细应用之类,扩展挑了常出现的
1. 概述
1.1 类间关系
1.1.1 依赖(dependency):
一个类A使用到了另一个类B,而这种使用关系是具有偶然性的、临时性的、非常弱的,但是B类的变化会影响到A。
-
应用:机器生产零件,充电电池通过充电器来充电,自行车通过打气筒来充气,人借助螺丝刀拧螺丝,人借用船过河,艺术家鉴赏艺术品,警察抓小偷,小猫钓鱼,学生读书,某人买 车。
-
代码:局部变量,方法中的参数,对静态方法的调用等。
class B{...}class A{...}A::Function1(B &b) //或 A::Function1(B *b) //或 A::Function1(B b)//或 B*A::Function1() //或 B&A::Function1()//或 int A::Function1(){ B* pb=newB; /*... */ delete pb; }//或 int A::Function2(){ B::sf(); }
-
UML:虚线箭头,类A指向类B。
1.1.2 关联(association):
一个对象的实例与另一个对象的一些特定实例存在固定的对应关系,一般是长期性的,而且双方的关系一般是平等的
-
应用:客户和订单(1:N),公司和员工(1:N),主人和汽车(1:N),师傅和徒弟(1:N),丈夫和妻 子(1:1),飞机和航班(1:N),学生和课程(N:N)。
-
代码:
class B {...}; class A { B* b; .....}; A::afun1() { b->bfun1(); } A::afun2() { b->bfun2(); }
-
UML:实线箭头,类A指向类B,表示单向关联。如果使用双箭头或不使用箭头表 示双向关联。
1.1.3 聚合/聚集(aggregation):
关联关系的一种特例。聚合指的是整体与部分之间的关系,体现的是整体与部分、拥有的关系,即has-a的关系,此时整体与部分之间是可分离的,可以具有各自的生命周期,部分可以属于多个整体对象,也可以为多个整体对象共享
-
应用:自行车和车把、响铃、轮胎,汽车和引擎、轮胎、刹车装置,计算机和主板、CPU、 内存、硬盘,航母编队和航母、驱护舰艇、舰载机、潜艇,课题组和科研人员。
-
代码:
class B {...} class A { B* b; .....}class CCar {public:CTyre *cTyre[4]; }; class CTyre { // Do something };
-
UML:尾部为空心菱形的实线箭头(也可以没箭头),类A指向类B。
1.1.4 组合/合成(composition):
关联关系的一种特例。体现的是一种contains-a的关系,这种关系比聚合更强,也称为强聚合,区别为“部分”不能脱离“整体”单独存在,就是说, “部分”的生命期不能比“整体”还要长。同样体现整体与部分间的关系,但此时整体与部分是不可分的,整体的生命周期结束也就意味着部分的生命周期结束。整体类负责部分类对象的生存与消亡。
-
应用:公司和部门,人和大脑、四肢,窗口和标题栏、菜单栏、状态栏。
-
代码:
class B{...} class A{ B b; ...} 或 class A{public: A():pb(new B){}~A(){ delete pb; }private: B *pb; ... } // Company.h #include "Department.h" class CCompany {public:CDepartment cDepartment[N]; }; // Department.h class CDepartment {// Do something };
-
UML:尾部为实心菱形的实线箭头(也可以没箭头),类A指向类B。
1.1.4 泛化(generalization):
一般与特殊、一般与具体之间关系的描述,具体描述建立在一般描述的基础之上, 并对其进行了扩展。
-
应用:狗是对动物的具体描述,一般把狗设计为动物的子类。
-
代码:通过继承实现
class B { } class A : public B { } // Animal.h class CAnimal { public:// implementvirtual void EatSomething(){// Do something} }; // Tiger.h #include "Animal.h" class CTiger : public CAnimal {// Do something };
-
UML:空心三角形箭头的实线,子类指向父类。
1.1.5 实现(realization):
一种类与接口的关系,表示类是接口所有特征和行为的实现。从广义上来说,类模板 和模板类也是一种实现关系。
-
代码:泛化父类换成接口
// Animal.h class IAnimal { public:// interfacevirtual void EatSomething() = 0; }; class Animal : public IAnimal {// Do something };
-
UML:空心三角形箭头的虚线,实现类指向接口。
1.2 设计原则
-
类的设计原则:
- 设计目标:开闭原则、里氏代换原则、迪米特原则
- 设计方法:单一职责原则、接口分隔原则、依赖倒置原则、组合/聚合复用原则
1.2.1 开闭原则(The Open-Closed Principle ,OCP)
- 在进行面向对象设计中,设计类或其他软件实体时,应该遵循:
- 对扩展开放(open) :某模块的功能是可扩展的,则该模块是扩展开放的。软件系统的功能上的可扩展性要求模块是扩展开放的。
- 对修改关闭(closed):某模块被其他模块调用,如果该模块的源代码不允许修改,则该模块修改关闭的。软件系统的功能上的稳定性,持续性要求是修改关闭的。
- 开闭原则的相对性:构建100%满足开闭原则的软件系统是相当困难的。
1.2.2 里氏替换原则(Liskov Substitution Principle ,LSP)
- 所有引用基类的地方必须能透明地使用其派生类的对象。即不能违反下面两个条件
-
不应该在代码中出现if/else之类对派生类类型进行判断的条件。 以下代码就违反了LSP定义。
void DrawShape(const Shape& s) {if (typeid(s) == typeid(Square))DrawSquare(static_cast<Square&>(s));else if (typeid(s) == typeid(Circle))DrawCircle(static_cast<Circle&>(s));}
-
派生类对象应当可以替换基类对象并出现在基类对象能够出现的任何地方, 或者说如果我们把代码中使用基类对象的地方用它的派生类对象来代替,代码还能正常工作。
-
如果两个具体的类A,B之间的关系违反了LSP 的设计,那么根据具体的情况可以在下面的两种重构方案中选择一种:
-
创建一个新的抽象类C,作为两个具体类的基类,将A,B的共同行为移动到C中来解决问题。(鲸鱼类继承鱼类错误)
-
从B 到A 的继承关系改为关联关系。(自行车与人例子)
-
-
LSP给了我们一个 判断和设计类之间关系的基准:需不需要继承,以及怎样设计继承关系。
-
尽量从抽象类继承,而不是从具体类继承。
1.2.3 迪米特原则(最少知道原则)(Law of Demeter,LoD)
-
只与你直接的朋友们通信,不要跟”陌生人”说话。
- 一个软件实体应当尽可能少地与其他软件实体发生相互作用(只和你的”朋友” 通信)。
- 每一个软件实体对其他软件实体都只有最少的知识,而且局限于那些与本软件实体密切相关的软件实体(跟”朋友”通信越少越好,具体来说就是一个类对自己依赖的其它类知道的越少越好)。
-
”朋友”条件:
- 当前对象本身(this)
- 以参数形式传入到当前对象方法中的对象(依赖)
- 当前对象的实例变量直接引用的对象(关联)
- 当前对象的实例变量如果是一个聚集,那么聚集中的元素也都是朋友(聚集)
- 当前对象所创建的对象(组合)
解析:
- 出现在成员变量,方法参数,方法返回值中的类为本类的朋友,而出现在局部变量中的类则不是朋友(尽量不要在本类中通过局部变量的形式使用其它陌生类。)。
- 尽量不暴露独属于类本身的方法和属性,使其它类对自己知道的更少。
-
初衷:降低类之间的耦合(通过中介类连接,但容易增加了系统的复杂度)
-
示例
- 下面的代码在方法体内部依赖了其他类,违反了迪米特原则
class Teacher {
public:void command(GroupLeader groupLeader) {list<Student> listStudents = new list<Student>;for (int i = 0; i < 20; i++) {listStudents.add(new Student());}groupLeader.countStudents(listStudents);}
};
修改:
class Teacher {
public:void command(GroupLeader groupLeader) {groupLeader.countStudents();}
};
class GroupLeader {
private:list<Student> listStudents;
public:GroupLeader(list<Student> _listStudents) {this.listStudents = _listStudents;}void countStudents() {cout << "女生数量是:" << listStudents.size() << endl;}
};
2. 下面的代码对”朋友”知道的太多,也违反了迪米特原则
class WashingMachine {
public:void receiveClothes();void wash();void drying();
};
class Person {
public:void washClothes(WashingMachine& wm) {wm.receiveClothes();wm.wash();wm.drying();}
};
修改:
class WashingMachine {
public:void automatic();
private:void receiveClothes();void wash();void drying();
};
class Person {
public:void washClothes(WashingMachine& wm) {wm.automatic();}
};
-
总结:
- 朋友间也是有距离的:多使用private、protected,尽量不要对外公布太多的public方法和非静态的public变量。
- 如果一个方法放在本类中,既不增加类间关系,也对本类不产生负面影响,就放置在本类中。
1.2.4 单一职责原则(Single Responsibility Principle,SRP)
-
核心含义:只能让一个类有且仅有一个职责(一个职责可以有多个函数/方法)。
永远不要让一个类存在多个改变的理由。
-
反例:多个职责也会影响该类不同职责的使用者
- 如果一个职责使用了外部类库,则使用另外一个职责的用户却也不得不包含这个未被使用的外部类库。
- 某个用户由于某个原因需要修改其中一个职责,另外一个职责的用户也将受到影响,他将不得不重新编译和配置。
-
职责划分:不同职责分成不同的接口并分别实现
-
单一职责原则从职责(改变理由)的侧面上为我们对类(接口)的抽象的颗粒度建立了判断基准:在为系统设计类(接口)的时候应该保证它们的单一职责性。
(颗粒度:类(接口)的抽象颗粒度指的是在设计类或接口时,抽象的精细程度。颗粒度越细,表示抽象的层次越低,每个类或接口包含的功能越具体;颗粒度越粗,表示抽象的层次越高,每个类或接口包含的功能越通用。)
1.2.5 接口分隔原则(Interface Segregation Principle,ISP)
-
不能强迫用户去依赖那些他们不使用的接口。包含意思:
- 接口的设计原则:接口的设计应该遵循最小接口原则,不要把用户不使用的方法塞进同一个接口里。如果一个接口的方法没有被使用到,则说明该接口过胖, 应该将其分割成几个功能专一的接口。
- 接口的依赖(继承)原则:如果一个接口a继承另一个接口b,则接口a相当于继承了接口b的方法,那么继承了接口b后的接口a也应该遵循上述原则:不应该包含用户不使用的方法。反之,则说明接口a被b给污染了,应该重新设计它们的关系。
-
违反例:
依赖Door接口的CommonDoor却不得不实现未使用的alarm()方法。 违反了ISP原则。
- 修改:
- 通过多重继承实现
AlarmDoor有2种实现方案:(第2种方案更具有实用性:解耦,复用度高,单一职责)
- 通过关联实现(耦合度更低,组合/聚合复用原则)
- 接口分隔原则从对接口的使用上为我们对接口抽象的颗粒度建立了判断基准:在为系统设计接口的时候,使用多个专门的接口代替单一的胖接口。
单一职责与接口分隔
- 共同点:
- 两者都是为了提高内聚性、降低耦合性,体现了封装的思想;
- 最终表现出来的都是将接口约束到最小功能。
- 不同点:
单一职责 | 接口分隔 | |
---|---|---|
针对内容不同 | 则针对的是模块、类、接口的设计 | 更侧重于接口的设计 |
思考角度不同 | 是从软件实体本身的职责/功能是否单一来考虑 | 是从用户的角度来考虑接口约束问题 |
在系统外通过文档约束“不使用的方法不要访问” | 允许 | 不允许 |
1.2.6 依赖倒置原则(Dependency Inversion Principle ,DIP)
-
- 高层模块不应该依赖于低层模块,二者都应该依赖于抽象
- 抽象不应该依赖于细节,细节应该依赖于抽象
-
“BadDesign”:很大原因是“高层模块”过分依赖“低层模块”。
- 系统很难改变,因为每个改变都会影响其他很多部分。
- 当你对某地方做一修改,系统的看似无关的其他部分都不工作了。
- 系统很难被另外一个应用重用,因为很难将要重用的部分从系统中分离开来。
在高层模块与低层模块之间,引入一个抽象接口层。
- 熔炉示例:考虑一个控制熔炉调节器的软件。该软件从一个IO通道中读取当前的温度,并通过向另一个IO通道发送命令来指示熔炉的开或者关。
//温度调节器的简单算法
const byte THERMONETER = 0x86;
const byte FURNACE = 0x87;
const byte ENGAGE = 1;
const byte DISENGAGE = 0;
void Regulate(double minTemp, double maxTemp)
{for (;;){while (in(THERMONETER) > minTemp)//夹杂着许多低层细节wait(1);out(FURNACE, ENGAGE);while (in(THERMONETER) < maxTemp)wait(1);out(FURNACE, DISENGAGE);}
}
//通用的调节器
void Regulate(Thermometer t, Heater h, double minTemp,double maxTemp)
{for (;;){while (t.Read() > minTemp)//不再依赖于任何温度计或者熔炉的特定细节wait(1);h.Engate();while (t.Read() < maxTemp)wait(1);h.Disengage();}
}
- 启发
- 依赖于抽象
//任何对象都不应该持有一个指向具体类的指针或引用
class class1 {class2* cls2 = new class2();
}
class class2 {.......
}
//任何类都不应该从具体类派生
- 设计接口而非设计实现
抽象类/接口:倾向于较少的变化;抽象是关键点,它易于修改和扩展;不要强制修改那些抽象接口/类
例外:有些类不可能变化,在可以直接使用具体类的情况下,不需要插入抽象层。如:字符串类
- 避免传递依赖
- 避免高层依赖于低层
使用抽象类/接口和继承/实现来有效地消除传递依赖
1.2.7 组合/聚合复用原则(Composite/Aggregate Reuse Principle ,CARP)
-
尽量使用组合/聚合而非继承来达到复用目的。
-
只有当以下的Coad条件全部被满足时,才应当使用继承关系:
- 派生类是基类的一个特殊种类,而不是基类的一个角色,也就是区分"has a"和"is a"。只有"is a"关系才符合继承关系,"has a"关系应当用聚合来描述。
- 永远不会出现需要将派生类换成另外一个类的派生类的情况。否则不要使用继承。
- 派生类具有扩展基类的责任,而不是具有置换掉(override)或注销掉 (nullify)基类的责任。如果一个派生类需要大量的置换掉基类的行为,那么这 个类就不应该是这个基类的派生类。
- 只有在分类学角度上有意义时,才可以使用继承。
-
通过组合/聚合复用的优缺点
优点:
- 新对象存取成员对象的唯一方法是通过成员对象的接口;
- 这种复用是黑箱复用,因为成员对象的内部细节是新对象所看不见的;
- 这种复用更好地支持封装性;
- 这种复用实现上的相互依赖性比较小;
- 每一个新的类可以将焦点集中在一个任务上;
- 这种复用可以在运行时间内动态进行,新对象可以动态的引用与子对象类型相同的对象。(如添加成员)
- 作为复用手段可以应用到几乎任何环境中去。
缺点:
系统中会有较多的对象需要管理。
-
通过继承来进行复用的优缺点
优点:
- 新的实现较为容易,因为基类的大部分功能可以通过继承的关系自动进入派生类。
- 修改和扩展继承而来的实现较为容易。
缺点:
- 继承复用破坏封装性,因为继承将基类的实现细节暴露给派生类。由于基类的内部细节常常是对于派生类透明的,所以这种复用是透明的复用,又称“白箱” 复用。
- 如果基类发生改变,那么派生类的实现也不得不发生改变。
- 从基类继承而来的实现是静态的,不可能在运行时间内发生改变,没有足够的灵活性。
2. 设计模式
-
GoF 最先将模式(特定环境中解决问题的一种方案)的概念引入软件工程领域
-
设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结,使用设计模式是为了可复用代码、让代码更容易被他人理解、保证代码可靠性等。
-
基本要素:模式名称、问题、目的、解决方案、效果、实例代码和相关设计模式
关键元素包括以下四个方面:模式名称 (Pattern name) 、问题 (Problem) 、解决方案 (Solution) 、效果 (Consequences)
-
分类:
目的:
- 创建型模式主要用于创建对象。
- 结构型模式主要用于处理类或对象的组合。
- 行为型模式主要用于描述对类或对象怎样交互和怎样分配职责。
范围:
- 类模式处理类和子类之间的关系,这些关系通过继承建立,在编译时刻就被确定下来,是属于静态的。
- 对象模式处理对象间的关系,这些关系在运行时刻变化,更具动态性。
范围/目的 | 创建型模式 | 结构型模式 | 行为型模式 |
---|---|---|---|
类模式 | 工厂方法模式 | (类)适配器模式 | 解释器模式 模板方法模式 |
对象模式 | 抽象工厂模式 建造者模式 原型模式 单例模式 | (对象)适配器模式 桥接模式 组合模式 装饰模式 外观模式 |
职责链模式 命令模式 迭代器模式 中介者模式 备忘录模式 观察者模式 状态模式 策略模式 |
- 优点:设计模式是从许多优秀的软件系统中总结出的成功的、能够实现可维护性复用的设计方案,使用这些方案将避免我们做一些重复性的工作,而且可以设计出高质量的软件系统。
2.1 创建型模式
-
创建型模式(Creational Pattern)关注的是对象的创建,将创建对象(类的实例化)的过程进行了抽象和封装,分离了对象创建和对象使用。作为客户程序仅仅需要去使用对象,而不再关心创建对象过程中的逻辑。
类创建型模式使用继承改变被实例化的类,对象创建型模式将实例化委托给另一个对象。
-
特点:
-
客户不知道对象的具体类是什么(除非看源代码)
-
隐藏了对象实例是如何被创建和组织的
-
2.1.1 简单工厂模式
- 简单工厂模式(Simple Factory Pattern)(静态工厂方法(Static Factory Method)):在简单工厂模式中,可以根据参数的不同返回不同类的实例。
tag:对象创建型模式、违背了“开闭原则”
本质:“如何选择”实现
应用:简单电视机工厂、创建不同权限等级的用户对象、Java加密技术、工具类java.text.DateFormat
- 结构:
-
代码:
抽象产品角色
public abstract class AbstractPay
{public abstract void pay();
}
具体产品角色
public class CashPay extends AbstractPay
{public void pay(){//现金支付处理代码}
}
工厂角色
public class PayMethodFactory
{public static AbstractPay getPayMethod(String type)//客户端无须知道所创建的具体产品类的类名,只需要知道具体产品类所对应的参数即可{if(type.equalsIgnoreCase("cash")){return new CashPay(); //根据参数创建具体产品}else if(type.equalsIgnoreCase("creditcard")){return new CreditcardPay(); //根据参数创建具体产品}……}
} //一旦添加新产品就不得不修改工厂逻辑//在实际开发中,还可以在调用时将所传入的参数保存在XML等格式的配置文件中,修改参数时无须修改任何Java源代码。
- 适用环境
- 工厂类负责创建的对象比较少。
- 客户端只知道传入工厂类的参数,对于如何创建对象不关心。
2.1.2 工厂方法模式
- 工厂方法模式(Factory Method Pattern),工厂父类负责定义创建产品对象的公共接口,而工厂子类则负责生成具体的产品对象,这样做的目的是将产品类的实例化操作延迟到工厂子类中完成,即通过工厂子类来确定究竟应该实例化哪一个具体产品类。
tag:符合了“开闭原则”、体现了“依赖倒置原则”、类创建型模式
本质:延迟到子类来选择实现
应用:电视机工厂、日志记录器、Java消息服务JMS、JDBC中的工厂方法
扩展:当只有一个具体工厂,在具体工厂中可以创建所有的产品对象,并且工厂方法设计为静态方法时,工厂方法模式就退化成简单工厂模式。
- 结构:
-
代码:
抽象工厂类:
public abstract class PayMethodFactory
{public abstract AbstractPay getPayMethod();
}
具体工厂类:
public class CashPayFactory extends PayMethodFactory
{public AbstractPay getPayMethod(){return new CashPay();}
}
Client:
PayMethodFactory factory;
AbstractPay payMethod;
factory=new CashPayFactory();
/*一般不直接使用new关键字来创建对象,而是将具体类的类名写入**配置文件**中,再通过Java的**反射机制**,读取XML格式的配置文件,根据存储在XML文件中的类名字符串生成对象。*/
payMethod =factory.getPayMethod();
payMethod.pay();
Client改:
<?xml version="1.0"?>
<config><className>CashPayFactory</className>
</config>
//工具类XMLUtil的 getBean() 方法//创建DOM文档对象
DocumentBuilderFactory dFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = dFactory.newDocumentBuilder();
Document doc;
doc = builder.parse(new File("config.xml")); //获取包含类名的文本节点
NodeList nl = doc.getElementsByTagName("className");
Node classNode=nl.item(0).getFirstChild();
String cName=classNode.getNodeValue();//通过类名生成实例对象并将其返回
Class c=Class.forName(cName);
Object obj=c.newInstance();
return obj;
PayMethodFactory factory;
AbstractPay payMethod;
factory=(PayMethodFactory)XMLUtil.getBean(); //getBean()的返回类型为Object,此处需要进行强制类型转换
payMethod =factory.getPayMethod();
payMethod.pay();
- 适用环境
- 一个类不知道它所需要的对象的类:客户端不需要知道具体产品类的类名,客户端需要知道创建具体产品的工厂类。
- 一个类通过其子类来指定创建哪个对象。
- 将创建对象的任务委托给多个工厂子类中的某一个,客户端在使用时可以无须关心是哪一个工厂子类创建产品子类,需要时再动态指定,可将具体工厂类的类名存储在配置文件或数据库中。
2.1.3 抽象工厂模式
-
抽象工厂模式(Abstract Factory Pattern)(Kit模式):提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。工厂方法模式针对的是一个产品等级结构,而抽象工厂模式则需要面对多个产品等级结构。
tag:对象创建型模式、符合“开闭原则”---倾斜性(增加新的工厂和产品族容易,增加新的产品等级结构麻烦)
本质:选择产品簇的实现。如果抽象工厂里面只定义一个方法,直接创建产品,那么就退化称为工厂方法了。
应用:电器工厂、数据库操作工厂 、Java SE AWT(抽象窗口工具包)、更换界面主题
- 结构:ConcreteProductA1与ConcreteProductB1为同一产品族,与ConcreteProductA2为同一产品等级结构
-
代码:
抽象工厂类:每一个方法对应一个产品等级结构
public abstract class AbstractFactory
{public abstract AbstractProductA createProductA();public abstract AbstractProductB createProductB();
}
具体工厂类:生成一组具体产品,这些产品构成了一个产品族
public class ConcreteFactory1 extends AbstractFactory
{public AbstractProductA createProductA(){return new ConcreteProductA1();}public AbstractProductB createProductB(){return new ConcreteProductB1();}
}
- 适用环境
- 一个系统不应当依赖于产品类实例如何被创建、组合和表达的细节,这对于所有类型的工厂模式都是重要的。
- 系统中有多于一个的产品族,而每次只使用其中某一产品族。
- 属于同一个产品族的产品将在一起使用,这一约束必须在系统的设计中体现出来。
- 系统提供一个产品类的库,所有的产品以同样的接口出现,从而使客户端不依赖于具体实现。
2.1.4 建造者模式
-
建造者模式(Builder Pattern)(生成器模式):将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
建造者模式是一步一步创建一个复杂的对象,它允许用户只通过指定复杂对象的类型就可以构建它们,用户不需要知道内部的具体构建细节。
tag:对象创建型模式、符合“开闭原则”
本质:分离整体构建算法和部件构造
应用:KFC套餐 、JavaMail、设计游戏背景和人物
扩展:
1. 简化:
省略抽象建造者角色:如果系统中只需要一个具体建造者的话,可以省略掉抽象建造者。
省略指挥者角色:在具体建造者只有一个的情况下,如果抽象建造者角色已经被省略掉,那么还可以省略指挥者角色。
把指挥者类和抽象建造者进行合并,简化了系统结构,但同时也加重了抽象建造者类的职责,也不符合单一职责原则,如果construct()过于复杂,建议还是封装到指挥者类中。
2. 对比
建造者模式 | 抽象工厂模式 |
---|---|
返回一个组装好的完整产品 | 抽象工厂模式返回一系列相关的产品 |
客户端可以不直接调用建造者的相关方法,而是通过指挥者类来指导如何生成对象 | 客户端实例化工厂类,然后调用工厂方法获取所需产品对象 |
汽车组装工厂 | 汽车配件生产工厂 |
- 结构:
-
代码:
一个典型的复杂对象其类代码:
public class Product
{private String partA; //可以是任意类型private String partB;private String partC;//partA的Getter方法和Setter方法省略//partB的Getter方法和Setter方法省略//partC的Getter方法和Setter方法省略
}
抽象建造者类中定义了产品的创建方法和返回方法,其典型代码如下:
public abstract class Builder
{protected Product product=new Product();public abstract void buildPartA();public abstract void buildPartB();public abstract void buildPartC();public Product getResult(){return product;}
}
指挥者类Director,该类的作用主要有两个:一方面它隔离了客户与生产过程;另一方面它负责控制产品的生成过程。代码示例如下:
public class Director
{private Builder builder;public Director(Builder builder){this.builder=builder;}public void setBuilder(Builder builder){this.builder=builer;}public Product construct(){builder.buildPartA();builder.buildPartB();builder.buildPartC();return builder.getResult();}
}
Client:
……
Builder builder = new ConcreteBuilder();//只需确定具体建造者的类型即可
Director director = new Director(builder);
Product product = director.construct();
……
- 适用环境
- 需要生成的产品对象有复杂的内部结构,这些产品对象通常包含多个成员属性。
- 需要生成的产品对象的属性相互依赖,需要指定其生成顺序。
- 对象的创建过程独立于创建该对象的类。在建造者模式中引入了指挥者类,将创建过程封装在指挥者类中,而不在建造者类中。
- 隔离复杂对象的创建和使用,并使得相同的创建过程可以创建不同的产品对象。
2.1.5 原型模式
-
原型模式(Prototype Pattern):用原型实例指定创建对象的种类,并且通过复制这些原型创建新的对象。原型模式允许一个对象再创建另外一个可定制的对象,无须知道任何创建的细节。
原型模式的基本工作原理是通过将一个原型对象传给那个要发动创建的对象,这个要发动创建的对象通过请求原型对象拷贝原型自己来实现创建过程。
tag:对象创建模式、符合里氏替换原则、违背了“开闭原则”(需要为每一个类配备一个克隆方法)
本质:克隆生成对象
应用:邮件复制、ctrl+c/v、Struts2中Action对象的创建、Spring中创建新的Bean实例
扩展:
1. 原型管理器(Prototype Manager):定义了一个集合用于存储原型对象(区别于普通的用单独实例化对象来克隆)。
2. 通过原型模式获得相同对象后可以再对其属性进行修改,从而获取所需对象。
- 结构:
-
代码:
所有的Java类都继承自java.lang.Object,而Object类提供一个clone()方法,可以将一个Java对象复制一份。因此在Java中可以直接使用Object提供的clone()方法来实现对象的克隆
public class PrototypeDemo implements Cloneable //必须实现一个标识接口Cloneable,表示这个Java类支持复制 {……public Object clone( ){Object object = null;try {object = super.clone();} catch (CloneNotSupportedException exception) {System.err.println("Not support cloneable");}return object;}…… }/* clone满足 (1) 对任何的对象x,都有x.clone() !=x,即克隆对象与原对象不是同一个对象。 (2) 对任何的对象x,都有x.clone().getClass()==x.getClass(),即克隆对象与原对象的类型一样。 (3) 如果对象x的equals()方法定义恰当,那么x.clone().equals(x)应该成立。 */
-
浅克隆和深克隆:类似于浅拷贝和深拷贝
- 浅克隆:clone();深克隆:保存状态,可辅助实现撤销操作
-
适用环境
需要避免创建一个与产品类层次平行的工厂类层次时,并且类的实例对象只能有几个不同状态组合中的一种时,建立相应数目的原型并克隆它们可能比每次用合适的状态手工实例化该类更方便一些。比如在画图工具里,要画圆只需要拖动工具条的画圆工具到绘图区即可,而不需要从头开始一点一点的画一个圆,而且如果需要不同大小和颜色的圆,只需要复制几个圆,然后再修改他们的大小和颜色即可。
2.1.6 单例模式
-
单例模式(Singleton Pattern)(单件模式或单态模式):单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。
单例模式有三个要点:一是某个类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实例。
tag:对象创建型模式、一定程度上违背了“单一职责原则”
本质:控制实例数目
应用:身份证号码、打印池、java.lang.Runtime类 、数据库主键编号生成器、默认情况下Spring会通过单例模式创建bean实例
扩展:
1. 饿汉式单例类在自己被加载时就将自己实例化。单从资源利用效率角度来讲,这个比懒汉式单例类稍差些。从速度和反应时间角度来讲(少了if判断),则比懒汉式单例类稍好些。
2. 懒汉式单例类在实例化时,必须处理好在多个线程同时首次引用此类时的访问限制问题,特别是当单例类作为资源控制器,在实例化时必然涉及资源初始化,而资源初始化很有可能耗费大量时间,这意味着出现多线程同时首次引用此类的机率变得较大,需要通过同步化机制进行控制。public static synchronized Singleton get Instance()。
- 结构:
- 代码:
public class Singleton
{private static Singleton instance=null; //1.静态私有成员变量,全局访问点//2.私有构造函数,确保用户无法通过new关键字直接实例化它private Singleton(){ }//3.静态公有工厂方法,返回唯一实例public static Singleton getInstance(){if(instance==null)instance=new Singleton(); return instance;}
}
- 适用环境
- 系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器,或者需要考虑资源消耗太大而只允许创建一个对象。
- 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。
- 在一个系统中要求一个类只有一个实例时才应当使用单例模式。反过来,如果一个类可以有几个实例共存,就需要对单例模式进行改进,使之成为多例模式。
2.2 结构型模式
- 结构型模式(Structural Pattern)描述如何将类或者对象结合在一起形成更大的结构
类结构型模式关心类的组合,由多个类可以组合成一个更大的系统,在类结构型模式中一般只存在继承关系和实现关系。
对象结构型模式关心类与对象的组合,通过关联关系使得在一个类中定义另一个类的实例对象,然后通过该对象调用其方法。根据“合成复用原则”,在系统中尽量使用关联关系来替代继承关系(同行为型模式),因此大部分结构型模式都是对象结构型模式。
2.2.1 适配器模式
- 适配器模式(Adapter Pattern) (包装器(Wrapper)):将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作。
tag:类/对象结构型模式、符合“开闭原则”
本质:转换匹配,复用功能
应用:仿生机器人 、加密适配器、JDBC驱动软件都是一个介于JDBC接口和数据库引擎接口之间的适配器软件、InputStreamAdapter类
扩展:
默认适配器模式(Default Adapter Pattern)或缺省适配器模式、接口适配器模式:一个接口不想使用其所有的方法的情况。设计一个抽象类实现接口,并为该接口中每个方法提供一个默认实现(空方法),那么该抽象类的子类可有选择地覆盖父类的某些方法来实现需求。
双向适配器 :适配器中同时包含对目标类和适配者类的引用,适配者的使用者可以通过它调用目标类中的方法,目标类的使用者也可以通过它调用适配者类中的方法
智能适配器:在转换匹配的过程中,适配器还可以在转换调用的前后实现一些功能处理,进一步还可以按需复用不同的适配者,称为智能适配器。
适配多个适配者:在转换匹配的过程中,可以适配多个适配者,也就是说在实现目标接口时,需要调用多个模块的功能,适配多个适配者才能满足目标接口要求。
- 结构:
- Target:目标抽象类定义客户所需接口,可以是一个抽象类或接口,也可以是具体类
类适配器结构:
类适配器代码:
public class Adapter extends Adaptee implements Target//继承Adaptee
{public void request(){specificRequest();}
}
对象适配器结构:
对象适配器代码:
public class Adapter extends Target//Target为抽象类
{private Adaptee adaptee;//关联Adapteepublic Adapter(Adaptee adaptee){this.adaptee=adaptee;}public void request(){adaptee.specificRequest();}
}
diff:
类适配器模式 | 对象适配器模式 | |
---|---|---|
优点 | 适配器类是适配者类的子类,因此可以在适配器类中置换一些适配者的方法,使得适配器的灵活性更强。 | 一个对象适配器可以把多个不同的适配者适配到同一个目标,也就是说,同一个适配器可以把适配者类和它的子类都适配到目标接口。 |
缺点 | Java、C#等不支持多重继承的语言,一次最多只能适配一个适配者类,而且目标抽象类只能为抽象类,不能为具体类,其使用有一定的局限性,不能将一个适配者类和它的子类都适配到目标接口 | 与类适配器模式相比,要想置换适配者类的方法就不容易。如果一定要置换掉适配者类的一个或多个方法,就只好先做一个适配者类的子类,将适配者类的方法置换掉,然后再把适配者类的子类当做真正的适配者进行适配 |
- 适用环境
- 系统需要使用现有的类,而这些类的接口不符合系统的需要。
- 想要建立一个可以重复使用的类(适配器类),用于与一些彼此之间没有太大关联的一些类(目标类和适配者类),包括一些可能在将来引进的类(适配者类的子类)一起工作。
2.2.2 桥接模式
- 桥接模式(Bridge Pattern)(柄体(Handle and Body)模式或接口(Interface)模式):将抽象部分与它的实现部分分离,使它们都可以独立地变化。桥接模式将两个独立变化的维度设计为两个独立的继承等级结构,并且在抽象层建立一个抽象关联。
tag:对象结构型模式、符合“单一职责原则”、符合“开闭原则”
本质:分离抽象和实现
应用:模拟毛笔、跨平台视频播放器、Java虚拟机实现了平台的无关性、AWT中的Peer架构
扩展:
适配器模式与桥接模式的联用:桥接模式初步设计完成之后,当发现系统与已有类无法协同工作时,可以采用适配器模式。
- 结构:我们将具有两个独立变化维度的类的一些普通业务方法和与之关系最密切的维度设计为“抽象类”层次结构(抽象部分),而将另一个维度设计为“实现类”层次结构(实现部分)。
-
代码:
典型的实现类接口代码:
public interface Implementor
{public void operationImpl();
}
典型的抽象类代码:
public abstract class Abstraction
{protected Implementor impl; //定义实现类接口对象public void setImpl(Implementor impl){this.impl=impl;}public abstract void operation(); //声明抽象业务方法
}
典型的扩充抽象类代码:
public class RefinedAbstraction extends Abstraction
{public void operation(){//业务代码impl.operationImpl(); //调用实现类的方法//业务代码}
}
- 适用环境
- 如果一个系统需要在抽象化和具体化之间增加更多的灵活性,避免在两个层次之间建立静态的继承关系,通过桥接模式可以使它们在抽象层建立一个关联关系。
- “抽象部分”和“实现部分”可以以继承的方式独立扩展而互不影响,在程序运行时可以动态将一个抽象化子类的对象和一个实现化子类的对象进行组合,即系统需要对抽象化角色和实现化角色进行动态耦合。
- 一个类存在两个(或多个)独立变化的维度,且这两个(或多个)维度都需要独立进行扩展。
- 对于那些不希望使用继承或因为多重/多层继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。
2.2.3 组合模式
- 组合模式(Composite Pattern)(“整体-部分”(Part-Whole)模式):组合多个对象形成树形结构以表示“整体-部分”关系的层次结构。组合模式对单个对象(即叶子对象)和组合对象(即容器对象)的使用具有一致性。
tag:对象结构型模式
本质:统一叶子对象和组合对象
应用:水果盘、文件浏览、XML文档、JDK的AWT/Swing
扩展:
-
透明组合模式:即标准结构。不够安全,为叶子结点提供add()、remove()以及getChild()等方法是没有意义的。
-
安全组合模式:抽象组件Component中没有声明任何用于管理成员对象的方法。缺点不够透明,客户端不能完全针对抽象编程(叶子组件和容器组件具有不同的方法),必须有区别地对待叶子组件和容器组件。
- 更复杂的组合模式。可以对叶子节点和容器节点进行抽象,得到抽象叶子节点和抽象容器节点组件
- 结构:关键是定义了一个抽象组件类Composit,它既可以代表叶子,又可以代表容器
-
代码:
典型的抽象组件角色代码:
public abstract class Component
{ //用于访问和管理成员子组件的方法public abstract void add(Component c); //增加成员public abstract void remove(Component c); //删除成员public abstract Component getChild(int i); //获取成员//业务方法public abstract void operation();
}
典型的叶子组件角色代码:
public class Leaf extends Component//继承了需要叶子组件中需要实现在抽象组件类中声明的所有方法
{/*但是叶子组件不能再包含子组件,因此在叶子组件中实现子组件管理和访问方法时需要提供异常处理或错误提示。*/public void add(Component c){ //异常处理或错误提示 } public void remove(Component c){ //异常处理或错误提示 }public Component getChild(int i){ //异常处理或错误提示 }public void operation(){ //叶子组件具体业务方法的实现 }
}
典型的容器组件角色代码:
public class Composite extends Component {private ArrayList list = new ArrayList();public void add(Component c) { list.add(c); }public void remove(Component c) { list.remove(c); }public Component getChild(int i ) { (Component)list.get(i); }public void operation(){ //容器组件具体业务方法的实现 //递归调用成员组件的业务方法 for(Object obj:list) { ((Component)obj).operation(); }}
}
- 适用环境
- 如果想表示对象的整体-部分层次结构,可以选用组合模式,把整体和部分的操作统一起来,使得层次结构实现更简单,从外部来使用这个层次结构也简单。
- 如果希望统一地使用组合结构中的所有对象,可以选用组合模式,这正是组合模式提供的主要功能。
2.2.4 装饰模式
- 装饰模式(Decorator Pattern) :动态地给一个对象增加一些额外的职责(Responsibility),就增加对象功能来说,装饰模式比生成子类实现更为灵活。其别名也可以称为包装器(Wrapper),与适配器模式的别名相同,但它们适用于不同的场合。根据翻译的不同,装饰模式也有人称之为“油漆工模式”。
tag:对象结构型模式
本质:动态组合
应用:变形金刚!?、多重加密系统、javax.swing中JList组件(ScrollPane充当装饰器)、Java IO
扩展:
1. 透明装饰模式(多重加密系统) | 2. 半透明装饰模式(变形金刚) |
---|---|
要求客户端完全针对抽象编程,装饰模式的透明性要求客户端程序不应该声明具体组件类型和具体装饰类型,而应该全部声明为抽象组件类型Component | 允许用户在客户端声明具体装饰者类型ConcreteDecorator的对象,调用在具体装饰者中新增的方法。 |
- 装饰模式的简化:如果只有一个具体组件类,那么抽象装饰类可以作为该具体组件类的直接子类,即没有抽象组件类。
- 结构:装饰模式的核心在于抽象装饰类的设计。组件类和装饰类都实现了相同的抽象组件接口,客户端并不会觉得对象在装饰前和装饰后有什么不同
-
代码:
抽象装饰类典型代码:
public class Decorator extends Component
{private Component component; //维持一个对抽象组件对象的引用public Decorator(Component component) //注入一个抽象组件类型的对象{this.component=component;}public void operation(){component.operation(); //调用原有业务方法}
}
在Decorator的子类即具体装饰类中将继承operation()方法并根据需要进行扩展,其典型代码:
public class ConcreteDecorator extends Decorator
{public ConcreteDecorator(Component component){super(component);}public void operation(){super.operation(); //调用原有业务方法addedBehavior(); //调用新增业务方法}public void addedBehavior() //新增业务方法{……}
}
- 适用环境
- 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
- 当不能采用继承的方式对系统进行扩展或者采用继承不利于系统扩展和维护时可以使用装饰模式。不能采用继承的情况主要有两类:第一类是系统中存在大量独立的扩展,为支持每一种扩展或者扩展之间的组合将产生大量的子类,使得子类数目呈爆炸性增长;第二类是因为类已定义为不能被继承(如Java语言中的final类)。
2.2.5 外观模式
- 外观模式(Facade Pattern)(门面模式):为子系统中的一组接口提供一个统一的入口。外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。主要目的在于降低系统的复杂程度,降低了类与类之间的耦合关系。
tag:对象结构型模式、迪米特原则的一种具体实现、一定程度上并不符合开闭原则(增加移除子系统需要在外观类增加或删除引用,扩展三解决)
本质:封装交互,简化调用
应用:电器总开关、文件加密、在大部分数据操作代码中都多次定义了三个对象(Connection Statement ResultSet),可放入外观类中、Java EE框架中SessionFacade
扩展:
1. 一个系统可有多个外观类
1. 不要通过继承一个外观类在子系统中加入新的行为(应该通过修改原有子系统类或增加新的子系统类来实现)
- 通过引入抽象外观类保持开闭原则,每个具体外观引用的子系统不同
- 结构:
-
代码:
典型的子系统角色代码:
class SubSystemA
{ public void MethodA() { //业务实现代码 }
}
class SubSystemB
{ public void MethodB() { ...}
}
class SubSystemC
{ ...
}
在引入外观类之后,与子系统业务类之间的交互统一由外观类来完成,在外观类中通常存在如下代码:
public class Facade
{private SubSystemA obj1 = new SubSystemA();private SubSystemB obj2 = new SubSystemB();private SubSystemC obj3 = new SubSystemC();public void Method(){obj1.MethodA();obj2.MethodB();obj3.MethodC();}
}
Client:
class Program
{ static void Main(string[] args) { Facade facade = new Facade(); facade.Method(); }
}
- 适用环境
- 当要为一个复杂子系统提供一个简单接口时可以使用外观模式。该接口可以满足大多数用户的需求,而且用户也可以越过外观类直接访问子系统。
- 客户端程序与多个子系统之间存在很大的依赖性。引入外观类可以将子系统与客户端解耦,从而提高子系统的独立性和可移植性。
- 在层次化结构中,可以使用外观模式定义系统中每一层的入口,层与层之间不直接产生联系,而通过外观类建立联系,降低层之间的耦合度。
2.2.7 代理模式
- 代理模式(Proxy Pattern) :给某一个对象提供一个代理,并由代理对象控制对原对象的引用。
tag:对象结构型模式、符合开闭原则
本质:控制对象访问
应用:论坛权限控制代理 、数学运算代理 、Java RMI 、Spring 框架中的AOP技术
扩展
-
种类:
- 远程代理(Remote Proxy):客户完全可以认为被代理的远程业务对象是在本地而不是在远程,而远程代理对象承担了大部分的网络通信工作,并负责对远程业务方法的调用。
- 虚拟代理(Virtual Proxy):如果需要创建一个资源消耗较大的对象,先创建一个消耗相对较小的对象来表示,真实对象只在需要时才会被真正创建。对大图浏览的控制:通过代理技术结合多线程编程将真实图片的加载放到后台来操作,不影响前台图片的浏览。
- 保护代理(Protect Proxy):控制对一个对象的访问,可以给不同的用户提供不同级别的使用权限。
- 缓冲代理(Cache Proxy):为某一个目标操作的结果提供临时的存储空间,以便多个客户端可以共享这些结果。
- 智能引用代理(Smart Reference Proxy):当一个对象被引用时,提供一些额外的操作,例如将对象被调用的次数记录下来等。
- 防火墙代理(Firewall Proxy):控制网络资源的访问,保护主题免于“坏客户”的侵害。常用在公司的防火墙系统。
- 同步代理(Synchronization Proxy):在多线程的情况下为主题提供安全的访问。常用在JavaSpaces,为分散式环境内的潜在对象集合提供同步访问控制。
- 写入时复制代理(Copy-On-Write Proxy):用来控制对象的复制,方法是延迟对象的复制,直到客户真的需要为止。这是虚拟代理的变体。
-
结构:核心是代理类,为了让客户端能够一致性地对待真实对象和代理对象,在代理模式中引入了抽象层。
-
代码:
抽象主题类声明了真实主题类和代理类的公共方法,它可以是接口、抽象类或具体类
public interface AbstractSubject
{public void request();
}
真实主题类继承了抽象主题类,提供了业务方法的具体实现,其典型代码如下:
public class RealSubject implements AbstractSubject
{ public void request(){ //业务方法具体实现代码 }
}
代理类也是抽象主题类的子类,它维持一个对真实主题对象的引用,调用在真实主题中实现的业务方法,在调用时可以在原有业务方法的基础上附加一些新的方法来对功能进行扩充或约束,最简单的代理类实现代码如下:
public class Proxy implements AbstractSubject
{ private RealSubject realsubject = new RealSubject(); //维持一个对真实主题对象的引用 public void prerequest() { ... } public void request() { prerequest(); realsubject.request(); //调用真实主题对象的方法 postrequest(); } public void postrequest() { ... }
}
- 适用环境
- 当客户端需要访问远程主机中的对象时可以使用远程代理。
- 当需要用一个消耗资源较少的对象来代表一个消耗资源较多的对象,从而降低系统开销、缩短运行时间时可以使用虚拟代理,例如一个对象需要很长时间才能完成加载时。
- 当需要为某一个被频繁访问的操作结果提供一个临时存储空间,以供多个客户端共享访问这些结果时可以使用缓冲代理。通过使用缓冲代理,系统无须在客户端每一次访问时都重新执行操作,只需直接从临时缓冲区获取操作结果即可。
- 当需要控制对一个对象的访问,为不同用户提供不同级别的访问权限时可以使用保护代理。
- 当需要为一个对象的访问(引用)提供一些额外的操作时可以使用智能引用代理。
2.3 行为型模式
-
行为型模式(Behavioral Pattern)是对在不同的对象之间划分责任和算法的抽象化。
-
行为型模式不仅仅关注类和对象的结构,而且重点关注它们之间的相互作用。
行为型模式可分为下两种:
类行为型模式:类的行为型模式使用继承关系在几个类之间分配行为,类行为型模式主要通过多态等方式来分配父类与子类的职责。
对象行为型模式:对象的行为型模式则使用对象的聚合关联关系来分配行为,对象行为型模式主要是通过对象关联等方式来分配两个或多个类的职责。根据“合成复用原则”,系统中要尽量使用关联关系来取代继承关系,因此大部分行为型设计模式都属于对象行为型设计模式。
2.3.1 职责链模式
- 职责链模式(Chain of Responsibility Pattern)(责任链模式):避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止。
tag:对象行为型模式,符合“开闭原则”
本质:分离职责,动态组合
应用:审批、申请、Java中的异常处理机制、web中用过滤器(Filter)链来对请求数据进行过滤、工作流系统中实现公文的分级审批、早期的Java AWT事件模型
扩展
纯的职责链模式 | 不纯的职责链模式 |
---|---|
一个具体处理者对象:要么承担全部责任,要么将责任推给下家 | 允许某个请求被一个具体处理者部分处理后再向下传递,或者一个具体处理者处理完某请求后其后继处理者可以继续处理该请求 |
要求一个请求必须被某一个处理者对象所接收 | 一个请求可以最终不被任何处理者对象所接收 |
- 结构:
-
代码:
职责链模式的核心在于抽象处理者类的设计,抽象处理者的典型代码:
public abstract class Handler
{ //维持对下家的引用protected Handler successor;//方便子类访问请求public void setSuccessor(Handler successor){this.successor=successor;}public abstract void handleRequest(String request);
}
具体处理者是抽象处理者的子类,它具有两大作用:第一是处理请求,不同的具体处理者以不同的形式实现抽象请求处理方法handleRequest();第二是转发请求,如果该请求超出了当前处理者类的权限,可以将该请求转发给下家。
public class ConcreteHandler extends Handler
{public void handleRequest(String request){if(请求request满足条件){...... //处理请求; }else{ this.successor.handleRequest(request); //转发请求 }}
}
Client:职责链模式并不创建职责链,一般是在使用该职责链的客户端中创建职责链。
class Client { public static void main(String[] args) { Handler ha, hb, hc;ha = new ConcreteHandlerA( );hb = new ConcreteHandlerB( );hc = new ConcreteHandlerC( );ha.setSuccessor(hb); //创建职责链hb.setSuccessor(hc);String req = new String(“some request”);ha.handleRequest(req);}
}
- 适用环境
-
有多个对象可以处理同一个请求,具体哪个对象处理该请求待运行时刻再确定,客户端只需将请求提交到链上,而无须关心请求的处理对象是谁以及它是如何处理的。
-
在不明确指定接收者的情况下,向多个对象中的一个提交一个请求。
-
可动态指定一组对象处理请求,客户端可以动态创建职责链来处理请求,还可以改变链中处理者之间的先后次序。
2.3.2 命令模式
- 命令模式(Command Pattern)(动作(Action)或事务(Transaction)):将一个请求封装为一个对象,从而让我们可用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。可以将请求发送者和接收者完全解耦。
tag:对象行为型模式、满足“开闭原则”
本质:封装请求
应用:电视机遥控器 、功能键设置
扩展:命令队列(Command继承CommandQueue)、请求日志、撤销操作、宏命令(组合加命令模式)
- 结构:
核心在于引入了命令类,通过命令类来降低发送者和接收者的耦合度,请求发送者只需指定一个命令对象,再通过命令对象来调用请求接收者的处理方法
顺序图:
-
代码:
抽象命令类
public abstract class Command
{public abstract void execute();
}
具体命令类与请求接收者关联,实现了在抽象命令类中声明的execute()方法,并在实现时调用接收者的请求响应方法action()。
public class ConcreteCommand extends Command
{private Receiver receiver; //维持一个对请求接收者对象的引用public void execute(){receiver.action(); //调用请求接收者的业务处理方法}
}
请求发送者即调用者,将针对抽象命令类进行编程,可以通过构造注入或者设值注入的方式在运行时传入具体命令类对象,并在业务方法中调用命令对象的execute()方法。
public class Invoker
{private Command command; //维持一个对命令对象的引用public Invoker(Command command) //构造注入 { this.command=command; }public void setCommand(Command command) //设值注入 { this.command=command; }public void call() //业务方法,用于调用命令类的方法{ command.execute(); }
}
请求接收者Receiver类具体实现对请求的业务处理
public class Receiver
{public void action(){//具体操作}
}
- 适用环境
-
如果需要抽象出需要执行的动作,并参数化这些对象,可以选用命令模式,把这些需要执行的动作抽象成为命令,然后实现命令的参数化配置。
-
如果需要在不同的时刻指定、排列和执行请求,可以选用命令模式,把这些请求封装成为命令对象,然后实现把请求队列化。
-
如果需要支持取消操作,可以选用命令模式,通过管理命令对象,能很容易的实现命令的恢复和重做的功能。
-
如果需要支持当系统崩溃时,能把对系统的操作功能重新执行一遍。
-
建立一个由支持可撤销操作的命令对象组成的链表,正向遍历,即执行事务处理操作集合;一旦某个事务操作出现异常,便改为反向遍历,即执行事务回滚操作集合。
2.3.4 迭代器模式
- 迭代器模式(Iterator Pattern) (游标(Cursor)):提供一种方法来访问聚合对象,而不用暴露这个对象的内部表示。包含聚合和迭代器两个层次结构。应用了工厂方法模式。
tag:对象行为模式
本质:控制访问聚合对象中的元素
应用:电视机遥控器 、Java内置迭代器
- 结构:
-
代码:
在抽象迭代器中声明了用于遍历聚合对象中所存储元素的方法,典型代码如下所示:
interface Iterator { public void first(); //将游标指向第一个元素 public void next(); //将游标指向下一个元素 public boolean hasNext(); //判断是否存在下一个元素 public Object currentItem(); //获取游标指向的当前元素
}
在具体迭代器中将实现抽象迭代器声明的遍历数据的方法,如下代码所示:
class ConcreteIterator implements Iterator { private ConcreteAggregate objects; //维持一个对具体聚合对象的引用,以便于访问存储在聚合对象中的数据 private int cursor; //定义一个游标,用于记录当前访问位置 public ConcreteIterator(ConcreteAggregate objects) { this.objects=objects; } public void first() { ...... } public void next() { ...... } public boolean hasNext() { ...... } public Object currentItem() { ...... }
}
聚合类用于存储数据并负责创建迭代器对象,最简单的抽象聚合类代码如下所示:
interface Aggregate { Iterator createIterator();
}
具体聚合类作为抽象聚合类的子类,一方面负责存储数据,另一方面实现了在抽象聚合类中声明的工厂方法createIterator(),用于返回一个与该具体聚合类对应的具体迭代器对象,代码如下所示:
class ConcreteAggregate implements Aggregate { ...... public Iterator createIterator() { return new ConcreteIterator(this); } ......
}
具体迭代器类放在具体聚合类里
- 适用环境
- 访问一个聚合对象的内容而无须暴露它的内部表示。将聚合对象的访问与内部数据的存储分离,使得访问聚合对象时无须了解其内部实现细节。
- 需要为一个聚合对象提供多种遍历方式。
- 为遍历不同的聚合结构提供一个统一的接口,在该接口的实现类中为不同的聚合结构提供不同的遍历方式,而客户端可以一致性地操作该接口。
2.3.5 中介者模式
- 中介者模式(Mediator Pattern):用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。中介者模式又称为调停者模式。
tag:对象行为型模式
本质:封装交互
应用:虚拟聊天室、GUI应用程序、MVC 控制器Controller 作为一种中介者
diff:
中介者模式 | 外观模式 |
---|---|
提供多个平等的同事对象之间交互关系的封装,一般是用在内部实现上 | 封装的是子系统外部和子系统内部模块间的交互 |
实现的是内部多个模块间多向的交互 | 单向的交互,从子系统外部来调用子系统内部 |
目的主要是松散多个模块之间的耦合 | 简化客户端的调用 |
-
结构:
模式的核心在于中介者类的引入,它承担两方面职责:
-
中转作用(结构性):通过中介者提供的中转作用,各个同事对象就不再需要显式引用其他同事,当需要和其他同事进行通信时,可通过中介者来实现间接调用。
-
协调作用(行为性):中介者可以更进一步的对同事之间的关系进行封装,同事可以一致的和中介者进行交互,而不需要指明中介者需要具体怎么做。
-
- 代码:
典型的抽象中介者类代码:
abstract class Mediator { protected ArrayList<Colleague> colleagues; //用于存储同事对象 //注册方法,用于增加同事对象 public void register(Colleague colleague) { colleagues.add(colleague); coolleague.set(this);} //声明抽象的业务方法 public abstract void operation();
}
典型的具体中介者类代码
class ConcreteMediator extends Mediator { //实现业务方法,封装同事之间的调用 public void operation() { ...... ((Colleague)(colleagues.get(0))).method1(); //通过中介者调用同事类的方法 ...... }
}
在抽象同事类中维持了一个抽象中介者的引用,用于调用中介者的方法,典型的抽象同事类代码:
abstract class Colleague {protected Mediator mediator; //维持一个抽象中介者的引用 public Colleague(Mediator mediator) {this.mediator=mediator;}public set(Mediator mediator){this.mediator=mediator;}public abstract void method1(); //声明自身方法,处理自己的行为
}
典型的具体同事类代码:
class ConcreteColleague extends Colleague {public ConcreteColleague(Mediator mediator) {super(mediator);}//实现自身方法 public void method1() {......}//定义依赖方法,与中介者进行通信public void method2() {mediator.operation();}
}
其中method1()方法是同事类的自身方法(Self-Method),用于处理自己的行为,而method2()方法是依赖方法(Depend-Method),用于调用在中介者中定义的方法,依赖中介者来完成相应的行为,例如调用另一个同事类的相关方法。
同事调用依赖方法与中介联系->中介依次调用同事自身方法
- 适用环境
-
如果一组对象之间的通信方式比较复杂,导致相互依赖、结构混乱,可以采用中介者模式,把这些对象相互的交互管理起来,各个对象都只需要和中介者交互,从而使得各个对象松散耦合,结构也更清晰易懂。
-
如果一个对象引用很多的对象,并直接跟这些对象交互,导致难以复用该对象。可以采用中介者模式,把这个对象跟其它对象的交互封装到中介者对象里面,这个对象就只需要和中介者对象交互就可以了
2.3.6 备忘录模式
- 备忘录模式(Memento Pattern)(Token模式或快照模式(Snapshot Pattern)):在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态。
tag:对象行为型模式
本质:保存和恢复内部状态
应用:用户信息操作撤销 、文字或者图像编辑软件、数据库管理系统DBMS回滚
扩展:
-
备忘录模式和命令模式组合使用。例子:组合备忘录和命令
命令模式实现中,在实现命令的撤销和重做的时候,可以使用备忘录模式,在命令操作的时候记录下操作前后的状态,然后在命令撤销和重做的时候,直接使用相应的备忘录对象来恢复状态就可以了。
在这种撤销的执行顺序和重做执行顺序可控的情况下,备忘录对象还可以采用增量式记录的方式,可以减少缓存的数据量。
-
备忘录模式和原型模式组合使用。
在原发器对象创建备忘录对象的时候,如果原发器对象中全部或者大部分的状态都需要保存,一个简洁的方式就是直接克隆一个原发器对象。也就是说,这个时候备忘录对象里面存放的是一个原发器对象的实例。
- 结构:
- 代码:
使用备忘录模式时首先应该存在一个原发器类Originator,在真实业务中,原发器类是一个具体的业务类,它包含一些用于存储成员数据的属性,典型代码如下:
package dp.memento;
public class Originator { private String state; public Originator(){ } // 创建一个备忘录对象 public Memento createMemento() { return new Memento(this); } // 根据备忘录对象恢复原发器状态 public void restoreMemento(Memento m) { state = m.state; } public void setState(String state) { this.state=state; } public String getState() { return this.state; }
}
对于备忘录类Memento而言,它通常提供了与原发器相对应的属性(可以是全部,也可以是部分)用于存储原发器的状态
package dp.memento;
class Memento { private String state; public Memento(Originator o) { state = o.getState(); } public void setState(String state) { this.state=state; } public String getState() { return this.state; }
}
对于负责人类Caretaker,它用于保存备忘录对象,并提供getMemento()方法用于向客户端返回一个备忘录对象,原发器通过使用这个备忘录对象可以回到某个历史状态。:
package dp.memento;
public class Caretaker { private Memento memento; public Memento getMemento() { return memento; } public void setMemento(Memento memento) { this.memento=memento; }
}
- 适用环境
- 保存一个对象在某一个时刻的全部状态或部分状态,这样以后需要时它能够恢复到先前的状态,实现撤销操作。
- 防止外界对象破坏一个对象历史状态的封装性,避免将对象历史状态的实现细节暴露给外界对象。
2.3.7 观察者模式
- 观察者模式(Observer Pattern):定义对象间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。
tag:对象行为型模式,满足“开闭原则”
本质:触发联动
应用:猫、狗与老鼠 ,自定义登录控件 ,商品打折信息
diff:
中介者 | 观察者 |
---|---|
主体为同事类,同事类调用依赖方法 | 主体为subject,调用notify()通知Observer |
MVC模式是一种架构模式,它包含三个角色:模型(Model),视图(View)和控制器(Controller)。观察者模式可以用来实现MVC模式,观察者模式中的观察目标就是MVC模式中的模型(Model),而观察者就是MVC中的视图(View),控制器(Controller)充当两者之间的中介者(Mediator)。
- 结构:
- 代码:
典型的抽象目标类:
import java.util.*;
abstract class Subject {//定义一个观察者集合用于存储所有观察者对象 protected ArrayList<Observer> observers= new ArrayList<Observer> ( );//注册方法,用于向观察者集合中增加一个观察者 public void attach(Observer observer) { observers.add(observer); }//注销方法,用于在观察者集合中删除一个观察者 public void detach(Observer observer) { observers.remove(observer); }//声明抽象通知方法 public abstract void notify();
}
具体目标类ConcreteSubject:
class ConcreteSubject extends Subject {//实现通知方法 public void notify() {//遍历观察者集合,调用每一个观察者的响应方法 for(Object obs:observers) {((Observer)obs).update();}}
}
抽象观察者角色一般定义为接口,通常只声明一个update()方法,为不同观察者的更新(响应)行为定义相同的接口:
interface Observer { //声明响应方法 public void update();
}
具体观察者ConcreteObserver:
class ConcreteObserver implements Observer { //实现响应方法 public void update() { //具体响应代码 }
}
- 适用环境
-
一个抽象模型有两个方面,其中一个方面依赖于另一个方面,将这两个方面封装在独立的对象中使它们可以各自独立地改变和复用。
-
一个对象的改变将导致一个或多个其他对象也发生改变,而并不知道具体有多少对象将发生改变,也不知道这些对象是谁。
-
需要在系统中创建一个触发链,A对象的行为将影响B对象,B对象的行为将影响C对象……,可以使用观察者模式创建一种链式触发机制。
2.3.8 状态模式
- 状态模式(State Pattern):允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。状态模式将一个对象的状态从该对象中分离出来,封装到专门的状态类中,使得对象状态可以灵活变化,对于客户端而言,无须关心对象状态的转换以及对象所处的当前状态,无论对于何种状态的对象,客户端都可以一致处理
tag:对象行为型模式,对“开闭原则”的支持并不太好(增加新的状态类需要修改那些负责状态转换的源代码,简单状态模式遵循)
本质:根据状态来分离和选择行为。
应用:论坛用户等级、银行账户、政府OA办公系统中、RPG游戏
扩展:如果存在多个环境类,它们之间共享一个状态,状态定义为环境的静态成员对象
diff:
| 都是在状态发生改变的时候触发行为 |
状态模式 | 观察者模式 |
---|---|
根据状态来选择不同的处理 | 行为是固定的,那就是通知所有的观察者 |
结构:
- *Context和State类多为双向关联,让State可以改变状态
-
代码:
状态模式的关键是引入了一个抽象类来专门表示对象的状态,而对象的每一种具体状态类都继承了该类,并在不同具体状态类中实现了不同状态的行为,包括各种状态之间的转换。
典型的抽象状态类代码:
abstract class State { //声明抽象业务方法,不同的具体状态类可以不同的实现 public abstract void handle();
}
典型的具体状态类:
class ConcreteState extends State { public void handle() { //方法具体实现代码 }
}
环境类维持一个对抽象状态类的引用:
class Context { private State state; //维持一个对抽象状态对象的引用 private int value; //其他属性值,该属性值的变化可能会导致对象状态发生变化 public void setState(State state) { //注入不同的具体状态对象this.state = state; } public void request() { //其他代码 state.handle(); //调用状态对象的业务方法 //其他代码 }
}
状态转换可以包含两种方法:changeState方法放在环境类或者状态类来决定
放在状态类可设函数checkState,每次执行后测状态是否改变
//class Context
public void changeState() { if (value == 0) { //判断属性值,根据属性值进行状态转换 this.setState(new ConcreteStateA()); } else if (value == 1) { this.setState(new ConcreteStateB()); }
}
//class ConcreteState
public void changeState(Context ctx) { if (ctx.getValue() == 1) { //根据环境对象中的属性值进行状态转换 ctx.setState(new ConcreteStateB()); } else if (ctx.getValue() == 2) { ctx.setState(new ConcreteStateC()); }
}
- 适用环境
- 对象的行为依赖于它的状态(如某些属性值),状态的改变将导致行为的变化。
- 在代码中包含大量与对象状态有关的条件语句,这些条件语句的出现,会导致代码的可维护性和灵活性变差,不能方便地增加和删除状态,并且导致客户类与类库之间的耦合增强。
2.3.9 策略模式
- 策略模式(Strategy Pattern)(政策模式(Policy)):定义一系列算法,将每一个算法封装起来,并让它们可以相互替换。策略模式让算法独立于使用它的客户而变化。策略模式只适用于客户端知道所有的算法或行为的情况。
tag:对象行为型模式、满足“开闭原则”
本质:分离算法,选择实现。
应用:排序策略、旅游出行策略 、Java SE中GUI控件进行布局(Layout)
diff:
状态模式 | 策略模式 | |
---|---|---|
决定是否使用 | 系统中某个类的对象存在多种状态,不同状态下行为有差异,而且这些状态之间可以发生转换时; | 如果系统中某个类的某一行为存在多种实现方式,而且这些实现方式可以互换时。 |
用户 | 需要知道所选的具体策略是哪一个 | 客户端无须关心具体状态,环境类的状态会根据用户的操作自动转换 |
结构 | 具体策略类无须关心环境类 | 而状态模式中具体状态往往需要维护一个环境类的引用,以便通过该引用实现状态的切换,因此环境类和状态类之间存在一种双向的关联关系。 |
- 结构:
-
代码:
使用策略模式时,需要将算法从Context类中提取出来,首先应该创建一个抽象策略类:
abstract class AbstractStrategy {
public abstract void algorithm(); //声明抽象算法
}
然后再将封装每一种具体算法的类作为该抽象策略类的子类:
class ConcreteStrategyA extends AbstractStrategy {
//算法的具体实现
public void algorithm() {
//算法A
}
}
对于Context类而言,在它与抽象策略类之间建立一个关联关系(更多例子画为聚合??):
class Context { private AbstractStrategy strategy; //维持一个对抽象策略类的引用
public void setStrategy(AbstractStrategy strategy) {
this.strategy= strategy;
}
//调用策略类中的算法
public void algorithm() {
strategy.algorithm();
}
}
- 适用环境
- 一个系统需要动态地在几种算法中选择一种。
- 一个对象有很多的行为,把这些行为转移到相应的具体策略类里面。
- 不希望客户端知道复杂的、与算法相关的数据结构,在具体策略类中封装算法与相关的数据结构,可以提高算法的保密性与安全性。
2.3.10 模板方法模式
- 模板方法模式(Template Method Pattern):定义一个操作中算法的框架,而将一些步骤延迟到子类中。模板方法模式使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
tag:类行为型模式,基于继承的代码复用技术,很好地体现了设计原则中的开闭原则和里式替换原则
本质:固定算法骨架
应用:银行业务办理流程、数据库操作模板、应用于框架设计(如Spring,Struts等)
diff:
模板方法模式 | 策略模式 |
---|---|
封装的是算法的骨架 | 把某个步骤的具体实现算法封装起来 |
可以在模板方法中使用策略模式,就是把那些变化的算法步骤通过使用策略模式来实现。
- 结构:只存在父类与子类之间的继承关系
-
代码:
模板方法是定义在抽象类中的、把基本操作方法组合(可为具体或抽象)在一起形成一个总算法或一个总行为的方法。由子类不加以修改地完全继承下来。是具体方法,因此模板方法模式中的抽象层只能是抽象类,而不是接口。
基本方法是实现算法各个步骤的方法,是模板方法的组成部分。基本方法分为三种:抽象方法(Abstract Method)、具体方法(Concrete Method)和钩子方法(Hook Method)。
-
抽象方法在抽象类声明、由其具体子类实现。
-
具体方法在一个抽象类或具体类声明并实现,其子类可以进行覆盖也可以直接继承。
-
钩子方法在一个抽象类或具体类声明并实现,而其子类可能会加以扩展。通常在父类中给出的实现是一个空实现,并以该空实现作为方法的默认实现,当然钩子方法也可以提供一个非空的默认实现。
钩子方法有两类:
第一类钩子方法可以与一些具体步骤挂钩,以实现在不同条件下执行模板方法中的不同步骤,这类钩子方法的返回类型通常是bool类型的,这类方法名一般为IsXXX(),用于对某个条件进行判断,如果条件满足则执行某一步骤,否则将不执行,如下代码片段所示:
public void TemplateMethod() { //模板方法 Open(); Display(); if (IsPrint()) { Print(); } //通过钩子方法来确定某步骤是否执行 } public bool IsPrint() { //钩子方法 return true; /*如果不希望某方法执行,可在其子类中覆盖钩子方法,将其返回值改为false即可,这种类型的钩子方法可以控制方法的执行,对一个算法进行约束。*/ }
第二类钩子方法就是实现体为空的具体方法,子类可以根据需要覆盖或者继承这些钩子方法,与抽象方法相比,可以不用覆盖父类定义的钩子方法,但是如果没有覆盖父类中声明的抽象方法,编译将报错。
抽象类的典型代码:
-
abstract class AbstractClass {//模板方法public void TemplateMethod() {PrimitiveOperation1();PrimitiveOperation2();PrimitiveOperation3();}/*对于所有子类都相同的基本方法可在父类提供具体实现,否则在父类声明为抽象方法或钩子方法,由不同的子类提供不同的实现。*///基本方法—具体方法public void PrimitiveOperation1() { //实现代码 }//基本方法—抽象方法public abstract void PrimitiveOperation2 ();//基本方法—钩子方法public virtual void PrimitiveOperation3 () {}
}
抽象类的子类中提供抽象步骤的实现,也可覆盖父类中已经实现的具体方法:
class ConcreteClass: AbstractClass { public override void PrimitiveOperation2() { //实现代码} public override void PrimitiveOperation3() { //实现代码 }
}
- 适用环境
-
对一些复杂的算法进行分割,将其算法中固定不变的部分设计为模板方法和父类具体方法,而一些可以改变的细节由其子类来实现。即:一次性实现一个算法的不变部分,并将可变的行为留给子类来实现。
-
各子类中公共的行为应被提取出来并集中到一个公共父类中以避免代码重复。
-
需要通过子类来决定父类算法中某个步骤是否执行,实现子类对父类的反向控制。