结构型模式
这次主要说结构型模式。这类模式还是在各种场合比较常见的。
适配器模式
这个模式主要用来解决一些接口不相互兼容的情况。举个例子:现在有一类电器,都是插在110V的电压的电源上。然而电源现在只有220V的。为了让电器能够用上电,必须在220V电源和110V用电器之间弄一个转换器把电压转成110V。
在这里需要明确一下这个模式的几个参与者:
参与者
- Client:使用者,与符合Target接口的对象协同工作。这个例子里就是用电器。
- Target:与Client使用的特定领域相关的接口。这里就是符合110V电压标准的那类电源插孔。
- Adaptee:已经存在的接口,它需要适配。这里就是220V电源。
- Adapter:适配器,对Adaptee的接口与Target的接口进行适配。这个例子里就是110V转换器。
这里有个问题:为什么需要一个Target,直接用Adapter不可以么?回答:Client需要的是符合某个接口规范的一类类,而不是具体的某个类。Adapter是一定实现了Target的接口的。就像转换器可以有很多种类和品牌,有些甚至可以带一些无关的花哨的功能,但是一定都具有转换功能并提供一个符合要求的输出插孔。
这个设计模式有两种实现方法:
- 使用多重继承,Adapter继承Target和Adaptee。
- 使用对象组合。Adapter仍然继承Target且拥有Adaptee对象实例。
对于多重继承的方法,Adapter应该私有继承Adaptee(防止公有接口暴露)。
这个例子的代码写下来应该是这样的: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19class PowerSupport110{
public:
virtual Power110 getPower110();
};//Target
class ChinaPowerSupport220{
public:
virtual Power220 getPower220(){
//......
return /*......*/;
}
};//Adaptee
class MyAdapter110:public PowerSupport110, private ChinaPowerSupport220 {
public:
virtual Power110 getPower110(){
return convert110(getPower220());
}
};//Adapter
代理模式
这个模式为访问某个对象提供了控制手段。举2个例子:
- 1.打开网页的时候某些大的图片、动画可能要过一段时间才能加载出来,但是网页提供了一个占位符,即使没加载出来,我们也可以查看图片的信息,控制重新加载内容,或者另存为到本地。
- 2.C++里的智能指针(比如shared_ptr)。
参与者
这个模式的参与者有如下的几个(以第1个网站的例子说明):
- Subject:声明了RealSubject和Proxy的共同接口,这样任何使用RealSubject的地方都可以使用Proxy。这个例子中就是网页里的图片(准确说是图片的接口,因为操作都是针对接口的)。
- Proxy:内部含有RealSubject的引用,从而可以操作RealSubject对象;提供一个与Subject接口相同的接口,这样就可以替代实体;控制实体的存取,并可能负责创建和删除它。还有其它功能依赖于代理的类型。
- RealSubject:定义了Proxy代表的实体。
代理的类型:
- 1.远程代理:为一个对象在不同的地址空间提供局部代表。
- 2.虚代理:根据需求创建开销很大的对象。(占位符的例子)
- 3.保护代理:控制对原始对象的访问(比如希望它们有不同的访问权限)。
- 4.智能指引:在访问对象的同时执行一些附加的操作(如:C++的shared_ptr)。
以图片加载的例子为例的代码:
1 |
|
装饰器模式
有时候我们希望动态的给对象添加职责(不是整个类,而是某个对象),就可以用这个模式。
整个模式中的参与者:
- Component:定义了一个对象接口,可以给这些对象动态添加职责。它是抽象的。
- ConcreteComponent:定义了对象,可以给这些对象动态添加职责。它继承自Component。可以给这个对象添加职责。
- Decorator:维持一个指向Component对象的指针,且定义了与Component接口一致的接口。
- ConcreteDecorator:维持一个指向Component对象的指针,且定义了与Component接口一致的接口。
应用举例
比较典型的装饰器的用法是Java里各种I/O流。比如:InputStream这个抽象类是Component。FileInputStream是ConcreteComponent,它实现了InputStream的read接口,然而它除了简单的read一些东西外,并没有什么高级功能。
如果希望扩展出高级功能就需要Decorator。Java中比较典型的是FilterInputStream(相当于Decorator)。它的子类:BufferedInputStream、DataInputStream就是ConcreteDecorator。这两个ConcreteDecorator的构造函数的参数都是InputStream类型。
BufferedInputStream是带缓冲的,DataInputStream的作用是从流中读出不同的Java数据类型。那么,如果用BufferedInputStream修饰FilterInputStream,再用DataInputStream修饰刚才的BufferedInputStream,就可以从带缓冲的文件流中读各种Java数据类型。 即: 1
DataInputStream inputstream = new DataInputStream(new BufferedInputStream(new FileInputStream("Data.txt")));
下面是一份Java代码, 代码中View对象被滚动条修饰后又被边框Border修饰: 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
50
51
52
53
54
55
56
57
58
59package com.vbill;
interface VisualComponent{
void Draw();
}
class View implements VisualComponent{
public void Draw(){
System.out.println("Drawing view!");
}
}
abstract class Decorator implements VisualComponent{
private VisualComponent comp;
public Decorator(VisualComponent comp){
this.comp = comp;
}
public void Draw(){
comp.Draw();
}
}
class Border extends Decorator{
public Border(VisualComponent comp){
super(comp);
}
public void Draw(){
super.Draw();
System.out.println("Drawing border!");
}
}
class ScrollBar extends Decorator{
public ScrollBar(VisualComponent comp){
super(comp);
}
public void Draw(){
super.Draw();
System.out.println("Drawing scrollbar!");
}
}
public class Main {
public static void main(String[] args) {
Border b = new Border(new ScrollBar(new View()));
b.Draw();
}
}
外观模式
如果现在有一个很复杂的系统(编译器之类的),但是用户不关心这个复杂系统的内部各个子系统的工作细节,那么可以引入一个Facade类,由它提供我们关心的部分,并负责将我们的请求代理给合适的子系统对象(比如我们要编译程序,编译器的lexer, parser等等部件我们不关心,我们只要一系列编译指令和能设置的参数选项)。
举个例子:现在有个工厂,某客户要进货。工厂有3个部门分别负责订单处理,生产,出货。
在没有外观模式之前可能是这样的(以下是Java代码): 1
2
3
4
5
6
7
8
9
10// 以下代码写在客户代码里
// 依次创建三个部门实例
Order order = new Order();
Producer producer = new Producer();
Sender sender = new Sender();
// 依次调用三个部门实例的方法
order.order(1000); //订1000件
Goods goods = producer.produce(order);
sender.send(goods);
使用外观模式: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21public class Factory {
//定义被封装的三个部门
Order order;
Producer producer;
Sender sender;
// 构造器
public Factory() {
this.order = new Order();
this.producer = new producer();
this.sender = new Sender();
}
public Goods buyThings(int num) {
//把整个流程封装
order.order(num); //订num件
Goods goods = producer.produce(order);
sender.send(goods);
return goods;
}
}
然后就可以使用了: 1
2Factory f = new Factory();
Goods g = f.buyThings(1000);
桥接模式
这个模式可以实现抽象与实现分离,使得它们可以各自独立变化。
举例:现在有一个Window类,它提供了基本的绘图功能(绘制文字,矩形)。其它各种具体的Window里的绘图功能都是基于Window里这些基本操作实现的。现在我们希望Window类跨平台,或者运行时修改基本绘图功能的实现。如果使用继承机制,那么必须位每个平台或实现继承出一个类。因为继承是静态的,所以每增加一个平台Window的每种子类都要继承出一个对应平台的类,这是很麻烦的。
改进的方法是:把平台相关的部分操作独立出来。因为这些操作接口相同,因此可以独立成一个抽象类(比如叫WindowImp,它包含绘制文字、矩形的接口)。那么这些独立出来的每种平台的具体实现就继承自这个独立出的抽象类。然后在原来的Window类里维护一个抽象类的指针。原来用到的平台相关的操作,现在都通过指针指向的对象来实现。当需要改变这部分实现的时候,只让这个指针指向其它版本的实现就可以了。
模式的参与者
- Abstraction:抽象部分的接口。维护一个Implementor的对象指针。
- RefinedAbstraction:扩展Abstraction的接口。
- Implementor:定义实现类的接口,该接口不用和Abstraction的完全一致;事实上它们可以完全不同。一般Implementor接口提供基本操作,而Abstraction里定义了基于这些基本操作的较高层次的操作。
- ConcreteImplementor:真正实现Implementor接口的对象。
这个模式之所以叫”桥接“,就是因为Abstraction和Implementor(本例中的WindowImp是Implementor,Abstraction是Window)起到了桥梁作用,将各个平台具体的实现ConcreteImplementor和RefinedAbstraction(各类具体的Window)联系了起来。
下面是例子的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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85package com.vbill;
interface WindowImp{
void DevDrawText();
void DevDrawLine();
}
class LinuxWindowImp implements WindowImp{
public void DevDrawText() {
System.out.println("Drawing text on linux");
}
public void DevDrawLine() {
System.out.println("Drawing line on linux");
}
}
class WindowsWindowImp implements WindowImp{
public void DevDrawText() {
System.out.println("Drawing text on Windows");
}
public void DevDrawLine() {
System.out.println("Drawing line on Windows");
}
}
abstract class Window{//声明为abstract 防止实例化
private WindowImp imp;
protected void DrawText(){
imp.DevDrawText();
}
protected void DrawRect(){
imp.DevDrawLine();
imp.DevDrawLine();
imp.DevDrawLine();
imp.DevDrawLine();
System.out.println("Rect ok!");
}
public Window(){imp = new WindowsWindowImp();}
public void ChangeAPI(){
if(imp instanceof WindowsWindowImp){
imp = new LinuxWindowImp();
}else{
imp = new WindowsWindowImp();
}
}
}
class BigWindow extends Window{
public void DrawBigWindow(){
System.out.println("Big window ok!");
DrawRect();
DrawText();
}
}
class SmallWindow extends Window{
public void DrawSmallWindow(){
System.out.println("Small window ok!");
DrawRect();
}
}
public class Main {
public static void main(String[] args) {
BigWindow b = new BigWindow();
b.DrawBigWindow();
b.ChangeAPI();
b.DrawBigWindow();
SmallWindow s = new SmallWindow();
s.DrawSmallWindow();
s.ChangeAPI();
s.DrawSmallWindow();
}
}
组合模式
将对象组合成树形结构以表示”整体-部分“的层次结构。Composite使得用户对单个对象和组合对象的使用具有一致性。
举个最简单的例子:做PPT的时候,Powerpoint提供了许多基本图形。我们可以画几个基本图形,然后点击”组合“。这样就行成了组合图形。而且软件对新的组合图形还有基本图形的操作基本是一样的。这种方式在其它许多绘图软件里也很常见。
这个模式的关键在于提供了一个抽象类。基本的图形类继承自它,图形的组合容器类也继承自它。这样用户操纵抽象类的时候,基本图形和容器看上去就是一样的。
这样看上取,对象的组合关系就像一棵树一样,是递归的。
模式的参与者
Component
--为组合中的对象声明接口。
--在适当情况下实现所有类共有接口的缺省行为。
--声明一个接口,用于访问和管理Component的子组件
--(可选)在递归结构中定义一个接口,用于访问一个父部件,并在合适的情况下实现它。
在上面例子中,把图形抽象成Graphic类,就是Component。
Leaf
--在组合中表示叶节点对象。叶节点没有子节点
显然基本的图形就是Leaf。
Composite
--定义有子部件的那些部件的行为。
--存储子部件。
--在Component接口中实现与子部件有关的操作。
显然容器类就是Composite。它可以存放多个基本图形和容器的组合。本例中可以命名它为Pic。
Client
--通过Component的接口操纵组合部件。
这里有个问题:模式中所有的类均继承自Component。但是Component提供了许多对Leaf而言不必要的接口,比如添加/删除组合部件,返回孩子部件的代码。解决这个问题的方法是,在Component中提供默认的操作(什么都不做,返回无效值,抛出异常等等),然后在Leaf型的对象中不要重写这些无关的方法,在用到这些接口方法的Composite类型中重写这些方法。
下面是例子的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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86package com.vbill;
import java.util.*;
abstract class PicComponent{
abstract void show();
void add(PicComponent p){}
void remove(PicComponent p){}
PicComponent getChild(int c){ return null; }
}
class Rectangle extends PicComponent{
public void show() {
System.out.println("Rectangle");
}
}
class Text extends PicComponent{
public void show() {
System.out.println("Text");
}
}
class Triangle extends PicComponent{
public void show() {
System.out.println("Triangle");
}
}
class Picture extends PicComponent{
private LinkedList<PicComponent> comps;
public Picture(){
comps = new LinkedList<PicComponent>();
}
public void show() {
System.out.println("{");
for(PicComponent p : comps){
p.show();
}
System.out.println("}");
}
public void add(PicComponent p) {
comps.add(p);
}
public void remove(PicComponent p) {
comps.remove(p);
}
public PicComponent getChild(int c) {
return comps.get(c);
}
}
public class Main {
public static void main(String[] args) {
Picture bigpic = new Picture();
Text txt = new Text();
Triangle tri = new Triangle();
Rectangle rect = new Rectangle();
Picture smallpic = new Picture();
smallpic.add(txt);
smallpic.add(tri);
smallpic.add(rect);
Rectangle rect2 = new Rectangle();
bigpic.add(rect2);
bigpic.add(smallpic);
bigpic.show();
}
}
享元模式
该模式运用共享技术,支持大量细粒度对象。
比如:现在需要开发一个文本编辑器。其中文本框内每个字符都有自己的字形、位置等等信息。如果为每个字符都创建一个对象实例,那么内存空间的开销将会非常巨大。
实际上在创建对象的时候有写信息是可以共享的。比如,文字可能是ASCII字符集里的。那么每个对象就不必维护自己的字形,只需要记录自己的位置就可以了。
到这里就涉及2个概念:
- 外部状态:会随着不同的场景变化,这些信息不能被共享。
- 内部状态:独立于具体场景的信息,这些信息使得flyweight可以被共享。
那么,这个例子里字形就是内部状态,位置就是外部状态。
模式的参与者
Flyweight:
--描述一个接口,通过它flyweight可以接受并作用于外部状态。
ConcreteFlyweight: --实现Flyweight接口,并为内部状态(如果有)增加存储空间。ConcreteFlyweight对象必须是可共享的。它所存储的状态必须是内部的(独立于ConcreteFlyweight)对象的场景。
UnsharedConcreteFlyweight:
--Flyweight接口使得共享成为可能,但是也可以有一些对象不被共享。比如:在文本编辑器里,多个字符可以组成一个行对象。这样ConcreteFlyweight对象(字符)就是UnsharedConcreteFlyweight的子节点。
FlyweightFactory:
--创建并管理Flyweight对象。当用户请求一个Flyweight的时候,它提供一个已经创建的实例,或创建一个(如果不存在的话)。
Client:
--维持对Flyweight的引用,计算/存储多个Flyweight的外部状态。
如果外部状态是算出来的而不是存储起来的,空间节约将达到最大。比如:字符在屏幕中的位置是根据窗口尺寸和它是第几个字符算出来的。