设计模式之桥梁模式和策略模式的区别
最后更新于:2022-04-01 11:05:33
桥接(Bridge)模式是结构型模式的一种,而策略(strategy)模式则属于行为模式。以下是它们的UML结构图。
桥梁模式:
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-20_57175853e617e.jpg "")
策略模式:
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-21_571890ecc9f8f.jpg "")
在桥接模式中,Abstraction通过聚合的方式引用Implementor。
### 举一个例子:
**策略模式:**我要画圆,要实心圆,我可以用solidPen来配置,画虚线圆可以用dashedPen来配置。这是strategy模式。
**桥接模式:**同样是画圆,我是在windows下来画实心圆,就用windowPen+solidPen来配置,在unix下画实心圆就用unixPen+solidPen来配置。如果要再windows下画虚线圆,就用windowsPen+dashedPen来配置,要在unix下画虚线圆,就用unixPen+dashedPen来配置。
画圆方法中,策略只是考虑算法的替换,而桥接考虑的则是不同平台下需要调用不同的工具,接口只是定义一个方法,而具体实现则由具体实现类完成。
### 区别:
桥接模式:不仅Implementor具有变化(ConcreteImplementor),而且Abstraction也可以发生变化(RefinedAbstraction),而且两者的变化是完全独立的,RefinedAbstraction与ConcreateImplementor之间松散耦合,它们仅仅通过Abstraction与Implementor之间的关系联系起来。强调Implementor接口仅提供基本操作,而Abstraction则基于这些基本操作定义更高层次的操作。
策略模式:并不考虑Context的变化,只有算法的可替代性。强调Strategy抽象接口的提供的是一种算法,一般是无状态、无数据的,Context简单调用这些算法完成其操作。
所以相对策略模式,桥接模式要表达的内容要更多,结构也更加复杂。
桥接模式表达的主要意义其实是接口隔离的原则,即把本质上并不内聚的两种体系区别开来,使得它们可以松散的组合,而策略在解耦上还仅仅是某一个算法的层次,没有到体系这一层次。
从结构图中可以看到,策略模式的结构是包容在桥接模式结构中的,Abstraction与Implementor之间就可以认为是策略模式,但是桥接模式一般Implementor将提供一系列的成体系的操作,而且Implementor是具有状态和数据的静态结构。而且桥接模式Abstraction也可以独立变化。
参考:[http://c610367182.iteye.com/blog/1965668](http://c610367182.iteye.com/blog/1965668)
[http://www.blogjava.net/wangle/archive/2007/04/25/113545.html](http://www.blogjava.net/wangle/archive/2007/04/25/113545.html)
设计模式之UML(一)类图以及类间关系(泛化 、实现、依赖、关联、聚合、组合)
最后更新于:2022-04-01 11:05:31
类图用于描述系统中所包含的类以及它们之间的相互关系,帮助人们简化对系统的理解,它是系统分析和设计阶段的重要产物,也是系统编码和测试的重要模型依据。接下来我们就来谈谈类图的组成,在下一篇中我们将讨论一下类图之间的关系。
### 一、类图的组成
1. 普通类
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-21_571890ed22d4c.jpg "")
上图就是一个UML的普通类图,从上图我们看出,一个UML通常由三部分组成。
**第一部分是类名:**每个类都必须有一个名字,类名是一个字符串。
**第二部分是类的属性(Attributes):**属性是指类的性质,即类的成员变量。一个类可以有任意多个属性,也可以没有属性。
属性的格式:
~~~
可见性 名称:类型 [ = 缺省值 ]
~~~
其中:
**可见性**表示该属性对于类外的元素而言是否可见,包括公有(public)、私有(private)和受保护(protected)三种,在类图中分别用符号+、-和#表示。
**名称**表示属性名,用一个字符串表示。
**类型**表示属性的数据类型,可以是基本数据类型,也可以是用户自定义类型。
**缺省值**是一个可选项,即属性的初始值。
**第三部分是类的操作(Operations):**操作是类的任意一个实例对象都可以使用的行为,是类的成员方法。
操作的格式:
~~~
可见性 名称(参数列表) [ : 返回类型]
~~~
其中:
**可见性**的定义与属性的可见性定义相同。
**名称**即方法名,用一个字符串表示。
**参数列表**表示方法的参数,其语法与属性的定义相似,参数个数是任意的,多个参数之间用逗号“,”隔开。
**返回类型**是一个可选项,表示方法的返回值类型,依赖于具体的编程语言,可以是基本数据类型,也可以是用户自定义类型,还可以是空类型(void),如果是构造方法,则无返回类型。
**下划线**表示这个方法是静态方法。
2. 抽象类
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-21_571890ed356a1.jpg "")
就像上图展示的,抽象类和普通类的区别就是抽象类的名称是用斜体写的,并且抽象类中的抽象方法也是斜体的。
3. 接口
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-21_571890ed4697e.jpg "")
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-21_571890ed59ab5.jpg "")
接口有上边两种方法,第一种方法是在类的前边加上<>,这样这个类就变成了接口类;第二种方法是先声明一个接口,圆圈旁边就是接口的名称,然后与实现它的类联系起来,对第二种方法的表示我个人还是不太能理解,圆圈里并没有写这个接口有什么方法啊,实现它的类中到时有接口的方法,但是又怎么能区分的出哪些是接口的哪些是本身的呢?还请知道的大神讲解一下。
### 二、类图中的关系
1. 关联关系
关联(Association)关系是类与类之间最常用的一种关系,它是一种结构化关系,用于表示一类对象与另一类对象之间有联系,如汽车和轮胎、师傅和徒弟、班级和学生等等。在UML类图中,用实线连接有关联关系的对象所对应的类,在使用Java、C#和C++等编程语言实现关联关系时,**通常将一个类的对象作为另一个类的成员变量**。在使用类图表示关联关系时可以在关联线上标注角色名,一般使用一个表示两者之间关系的动词或者名词表示角色名(有时该名词为实例对象名),关系的两端代表两种不同的角色,因此在一个关联关系中可以包含两个角色名,角色名不是必须的,可以根据需要增加,其目的是使类之间的关系更加明确。
在UML中,关联关系通常又包含如下几种形式:
**(1) 双向关联**
默认情况下,关联是双向的。例如:顾客(Customer)购买商品(Product)并拥有商品,反之,卖出的商品总有某个顾客与之相关联。因此,Customer类和Product类之间具有双向关联关系,如图所示:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-21_571890ed70d3c.jpg)
**(2) 单向关联**
类的关联关系也可以是单向的,单向关联用带箭头的实线表示。例如:顾客(Customer)拥有地址(Address),则Customer类与Address类具有单向关联关系,如图所示:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-21_571890ed8a301.jpg)
**(3) 自关联**
在系统中可能会存在一些类的属性对象类型为该类本身,这种特殊的关联关系称为自关联。例如:一个节点类(Node)的成员又是节点Node类型的对象,如图所示:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-21_571890eda0ac3.jpg)
**(4) 多重性关联**
多重性关联关系又称为重数性(Multiplicity)关联关系,表示两个关联对象在数量上的对应关系。在UML中,对象之间的多重性可以直接在关联直线上用一个数字或一个数字范围表示。
对象之间可以存在多种多重性关联关系,常见的多重性表示方式如表所示:
| 表示方式 | 多重性说明 |
|-----|-----|
| 1..1 | 表示另一个类的一个对象只与该类的一个对象有关系 |
| 0..* | 表示另一个类的一个对象与该类的零个或多个对象有关系 |
| 1..* | 表示另一个类的一个对象与该类的一个或多个对象有关系 |
| 0..1 | 表示另一个类的一个对象没有或只与该类的一个对象有关系 |
| m..n | 表示另一个类的一个对象与该类最少m,最多n个对象有关系 (m≤n) |
例如:一个界面(Form)可以拥有零个或多个按钮(Button),但是一个按钮只能属于一个界面,因此,一个Form类的对象可以与零个或多个Button类的对象相关联,但一个Button类的对象只能与一个Form类的对象关联,如图所示
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-21_571890edbd60f.jpg)
2. 聚合关系
**聚合是关联关系的一种特例,他体现的是整体与部分、拥有的关系,即has-a的关系**,此时整体与部分之间是可分离的,他们可以具有各自的生命周期,部分可以属于多个整体对象,也可以为多个整体对象共享在UML中,聚合关系用带空心菱形的直线表示。例如:汽车发动机(Engine)是汽车(Car)的组成部分,但是汽车发动机可以独立存在,因此,汽车和发动机是聚合关系,如图所示:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-21_571890edd39b7.jpg)
在代码实现聚合关系时,成员对象通常作为构造方法、Setter方法或业务方法的参数注入到整体对象中。
3.组合关系
**组合也是关联关系的一种特例,他体现的是一种contains-a的关系**,这种关系比聚合更强,也称为强聚合;他同样体现整体与部分间的关系,但此时整体与部分是不可分的,整体的生命周期结束也就意味着部分的生命周期结束。在UML中,组合关系用带实心菱形的直线表示。例如:人的头(Head)与嘴巴(Mouth),嘴巴是头的组成部分之一,而且如果头没了,嘴巴也就没了,因此头和嘴巴是组合关系,如图所示
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-21_571890edf069a.jpg)
4.依赖关系
可以简单的理解,就是**一个类A使用到了另一个类B**,而这种使用关系是具有偶然性的、临时性的、非常弱的,但是B类的变化会影响到A;比如某人要过河,需要借用一条船,此时人与船之间的关系就是依赖;表现在代码层面,为类B作为参数被类A在某个method方法中使用;
在UML中,依赖关系用带箭头的虚线表示,由依赖的一方指向被依赖的一方。例如:驾驶员开车,在Driver类的drive()方法中将Car类型的对象car作为一个参数传递,以便在drive()方法中能够调用car的move()方法,且驾驶员的drive()方法依赖车的move()方法,因此类Driver依赖类Car,如图所示:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-21_571890ee15224.jpg)
5.泛化关系
**泛化(Generalization)关系也就是继承关系,用于描述父类与子类之间的关系**,父类又称作基类或超类,子类又称作派生类。在UML中,泛化关系用带空心三角形的直线来表示。在代码实现时,我们使用面向对象的继承机制来实现泛化关系,如在Java语言中使用extends关键字、在C++/C#中使用冒号“:”来实现。例如:Student类和Teacher类都是Person类的子类,Student类和Teacher类继承了Person类的属性和方法,Person类的属性包含姓名(name)和年龄(age),每一个Student和Teacher也都具有这两个属性,另外Student类增加了属性学号(studentNo),Teacher类增加了属性教师编号(teacherNo),Person类的方法包括行走move()和说话say(),Student类和Teacher类继承了这两个方法,而且Student类还新增方法study(),Teacher类还新增方法teach()。如图所示:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-21_571890ee2d928.jpg)
6.实现关系
接口之间也可以有与类之间关系类似的继承关系和依赖关系,但是接口和类之间还存在一种实现(Realization)关系,在这种关系中,**类实现了接口,类中的操作实现了接口中所声明的操作**。在UML中,类与接口之间的实现关系用带空心三角形的虚线来表示。例如:定义了一个交通工具接口Vehicle,包含一个抽象操作move(),在类Ship和类Car中都实现了该move()操作,不过具体的实现细节将会不一样,如图所示:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-21_571890ee43b08.jpg)
### 三、关系之间的区别
1.聚合与组合
(1)聚合与组合都是一种结合关系,只是额外具有整体-部分的意涵。
(2)部件的生命周期不同
聚合关系中,整件不会拥有部件的生命周期,所以整件删除时,部件不会被删除。再者,多个整件可以共享同一个部件。
组合关系中,整件拥有部件的生命周期,所以整件删除时,部件一定会跟着删除。而且,多个整件不可以同时间共享同一个部件。
(3)聚合关系是“has-a”关系,组合关系是“contains-a”关系。
2.关联和聚合
(1)表现在代码层面,和关联关系是一致的,只能从语义级别来区分。
(2)关联和聚合的区别主要在语义上,关联的两个对象之间一般是平等的,例如你是我的朋友,聚合则一般不是平等的。
(3)关联是一种结构化的关系,指一种对象和另一种对象有联系。
(4)关联和聚合是视问题域而定的,例如在关心汽车的领域里,轮胎是一定要组合在汽车类中的,因为它离开了汽车就没有意义了。但是在卖轮胎的店铺业务里,就算轮胎离开了汽车,它也是有意义的,这就可以用聚合了。
3.关联和依赖
(1)关联关系中,体现的是两个类、或者类与接口之间语义级别的一种强依赖关系,比如我和我的朋友;这种关系比依赖更强、不存在依赖关系的偶然性、关系也不是临时性的,一般是长期性的,而且双方的关系一般是平等的。
(2)依赖关系中,可以简单的理解,就是一个类A使用到了另一个类B,而这种使用关系是具有偶然性的、临时性的、非常弱的,但是B类的变化会影响到A。
4.综合比较
这几种关系都是语义级别的,所以从代码层面并不能完全区分各种关系;但总的来说,后几种关系所表现的强弱程度依次为:
**组合>聚合>关联>依赖;**
设计模式(十二)责任链模式
最后更新于:2022-04-01 11:05:29
### 一、击鼓传花
击鼓传花是一种热闹而又紧张的饮酒游戏。在酒宴上宾客一次坐定位置,由一人击鼓,击鼓的地方与传花的地方是分开的,以示公正。开始击鼓时,花束就开始依次传递,鼓声一落,如果花束在某人手中,则该人就得饮酒。
比如说,贾母、贾赦、贾政、贾宝玉和贾环是五个参加击鼓传花游戏的传花者,他们组成一个环链。击鼓者将花传给贾母,开始传花游戏。花由贾母传给贾赦,贾赦传给贾政,贾政传给贾宝玉,贾宝玉传给贾环,贾环再传给贾母,由此往复,如下图所示。
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-21_571890ece22b0.jpg "")
击鼓传花便是一种典型的责任链模式。
### 二、什么是责任链模式
责任链模式是一种对象的行为模式。在责任链模式里,很多对象由每一个对象对其下家的引用而连接起来形成一条链。请求在这个链上传递,直到链上的某一个对象决定处理此请求。发出这个请求的客户端并不知道链上的哪一个对象最终处理这个请求,这使得系统可以在不影响客户端的情况下动态地重新组织和分配责任。
### 三、责任链模式的结构
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-21_571890ed0cc4a.jpg "")
● 抽象处理者(Handler)角色:定义出一个处理请求的接口。如果需要,接口可以定义 出一个方法以设定和返回对下家的引用。这个角色通常由一个Java抽象类或者Java接口实现。上图中Handler类的聚合关系给出了具体子类对下家的引用,抽象方法handleRequest()规范了子类处理请求的操作。
● 具体处理者(ConcreteHandler)角色:具体处理者接到请求后,可以选择将请求处理掉,或者将请求传给下家。由于具体处理者持有对下家的引用,因此,如果需要,具体处理者可以访问下家。
### 四、责任链模式的实例
我为更清楚的看出责任链模式的结构,我们先来看一看最简答的代码实现。
~~~
package com.designpattern.pre1;
/**
* 抽象处理者,定义处理者的接口
*
* @author 98583
*
*/
public abstract class Handler {
/**
* 下一个处理者
*/
protected Handler successor;
/**
* 每个处理者的处理方法
*/
public abstract void handleRequest();
/**
* 设置下一个处理者
*
* @param successor
*/
public void setSuccessor(Handler successor) {
this.successor = successor;
}
/**
* 获取下一个处理者
*
* @return
*/
public Handler getSuccessor() {
return successor;
}
}
~~~
~~~
package com.designpattern.pre1;
/**
* 具体处理者
*
* @author 98583
*
*/
public class ConcreteHandler extends Handler {
/**
* 处理方法
*/
public void handleRequest() {
/**
* 如果有下一个处理者就交给下一个处理者,但是实际情况中应该判断这个处理者能否处理这个问题,不能处理才传给下一个处理者
*/
if (getSuccessor() != null) {
System.out.println("The request is passed to " + getSuccessor());
getSuccessor().handleRequest();
}// 在这个处理者中处理
else {
System.out.println("The request is handled here.");
}
}
}
~~~
~~~
package com.designpattern.pre1;
public class Client {
static private Handler handler1, handler2;
public static void main(String[] args) {
/**
* 定义两个处理者
*/
handler1 = new ConcreteHandler();
handler2 = new ConcreteHandler();
handler1.setSuccessor(handler2);
handler1.handleRequest();
}
}
~~~
现在了解了责任链模式的基本结构,我们可以来实现上边的红楼梦中的击鼓传花的故事了。
~~~
package com.designpattern.chainofresp;
/**
* 相当于抽象的Handler
* @author 98583
*
*/
abstract class Player {
abstract public void handle(int i);
/**
* 下一位处理者
*/
private Player successor;
public Player() {
successor = null;
}
/**
* 设置下一个处理者
* @param aSuccessor
*/
protected void setSuccessor(Player aSuccessor) {
successor = aSuccessor;
}
/**
* 传给下一个处理者,这个方法本不应该出现在这,因为每个处理者中都有这个方法,所以就放到父类中来了
* @param index
*/
public void next(int index) {
if (successor != null) {
successor.handle(index);
} else {
System.out.println("Program terminated.");
}
}
}
~~~
~~~
package com.designpattern.chainofresp;
/**
* 贾母的类相当于ConcreteHandler
* @author 98583
*
*/
class JiaMu extends Player {
public JiaMu(Player aSuccessor) {
this.setSuccessor(aSuccessor);
}
/**
* 这个处理和的处理方法
*/
public void handle(int i) {
if (i == 1) {
System.out.println("Jia Mu gotta drink!");
} else {
System.out.println("Jia Mu passed!");
/**
* 传给下一个处理者
*/
next(i);
}
}
}
~~~
其他人的类与贾母的类相似,不再贴出,最后附上源码。
~~~
package com.designpattern.chainofresp;
/**
* 击鼓者,相当于客户端
* @author 98583
*
*/
public class DrumBeater
{
private static Player player;
static public void main(String[] args)
{
JiaMu jiaMu = new JiaMu(null);
jiaMu.setSuccessor( new JiaShe (
new JiaZheng(
new JiaBaoYu(
new JiaHuan( jiaMu ) ) ) ) );
player = jiaMu;
player.handle(4);
}
}
~~~
### 五、责任链模式
**优点:**
- 降低耦合度
- 可简化对象的相互连接
- 增强给对象指派职责的灵活性
- 增加新的请求处理类很方便
**缺点:**
- 不能保证请求一定被接收
- 系统性能将受到一定影响,而且在进行代码调试时不太方便;可能会造成循环调用
### 六、适用环境
1、有多个对象可以处理同一个请求,具体哪个对象处理该请求由运行时刻自动确定。
2、在不明确指定接收者的情况下,向多个对象中的一个提交一个请求。
3、可动态指定一组对象处理请求
[http://download.csdn.net/detail/xingjiarong/9329017](http://download.csdn.net/detail/xingjiarong/9329017)
设计模式(十一)策略模式
最后更新于:2022-04-01 11:05:27
### 一、打折的烦恼
有一家卖书的网站想做一套结算系统,其中的一部分就是计算书的价格,这家网站上的书基本上都有优惠,而且不同种类的书优惠不同,比如漫画书打9折,小说打6折等等,他们刚开始的设计是这样的。
方案一:在客户端进行判断
~~~
if(book is comic)
price*=0.9;
else if(book is novel)
price*=0.6;
~~~
看起来好像也没什么问题,但是当我们的书种类非常的时候,客户端的代码就会显得非常臃肿,并且每当我们添加一种新的书就要添加一个if-else语句,如果这本书下架了就要删除相应的语句,显得很麻烦。
方案二:
我们可以定义一个Book的抽象类,每种书都继承自这个类并且每个类中都实现自己的计算价格的方法,这样就避免了客户端的臃肿,但是这会将算法和书的实现耦合到一块,如果我们想要修改折扣的幅度,那么对应的书的类就要修改,不符合“开-闭”原则。
方案三:
我们将每一种计算价格的方法都封装成一个策略类,在客户端中可以动态的设定,这种书使用哪种策略,当有新的打折方式时,我们可以增加一个新的策略类,而不需要去修改书的代码,将书和算法完全分离开来,减小了耦合度,符合“开-闭”原则。
### 二、策略模式的定义和结构
上面的第三种方案就是我们说的策略模式。策略模式将每一个算法封装起来,并让它们可以相互替换。策略模式让算法独立于使用它的客户而变化。
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-21_571890ecc9f8f.jpg "")
策略模式涉及到三个角色:
环境(Context)角色:这个是要使用策略(算法)的类,持有一个Strategy类的引用。
抽象策略角色(Strategy):定义策略类应该有的接口。
具体策略角色(Concrete):具体实现相关的算法。
### 三、模式用例
~~~
package com.designpattern.adapter.strategy;
/**
* 各种书的抽象类
* @author 98583
*
*/
public abstract class Book {
/**
* 原价
*/
protected double basePrice = 0.0;
/**
* 策略的引用
*/
protected Strategy strategy;
public Book(double basePrice){
this.basePrice = basePrice;
}
/**
* 设置策略
* @param strategy
*/
public void setStrategy(Strategy strategy){
this.strategy = strategy;
}
/**
* 利用策略的统一接口实现获取书的应付价格
* @return
*/
public double getPrice(){
return strategy.getDiscount(basePrice);
}
/**
* 抽象的展示方法,每类书都有自己的实现方式
*/
public abstract void show();
}
~~~
~~~
package com.designpattern.adapter.strategy;
public class Comic extends Book{
public Comic(double basePrice) {
super(basePrice);
}
public void show() {
System.out.println("The Comic Book is "+getPrice());
}
}
~~~
~~~
package com.designpattern.adapter.strategy;
public abstract class Strategy {
public abstract double getDiscount(double basePrice);
}
~~~
~~~
package com.designpattern.adapter.strategy;
public class NovelStrategy extends Strategy{
public double getDiscount(double basePrice) {
return 0.6*basePrice;
}
}
~~~
~~~
package com.designpattern.adapter.strategy;
public class Client {
public static void main(String[] args) {
Book book = new Comic(12.3);
Strategy strategy = new ComicStrategy();
//设置采用何种策略
book.setStrategy(strategy);
book.show();
}
}
~~~
小说类的代码和漫画类的代码类似,不再贴出,最后会附上源码。
### 四、策略模式的优缺点
**优点:**
- 策略模式提供了对“开闭原则”的完美支持,用户可以在不修改原有系统的基础上选择算法或行为,也可以灵活地增加新的算法或行为。
- 策略模式提供了管理相关的算法族的办法。
- 策略模式提供了可以替换继承关系的办法。
- 使用策略模式可以避免使用多重条件转移语句。
**缺点:**
- 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。
- 策略模式将造成产生很多策略类,可以通过使用享元模式在一定程度上减少对象的数量。
### 五、策略模式的使用情况
1、如果在一个系统里面有许多类,它们之间的区别仅在于它们的行为,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。
一个系统需要动态地在几种算法中选择一种。
2、如果一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重的条件选择语句来实现。
3、不希望客户端知道复杂的、与算法相关的数据结构,在具体策略类中封装算法和相关的数据结构,提高算法的保密性与安全性。
源码下载:[http://download.csdn.net/detail/xingjiarong/9324481](http://download.csdn.net/detail/xingjiarong/9324481)
设计模式(十)适配器模式
最后更新于:2022-04-01 11:05:24
### 一、问题引入
说起适配器其实在我们的生活中是非常常见的,比如:如果你到日本出差,你会发现日本的插座电压都是110V的,而我们的手机充电器和笔记本充电器都是220V,所以你到了日本之后就没办法充电了,这时候我们通常会怎么办呢,当然是使用一个升压的变压器将电压升高到220V,这样我们的手机通过一个变压器(适配器)就能使用原本不能使用的插座了。
又比如说,有的国家的插座都是三孔的,而我们的手机大部分都是两孔的,这是你也没办法直接把充电器插到插座上,这时我们可以使用一个适配器,适配器本身是三孔的,它可以直接插到三孔的插头上,适配器本身可以提供一个两孔的插座,然后我们的手机充电器就可以插到适配器上了,这样我们原本只能插到两孔上的插头就能用三孔的插座了。
在我们的面向对象里也存在这个问题,假设一个软件系统,你希望它能和一个新的厂商类库搭配使用,但是这个新厂商所设计出来的接口,不同于旧厂商的接口,就像下图这样:
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-21_571890ec4fd73.jpg "")
你不想改变现有的代码,解决这个问题(而且你也不能改变厂商的代码)。所以该怎么做?这个嘛,你可以写一个类,将新厂商的接口转化成你所希望的接口。
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-21_571890ec762f3.jpg "")
这个适配器工作起来就如同一个中间人,它将客户所发出的请求转换成厂商类能理解的请求。
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-21_571890ec8a9fe.jpg "")
这样的话,就能在不改变现有代码的情况下使用原本不匹配的类库了。
### 二、适配器模式的相关概念
经过上边的三个例子,我们可以总结出适配器模式的使用过程:
1、客户通过目标接口调用适配器的方法对适配器发出请求。
2、适配器使用被适配者接口把请求转化成被适配者的一个或多个调用接口。
3、客户接收到调用的结果,但并未察觉这一切是适配在起转化作用。
所以适配器模式的正式定义就是:
适配器模式将一个类的接口,转化成客户期望的另一个接口。适配器让原本接口不兼容的类可以合作无间。
### 三、对象适配器
适配器其实是分为对象适配器和类适配器两种,两种的工作原理不太一样。对象适配器是使用组合的方法,在Adapter中会保留一个原对象(Adaptee)的引用,适配器的实现就是讲Target中的方法委派给Adaptee对象来做,用Adaptee中的方法实现Target中的方法。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-21_571890ec9b535.jpg)
这种类型的好处就是,Adpater只需要实现Target中的方法就好啦。
现在我们通过一个用火鸡冒充鸭子的例子来看看如何使用适配器模式。
~~~
package com.designpattern.adapter.object;
public abstract class Duck {
/**
* 嘎嘎叫
*/
public abstract void quack();
public abstract void fly();
}
~~~
~~~
package com.designpattern.adapter.object;
public abstract class Turkey {
/**
* 火鸡叫
*/
public abstract void gobble();
public abstract void fly();
}
~~~
~~~
package com.designpattern.adapter.object;
public class WildTurkey extends Turkey {
public void gobble() {
System.out.println("Gobble gobble");
}
public void fly() {
System.out.println("I'm flying a short distance");
}
}
~~~
~~~
package com.designpattern.adapter.object;
/**
* 用火鸡冒充鸭子
* @author 98583
*
*/
public class TurkeyAdapter extends Duck {
/**
* 保留火鸡的引用
*/
Turkey turkey;
public TurkeyAdapter(Turkey turkey) {
this.turkey = turkey;
}
/**
* 利用火鸡的叫声来实现鸭子的叫声
*/
public void quack() {
turkey.gobble();
}
/**
* 利用火鸡的飞的方法来实现鸭子的飞的方法
*/
public void fly() {
for (int i = 0; i < 5; i++) {
turkey.fly();
}
}
}
~~~
~~~
package com.designpattern.adapter.object;
/**
* 用火鸡冒充鸭子
* @author 98583
*
*/
public class Client {
public static void main(String[] args) {
WildTurkey turkey = new WildTurkey();
Duck turkeyAdapter = new TurkeyAdapter(turkey);
System.out.println("The Turkey says...");
turkey.gobble();
turkey.fly();
System.out.println("\nThe TurkeyAdapter says...");
testDuck(turkeyAdapter);
}
static void testDuck(Duck duck) {
duck.quack();
duck.fly();
}
}
~~~
鸭子和火鸡有相似之处,他们都会飞,虽然飞的不远,他们不太一样的地方就是叫声不太一样,现在我们有一个火鸡的类,有鸭子的抽象类也就是接口。我们的适配器继承自鸭子类并且保留了火鸡的引用,重写鸭子的飞和叫的方法,但是是委托给火鸡的方法来实现的。在客户端中,我们给适配器传递一个火鸡的对象,就可以把它当做鸭子来使用了。
### 四、类适配器
与对象适配器不同的是,类适配器是通过类的继承来实现的。Adpater直接继承了Target和Adaptee中的所有方法,并进行改写,从而实现了Target中的方法。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-21_571890ecb3094.jpg)
这种方式的缺点就是必须实现Target和Adaptee中的方法,由于Java不支持多继承,所以通常将Target设计成接口,Adapter继承自Adaptee然后实现Target接口。
我们使用类适配器的方式来实现一下上边的用火鸡来冒充鸭子。
~~~
package com.designpattern.adapter.classmethod;
/**
* 由于Java不支持多继承,所以通常将Target声明为接口
* @author 98583
*
*/
public interface Duck {
/**
* 嘎嘎叫
*/
public void quack();
public void duckFly();
}
~~~
~~~
package com.designpattern.adapter.classmethod;
/**
* 目前已有的火鸡类的抽象类
* @author 98583
*
*/
public abstract class Turkey {
/**
* 火鸡叫
*/
public abstract void gobble();
public abstract void turkeyFly();
}
~~~
~~~
package com.designpattern.adapter.classmethod;
/**
* 用火鸡冒充鸭子,不再保留火鸡类的引用,需要实现鸭子类和火鸡类的方法
* @author 98583
*
*/
public class TurkeyAdapter extends Turkey implements Duck {
/**
* 利用火鸡的叫声来实现鸭子的叫声
*/
public void quack() {
gobble();
}
/**
* 利用火鸡的飞的方法来实现鸭子的飞的方法
*/
public void turkeyFly() {
for (int i = 0; i < 5; i++) {
System.out.println("I'm flying a short distance");
}
}
/**
* 使用火鸡类的方法来实现鸭子类的方法
*/
public void duckFly() {
turkeyFly();
}
/**
* 火鸡的叫声
*/
public void gobble() {
System.out.println("Gobble gobble");
}
}
~~~
~~~
package com.designpattern.adapter.classmethod;
/**
* 用火鸡冒充鸭子
* @author 98583
*
*/
public class Client {
public static void main(String[] args) {
Duck turkeyAdapter = new TurkeyAdapter();
System.out.println("\nThe TurkeyAdapter says...");
testDuck(turkeyAdapter);
}
static void testDuck(Duck duck) {
duck.quack();
duck.duckFly();
}
}
~~~
其实两种方法的效果是一样的,只是用的方法不一样。Java不支持多继承,所以将Duck声明为接口,Adapter继承自火鸡类并且实现了Duck的方法,但是实现Duck的方法不再是委派给火鸡类的对象,而是直接调用火鸡类的方法,因为在Adapter中实现了火鸡类的方法,所以可以直接调用。
### 五、缺省适配器
鲁达剃度的故事就很好的说明了缺省适配器的作用。一般的和尚都是吃斋,念经,打坐,撞钟和习武,但是鲁达只是喝酒喝习武,所以‘鲁达不能剃度(不能当做和尚使用),要想让鲁达可以当做和尚使用就要让他实现和尚的所有方法,但是这样做时候鲁达就不是鲁达了。我们可以找一个中间者,比如鲁达是天星的一位,我们可以让天星实现和尚所有的方法,再让鲁达继承自天星。代码如下:
这是定义的和尚接口,和尚都应该做以下的事。
~~~
package com.designpattern.adapter.defaultmethod;
public interface Monk {
public void chizha();
public void nianjing();
public void dazuo();
public void zhuangzhong();
public void xiwu();
}
~~~
这是天星类,为每个方法提供一个空实现,其他继承自该类的子类可以重写父类的方法。
~~~
package com.designpattern.adapter.defaultmethod;
public abstract class Star implements Monk{
public void chizha(){}
public void nianjing(){}
public void dazuo(){}
public void zhuangzhong(){}
public void xiwu(){}
}
~~~
鲁达继承自天星,并且添加了喝酒的方法。
~~~
package com.designpattern.adapter.defaultmethod;
public class Luda extends Star{
public void xiwu(){
System.out.println("鲁达习武");
}
public void hejiu(){
System.out.println("鲁达喝酒");
}
}
~~~
我们看到通过天星类(缺省适配器),鲁达不需要再实现自己不需要的方法了。
### 六、优缺点
**优点:**
- 将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,而无须修改原有代码。
- 增加了类的透明性和复用性,将具体的实现封装在适配者类中,对于客户端类来说是透明的,而且提高了适配者的复用性。
- 灵活性和扩展性都非常好,通过使用配置文件,可以很方便地更换适配器,也可以在不修改原有代码的基础上增加新的适配器类,完全符合“开闭原则”。
**类适配器模式还具有如下优点:**
由于适配器类是适配者类的子类,因此可以在适配器类中置换一些适配者的方法,使得适配器的灵活性更强。
**对象适配器模式还具有如下优点:**
一个对象适配器可以把多个不同的适配者适配到同一个目标,也就是说,同一个适配器可以把适配者类和它的子类都适配到目标接口。
**缺点:**
**类适配器模式的缺点如下:**
对于Java、C#等不支持多重继承的语言,一次最多只能适配一个适配者类,而且目标抽象类只能为抽象类,不能为具体类,其使用有一定的局限性,不能将一个适配者类和它的子类都适配到目标接口。
**对象适配器模式的缺点如下:**
与类适配器模式相比,要想置换适配者类的方法就不容易。如果一定要置换掉适配者类的一个或多个方法,就只好先做一个适配者类的子类,将适配者类的方法置换掉,然后再把适配者类的子类当做真正的适配者进行适配,实现过程较为复杂。
### 七、适用环境
1、系统需要使用现有的类,而这些类的接口不符合系统的需要。
2、想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作。
源码下载:[http://download.csdn.net/detail/xingjiarong/9322221](http://download.csdn.net/detail/xingjiarong/9322221)
设计模式(九)装饰模式(Decorator)
最后更新于:2022-04-01 11:05:22
### 一、咖啡店的故事
这次我们借用HeadFirst中的咖啡店的故事来讨论一下装饰模式。咖啡店中有各种种类的咖啡和咖啡需要加的配料。有一家咖啡店为了提高效率打算开发一套咖啡订购系统,用户可以根据清单选择咖啡和咖啡所加的配料,系统可以自动的计算总价格。
第一种方案是这个样子的:
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-20_571758542aea6.jpg "")
Beverge是一个抽象类,店内所有的饮料都必须继承自这个类。description用来描述这个是什么类型的饮料例如:Dark Roast。getDescription()方法返回这种饮料的描述,cost()返回这种饮料的描述,但是cost()方法是抽象的,具体的实现由子类决定,因为每中不同饮料价格都不相同。下面的四个类代表四种不同的饮料,他们都实现了父类的cost方法。
因为用户购买咖啡时还需要搭配一些调料,所以每种咖啡又分为各种不同的种类,于是系统就变成了这个样子。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-21_571890eb7043f.jpg)
这种方案造成的最大的问题就是类的数量众多,维护成本非常大。试想如果牛奶的价格上涨,那么每种添加牛奶的咖啡就都必须修改自己的价格,所以这种方案不符合“开-闭”原则。
第二种方案:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-21_571890eba4d39.jpg)
milk,soy,mocha,whip都是代表有没有这种调料的布尔值,下面的hasMilk()方法等是返回这个变量的值,setMilk()方法是设置这个变量的值。并且Beverage中的cost()方法也不再是抽象方法而是返回所加的各种调料的价格,子类会覆盖这个cost方法,子类的cost方法会返回总的价格,首先调用父类的cost方法得到调料的价格,然后再加上咖啡的价格,返回总的价格。
采用这种方法只需要5各类就能表示出这家咖啡店中所有的咖啡和配料组合。但是仍然存在问题,试想,如果我们如果增加了一种新的调料,那么Beverage类不就需要更改吗,如果客户想买双倍摩卡咖啡怎么办?如果以后开发出了一种新的饮料,比如说“茶”,对于这种饮料而言某些调料是不合适的,比如说(奶泡),但是它还是继承了所有的方法,包括不合适的。
第三种方案:装饰模式
我们把咖啡本身当做是主要的本体,而把奶泡等调料当做是咖啡的装饰,我们通过给本体添加不同的装饰来获得不同的结果。
这是咖啡的本体,我们以DarkRoast为例,这种咖啡是继承自Beverage的,它的cost()是用来返回咖啡的价格。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-21_571890ebbeec1.jpg)
如果客户想要摩卡咖啡,就建立一个Mocha对象,并用他将DarkRoast对象包起来(装饰)。Mocha就是一个装饰者,他的类型和他所装饰的类型是一样的,都是Beverage。Mocha也有一个cost方法,它的cost()方法会首先调用所装饰的对象DarkRoast类的cost()方法获得咖啡的价格,然后再加上自己本身的价格形成最后总的价格。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-21_571890ebcebcb.jpg)
如果顾客也想要加入奶泡,那就在建立一个Whip的装饰者,并用他将上边的Mocha对象包起来,Whip也是继承自Beverage。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-21_571890ebe4271.jpg)
现在,当为顾客计算价格的时候,通过最外圈装饰者(Whip)的cost就可以办到。Whip的cost()会委托它所装饰的对象计算出价钱,然后加上奶泡的价钱。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-21_571890ec0dc8c.jpg)
### 二、装饰模式的定义
装饰模式(Decorator Pattern) :动态地给一个对象增加一些额外的职责(Responsibility),就增加对象功能来说,装饰模式比生成子类实现更为灵活。
### 三、装饰模式的结构
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-21_571890ec37ad3.jpg)
从上图中可以看出,装饰模式一共有四部分组成:
1、抽象组件角色(Component):定义一个对象接口,以规范准备接受附加责任的对象,即可以给这些对象动态地添加职责。
2、具体组件角色(ConcreteComponent) :被装饰者,定义一个将要被装饰增加功能的类。可以给这个类的对象添加一些职责。
3、抽象装饰器(Decorator):维持一个指向构件Component对象的实例,并定义一个与抽象组件角色Component接口一致的接口。
4、具体装饰器角色(ConcreteDecorator):向组件添加职责。
### 四、装饰模式的实例
下面是上边所说的咖啡馆的代码。
~~~
package com.designpattern.decorator;
/**
* 抽象组件,装饰者和被装饰者都继承自它
* @author 98583
*
*/
public abstract class Beverage {
/**
* 饮料的名称,用来代表是哪种饮料
*/
protected String description = "Unknown Beverage";
public String getDescription() {
return description;
}
/**
* 每个子类都有自己的实现方法
* @return
*/
public abstract double cost();
}
~~~
~~~
package com.designpattern.decorator;
/**
* 抽象装饰者,定义具体装饰者要实现的代码
* @author 98583
*
*/
public abstract class CondimentDecorator extends Beverage {
public abstract String getDescription();
}
~~~
~~~
package com.designpattern.decorator;
/**
* 具体的装饰者
* @author 98583
*
*/
public class Mocha extends CondimentDecorator {
/**
* 保留一个被装饰者的引用
*/
Beverage beverage;
public Mocha(Beverage beverage) {
this.beverage = beverage;
}
public String getDescription() {
return beverage.getDescription() + ", Mocha";
}
public double cost() {
return .20 + beverage.cost();
}
}
~~~
~~~
package com.designpattern.decorator;
/**
* 被装饰者
* @author 98583
*
*/
public class DarkRoast extends Beverage {
public DarkRoast() {
description = "DarkRoast";
}
public double cost() {
return .99;
}
}
~~~
~~~
package com.designpattern.decorator;
public class StarbuzzCoffee {
public static void main(String[] args) {
Beverage beverage = new Espresso();
System.out.println(beverage.getDescription() + " $" + beverage.cost());
Beverage beverage2 = new DarkRoast();
beverage2 = new Mocha(beverage2);
beverage2 = new Mocha(beverage2);
beverage2 = new Whip(beverage2);
System.out
.println(beverage2.getDescription() + " $" + beverage2.cost());
Beverage beverage3 = new HouseBlend();
beverage3 = new Soy(beverage2);
beverage3 = new Mocha(beverage2);
beverage3 = new Whip(beverage2);
System.out
.println(beverage3.getDescription() + " $" + beverage3.cost());
}
}
~~~
其他类的代码都是相似的,这里不再贴出,最后会附上源码。
### 五、装饰模式的优缺点
**装饰模式的优点:**
- 装饰模式与继承关系的目的都是要扩展对象的功能,但是装饰模式可以提供比继承更多的灵活性。
- 可以通过一种动态的方式来扩展一个对象的功能,通过配置文件可以在运行时选择不同的装饰器,从而实现不同的行为。
- 通过使用不同的具体装饰类以及这些装饰类的排列组合,可以创造出很多不同行为的组合。可以使用多个具体装饰类来装饰同一对象,得到功能更为强大的对象。
- 具体构件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构件类和具体装饰类,在使用时再对其进行组合,原有代码无须改变,符合“开闭原则”
**装饰模式的缺点:**
- 使用装饰模式进行系统设计时将产生很多小对象,这些对象的区别在于它们之间相互连接的方式有所不同,而不是它们的类或者属性值有所不同,同时还将产生很多具体装饰类。这些装饰类和小对象的产生将增加系统的复杂度,加大学习与理解的难度。
- 这种比继承更加灵活机动的特性,也同时意味着装饰模式比继承更加易于出错,排错也很困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为烦琐。
### 六、装饰模式的适用环境
1、在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
2、需要动态地给一个对象增加功能,这些功能也可以动态地被撤销。
3、当不能采用继承的方式对系统进行扩充或者采用继承不利于系统扩展和维护时。不能采用继承的情况主要有两类:第一类是系统中存在大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长;第二类是因为类定义不能继承(如final类).
源码下载:[http://download.csdn.net/detail/xingjiarong/9317741](http://download.csdn.net/detail/xingjiarong/9317741)
设计模式(八)桥梁模式(Bridge)
最后更新于:2022-04-01 11:05:20
### 一、写在前面
之前有读者评论说,前边的文章开头理论性太强了,显得晦涩难懂,会把读者搞晕,谢谢这位读者,同时也希望其他的读者多提意见,帮助我改正提高博客,为了改进之前的问题,今天我们先用例子引入,然后再给出桥梁模式的相关概念。
### 二、问题引入
例子1:
现需要提供大中小3种型号的画笔,能够绘制5种不同颜色,如果使用蜡笔,我们需要准备3*5=15支蜡笔,也就是说必须准备15个具体的蜡笔类。而如果使用毛笔的话,只需要3种型号的毛笔,外加5个颜料盒,用3+5=8个类就可以实现15支蜡笔的功能。实际上,**蜡笔和毛笔的关键一个区别就在于笔和颜色是否能够分离。**
例子2:
设想如果要绘制矩形、圆形、椭圆、正方形,我们至少需要4个形状类,但是如果绘制的图形需要具有不同的颜色,如红色、绿色、蓝色等,此时至少有如下两种设计方案:
第一种设计方案是为每一种形状都提供一套各种颜色的版本。
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-20_57175853bbaf3.jpg "")
这种设计方案就是类似于实例一中的蜡笔,颜色和形状紧密结合起来,必须为每一种形状准备各种颜色的版本,加入我们现在要求加入一种颜色蓝色,那么每一种形状都需要修改,所以这种设计方案的缺点是显而易见的,一是不符合“开-闭”原则,二是需要的类非常多,编码重复性较高。
第二种设计方案是根据实际需要对形状和颜色进行组合。
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-20_57175853cfb8b.jpg "")
上下两部分图是分别从不同的角度描述这种方案,上边的图说的是如何利用形状和颜色进行组合,下边的图说的是各个类的继承和组合关系。第二种方案需要为所有的图形声明一个共同的父类,为所有的颜色声明一个父类,两个父类各有自己的具体实现,我们需要的产品就是有两种具体的产品进行组合得到的。这样在加入新的颜色或者形状的时候不用修改其他的类,而且大大的减少了代码量。而这第二种方案就是我们今天要讨论的桥梁模式。
### 三、什么是桥梁模式
上边的蜡笔和图形的例子他们都有一个共同的特点就是他们都有两个变化因素,蜡笔是粗细和形状,图形是形状和颜色,不管是毛笔还是图形的第二种解决方案他们比较好的原因都是将这两种变化因素分开了,使得两种因素可以独立的变化。蜡笔的颜色和蜡笔本身是分不开的,所以就造成必须使用15支色彩、大小各异的蜡笔来绘制图画。而毛笔与颜料能够很好的分开,各自独立变化,便简化了操作。
**所以桥梁模式的用意是“将抽象化与实现化脱耦,使得二者可以独立地变化。”**
那么什么是脱耦呢?我们先来看一下什么是耦合。两个类之间的关系分为两种,一种是强关联一种是弱关联,强关联是在编译时期就已经确定的,无法在运行时期动态的改变的关联;弱关联是可以动态地确定并且可以在运行时期动态改变的关联。显然,Java中继承是强关联而聚合是弱关联。耦合就是两个实体的行为的某种强关联,脱耦就是指将他们之间的强关联解除,但是在桥梁模式中是指将它们之间的强关联改换成弱关联。所以桥梁模式的精髓就是尽量使用聚合/组合来实现弱关联。
### 四、桥梁模式的结构
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-20_57175853e617e.jpg "")
这是具有一般性的桥梁模式的类图,我们可以看到桥梁模式一共有四部分组成:
- 抽象化角色:抽象化给出的定义,并保存一个对实现化对象的引用,就是图像类中的形状父类。
- 修正抽象化角色:扩展抽象化角色,改变和修正父类对抽象化的定义,比如形状下有正方形,圆形等图形。
- 实现化角色:这个角色给出具体角色的接口,但是不给出具体的实现,这个接口不一定和抽象化角色的接口定义相同,实际上两者可以完全不一样,好比形状的颜色接口。
- 具体实现化角色:这个角色给出实现化角色接口的具体实现,好比各种具体的颜色。
如果将Abstraction和Implementor看成两个岸边的话,那么聚合关系就像桥一样将他们连接起来,这就是这个模式为什么叫桥梁模式的原因。
### 五、桥梁模式的例子
我们来实现一下上边的毛笔的例子,套用上边的桥梁模式的类图我们可以画出毛笔的类图。
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-20_5717585407a46.jpg "")
~~~
package com.designpattern.bridge;
/**
* 这是抽象画笔类
* @author 98583
*
*/
public abstract class BrushPenAbstraction {
/**
* 保留对颜色的引用
*/
protected ImplementorColor imp;
/**
* 每种笔都有自己的实现
*/
public abstract void operationDraw();
public void setImplementor(ImplementorColor imp) {
this.imp = imp;
}
}
~~~
~~~
package com.designpattern.bridge;
/**
* 粗毛笔的实现
* @author 98583
*
*/
public class BigBrushPenRefinedAbstraction extends BrushPenAbstraction{
public void operationDraw() {
System.out.println("Big and "+imp.bepaint()+" drawing!");
}
}
~~~
~~~
package com.designpattern.bridge;
/**
* 颜色的接口
* @author 98583
*
*/
public abstract class ImplementorColor {
public abstract String bepaint();
}
~~~
~~~
package com.designpattern.bridge;
/**
* 红色的具体实现
* @author 98583
*
*/
public class OncreteImplementorRed extends ImplementorColor{
public String bepaint() {
return "red";
}
}
~~~
~~~
package com.designpattern.bridge;
/**
* 客户端
* @author 98583
*
*/
public class Client {
public static void main(String[] args) {
BrushPenAbstraction brushPen = new BigBrushPenRefinedAbstraction();
ImplementorColor col = new OncreteImplementorRed();
/**
* 设置颜色
*/
brushPen.setImplementor(col);
/**
* 画画
*/
brushPen.operationDraw();
}
}
~~~
其他的类都是类似的,不再给出,最后会附上源码。
### 五、桥梁模式的优缺点
桥接模式的优点:
1、分离抽象接口及其实现部分。
2、桥接模式提高了系统的可扩充性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统。
3、实现细节对客户透明,可以对用户隐藏实现细节。
桥接模式的缺点:
1、桥接模式的引入会增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进
2、桥接模式要求正确识别出系统中两个独立变化的维度,因此其使用范围具有一定的局限性。
源码下载:[http://download.csdn.net/detail/xingjiarong/9316367](http://download.csdn.net/detail/xingjiarong/9316367)
设计模式(七)门面模式(Facade Pattern 外观模式)
最后更新于:2022-04-01 11:05:18
### 一、模式定义
门面模式(Facade Pattern):外部与一个子系统的通信必须通过一个统一的外观对象进行,为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。门面模式又称为外观模式,它是一种对象结构型模式。
### 二、模式动机
现代的软件系统都非常复杂,尽管我们已经想尽一切方法将其“分而治之”,把一个系统划分为好几个较小的子系统了,但是仍然可能会存在这样的问题:子系统内有非常多的类,客户端往往需要和许多对象打交道之后 才能完成想要完成的功能。
在我们的生活中医院就是这样的。一般的医院都会分为挂号、门诊、化验、收费、取药等。看病的病人要想治好自己的病(相当于一个客户端想要实现自己的功能)就要和医院的各个部门打交道。首先,病人需要挂号,然后门诊,如果医生要求化验的话,病人就要去化验,然后再回到门诊室,最后拿药,经过一系列复杂的过程后才能完成看病的过程。如下图所示:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-20_571758537e3f9.jpg)
解决这种不便的方式就是引入门面模式。如果我们在医院设立一个接待员的话,病人只负责和接待员接触,由接待员负责与医院的各个部门打交道,如下图所示:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-20_5717585391597.jpg)
### 三、模式结构
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-20_57175853a499f.jpg)
从上图中我们可以看出门面模式一共有两种角色:
门面角色:客户端调用这个角色的方法。此角色知晓相关的子系统的功能和责任。正常情况下,本角色会将所有从客户端发来的请求委派到相应的子系统中去。
子系统角色:可以同时有一个或者多个子系统。每个子系统都不是一个单独的类,而是一个类的集合。每一个子系统都可以被客户端直接调用,或者被门面角色直接调用。子系统并不知道门面的存在,罪域子系统而言,门面仅仅是另一个客户端而已。
### 四、实例分析
这次我们来关注一下土豪的个人生活,话说土豪下班回到家里后首先要做的就是把灯打开,我们假设他一共需要打开三个灯,然后就是打开热水器烧水准备洗澡,在等待的过程还会打开电视机看新闻。如果我们用一般的方法来实现的话,代码就会是下面这个样子。
这是电灯的类,里边有打开的方法。
~~~
package com.designpattern.facade;
public class Light {
public void open(){
System.out.println("Light has been opened!");
}
}
~~~
这是热水器的类,里边有打开的方法。
~~~
package com.designpattern.facade;
public class Heater {
public void open(){
System.out.println("Heater has been opened!");
}
}
~~~
这是电视机的类,里边有打开的方法。
~~~
package com.designpattern.facade;
public class TV {
public void open(){
System.out.println("TV has been opened!");
}
}
~~~
在主函数里就要创建各种对象,并且调用他们的额open方法。我们看到主函数为了实现土豪下班回家这一个功能需要和三个电灯,一个热水器和一台电视机打交道,非常的复杂,所以这时候我们就应该使用门面模式。
~~~
package com.designpattern.facade;
public class Main {
public static void main(String[] args){
Light light1 = new Light();
Light light2 = new Light();
Light light3 = new Light();
Heater heater = new Heater();
TV tv = new TV();
/**
* 需要一步一步的操作
*/
light1.open();
light2.open();
light3.open();
heater.open();
tv.open();
}
}
~~~
在门面类中我们创建一个统一的open方法,来调度所有的开关。
~~~
package com.designpattern.facade;
public class Facade {
private Light light1, light2, light3;
private Heater heater;
private TV tv;
public Facade() {
light1 = new Light();
light2 = new Light();
light3 = new Light();
heater = new Heater();
tv = new TV();
}
public void open() {
light1.open();
light2.open();
light3.open();
heater.open();
tv.open();
}
}
~~~
这样在主函数类只需要使用门面类就可以了。
~~~
package com.designpattern.facade;
public class Main2 {
public static void main(String[] args) {
Facade facade = new Facade();
/**
* 一步操作就可以完成所有的准备工作
*/
facade.open();
}
}
~~~
### 五、模式的优缺点
**优点:**
- 对客户屏蔽子系统组件,减少了客户处理的对象数目并使得子系统使用起来更加容易。通过引入门面模式,客户代码将变得很简单,与之关联的对象也很少。
- 实现了子系统与客户之间的松耦合关系,这使得子系统的组件变化不会影响到调用它的客户类,只需要调整外观类即可。
- 降低了大型软件系统中的编译依赖性,并简化了系统在不同平台之间的移植过程,因为编译一个子系统一般不需要编译所有其他的子系统。一个子系统的修改对其他子系统没有任何影响,而且子系统内部变化也不会影响到外观对象。
- 只是提供了一个访问子系统的统一入口,并不影响用户直接使用子系统类。
**缺点:**
- 不能很好地限制客户使用子系统类,如果对客户访问子系统类做太多的限制则减少了可变性和灵活性。
- 在不引入抽象外观类的情况下,增加新的子系统可能需要修改外观类或客户端的源代码,违背了“开闭原则”。
### 五、使用场景
1、当要为一个复杂子系统提供一个简单接口时可以使用外观模式。该接口可以满足大多数用户的需求,而且用户也可以越过外观类直接访问子系统。
2、客户程序与多个子系统之间存在很大的依赖性。引入外观类将子系统与客户以及其他子系统解耦,可以提高子系统的独立性和可移植性。
3、在层次化结构中,可以使用外观模式定义系统中每一层的入口,层与层之间不直接产生联系,而通过外观类建立联系,降低层之间的耦合度。
### 六、需要注意的几点
**一个系统有多个外观类**
在外观模式中,通常只需要一个外观类,并且此外观类只有一个实例,换言之它是一个单例类。在很多情况下为了节约系统资源,一般将外观类设计为单例类。当然这并不意味着在整个系统里只能有一个外观类,在一个系统中可以设计多个外观类,每个外观类都负责和一些特定的子系统交互,向用户提供相应的业务功能。
**不要试图通过外观类为子系统增加新行为**
不要通过继承一个外观类在子系统中加入新的行为,这种做法是错误的。外观模式的用意是为子系统提供一个集中化和简化的沟通渠道,而不是向子系统加入新的行为,新的行为的增加应该通过修改原有子系统类或增加新的子系统类来实现,不能通过外观类来实现。
**外观模式与迪米特法则**
外观模式创造出一个外观对象,将客户端所涉及的属于一个子系统的协作伙伴的数量减到最少,使得客户端与子系统内部的对象的相互作用被外观对象所取代。外观类充当了客户类与子系统类之间的“第三者”,降低了客户类与子系统类之间的耦合度,外观模式就是实现代码重构以便达到“迪米特法则”要求的一个强有力的武器。
**抽象外观类的引入**
外观模式最大的缺点在于违背了“开闭原则”,当增加新的子系统或者移除子系统时需要修改外观类,可以通过引入抽象外观类在一定程度上解决该问题,客户端针对抽象外观类进行编程。对于新的业务需求,不修改原有外观类,而对应增加一个新的具体外观类,由新的具体外观类来关联新的子系统对象,同时通过修改配置文件来达到不修改源代码并更换外观类的目的。
源码下载:[http://download.csdn.net/detail/xingjiarong/9308029](http://download.csdn.net/detail/xingjiarong/9308029)
设计模式(六)原型模式
最后更新于:2022-04-01 11:05:15
### 一、说说鸣人的影分身
话说鸣人听了水木老师的建议偷出了卷轴并且学会了一招禁术:影分身之术。当鸣人使用影分身之术的时候就会有好多个和鸣人一模一样的人出现,就像复制出来的一样,这种影分身之术在面向对象的设计领域里就叫做原型模式。
### 二、什么是原型模式
有了上边的鸣人的例子,我们再理解圆形模式的定义应该会更简单了,GOF给它的定义是:用原型实例指定创建对象的种类并且通过拷贝这些原型对象创建新的对象。
在Java中提供了clone()方法来实现对象的克隆,所以原型模式(Prototype)实现变得简单的多了。
### 三、再来说说clone()方法
Java的所有类 都是从java.lang.Object类继承而来的,而Object类提供下面的方法对对象进行复制:
`protected Object clone()`
子类也可以将这个方法覆盖掉,用自己的逻辑实现自己的复制方法。可以被使用clone()方法的类都必须实现Cloneable接口,Cloneable接口只起一个作用就是在运行时期通知Java虚拟机可以安全地在这个类上使用clone方法。
克隆又分为两种:浅克隆和深度克隆
**浅度克隆:**
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-20_5717585331dd5.jpg)
如上图所示,浅度复制只是复制对象的值,我们知道对象的属性一共分为两种,基本类型和引用类型,对于浅度复制基本类型的数据会复制一份到新的对象中去,对于引用类型的属性仅仅复制引用的值,引用所指向的具体的对象不会复制,所以A和B实际上是用的同一个对象c,如果再A中改变c的属性,B中也能看到,因为改变的是两者共有对象。Java提供的clone方法就是这种类型的。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-20_5717585343203.jpg)
深度复制与浅度复制的不同就是深度复制不但会复制对象的引用,并且还会复制引用所指的对象。所以在第二幅图中A和B是指向的不同的对象,此时在A中操作c对象不会对B产生任何影响。
**克隆满足的条件:**
1、对任何对象x,都有:x.clone()!=x.换言之,克隆对象与原来的对象不是同一个对象。
2、对任何对象x,都有:x.clone().getClass==x.getClass(),换言之,克隆对象与原对象的类型一致。
3、如果对象x的equals()方法定义恰当的话,那么x.clone().equals(x)应该是成立的。
**关于equals方法的说明:**被克隆的对象按照他们的内部状态是否可变,划分为可变对象和不可变对象(String的内部数值是不能改变的)。对于可变对象只有当他们是同一个对象时才会返回true,而对于不变对象,当他们的内部状态值是一样的时候就认为是true,但是内部状态一直的不一定就是同一个对象。
### 四、原型模式的结构
原型模式模式分为两种,一种是不带管理类的原型模式,另一种是带管理类的原型模式。
下面这种是不带管理类的原型模式:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-20_5717585353dc5.jpg)
这种形式涉及到三个角色:
客户角色:客户提出创建对象的请求。
抽象原型角色:这是一个抽象角色,通常由一个Java接口或Java抽象类实现,这个类可能会继承Cloneable接口。
具体原型角色:被复制的对象。此角色需要实现抽象的原型角色所要求的接口。
我们通过一个实例来看一下具体的使用过程。
我们举一个大学里常见的例子,一个班里有一个学霸的话整个班级的作业就不用愁了,大家可以拿学霸的作业去复制嘛。
这个类是作业的抽象父类,定义了一些作业都要实现的方法,这里只实现了一个数学作业类,将来可以能有编程作业等。
~~~
package com.designpattern.prototype1;
public abstract class Homework implements Cloneable {
public abstract Object clone();
public abstract void show();
}
~~~
数学作业的类要实现自己的复制逻辑,因为数学作业和编程作业的抄袭的方法肯定是不一样的。
~~~
package com.designpattern.prototype1;
import java.util.Date;
public class MathHomework extends Homework{
/**
* 这里只是用一个日期类来表示一下深度复制
*/
private Date A = new Date();
private int a = 1;
public void show() {
System.out.println("Math clone");
}
/**
* 实现自己的克隆方法
*/
public Object clone(){
MathHomework m = null;
/**
* 深度复制
*/
m = (MathHomework) this.clone();
m.A = (Date)this.getA().clone();
return m;
}
public Date getA(){
return A;
}
}
~~~
客户端就可以使用学霸的作业抄袭了
~~~
package com.designpattern.prototype1;
public class Main {
public static void main(String[] args){
/**
* 建立一个学霸,全班同学的作业就靠他了
*/
MathHomework xueba = new MathHomework();
/**
* 学渣都是从学霸那复制来的
*/
MathHomework xuezha = (MathHomework)xueba.clone();
xuezha.show();
}
}
~~~
那如果一个班里有两个学霸呢,那肯定班里的同学有的会超A同学的,有的会抄B同学的,这样的话系统里就必须要保留两个原型类,这时候使用我们的带有管理类的原型模式就比较方便了。
此时的结构图是这样的:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-20_571758536576c.jpg)
新增加的管理类:
~~~
package com.designpattern.prototype1;
import java.util.Map;
public class Manager {
private static Manager manager;
private Map prototypes = null;
private Manager() {
manager = new Manager();
}
//使用了简单工厂模式
public static Manager getManager() {
if (manager == null)
manager = new Manager();
return manager;
}
public void put(String name,Homework prototype){
manager.put(name, prototype);
}
public Homework getPrototype(String name){
if(prototypes.containsKey(name)){
return (Homework) ((Homework)prototypes.get(name)).clone();
}else{
Homework homework = null;
try{
homework = (Homework)Class.forName(name).newInstance();
put(name, homework);
}catch(Exception e){
e.printStackTrace();
}
return homework;
}
}
}
~~~
~~~
package com.designpattern.prototype1;
public class MainManager {
public static void main(String[] args){
/**
* 建立一个学霸,全班同学的作业就靠他了
*/
MathHomework xueba = new MathHomework();
Manager.getManager().put("com.designpattern.prototype1.MathHomework", xueba);
/**
* 学渣都是从学霸那复制来的
*/
MathHomework xuezha = (MathHomework) Manager.getManager().getPrototype("com.designpattern.prototype1.MathHomework");
xuezha.show();
}
}
~~~
简单形式和登记形式的原型模式各有其长处和短处,如果需要创建的原型对象数目较少 而且比较固定的话可以采取简单形式,如果创建的原型对象数目不固定的话建议采取第二种形式。
### 五、原型模式的优缺点
**优点:**
1、将产品的创建过程封装起来,客户端不需要了解产品的具体创建流程。
2、利用Java的clone方法来创建对象肯定要比使用new来创建对象快很多,尤其是那些很复杂的对象的时候。
3、可以在不修改其他代码的情况下添加新的产品,符合“开-闭”原则。
**缺点:**原型模式的最大缺点就是每一个类必须都有一个clone方法,如果这个类的组成不太复杂的话还比较好,如果类的组成很复杂的话,如果想实现深度复制就非常困难了。
### 六、原型模式的选择
假设一个系统的产品类是动态加载的,而且产品类具有一定的等级结构。这个时候如果采用工厂模式的话,工厂类就不得不具有一个相应的等级结构。而产品类的等级结构一旦发生变化,工厂类的等级结构就不得不有一个相应的变化,这对于产品结构可能经常变化的系统来说采用工厂模式是很不方便的。这个时候如果采用原型模式,给每个产品类装配一个clone方法便可以避免使用工厂方式所带来的具有固定等级结构的工厂类。
源码下载:[http://download.csdn.net/detail/xingjiarong/9308023](http://download.csdn.net/detail/xingjiarong/9308023)
设计模式(五)创建者模式(Builder)
最后更新于:2022-04-01 11:05:13
### 一、模式定义
造者模式(Builder Pattern):将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
建造者模式是一步一步创建一个复杂的对象,它允许用户只通过指定复杂对象的类型和内容就可以构建它们,用户不需要知道内部的具体构建细节。建造者模式属于对象创建型模式。根据中文翻译的不同,建造者模式又可以称为生成器模式。
### 二、模式动机
无论是在现实世界中还是在软件系统中,都存在一些复杂的对象,它们拥有多个组成部分,如汽车,它包括车轮、方向盘、发送机等各种部件。而对于大多数用户而言,无须知道这些部件的装配细节,也几乎不会使用单独某个部件,而是使用一辆完整的汽车,可以通过建造者模式对其进行设计与描述,建造者模式可以将部件和其组装过程分开,一步一步创建一个复杂的对象。用户只需要指定复杂对象的类型就可以得到该对象,而无须知道其内部的具体构造细节。
在软件开发中,也存在大量类似汽车一样的复杂对象,它们拥有一系列成员属性,这些成员属性中有些是引用类型的成员对象。而且在这些复杂对象中,还可能存在一些限制条件,如某些属性没有赋值则复杂对象不能作为一个完整的产品使用;有些属性的赋值必须按照某个顺序,一个属性没有赋值之前,另一个属性可能无法赋值等。
复杂对象相当于一辆有待建造的汽车,而对象的属性相当于汽车的部件,建造产品的过程就相当于组合部件的过程。由于组合部件的过程很复杂,因此,这些部件的组合过程往往被“外部化”到一个称作建造者的对象里,建造者返还给客户端的是一个已经建造完毕的完整产品对象,而用户无须关心该对象所包含的属性以及它们的组装方式,这就是建造者模式的模式动机。
### 三、模式结构
比较常见的是下面这种只有一个产品的模式结构,大多数的书中也是这样讲的。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-20_57175852f3660.jpg)
从图中我们可以看出,创建者模式由四部分组成。
- 抽象创建者角色:给出一个抽象接口,以规范产品对象的各个组成成分的建造。一般而言,此接口独立于应用程序的商业逻辑。模式中直接创建产品对象的是具体创建者角色。具体创建者必须实现这个接口的两种方法:一是建造方法,比如图中的buildPart1和buildPart2方法;另一种是结果返回方法,即图中的getProduct方法。一般来说,产品所包含的零件数目与建造方法的数目相符。换言之,有多少零件,就有多少相应的建造方法。
- 具体创建者角色:他们在应用程序中负责创建产品的实例。这个角色要完成的任务包括:
1、实现抽象创建者所声明的抽象方法,给出一步一步的完成产品创建实例的操作。 2、在创建完成后,提供产品的实例。
- 导演者角色:这个类调用具体创建者角色以创建产品对象。但是导演者并没有产品类的具体知识,真正拥有产品类的具体知识的是具体创建者角色。
- 产品角色:产品便是建造中的复杂对象。一般说来,一个系统中会有多于一个的产品类,而且这些产品类并不一定有共同的接口,而完全可以使不相关联的。
创建者模式的流程是这样的,客户端需要创建什么对象实例就创建一个导演类和这个对象的创建者,将创建者传给导演类,导演类会使用创建者来创建具体的产品。但是导演类并不清楚产品究竟是怎么创建出来的,产品的实际创建过程是由具体工厂来负责的,具体工厂在创建时也是分为若干步骤,比如图中表示出了两个部分part1和part2,分别对应产品的两个零件,具体工厂就是通过一点一点的创建产品的每个部分最后组成产品。
在实际的应用中不可能只用创建者模式创建一种产品,下边就以两种产品为例讨论一下怎么样通过创建者模式创建多种产品。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-20_57175853174cb.jpg)
要想使用创建者模式创建多种产品的话,有一个隐含的前提,那就是这些产品有共同的特性,也就是说可以用共同的接口实现。比如我们的上图中Product1和Product2都继承自AbstractProduct,并且这两种产品都有part1和part2两部分,但是每种产品的part1和part2实现方式不同,这时就可以使用创建者模式。除了这一点外,另外需要主要的是在AbsractBuilder中返回的是产品的共同父类,这样才能满足多个产品的返回都正常。
可能有人会产生疑问,如果一个产品有3部分组成而另一个产品由2部分组成,是不是就不能使用创建者模式了呢。不是的,同样可以使用创建者模式,在产品类中定义三部分,在只有2不部分的产品中不属于自己的哪一部分设为空就好了。
### 四、实例分析
前边我们讲了土豪坐车的例子,但是还没讲土豪的汽车是怎么生产出来的,我们知道汽车的生产过程是很复杂的,同时汽车又是由很多组建组成的。但是土豪不需要知道汽车是怎样制造出来的,其中的零件怎么安装,按照什么顺序安装等等,这些土豪都不需要知道。所以我们应该将汽车生产这一部分移到一个特定的地方。
可能有人会想我们可以在Audi类的构造方法中进行零件的生产和拼装,但是这样做的话Benz车和Bmw车都需要在构造方法中加入制造零件和拼装组建的功能。一是显得的很麻烦,因为每个类都增加了一块很复杂的代码,同时我们应当认识到,虽然Audi和Bnez的制造细节不同但是大体的流程是差不多的,大家都由引擎,轮胎,方向盘等组成,这些相似的代码在不同的类中重复是很不优雅的。
像这种产品的组成部分相同但是具体生产细节不同的情况特别适合使用创建者模式。
~~~
package com.designpattern.builder;
/**
* 抽象建造类,提供创建产品的共同接口,不同的产品可以有自己的具体实现
* @author
*
*/
public abstract class AbstractBuilder {
/**
* 创建引擎
*/
public abstract void buildEngine();
/**
* 创建车玻璃
*/
public abstract void buildGlass();
/**
* 创建方向盘
*/
public abstract void buildSteeringWheel();
/**
* 返回创建好的产品,为了兼容所有的产品,返回的类型定为共同的父类
* @return
*/
public abstract Car getCar();
}
~~~
~~~
package com.designpattern.builder;
/**
* Audi的具体创建者
* @author xingjiarong
*
*/
public class AudiBuilder extends AbstractBuilder{
/**
* 创建一个各部分都为空的对象
*/
Audi audi = new Audi();
/**
* 创建各个部分
*/
public void buildEngine() {
audi.engine = "AudiEngine";
}
public void buildGlass() {
audi.glass = 3.5;
}
public void buildSteeringWheel() {
audi.steeringWheel = "AudiSteeringWheel";
}
/**
* 返回创建好的对象
*/
public Car getCar() {
return audi;
}
}
~~~
~~~
package com.designpattern.builder;
/**
* 导演类,根据按照产品的建造和组装顺序组装产品
* @author xingjiarong
*
*/
public class Director {
private AbstractBuilder build;
public Director(AbstractBuilder build){
this.build = build;
}
/***
* 组成产品的方法,组成的过程可能是有顺序的
* @return
*/
public Car construct(){
build.buildSteeringWheel();
build.buildGlass();
build.buildEngine();
return build.getCar();
}
}
~~~
~~~
package com.designpattern.builder;
public abstract class Car {
/**
* 汽车引擎,实际应用中应该是一个对象,这里用字符串来表示
*/
public String engine;
/**
* 汽车玻璃,不同的汽车大小不一样,需要根据汽车的型号计算
*/
public double glass;
/**
* 汽车方向盘
*/
public String steeringWheel;
public abstract void drive();
}
~~~
~~~
package com.designpattern.builder;
public class Main {
public static void main(String[] args) throws Exception {
/**
* 创建导演类和Audi的建造者
*/
AudiBuilder build = new AudiBuilder();
Director director = new Director(build);
/**
* 利用导演类获得汽车而不是自己获得汽车
*/
Car car = director.construct();
//开车
car.drive();
}
}
~~~
Audi没有变化,Benz车的代码和Audi车的代码是相似的,不再贴具体的代码,最后会附上源码。
### 五、模式优点
- 在建造者模式中, 客户端不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象。
- 每一个具体建造者都相对独立,而与其他的具体建造者无关,因此可以很方便地替换具体建造者或增加新的具体建造者,用户使用不同的具体建造者即可得到不同的产品对象。
- 可以更加精细地控制产品的创建过程 。将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰,也更方便使用程序来控制创建过程。
- 增加新的具体建造者无须修改原有类库的代码,指挥者类针对抽象建造者类编程,系统扩展方便,符合“开闭原则”。
### 六、模式缺点
- 建造者模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制。
- 如果产品的内部变化复杂,可能会导致需要定义很多具体建造者类来实现这种变化,导致系统变得很庞大。
### 七、适用场景
- 需要生成的产品对象有复杂的内部结构,这些产品对象通常包含多个成员属性。
- 需要生成的产品对象的属性相互依赖,需要指定其生成顺序。
- 对象的创建过程独立于创建该对象的类。在建造者模式中引入了指挥者类,将创建过程封装在指挥者类中,而不在建造者类中。
- 隔离复杂对象的创建和使用,并使得相同的创建过程可以创建不同的产品。
### 八、与抽象工厂模式的区别
在抽象工厂模式中,每一次工厂对象被调用时都会返回一个完整的产品对象,而客户端有可能会决定把这些产品组装成一个更大更复杂的产品,也有可能不会。建造类则不同,它一点一点的建造出一个复杂的产品,而这个产品的组装过程就发生在创建者角色内部。建造者的客户端拿到的是一个完整的最后产品。
换言之,抽象工厂模式处在更加具体的尺度上,而建造者模式处在更加宏观的尺度上。一个系统可以由一个建造模式和一个抽象工厂模式组成,客户端通过调用这个创建角色,间接地调用另一个抽象工厂模式的工厂角色。工厂模式返回不同产品族的零件,而建造者模式则把他们组装起来。
源码下载:[http://download.csdn.net/detail/xingjiarong/9299923](http://download.csdn.net/detail/xingjiarong/9299923)
设计模式(四)单例模式
最后更新于:2022-04-01 11:05:11
**写在前边:**辛辛苦苦写了好几天终于能有一篇发到首页上了,其中的艰辛就不必多说了,我不是专家不发能首页,好多文章博乐也都不看,比起首页上那些空洞无味的文章,我觉得我的博客对一部分人能起到帮助的作用,如果您觉得我写的还可以就顶一下吧,您的支持是我最大的动力!
### 一、模式定义
单例模式(Singleton Pattern):单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它
提供全局访问的方法。
单例模式的要点有三个:一是某个类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实例。单例模式
是一种对象创建型模式。单例模式又名单件模式或单态模式。
### 二、模式结构
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-20_57175852e1d94.jpg "")
我们可以看到单例模式与我们之前所讲的工厂模式不同,它只有一个Singleton角色,并且这个类自行创建自己的实例,并向系统提供这个实
例,可以注意到我没有在图中吧实例的建造个数限制为1个,这一点我们后边会讲到多例模式。
单例模式分为懒汉式的和饿汉式,有的地方也会讲登记式的单例模式,下面我们就一起来学习一下这三种单例模式。
### 三、饿汉式的单例模式
饿汉式单例模式是Java语言中实现起来最为简单的单例模式,我们通过代码来看一下。
~~~
package com.designpattern.singleton;
public class EagerSingleton {
/**
* 直接创建一个本类的对象
*/
private static final EagerSingleton eagerSingleton = new EagerSingleton();
/**
* 覆盖默认的构造方法,将默认的构造方法声明为私有的,防止其他类直接创建对象
*/
private EagerSingleton(){}
/**
* 提供一个工厂方法来返回本类的唯一对象
* @return
*/
public static EagerSingleton getInstance(){
return eagerSingleton;
}
}
~~~
饿汉式单例模式就是一开始就自己创建一个私有的静态的本类对象,当这个类被加载时,静态变量eagerSingleton就会被初始化,这时这个
类的私有构造方法就会被调用。这个时候,单例类的唯一实例就被创建出来了。但需要获得单例类的对象是就调用getInstance方法。
需要注意的一点就是单例类的构造方法一定要声明为私有的,否则其他类就可以利用构造方法直接创建对象,使单例类不再是只有一个唯一
的实例。
另外,值得一提的是,由于构造方法是私有的,因此此类不能被继承。
饿汉式单例模式在自己加载时就将自己实例化,所以从资源利用效率的角度来讲,饿汉式单例模式不如懒汉式单例模式节省资源,但是饿汉
式单例模式的速度更快一些。
### 四、懒汉式的单例模式
懒汉式单例模式与饿汉式稍有不同,下面看一下代码:
~~~
package com.designpattern.singleton;
public class LazySingleton {
/**
* 单例类的唯一实例,但是不是加载时初始化
*/
private static LazySingleton lazySingleton = null;
/**
* 覆盖原有的默认构造方法,声明为私有的,防止其他类直接使用构造方法创建单例类对象,同时也使子类无法继承
*/
private LazySingleton() {
}
/**
* 线程互斥的获取实例
* @return
*/
public static synchronized LazySingleton getInstance() {
/**
* 如果实例为空就创建实例,创建的语句只会执行一次
*/
if (lazySingleton == null) {
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
~~~
从代码我们可以看出,懒汉式单例模式的创建对象是在第一次企图获得单例类的对象时。另外我们再getInstance方法中使用了
synchronization关键字,这是防止出现race condition,使两个线程new出来两个单例类的实例。构造方法同样是私有的,这也决定了子类
不能继承自这个类。
懒汉式单例模式与饿汉式单例模式相比,更节省资源,但是必须处理好在多个线程同时首次引用此类时的访问限制问题,特别是当单例类作
为资源控制器在实例化时必然涉及资源初始化,而资源初始化很有可能耗费时间,这就意味着出现多个线程同时首次引用此类的几率变得较
大。
### 五、登记式的单例模式
为了克服饿汉式单例模式以及懒汉式单例模式类均不可继承的缺点,产生了一种新的模式——登记式单例模式。
登记式的单例模式中父类中有一个集合,用来存储所有的子类的实例,当一个子类创建时,必须在父类的中登记,也就是把自己的实例加入
到父类的集合中,当其他类想要获取子类的实例时,就到父类的集合中查找,找到了就返回,如果找不到就创建这个子类的唯一实例。
这是父类的代码。
~~~
package com.designpattern.singleton;
import java.util.HashMap;
public class RegSingleton {
/**
* 建立一个HashMap来存储子类的完整类名和子类的实例
*/
private static HashMap registry = new HashMap<String, RegSingleton>();
/**
* 首先将本类的实例加入到HashMap中
*/
static{
RegSingleton x = new RegSingleton();
registry.put(x.getClass().getName(), x);
}
/**
* 构造方法不再是private的了,所以子类可以继承了
*/
protected RegSingleton(){
}
/**
* 根据子类传来的类名返回相应的实例
* @param name 想要获得的类的完整类名
* @return
*/
public static RegSingleton getInstance(String name){
/**
* 提供默认的类
*/
if(name == null){
name = "com.designpattern.singleton.RegSingleton";
}
/**
* 第一次引用这个类时创建类的实例,利用了反射机制
*/
if(registry.get(name) == null){
try{
registry.put(name, Class.forName(name).newInstance());
}catch(Exception e){
e.printStackTrace();
}
}
/**
* 返回子类想要的类的实例
*/
return (RegSingleton)registry.get(name);
}
}
~~~
这是子类的代码。
~~~
package com.designpattern.singleton;
public class RegSingletonChild extends RegSingleton{
/**
* 构造方法必须是公有的,否则父类无法产生子类的对象
*/
public RegSingletonChild(){}
/**
* 工厂方法,获取本类的唯一实例,实际上是借助了父类的getInstance方法
* @return
*/
public static RegSingletonChild getInstance(){
return (RegSingletonChild)RegSingleton.getInstance("com.designpattern.singleton.RegSingletonChild");
}
}
~~~
登记式的单例模式解决了懒汉式和饿汉式不能继承的缺点,但是子类中的构造方法变为了public的,所以其他类可以直接通过构造方法创建
类的实例而不用向父类中登记,这是登记式单例模式最大的缺点。
### 六、什么情况下使用单例模式
- 系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器,或者需要考虑资源消耗太大而只允许创建一个对象。
- 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。
- 在一个系统中要求一个类只有一个实例时才应当使用单例模式。反过来,如果一个类可以有几个实例共存,就需要对单例模式进行改进,
使之成为多例模式
可能有人会将一个系统中需要的“全局”变量放在一个单例类中,这样做是不对的。首先,单例模式针对的是只能有一个实例的类而不是变
量,其次一个设计得当的系统就不应该有所谓的“全局”变量,这些变量应该放到他们所描述的实体所对应的类中去。将这些变量从他们的
实体类中抽出来,放到一个不相干的单例类中去,使得这些变量产生错误的依赖关系和耦合关系。
在资源方面管理方面,单例模式用的更多,比如一台计算机连接着一台打印机,那么这个打印机就只有一台,也只应该有一个实例,试想如
果有两个打印机的实例,这两个实例同时操控打印机会出现什么情况,打出来的东西将是乱七八糟的。
所以应该在合适的情况下合理的使用设计模式,而不是一味的追求一个系统利用了多少模式。
### 七、单例模式的优点和缺点
**单例模式的优点:**
- 提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它,并为设计及开发团队提
供了共享的概念。
- 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象,单例模式无疑可以提高系统的性能。
- 允许可变数目的实例。我们可以基于单例模式进行扩展,使用与单例控制相似的方法来获得指定个数的对象实例。
**单例模式的缺点:**
- 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
- 单例类的职责过重,在一定程度上违背了“单一职责原则”。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,
包含一些业务方法,将产品的创建和产品的本身的功能融合到一起。
- 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为单例类,可能会导致共享连接池对象的程序过多而出现连接池
溢出;现在很多面向对象语言(如Java、C#)的运行环境都提供了自动垃圾回收的技术,因此,如果实例化的对象长时间不被利用,系统会认
为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致对象状态的丢失。
### 八、模式在JDK中的应用
Java中的Runtime对象就是一个使用单例模式的例子。在每一个Java应用程序里面,都有唯一的一个Runtime对象,通过这个对象应用程序可
以与其运行环境发生相互作用。
Runtime类提供一个静态工厂方法getRuntime():
`public static Runtime getRuntime();`
通过调用次方法,可以获得Runtime类唯一的一个实例:
`Runtime rt = Runtime.getRuntime();`
### 九、扩展:多例模式
单例是只有一个类实例,自然多例模式就是有多个类的实例了。下面就以有两个实例为例讲一下。
~~~
package com.designpattern.singleton;
import java.util.Random;
public class Die {
/**
* 两个类实例
*/
private static Die die1 = new Die();
private static Die die2 = new Die();
/**
* 私有的构造方法
*/
private Die(){}
/**
* 根据用户使用的标号来决定返回哪一个对象
* @param witchOne
* @return
*/
public static Die getInstance(int witchOne){
if(witchOne == 1)
return die1;
else
return die2;
}
/**
* 使用对象产生随机数
* @return
*/
public synchronized int dice(){
Random rand = new Random();
return rand.nextInt(6)+1;
}
}
~~~
~~~
package com.designpattern.singleton;
public class Client {
public static void main(String[] args){
Die die1 = Die.getInstance(1);
Die die2 = Die.getInstance(2);
System.out.println(die1.dice());
System.out.println(die2.dice());
}
}
~~~
我们看到多例模式的多例类中有多于一个对象实例,究竟返回哪一个对象取决于程序的业务逻辑。
源码下载:[http://download.csdn.net/detail/xingjiarong/9297315](http://download.csdn.net/detail/xingjiarong/9297315)
设计模式(三)抽象工厂模式
最后更新于:2022-04-01 11:05:08
### 一、模式定义
抽象工厂模式(Abstract Factory Pattern):提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。抽象工厂模式又
称为Kit模式,属于对象创建型模式。
### 二、产品族和等级结构
为了更清晰地理解抽象工厂模式,需要先引入两个概念:
**产品等级结构 :**产品等级结构即产品的继承结构,如一个抽象类是电视机,其子类有海尔电视机、海信电视机、TCL电视机,则抽象电
视机与具体品牌的电视机之间构成了一个产品等级结构,抽象电视机是父类,而具体品牌的电视机是其子类。
**产品族 :**在抽象工厂模式中,产品族是指由同一个工厂生产的,位于不同产品等级结构中的一组产品,如海尔电器工厂生产的海尔电视
机、海尔电冰箱,海尔电视机位于电视机产品等级结构中,海尔电冰箱位于电冰箱产品等级结构中。
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-20_571758527750b.jpg "")
从上边的图中我们可以更清晰的看到同族产品和同一等级结构的产品的联系和区别。海尔电视机和长虹电视机都是电视机的子类,这样的关
系叫做同一等级结构;海尔电视机和海尔电冰箱都是海尔公司生产的产品,所以他们都是同族产品。
我们还可以通过下边的相图可以更加清楚的看出来,横轴表示等级结构,向横轴做垂线,在同一条垂线上的是同一个等级结构,纵轴便是产
品组,向纵轴做垂线,同一条垂线上的是一个产品族。
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-20_57175852b3882.jpg "")
如果看了上边的两种解释你还是不能明白同族产品和同一等级结构的产品的话,那么可以看一下下面的这个非常暴力的例子。
你的爷爷和你的外公,你的爸爸和你舅舅,你和你的表弟都是同一等级结构,而你的爷爷和你的爸爸还有你是一个产品族,你的外公和你的
舅舅还有你的表弟是一个产品族,现在能明白什么是产品族什么是同一等级结构了吧。
### 三、模式动机
- 在工厂方法模式中具体工厂负责生产具体的产品,每一个具体工厂对应一种具体产品,工厂方法也具有唯一性,一般情况下,一个具体工
厂中只有一个工厂方法或者一组重载的工厂方法。但是有时候我们需要一个工厂可以提供多个产品对象,而不是单一的产品对象。
- 当系统所提供的工厂所需生产的具体产品并不是一个简单的对象,而是多个位于不同产品等级结构中属于不同类型的具体产品时需要使用
抽象工厂模式。
- 抽象工厂模式是所有形式的工厂模式中最为抽象和最具一般性的一种形态。抽象工厂模式与工厂方法模式最大的区别在于,工厂方法模式
针对的是一个产品等级结构,而抽象工厂模式则需要面对多个产品等级结构,一个工厂等级结构可以负责多个不同产品等级结构中的产品对
象的创建
。当一个工厂等级结构可以创建出分属于不同产品等级结构的一个产品族中的所有对象时,抽象工厂模式比工厂方法模式更为简单、有效
率。
### 四、模式结构
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-20_57175852c96eb.jpg "")
从上图看以看出抽象工厂模式和工厂方法模式类似都是由四部分组成。
- 抽象工厂(AbstractFactory)角色:担任这个角色的是抽象工厂模式的核心,是与应用系统的商业逻辑无关的。通常使用Java接口或者
抽象Java类实现。所有的具体工厂必须实现这个Java接口或继承这个抽象的Java类。
- 具体工厂(Factory)角色:这个角色直接在客户端的调用下创建产品的实例,这个角色含有选择合适的产品对象的逻辑,而这个逻辑是
与应用系统商业逻辑紧密相关的。
- 抽象产品(AbstractProduct)角色:担任这个角色的类是抽象工厂模式所创建的对象的父类,或它们共同拥有的接口。通常使用Java接
口或者抽象Java类实现这一角色。
- 具体产品(Product)角色:抽象工厂模式所创建的任何产品对象都是一个具体的产品类的实例。这是客户端最终需要的东西,其内部一
定充满了应用系统的商业逻辑。通常使用具体Java类实现这个角色。
与工厂方法模式不同的是,抽象工厂模式中的具体工厂不再是只能创建一种产品,一个具体的工厂可以创建一个产品族的产品。
### 五、实例分析
我们接着前边土豪的故事继续讲。话说这个土豪还有一个爱好,就是打猎。但是土豪打猎是有要求的(毕竟土豪嘛,要就就得高一点),他
如果坐Audi车去打猎,那么他就一定要使用AK47这把枪(这是去打猎吗?【偷笑】);如果他坐Benz车去打猎那么他就一定要用M4A1这把枪
,如果按照我们前边讲的工厂方法模式来编程,那么应该是建立一个Car的抽象工厂类CarFactory,然后Benz车的工厂继承自这个抽象的父类
并实现生产Benz车的方法,Audi车的工厂继承自这个抽象的父类并实现生产Audi车的方法。并且还要有一个生产Gun的抽象工厂类,由它的具
体子类工厂来实现生产AK47和M4A1。
这样做是非常麻烦的,我们已经知道了如果土豪做Audi的话那他一定是使用AK47,所以我们可以使用一个工厂来同时生产Audi车和AK47,注
意我说的前提是我们已经知道了土豪一定是Audi车和AK47一起使用的,如果不满足这个条件的话是不能使用抽象工厂模式来解决这个问题的
。
具体的代码如下:
~~~
package com.myfactory.abstractfactory;
/**
* 仅仅定义生产同一族产品的两个不同等级结构的产品接口,具体的实现由子类工厂来实现
* @author xing
*
*/
public abstract class AbstractFactory {
public abstract Car getCar();
public abstract Gun getGun();
}
~~~
~~~
package com.myfactory.abstractfactory;
/**
* Audi车的工厂同时生产Audi车和配套的AK47
* @author xing
*
*/
public class AudiFactory extends AbstractFactory{
public Car getCar() {
return new Audi();
}
public Gun getGun() {
return new AK47();
}
}
~~~
汽车的类和前边的例子是一样的这里不再重复,最后会附上完整的代码。
~~~
package com.myfactory.abstractfactory;
public abstract class Gun {
abstract void fire();
}
~~~
~~~
package com.myfactory.abstractfactory;
public class AK47 extends Gun{
public AK47(){
System.out.println("Create an AK47");
}
public void fire(){
System.out.println("AK47 start fire");
}
}
~~~
在Main函数中就可以通过一个工厂创建两个对象。
~~~
package com.myfactory.abstractfactory;
public class Main {
public static void main(String[] args) throws Exception {
//奔驰车司机
AbstractFactory factory = new BenzFactory();
//今天想做奥迪车
Car car = factory.getCar();
//开车
car.drive();
//获得开Benz时要用的枪
Gun gun = factory.getGun();
//开火
gun.fire();
}
}
~~~
### 六、模式优点
- 抽象工厂模式隔离了具体类的生成,使得客户并不需要知道什么被创建。由于这种隔离,更换一个具体工厂就变得相对容易。所有的具体
工厂都实现了抽象工厂中定义的那些公共接口,因此只需改变具体工厂的实例,就可以在某种程度上改变整个软件系统的行为。另外,应用
抽象工厂模式可以实现高内聚低耦合的设计目的,因此抽象工厂模式得到了广泛的应用。
当一个产品族中的多个对象被设计成一起工作时,它能够保证客户端始终只使用同一个产品族中的对象。这对一些需要根据当前环境来决
定其行为的软件系统来说,是一种非常实用的设计模式。
- 增加新的具体工厂和产品族很方便,无须修改已有系统,符合“开闭原则”。
### 七、模式缺点
- 在添加新的产品对象时,难以扩展抽象工厂来生产新种类的产品,这是因为在抽象工厂角色中规定了所有可能被创建的产品集合,要支持
新种类的产品就意味着要对该接口进行扩展,而这将涉及到对抽象工厂角色及其所有子类的修改,显然会带来较大的不便。
- 开闭原则的倾斜性(增加新的工厂和产品族容易,增加新的产品等级结构麻烦)。
### 八、适用场景
1、一个系统不应当依赖于产品类实例如何被创建、组合和表达的细节,这对于所有形态的工厂模式都是重要的。
2、这个系统的产品有多于一个的产品族,而系统只消费其中某一族的产品。
3、同属于同一个产品族的产品是在一起使用的,这一约束必须在系统的设计中体现出来。
4、系统提供一个产品类的库,所有的产品以同样的接口出现,从而使客户端不依赖与实现。
### 九、抽象工厂模式的退化
当抽象工厂模式中每一个具体工厂类只创建一个产品对象,也就是只存在一个产品等级结构时,抽象工厂模式退化成工厂方法模式;当工厂
方法模式中抽象工厂与具体工厂合并,提供一个统一的工厂来创建产品对象,并将创建对象的工厂方法设计为静态方法时,工厂方法模式退
化成简单工厂模式。
源码下载:[http://download.csdn.net/detail/xingjiarong/9296125](http://download.csdn.net/detail/xingjiarong/9296125)
设计模式(二)工厂方法模式
最后更新于:2022-04-01 11:05:06
### 一、模式定义
工厂方法模式(Factory Method Pattern)又称为工厂模式,也叫虚拟构造器(Virtual Constructor)模式或者多态工厂(Polymorphic Factory)模式,它属于类创建型模式。在工厂方法模式中,工厂父类负责定义创建产品对象的公共接口,而工厂子类则负责生成具体的产品对象,这样做的目的是将产品类的实例化操作延迟到工厂子类中完成,即通过工厂子类来确定究竟应该实例化哪一个具体产品类。
### 二、模式结构
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-20_571758164f814.jpg "")
我们可以看到工厂方法模式一共分为四个部分:
- 抽象工厂(AbstractCreator)角色:担任这个角色的是工厂方法模式的核心,它是与应用程序无关的。任何在模式中创建对象的工厂类必须继承或者实现这个接口,在实际的系统中,这个角色常常有Java抽象类来实现。
- 具体工厂(ConcreteCreator)角色:担任这个角色的是实现了抽象工厂接口的具体Java类。具体工厂角色含有与应用密切相关的逻辑,并且受到应用程序的调用以创建产品对象。
- 抽象产品(AbstractProduct)角色:工厂方法模式所创建的对象的超类型,也就是产品对象的共同父类或共同拥有的接口。在实际应用中这个角色常常由Java的抽象类来实现。
- 具体产品(ConcreteProduct)角色:这个角色实现了抽象产品角色所声明的接口,工厂方法所创建的每一个对象都是某个具体产品角色的实例。
### 三、模式动机
1、解决简单工厂模式中存在的“上帝类”的问题,将具体的生产任务放到子类中去。
2、解决简单工厂模式工厂角度不符合“开-闭”原则的问题。
3、解决简单工厂模式不能使用继承的问题。
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-20_57175816640af.jpg "")
### 四、实例分析
我们继续来分析上一篇博客中土豪开车的问题,我们使用了简单工厂模式后,程序好像已经很合理了,并且从实际生活来看也是比较符合逻辑的。但是可能会存在以下的问题。
1、如果土豪家里又买了很多车,那么Driver(工厂类)将会非常大,每次买一辆新的车都需要在这个工厂类中添加一个if-else语句,如果有的车坏了不能开了,还需要将对应的if-else语句删除,所以很麻烦,也不符合“开-闭”原则。
2、所有的产品创建都集中在Driver类中,一旦写错一个标点,所有的产品便都不能生产了。(可以想象成,只有一位司机师傅,这位司机师傅会开所有的车,一旦这一位司机师傅生病了,那么所有的汽车就都不能开了)。
所以我们可以使用工厂方法模式以下的改进。
首先,定义一个抽象的工厂类(Driver),这个抽象工厂类中有定义好的抽象方法getCar();用来告诉具体的工厂如何来获得汽车。
~~~
package com.myfactory.factory;
public abstract class Driver {
public abstract Car getCar();
}
~~~
然后,每一种车都配一个专门的司机,这个司机只负责或者一种车。
~~~
package com.myfactory.factory;
public class BenzDriver extends Driver{
public Car getCar() {
return new Benz();
}
}
~~~
这里只写了Benz车的类,其他的车的类和这个类基本相同,不再贴出代码,最后会附上源码。
在客户端中,创建对应的Driver类,让特定的Driver来生产特定的汽车。
~~~
package com.myfactory.factory;
public class Main {
public static void main(String[] args) throws Exception {
//奔驰车司机
Driver driver = new BenzDriver();
//今天想做奥迪车
Car car = driver.getCar();
//开车
car.drive();
}
}
~~~
通过改进,我们看到不再存在“上帝类”,有具体的工厂类来负责生产特定的产品,以后想要添加新的车的时候不必修改现有的工厂和产品的代码,只需要添加新的产品类和具体工厂类就可以了,不管是产品角度还是工厂角度都更加符合“开-闭”原则。
### 五、模式优点
- 在工厂方法模式中,工厂方法用来创建客户所需要的产品,同时还向客户隐藏了哪种具体产品类将被实例化这一细节,用户只需要关心所需产品对应的工厂,无须关心创建细节,甚至无须知道具体产品类的类名。
- 基于工厂角色和产品角色的多态性设计是工厂方法模式的关键。它能够使工厂可以自主确定创建何种产品对象,而如何创建这个对象的细节则完全封装在具体工厂内部。工厂方法模式之所以又被称为多态工厂模式,是因为所有的具体工厂类都具有同一抽象父类。
- 使用工厂方法模式的另一个优点是在系统中加入新产品时,无须修改抽象工厂和抽象产品提供的接口,无须修改客户端,也无须修改其他的具体工厂和具体产品,而只要添加一个具体工厂和具体产品就可以了。这样,系统的可扩展性也就变得非常好,完全符合“开闭原则”。
### 六、模式缺点
- 在添加新产品时,需要编写新的具体产品类,而且还要提供与之对应的具体工厂类,系统中类的个数将成对增加,在一定程度上增加了系统的复杂度,有更多的类需要编译和运行,会给系统带来一些额外的开销。
- 由于考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增加了系统的抽象性和理解难度,且在实现时可能需要用到DOM、反射等技术,增加了系统的实现难度。
### 七、适用场景
在以下情况下可以使用工厂方法模式:
1、一个类不知道它所需要的对象的类:在工厂方法模式中,客户端不需要知道具体产品类的类名,只需要知道所对应的工厂即可,具体的产品对象由具体工厂类创建;客户端需要知道创建具体产品的工厂类。
2、一个类通过其子类来指定创建哪个对象:在工厂方法模式中,对于抽象工厂类只需要提供一个创建产品的接口,而由其子类来确定具体要创建的对象,利用面向对象的多态性和里氏代换原则,在程序运行时,子类对象将覆盖父类对象,从而使得系统更容易扩展。
3、将创建对象的任务委托给多个工厂子类中的某一个,客户端在使用时可以无须关心是哪一个工厂子类创建产品子类,需要时再动态指定,可将具体工厂类的类名存储在配置文件或数据库中。
### 八、模式在JDK中的应用
1、Object中的toString方法:Object仅仅定义了返回String,由Object的子类来决定如何生成这个字符串。
2、抽象类java.util.Calendar的getInstance方法将根据不同的情况返回不同的Calendar子类的对象。
源码下载:[http://download.csdn.net/detail/xingjiarong/9294193](http://download.csdn.net/detail/xingjiarong/9294193)
设计模式(一)简单工厂模式
最后更新于:2022-04-01 11:05:04
### 一、模式定义
简单工厂模式(Simple Factory Pattern):又称为静态工厂方法(Static Factory Method)模式,它属于类创建型模式(同属于创建型模式的还有工厂方法模式,抽象工厂模式,单例模式,建造者模式)。在简单工厂模式中,可以根据参数的不同返回不同类的实例。简单工厂模式专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。
### 二、模式结构
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-20_571758163b57c.jpg "")
从上图可以看出,简单工厂模式由三部分组成:具体工厂、具体产品和抽象产品。
-
工厂类(Creator)角色:担任这个角色的是简单工厂模式的核心,含有与应用紧密相关的商业逻辑。工厂类在客户端的直接调用下创建产品对象,它往往由一个具体Java类实现。
-
抽象产品(AbstractProduct)角色:担任这个角色的类是由简单工厂模式所创建的对象的父类,或它们共同拥有的接口。抽象产品角色可以用一个Java接口或者Java抽象类实现。
-
具体产品(ConcreteProduct)角色:简单工厂模式所创建的任何对象都是这个角色的实例,具体产品角色由一个具体Java类实现。
### 三、模式动机
使用简单工厂模式可以将产品的“消费”和生产完全分开,客户端只需要知道自己需要什么产品,如何来使用产品就可以了,具体的产品生产任务由具体的工厂类来实现。工厂类根据传进来的参数生产具体的产品供消费者使用。这种模式使得更加利于扩展,当有新的产品加入时仅仅需要在工厂中加入新产品的构造就可以了。
### 四、实例分析
话说有一位土豪,他家有三辆汽车——Benz奔驰、Bmw宝马、Audi奥迪,还雇了司机为他开车。不过,土豪坐车时总是怪怪的:上Benz车后跟司机说“开奔驰车!”坐上Bmw后他说“开宝马车!”,坐上Audi说“开奥迪车!”。你一定说:这人有病!直接说开车不就行了?! 而当把土豪的行为放到我们程序设计中来时,会发现这是一个普遍存在的现象。幸运的是,这种有病的现象在OO(面向对象)语言中可以避免了。下面就以Java语言为例来讲解一下如何避免这种问题。
下面就是刚刚的故事所描述的场景,这段代码真的是有病啊。
这个是Audi车的类,在这个类中有driveAudi的方法。
~~~
package com.myfactory.pre1;
public class Audi {
public Audi(){
System.out.println("Create a Audi");
}
public void driveAudi(){
System.out.println("Audi start engine");
}
}
~~~
这个是Benz车的类,在这个类中有driveBenz 的方法。
~~~
package com.myfactory.pre1;
public class Benz {
public Benz(){
System.out.println("Create a Benz");
}
public void driveBenz(){
System.out.println("Benz start engine");
}
}
~~~
这个是Bmw车的类,在这个类中有driveBmw 的方法。
~~~
package com.myfactory.pre1;
public class Bmw {
public Bmw(){
System.out.println("Create a Bmw");
}
public void driveBmw(){
System.out.println("Bmw start engine");
}
}
~~~
主程序调用刚刚创建的三辆车的类。
~~~
package com.myfactory.pre1;
public class Main {
public static void main(String[] args) {
//今天想做奥迪车
Audi audi = new Audi();
//开奥迪车
audi.driveAudi();
}
}
~~~
学过程序设计的人都能看出来上边这段代码的结构非常的不好。有两点,一是,不管是Audi,Benz还是Bmw,大家不都是车吗,为什么不直接创建一个Car的父类,然后让其他的汽车的子类来继承呢,二是,不管是什么车肯定都能开啊,干嘛要分那么清楚呢,直接在父类中写一个drive方法让子类来实现不就好了,如果每辆汽车都有自己名字命名的drive方法,那如果main中稍微写错一点程序就跑不起来了。
所有针对以上的问题我们可以做如下的优化。
创建了一个新的类Car,作为所有汽车的父类,定义了一个抽象的drive方法,具体的实现由子类来实现。
~~~
package com.myfactory.pre2;
public abstract class Car {
abstract void drive();
}
~~~
这是改进后的Audi类实现了父类的drive方法。
~~~
package com.myfactory.pre2;
public class Audi extends Car{
public Audi(){
System.out.println("Create a Audi");
}
public void drive(){
System.out.println("Audi start engine");
}
}
~~~
Benz和Bmw都是类似的,这里为了节约篇幅我就不再贴出来了,最后回复上完整的源码。
这是改进后的新客户端:
~~~
package com.myfactory.pre2;
public class Main {
public static void main(String[] args) {
//今天想做奥迪车
Car car = new Audi();
//开车
car.drive();
}
}
~~~
这个方案看似完美但其实还是存在巨大的隐患。我提供对的实例是比较简单的,但实际应用中创建对象时可能不是一句话就能解决的事,比如创建奥迪车时需要发动机的型号,轮胎的大小,玻璃的尺寸,并且尺寸可能是由一系列复杂的运算计算出来的,所以如果把创建对象的任务放到客户端中,就是使客户端显得非常的臃肿,所以我们可以借助简单工厂模式进行改进。
我们创建一个工厂类,这个工厂类专门负责建造各种汽车,代码如下:
~~~
package com.myfactory.simplefactory;
public class Driver {
public static Car getCar(String type) throws Exception{
if(type.equals("Benz")){
return new Benz();
}else if(type.equals("Audi")){
return new Audi();
}else if(type.equals("Bmw")){
return new Bmw();
}else{
throw new Exception();
}
}
}
~~~
各种汽车的类还是不变,这里不再贴出代码,此时的客户端发生了变化。
~~~
package com.myfactory.simplefactory;
public class Main {
public static void main(String[] args) throws Exception {
//今天想做奥迪车
Car car = Driver.getCar("Audi");
//开车
car.drive();
}
}
~~~
我们看到,土豪不再需要自己new对象了,而是告诉Driver类今天我想要开什么车,Driver就会自动的将想要的汽车创建出来,然后土豪坐在车里只需要说开车就好了,这样不就符合我们的逻辑了吗。
在上面的那个例子中,Driver类就是我们说的工厂,他用if-else语句来判断需要创建什么类型的对象(当然也可以使用switch语句),这就是我们一直说的简单工厂模式。
### 五、模式优点
- 工厂类含有必要的判断逻辑,可以决定在什么时候创建哪一个产品类的实例,客户端可以免除直接创建产品对象的责任,而仅仅“消费”产品;简单工厂模式通过这种做法实现了对责任的分割,它提供了专门的工厂类用于创建对象。
- 客户端无须知道所创建的具体产品类的类名,只需要知道具体产品类所对应的参数即可,对于一些复杂的类名,通过简单工厂模式可以减少使用者的记忆量。
- 通过引入配置文件,可以在不修改任何客户端代码的情况下更换和增加新的具体产品类,在一定程度上提高了系统的灵活性。
- 当需要引入新的产品是不需要修改客户端的代码,只需要添加相应的产品类并修改工厂类就可以了,所以说从产品的角度上简单工厂模式是符合“开-闭”原则的。
### 六、模式缺点
- 由于工厂类集中了所有产品创建逻辑,工厂类一般被我们称作“全能类”或者“上帝类”,因为所有的产品创建他都能完成,这看似是好事,但仔细想想是有问题的。比如全国上下所有的事情都有国家主义一个人干会不会有问题,当然有!一旦不能正常工作,整个系统都要受到影响。
- 使用简单工厂模式将会增加系统中类的个数,在一定程序上增加了系统的复杂度和理解难度。
- 系统扩展困难,一旦添加新产品就不得不修改工厂逻辑,在产品类型较多时,有可能造成工厂逻辑过于复杂,不利于系统的扩展和维护。所以说从工厂的角度来说简单工厂模式是不符合“开-闭”原则的。
- 简单工厂模式由于使用了静态工厂方法,造成工厂角色无法形成基于继承的等级结构。
### 七、适用场景
在以下情况下可以使用简单工厂模式:
1、工厂类负责创建的对象比较少:由于创建的对象较少,不会造成工厂方法中的业务逻辑太过复杂。
2、客户端只知道传入工厂类的参数,对于如何创建对象不关心:客户端既不需要关心创建细节,甚至连类名都不需要记住,只需要知道类型所对应的参数。
### 八、模式在JDK中的应用
JDK类库中广泛使用了简单工厂模式,如工具类java.text.DateFormat,它用于格式化一个本地日期或者时间。
~~~
public final static DateFormat getDateInstance();
public final static DateFormat getDateInstance(int style);
public final static DateFormat getDateInstance(int style,Locale
locale);
~~~
源码下载:[http://download.csdn.net/detail/xingjiarong/9294193](http://download.csdn.net/detail/xingjiarong/9294193)
设计原则(四)依赖倒置原则(DIP)
最后更新于:2022-04-01 11:05:02
### 一、什么是依赖倒置原则
一种表述:
抽象不应当依赖于细节;细节应当依赖于抽象。
另一种表述:
要针对接口编程,不要针对实现编程。
针对接口编程的意思就是说,应当使用Java接口和抽象Java类进行变量的类型声明、参量的类型声明、方法的返回类型声明,以及数据类型的转换等。
不要针对实现编程的意思就是说,不应当使用具体Java类进行变量的类型声明、参量的类型声明、方法的返回类型声明,以及数据类型的转换等。
### 二、为什么要倒置
传统的过程性系统的设计方法倾向于使高层次的模块依赖于低层次的模块,抽象层次依赖于具体层次。倒置原则就是要把这个错误的依赖关系倒转过来。
抽象层次包含的是应用程序的商务逻辑和宏观的、对整个系统来说重要的战略性决定,是必然性的体现;而具体层次则含有一些次要的与实现有关的算法和逻辑,以及战术性的决定,带有相当大的偶然性选择。具体层次的代码经常会有变动,不能避免错误。抽象层次依赖于具体层次,使许多具体层次的细节的算法变化立即影响到抽象层次的宏观的商务逻辑,**倒置微观决定宏观,战术决定战略,偶然决定必然,**这不是很荒唐吗。
### 三、再看工厂方法模式
按照依赖倒置原则,客户端应该依赖于对象的抽象类型而不是它的具体类型,但是在Java中使用new创建一个具体对象实例时必须调用具体类的构造方法,所以Java语言给出的类的实例无法做到只依赖于抽象类型。但是我们可以做到部分依赖,因为Java中有多态的特性。例如A是一个抽象类,B是继承自A的一个具体子类,我们可以写如下的代码:
~~~
B b = new B();
~~~
这是一个完全依赖具体的不好的写法,我们可以利用多态性写成下面这样:
~~~
A a = new B();
~~~
这样就变成了部分依赖,前边的A是依赖于抽象的,这样当B换成其他的继承自A的子类的时候,其他的程序是不会受到影响的。即使是这样还是没有做到完全依赖于抽象,在设计模式中前边讲过的工厂方法模式就解决了这个问题。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-20_571757da0cf3d.jpg)
工厂方法模式将创建一个对象的工程封装起来,客户端仅仅得到这个实例化的结果,以及这个实例的抽象类型,当然工厂方法也不能避免要用new创建对象的限制,但是工厂模式将这个违反规则的做法推迟到了具体工厂角色中。将违反规则的做法孤立于易于控制的地方。
### 四、实例分析
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-20_571757da27124.jpg)
这是一个银行账户的例子,一个Account包含两个部分,AccountType和AccountStatus,这两个类都是抽象类,这两个类分别有两个具体的子类,Account在使用的是抽象类而不是子类,从而在Account的角度上是符合依赖倒置原则的。
~~~
package com.designphilsophy.dip;
/**
* 账户
* @author xingjiarong
*
*/
public class Account {
private AccountType accountType;
private AccountStatus accountStatus;
public Account(AccountType acctType) {
// write your code here
}
public void deposit(float amt) {
// write your code here
}
}
~~~
~~~
package com.designphilsophy.dip;
/**
* 账户的状态是开的状态还是超支状态
* @author xingjiarong
*
*/
public abstract class AccountStatus
{
public abstract void sendCorrespondence();
}
~~~
~~~
package com.designphilsophy.dip;
/**
* 账户的类型是储蓄卡还是支票
*
* @author xingjiarong
*
*/
public abstract class AccountType {
public abstract void deposit(float amt);
}
~~~
~~~
package com.designphilsophy.dip;
/**
* 支票
*
* @author xingjiarong
*
*/
public class Checking extends AccountType {
public void deposit(float amt) {
// write your code here
}
}
~~~
~~~
package com.designphilsophy.dip;
/**
* 储蓄卡
*
* @author xingjiarong
*
*/
public class Savings extends AccountType {
public void deposit(float amt) {
// write your code here
}
}
~~~
~~~
package com.designphilsophy.dip;
/**
* 开状态
*
* @author xingjiarong
*
*/
public class Open extends AccountStatus {
public void sendCorrespondence() {
// write your code here
}
}
~~~
在这个例子中,Account类依赖于AccountType和AccountStatus,但是这两个类都是抽象类,如果我们想再添加一个新的状态超支状态的话,不会对其他的类造成影响。
~~~
package com.designphilsophy.dip;
/**
* 超支状态
* @author xingjiarong
*
*/
public class Overdrawn extends AccountStatus {
public void sendCorrespondence() {
// write your code here
}
}
~~~
源码下载:[http://download.csdn.net/detail/xingjiarong/9309387](http://download.csdn.net/detail/xingjiarong/9309387)
设计原则(三)组合复用原则
最后更新于:2022-04-01 11:04:59
### 一、什么是组合复用原则
组合复用原则也叫合成/聚合复用原则(CARP),就是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分;新的对象通过向这些对象的委派达到复用已有功能的目的。
**这个原则的简短表述就是:要尽量使用组合,尽量不要使用继承。**
### 二、实现复用的两种方式
在面向对象的设计里,有两种基本的方法可以在不同的环境中复用已有的设计和实现,即通过组合或通过继承。
**组合**
由于组合可以将已有的对象纳入到新对象中,使之成为新对象的一部分,因此新对象可以调用已有对象的功能,这样做有下面的好处:
- 新对象存取成分对象的唯一方法是通过成分对象的接口。
- 这种复用是黑箱复用,因为成分对象的内部细节是新对象所看不见的。
- 这种复用支持包装。
- 这种复用所需要的依赖较少。
- 每一个新的类可以将焦点集中到一个任务上。
- 这种复用可以在运行时间动态进行,新对象可以动态的引用与成分对象类型相同的对象。
组合复用的缺点就是用组合复用建造的系统会有较多的对象需要管理。
**继承**
组合几乎可以用到任何环境中去,但是继承只能用到一些环境中。
继承复用通过扩展一个已有对象的实现来得到新的功能,基类明显的捕获共同的属性和方法,而子类通过增加新的属性和方法来扩展超类的实现。
继承的优点:
新的实现比较容易,因为基类的大部分功能都可以通过继承自动的进入子类。
修改或扩展继承而来的实现较为容易。
继承的缺点:
- 继承复用破坏了包装,因为继承超类的的实现细节暴露给子类。由于超类的内部细节常常对子类是透明的,因此这种复用是透明的复用,又称“白箱”复用。
- 如果超类的实现发生改变,那么子类的实现也不得不发生改变。因此,当一个基类发生改变时,这种改变就会像水中投入石子引起的水波一样,将变化一圈又一圈的传导到一级又一级的子类,使设计师不得不相应地改变这些子类,以适应超类的变化。
- 从超类继承而来的实现是静态的,不可能在运行时间内发生改变,因此没有足够的灵活性。
### 三、使用组合还是继承
按照组合复用原则我们应该首选组合,然后才是继承,使用继承时应该严格的遵守里氏替换原则,必须满足“Is-A”的关系是才能使用继承,而组合却是一种“Has-A”的关系。导致错误的使用继承而不是使用组合的一个重要原因可能就是错误的把“Has-A”当成了“Is-A”。
下面看一个例子:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-20_571757d9b9826.jpg)
人被继承到雇员,学生,经理子类。而实际上,雇员、学生和经理分别描述一种角色,而人可以同时有几种不同的角色。比如,一个人既然是经理了就一定是雇员,使用继承来实现角色,则只能使用每一个人具有一种角色,这显然是不合理的。错误的原因就是把角色的等级结构和人的等级结构混淆起来,把Has-A的关系误认为是Is-A的关系,通过下面的改正就可以正确的做到这一点。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-20_571757d9ca96a.jpg)
从上图可以看出,每一个人都可以有一个以上的角色,所以一个人可以同时是雇员又是经理。从这个例子可以看出,当一个类是另一个类的角色时,不应该使用继承描述这种关系。
设计原则(二)里氏替换原则(LSP)
最后更新于:2022-04-01 11:04:57
### 一、什么是里氏替换原则
里氏替换原则的严格表达是:
如果对每一个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有的对象o1都替换成o2时,程序P的行为没有变化,那么类型T2是类型T1的子类型。
换言之,一个软件实体如果使用的是一个基类的话,那么一定适用于其子类,而且它根本不能察觉出基类对象和子类对象的区别。
比如,假设有两个类,一个是Base类,另一个是Child类,并且Child类是Base的子类。那么一个方法如果可以接受一个基类对象b的话:method1(Base b)那么它必然可以接受一个子类的对象method1(Child c).
里氏替换原则是继承复用的基石。只有当衍生类可以替换掉基类,软件单位的功能不会受到影响时,基类才能真正的被复用,而衍生类也才能够在基类的基础上增加新的行为。
但是需要注意的是,反过来的代换是不能成立的,如果一个软件实体使用的是一个子类的话,那么它不一定适用于基类。如果一个方法method2接受子类对象为参数的话method2(Child c),那么一般而言不可以有method2(b).
### 二、墨子的智慧
《墨子:小取》中说,“白马,马也;乘白马,乘马也。骊马,马也;乘骊马,乘马也”。文中的骊马是黑的马。意思就是白马和黑马都是马,乘白马或者乘黑马就是乘马。在面向对象中我们可以这样理解,马是一个父类,白马和黑马都是马的子类,我们说乘马是没有问题的,那么我们把父类换成具体的子类,也就是乘白马和乘黑马也是没有问题的,这就是我们上边说的里氏替换原则。
墨子同时还指出了反过来是不能成立的。《墨子:小取》中说:“娣,美人也,爱娣,非爱美人也”。娣是指妹妹,也就是说我的妹妹是没人,我爱我的妹妹(出于兄妹感情),但是不等于我爱美人。在面向对象里就是,美人是一个父类,妹妹是美人的一个子类。哥哥作为一个类有“喜爱()”方法,可以接受妹妹作为参量。那么这个“喜爱()”不能接受美人类的实例,这也就说明了反过来是不能成立的。
### 三、正方形是不是长方形
上过数学课的人都知道,正方形是一种特殊的长方形,只不过是它的长和宽是一样的,也就是说我们在面向对象里我们应当将长方形设计成父类,将正方形设计成长方形的子类,但是我可以很负责的告诉你,**这样做是错误的,是不符合里氏替换原则的。**
~~~
package com.designphilsophy.lsp.version1;
/**
* 定义一个长方形类,只有标准的get和set方法
*
* @author xingjiarong
*
*/
public class Rectangle {
protected long width;
protected long height;
public void setWidth(long width) {
this.width = width;
}
public long getWidth() {
return this.width;
}
public void setHeight(long height) {
this.height = height;
}
public long getHeight() {
return this.height;
}
}
~~~
~~~
package com.designphilsophy.lsp.version1;
/**
* 定义一个正方形类继承自长方形类,只有一个side
*
* @author xingjiarong
*
*/
public class Square extends Rectangle {
public void setWidth(long width) {
this.height = width;
this.width = width;
}
public long getWidth() {
return width;
}
public void setHeight(long height) {
this.height = height;
this.width = height;
}
public long getHeight() {
return height;
}
}
~~~
~~~
package com.designphilsophy.lsp.version1;
public class SmartTest
{
/**
* 长方形的长不短的增加直到超过宽
* @param r
*/
public void resize(Rectangle r)
{
while (r.getHeight() <= r.getWidth() )
{
r.setHeight(r.getHeight() + 1);
}
}
}
~~~
在上边的代码中我们定义了一个长方形和一个继承自长方形的正方形,看着是非常符合逻辑的,但是当我们调用SmartTest类中的resize方法时,长方形是可以的,但是正方形就会一直增大,一直long溢出。但是我们按照我们的里氏替换原则,父类可以的地方,换成子类一定也可以,所以上边的这个例子是不符合里氏替换原则的。
问题由来:有一功能P1,由类A完成。现需要将功能P1进行扩展,扩展后的功能为P,其中P由原有功能P1与新功能P2组成。新功能P由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会导致原有功能P1发生故障。
解决方案:当使用继承时,遵循里氏替换原则。类B继承类A时,除添加新的方法完成新增功能P2外,尽量不要重写父类A的方法,也尽量不要重载父类A的方法。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-20_571757d98abc5.jpg)
刚才我们写的代码的结构就是上边那样的,对于这样不符合里氏替换原则原则的关系,我们在代码重构的时候一般采用下面的方法。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-20_571757d9a0ede.jpg)
我们再定义一个他们共同的父类,然后让正方形和长方形都继承自这个父类。
具体的代码如下:
~~~
package com.designphilsophy.lsp.version2;
/**
* 定义一个四边形类,只有get方法没有set方法
* @author xingjiarong
*
*/
public abstract class Quadrangle {
protected abstract long getWidth();
protected abstract long getHeight();
}
~~~
~~~
package com.designphilsophy.lsp.version2;
/**
* 自己声明height和width
* @author xingjiarong
*
*/
public class Rectangle extends Quadrangle {
private long width;
private long height;
public void setWidth(long width) {
this.width = width;
}
public long getWidth() {
return this.width;
}
public void setHeight(long height) {
this.height = height;
}
public long getHeight() {
return this.height;
}
}
~~~
~~~
package com.designphilsophy.lsp.version2;
/**
* 自己声明height和width
* @author xingjiarong
*
*/
public class Square extends Quadrangle
{
private long width;
private long height;
public void setWidth(long width) {
this.height = width;
this.width = width;
}
public long getWidth() {
return width;
}
public void setHeight(long height) {
this.height = height;
this.width = height;
}
public long getHeight() {
return height;
}
}
~~~
在基类Quadrange类中没有赋值方法,因此类似于SamrtTest的resize()方法不可能适用于Quadrangle类型,而只能适用于不同的具体子类Rectangle和Aquare,因此里氏替换原则不可能被破坏了。
### 四、为什么要符合里氏替换原则
里氏替换原则通俗的来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。它包含以下4层含义:
- 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
- 子类中可以增加自己特有的方法。
- 当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
- 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
看上去很不可思议,因为我们会发现在自己编程中常常会违反里氏替换原则,程序照样跑的好好的。所以大家都会产生这样的疑问,假如我非要不遵循里氏替换原则会有什么后果?来看一个例子。
~~~
package com.designphilsophy.lsp.version3;
public class A{
public int func1(int a, int b){
return a-b;
}
}
~~~
~~~
package com.designphilsophy.lsp.version3;
public class B extends A{
public int func1(int a, int b){
return a+b;
}
public int func2(int a, int b){
return func1(a,b)+100;
}
}
~~~
~~~
package com.designphilsophy.lsp.version3;
public class Client{
public static void main(String[] args){
B b = new B();
System.out.println("100-50="+b.func1(100, 50));
System.out.println("100-80="+b.func1(100, 80));
System.out.println("100+20+100="+b.func2(100, 20));
}
}
~~~
输入结果:
100-50=150
100-80=180
100+20+100=220
我们发现原本运行正常的相减功能发生了错误。原因就是类B在给方法起名时无意中重写了父类的方法,造成所有运行相减功能的代码全部调用了类B重写后的方法,造成原本运行正常的功能出现了错误。在本例中,引用基类A完成的功能,换成子类B之后,发生了异常。在实际编程中,我们常常会通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的几率非常大。
源码下载:[http://download.csdn.net/detail/xingjiarong/9308063](http://download.csdn.net/detail/xingjiarong/9308063)
设计原则(一)"开-闭"原则(OCP)
最后更新于:2022-04-01 11:04:55
经典力学的基石是牛顿三大定律。而面向对象的可复用设计的第一块基石,便是所谓的”开-闭“原则(Open-Closed Principle,常缩写为OCP)。
### 一、什么是开闭原则
“开-闭”原则讲的是:一个软件实体应当对扩展开放,对修改关闭。这一原则最早由Bertrand Meyer提出,英文原文是:
Software entities should be open for extension,but closed for modification.
这个原则说的是,在设计一个模块的时候应该使这个模块可以在不被修改的前提下被扩展。换言之,应该可以在不必修改源代码的情况下改变这个模块的行为。
### 二、使用开闭原则有什么好处
所有的软件系统都有一个共同的性质,即对他们的需求会随着时间的推移而发生变化。在软件系统面临新的需求的时候,系统的设计必须是稳定的。满足“开-闭”原则的设计可以给一个软件系统两个无可比拟的优越性:
1、通过扩展已有的软件系统,可以提供新的行为,以满足对软件的新的需求,使变化中的软件系统有一定的适应性和灵活性。
2、已有的软件模块,特别是最重要的抽象层模块不能再修改,这就使变化中的软件系统有一定的稳定性和延续性。
具有以上两个优点的软件系统是一个在高层次上实现了复用的系统,也是一个易于维护的系统。
### 三、如何做到符合开闭原则
我们来看一下在《西游记》中玉皇大帝在美猴王的挑战下是怎样维护天庭的秩序的。
当年大闹天宫时的美猴王便是玉帝天庭的新挑战。美猴王说:“皇帝轮流做,明年到我家。只教他搬出去,将天宫让与我!”对于这项挑战,太白金星给玉皇大帝提出的建议是:“臣启陛下……降一道招安圣旨,把他宣来上界……与他籍名在篆……一则不动众劳师,二则收仙有道也。”
换言之,不劳师动众、不破坏天规便是“闭”,收仙有道便是“开”。招安之法便是玉帝天庭的“开-闭”原则,通过给美猴王封一个“弼马温”的官职,便可使现有系统满足了变化的需求,而不必更改天庭的既有秩序,如下图所示。
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-20_571757d972a72.jpg "")
招安之法的关键便是不允许更改现有的天庭秩序,但允许将妖猴纳入现有的之中,从而扩展了这一秩序。用面相对象的语言来讲,不允许更改的是系统的抽象层,而允许扩展的是系统的实现层。
**抽象化是关键**
解决问题的关键在于抽象化。在像java这样的面向对象编程语言里面,可以给系统一个一劳永逸、不再改变的抽象设计,该设计允许有无穷无尽的行为在实现层被实现。在java语言里,可以给出一个或多个抽象java类或者java接口,规定出所有的具体类必须提供的方法的特征作为系统设计的抽象层。这个抽象层预见了所有的可能扩展,因此,在任何情况下都不会改变。这使得系统的抽象层不需要修改,从而满足了“开-闭”原则的第二条:对修改关闭。
同时,由于从抽象层导出的一个或多个新的具体类可以改变系统的行为,因此系统的设计对扩展是开放的,这就满足了“开-闭”原则的第一条:对扩展开放。
**对可变性的封装**
考虑你的设计中有什么可能会发生变化。这一思想用一句话总结为:“找到一个系统的可变因素,将它封装起来”。
这意味着两点:
1、一种可变性不应该散落在代码的很多角落里,而应该被封装到一个对象里面。同一种可变性的不同表象意味着同一个继承等级结构中的具体子类。
2、一种可变性不应该与另一种可变性混合在一起。
### 四、结语
尽管很多情况下,无法百分之百的做到“开-闭”原则,但是如果向这个方向上的努力能够得到部分的成功,也可以显著地改善一个系统的结构。
前言
最后更新于:2022-04-01 11:04:53
> 原文出处:[写最好的设计模式专栏](http://blog.csdn.net/column/details/xing-designpattern.html)
作者:[xingjiarong](http://blog.csdn.net/xingjiarong)
**本系列文章经作者授权在看云整理发布,未经作者允许,请勿转载!**
# 写最好的设计模式专栏
> 认真讲解 ,认真分析,认真总结每一个设计模式和设计原则,与实例相结合,理解更加深刻。