设计模式系列
概述
设计模式是程序员在面对同类软件工程设计问题所总结出来的有用的经验,模式不是代码,而是某类问题的通用解决方案,设计模式(Design pattern)代表了最佳的实践。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。
设计模式的本质提高软件的维护性,通用性和扩展性,并降低软件的复杂度。
设计模式是为了让程序具有更好的:
- 代码重用性(相同功能的代码,不用多次编写)
- 可读性(编程规范性,便于其他程序员的阅读与理解)
- 可扩展性(当需要增加新的功能是,非常的方便)
- 可靠性(当我们增加新的功能后,对原来的功能没有影响)
- 使程序呈现
高内聚
,低耦合
的特性
设计模式七大原则
-
单一职责原则
-
接口隔离原则
-
依赖倒转原则
-
里式替换原则
-
开闭原则
-
迪米特法则
-
合成复用原则
单一职责原则
一个类应该只负责一项职责(如类A负责两个不同职责:职责1,职责2。 当职责1需求变更而改变A时,可能造成职责2执行错误,所以需要将类A的粒度分解为 A1,A2)
好处:
- 提高类的可读性,可维护性
- 降低变更引起的风险
- 降低类的复杂度,一个类只负责一项职责。
接口隔离原则
客户端不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口上.
😳状态
依赖倒转原则
依赖倒转(倒置)的中心思想是面向接口编程
- 高层模块不应该依赖低层模块,二者都应该依赖其抽象
- 抽象不应该依赖细节,细节应该依赖抽象
依赖倒转原则是基于这样的设计理念:相对于细节的多变性,抽象的东西要稳定的 多。以抽象为基础搭建的架构比以细节为基础的架构要稳定的多(使用接口或抽象类的目的是制定好规范,而不涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成.)
注意
- 低层模块尽量都要有抽象类或接口,或者两者都有,程序稳定性更好。
- 变量的声明类型尽量是抽象类或接口, 这样我们的变量引用和实际对象间,就存在一个缓冲层,利于程序扩展和优化。
- 继承时遵循里氏替换原则。
里式替换原则
继承在给程序设计带来的弊端:比如使用继承会给程序带来侵入性,程序的可移植性降低,增加对象间的耦合性,如果一个类被其他的类所继承, 则当这个类需要修改时,必须考虑到所有的子类,并且父类修改后,所有涉及到子类的功能都有可能产生故障
在使用继承时,遵循里氏替换原则,在子类中尽量不要重写父类的方法,在适当的情况下,可以通过聚合,组合,依赖来解决问题。
原来的父类和子类都继承一个更通俗的基类,原有的继承关系去掉, 采用依赖,聚合,组合等关系代替.
开闭原则
Open Closed Principle,我们也称开闭原则为ocp原则
一个软件实体如类,模块和函数应该对扩展开放(对提供方),对修改关闭(对使用方)。用抽象构建框架,用实现扩展细节。(当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。)
迪米特法则
迪米特法则(Demeter Principle)又叫最少知道原则,即一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类不管多么复杂,都尽量将逻辑封装在类的内部。对外除了提供的public 方法,不对外泄露任何信息.
迪米特法则的核心是降低类之间的耦合
合成复用原则
合成复用原则(Composite Reuse Principle),是尽量使用合成/聚合
的方式,而不是使用继承
分类
- 创建型模式:
单例模式
、工厂模式
、原型模式
、建造者模式
- 结构型模式:
适配器模式
、桥接模式
、装饰模式
、组合模式
、外观模式
、享元模式
、代理模式
- 行为型模式:
模版方法模式
、命令模式
、访问者模式
、迭代器模式
、观察者模式
、中介者模式
、备忘录模式
、解释器模式(Interpreter模式)
、状态模式
、策略模式
、职责链模式(责任链模式)
创建型模式
单例模式
采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法(静态方法)
单例模式又分为8种方式:
- 饿汉式(静态常量)
- 饿汉式(静态代码块)
- 懒汉式(线程不安全)
- 懒汉式(线程安全,同步方法)
- 懒汉式(线程安全,同步代码块)
- 双重检查
- 静态内部类
- 枚举
饿汉式(静态常量)
步骤
- 构造器私有化(防止new)
- 类的内部创建对象
- 向外暴露一个静态的公共方法。getInstance
优缺点
**优点:**这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题
**缺点:**在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费
这种方式基于classloder机制避免了多线程的同步问题,不过,instance在类装载时就实例化,在单例模式中大多数都是调用getInstance方法, 但是导致类装载的原因有很多种,因此不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance就没有达到lazy loading的效果
结论:这种单例模式可用,可能造成内存浪费
我们使用的Runtime
就是使用该种模式
代码案例
1
2
3
4
5
6
7
8
9
10
|
class Singleton {
// 2、创建对象
private static Singleton instance = new Singleton();
// 1、构造器私有化(防止new)
private Singleton() {}
// 3、向外暴露一个静态的公共方法-getInstance,方便用户通过此方法来得到当前类对象实例
public static Singleton getInstance(){
return instance;
}
}
|
饿汉式(静态代码块)
步骤
- 构造器私有化(防止new)
- 使用静态代码块创建对象
- 向外暴露一个静态的公共方法。getInstance
优缺点
这种方式和上面的方式其实类似,只不过将类实例化的过程放在了静态代码块 中,也是在类装载的时候,就执行静态代码块中的代码,初始化类的实例。优缺点和上面是一样的。
结论:这种单例模式可用,但是可能造成内存浪费
代码
1
2
3
4
5
6
7
8
9
10
11
12
13
|
class Singleton {
private static Singleton instance;
// 2、使用静态代码块创建对象
static{
instance = new Singleton();
}
// 1、构造器私有化(防止new)
private Singleton() {}
// 3、向外暴露一个静态的公共方法-getInstance,方便用户通过此方法来得到当前类对象实例
public static Singleton getInstance(){
return instance;
}
}
|
懒汉式(线程不安全)
步骤
- 构造器私有化(防止new)
- 使用静态代码块创建对象
- 向外暴露一个静态的公共方法。getInstance
优缺点
- 起到了Lazy Loading的效果,但是只能在单线程下使用。
- 如果在多线程下,一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式
结论:在实际开发中,不要使用这种方式.
代码
1
2
3
4
5
6
7
8
9
10
11
12
13
|
class Singleton {
// 2、创建对象
private static Singleton instance;
// 1、构造器私有化(防止new)
private Singleton() {}
// 3、向外暴露一个静态的公共方法-getInstance,方便用户通过此方法来得到当前类对象实例
public static Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
|
懒汉式(线程安全,同步方法)
步骤
在懒汉式(线程不安全)的代码基础上,修改创建实例方法,增加synchronized
关键字
优缺点
效率太低了,每个线程在想获得类的实例时候,执行getInstance()方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例, 直接return就行了。方法进行同步效率太低
结论:在实际开发中,不推荐使用这种方式
代码
1
2
3
4
5
6
7
8
9
10
11
12
13
|
class Singleton {
// 2、创建对象
private static Singleton instance;
// 1、构造器私有化(防止new)
private Singleton() {}
// 3、向外暴露一个静态的公共方法-getInstance,方便用户通过此方法来得到当前类对象实例
public static synchronized Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
|
懒汉式(线程安全,同步代码块)
步骤
在懒汉式(线程不安全)的代码基础上,增加同步代码块,进行实例的创建
优缺点
这种同步并不能起到线程同步的作用,也会可能产生多个实例(假如一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行, 另一个线程也通过了这个判断语句,这时便会产生多个实例)。
结论:在实际开发中,不能使用这种方式
代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
class Singleton {
// 2、创建对象
private static Singleton instance;
// 1、构造器私有化(防止new)
private Singleton() {}
// 3、向外暴露一个静态的公共方法-getInstance,方便用户通过此方法来得到当前类对象实例
public static Singleton getInstance(){
if(instance == null){
synchronized (Singleton.class){
instance = new Singleton();
}
}
return instance;
}
}
|
双重检查
步骤
在上述方法中,再次增加一次校验
优缺点
- Double-Check概念是多线程开发中常使用到的,如代码中所示,我们进行了两次if (singleton == null)检查,这样就可以保证线程安全了。
- 这样,实例化代码只用执行一次,后面再次访问时,判断if (singleton == null),直接return实例化对象,也避免的反复进行方法同步.
- 线程安全,延迟加载,效率较高.
- 结论:在实际开发中,推荐使用这种单例设计模式
代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
class Singleton {
// 2、创建对象
private static Singleton instance;
// 1、构造器私有化(防止new)
private Singleton() {}
// 3、向外暴露一个静态的公共方法-getInstance,方便用户通过此方法来得到当前类对象实例
public static Singleton getInstance(){
if(instance == null){
synchronized (Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
|
静态内部类
步骤
优缺点
- 这种方式采用了类装载的机制来保证初始化实例时只有一个线程。
- 静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton的实例化。
- 类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。
- 优点:避免了线程不安全,利用静态内部类特点实现延迟加载,效率高
- 结论:推荐使用
代码
1
2
3
4
5
6
7
8
9
10
11
|
class Singleton {
// 1、构造器私有化(防止new)
private Singleton() {}
private static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonInstance.INSTANCE;
}
}
|
枚举
- 这借助JDK1.5中添加的枚举来实现单例模式。不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。
- 这种方式是Effective Java作者Josh Bloch 提倡的方式
- 结论:推荐使用
代码
1
2
3
4
|
enum Singleton {
INSTANCE;
public void method() {}
}
|
工厂模式
将实例化对象的代码提取出来,放到一个类中统一管理和维护,达到和主项目的依赖关系的解耦。从而提高项目的扩展和维护性。
而我们抽取出来的这个类,就是所谓的工厂类。
各个电脑实现类
Computer接口文件
1
2
3
|
public interface Computer {
void assemblyComputer();
}
|
MacbookComputer实现类
1
2
3
4
5
6
|
public class MacbookComputer implements Computer {
@Override
public void assemblyComputer() {
System.out.println("Assembly Macbook Computer");
}
}
|
SurfaceComputer实现类
1
2
3
4
5
6
7
|
public class SurfaceComputer implements Computer {
@Override
public void assemblyComputer() {
System.out.println("Assembly Surface Computer");
}
}
|
简单工厂模式
专⻔定义⼀个类⽤来创建其它类的实例,被创建的实例通常都具有共同的⽗类
代码
ComputerFactor简单工厂类
1
2
3
4
5
6
7
8
9
10
11
|
public class ComputerFactor {
public static Computer getInstance(String type){
Computer computer = null;
if(type.equals("MacbookComputer")){
computer = new MacbookComputer();
} else if (type.equals("SurfaceComputer")) {
computer = new SurfaceComputer();
}
return computer;
}
}
|
上述简单工厂类,违反了OCP开放封闭原则,可以使用反射优化如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public class ComputerFactor {
public static Computer getInstance(String type){
Computer computer = null;
try{
Class<?> clazz = Class.forName("cn.snailsir.factoryton." + type);
computer = (Computer) clazz.getDeclaredConstructor().newInstance();
}catch (Exception e){
e.printStackTrace();
}
return computer;
}
}
|
测试
1
2
3
4
|
public static void main(String[] args) {
Computer computer = ComputerFactor.getInstance("Macbook");
computer.assemblyComputer();
}
|
工厂方法模式
定义⼀个⽤来创建对象的接⼝,让⼦类决定实例化哪⼀个类,让⼦类决定实例化延迟到⼦类。
⼯⼚⽅法模式是针对每个产品提供⼀个⼯⼚类,在客户端中判断使⽤哪个⼯⼚类去创建对象
代码
ComputerFactor工厂接口
1
2
3
|
interface ComputerFactor {
Computer createComputer();
}
|
AppleFactory工厂类
1
2
3
4
5
6
|
public class AppleFactory implements ComputerFactor{
@Override
public Computer createComputer() {
return new MacbookComputer();
}
}
|
MsFactory工厂类
1
2
3
4
5
6
|
public class MsFactory implements ComputerFactor{
@Override
public Computer createComputer() {
return new SurfaceComputer();
}
}
|
测试
1
2
3
4
|
public static void main(String[] args) {
Computer computer = new AppleFactory().createComputer();
computer.assemblyComputer();
}
|
优缺点
优点
- 降低了代码耦合度,对象的⽣成交给⼦类去完成
- 实现了开放封闭原则 - 每次添加⼦产品 不需要修改原有代码
缺点
- 增加了代码量,每个具体产品都需要⼀个具体⼯⼚
- 当增加抽象产品 也就是添加⼀个其他产品族 需要修改⼯⼚ 违背OCP
简单⼯⼚模式 VS ⼯⼚⽅法模式:
- 对于简单⼯⼚模式⽽⾔,创建对象的逻辑判断放在了⼯⼚类中,客户不感知具体的类,但是其违背了开闭原则,如果要增加新的具体类,就必须修改⼯⼚类。
- 对于⼯⼚⽅法模式⽽⾔,是通过扩展来新增具体类的,符合开闭原则,但是在客户端就必须要感知到具体的⼯⼚类,也就是将判断逻辑由简单⼯⼚的⼯⼚类挪到客户端。
- ⼯⼚⽅法横向扩展很⽅便,假如该⼯⼚⼜有新的产品 Macbook Air 要⽣产,那么只需要创建相应的⼯⼚类和产品类去实现抽象⼯⼚接⼝和抽象产品接⼝即可,⽽不⽤去修改原有已经存在的代码。
抽象工厂模式
抽象⼯⼚模式:提供⼀个创建⼀系列相关或相互依赖对象的接⼝,⽽⽆需指定它们具体的类。
⼯⼚⽅法模式和抽象⼯⼚模式基本类似,可以这么理解:当⼯⼚只⽣产⼀个产品的时候,即为⼯⼚⽅法模式,⽽⼯⼚如果⽣产两个或以上的商品即变为抽象⼯⼚模式。
Calendar就是使用该种模式
代码实现
ProductionFactory接口(规定computer生产工厂接口)
1
2
3
|
public interface ProductionFactory {
public Computer createComputer();
}
|
AppleFactory(苹果笔记本生产工厂)实现ProductionFactory接口
1
2
3
4
5
6
|
public class AppleFactory implements ProductionFactory {
@Override
public Computer createComputer() {
return new MacbookComputer();
}
}
|
MsFactroy(微软笔记本生产工厂)实现ProductionFactory接口
1
2
3
4
5
6
|
public class MsFactroy implements ProductionFactory {
@Override
public Computer createComputer() {
return new SurfaceComputer();
}
}
|
测试
1
2
3
4
|
public static void main(String[] args) {
Computer computer = new AppleFactory().createComputer();
computer.assemblyComputer();
}
|
优缺点
优点:
-
代码解耦
-
实现多个产品族(相关联产品组成的家族),⽽⼯⼚⽅法模式的单个产品,可以满⾜更多的⽣产需求
-
很好的满⾜OCP开放封闭原则
-
抽象⼯⼚模式中我们可以定义实现不⽌⼀个接⼝,⼀个⼯⼚也可以⽣成不⽌⼀个产品类 对于复杂对象的⽣产相当灵活易扩展
缺点:
-
扩展产品族相当麻烦 ⽽且扩展产品族会违反OCP,因为要修改所有的⼯⼚
-
由于抽象⼯⼚模式是⼯⼚⽅法模式的扩展 总体的来说 很笨重
原型模式
通过将一个原型对象传给创建的对象的实例,这个创建的对象的实例通过请求原型对象拷贝它们自己来实施创建
这里涉及到了两个概念:浅拷贝
与深拷贝
浅拷贝
而浅拷贝,对于基本数据类型,我们修改拷贝后的数据,不会对原始数据造成影响,但是如果拷贝的数据是引用类型是,当我们对拷贝的数据进行修改时,也会对原始数据造成影响(基本数据类型在进行拷贝时,是将数据复制一份并赋值给拷贝数据。而引用数据类型,在进行浅拷贝时,是将引用数据的地址指针传递给拷贝后的数据。)
我们可以让类实现Cloneable
接口,并实现clone
方法就实现了浅拷贝
代码
sheep类库
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
|
public class Sheep implements Cloneable{
private String name ;
private String color ;
public Sheep friend ; //一个对象引用
public Sheep(String name,String color) {
this.name = name;
this.color = color;
}
@Override
public String toString() {
return "Sheep{" +
"name='" + name + '\'' +
", color='" + color + '\'' +
'}';
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
@Override
protected Object clone() {
Sheep sheep = null ;
try{
sheep = (Sheep)super.clone();
}catch (Exception e){
e.printStackTrace();
}
return sheep;
}
}
|
拷贝测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
public static void main(String[] args) {
System.out.println("浅拷贝~");
Sheep sheep = new Sheep("tom","白色");
sheep.friend = new Sheep("嘻洋洋","白色");
Sheep sheep1 = (Sheep)sheep.clone();
Sheep sheep2 = (Sheep)sheep.clone();
Sheep sheep3 = (Sheep)sheep.clone();
Sheep sheep4 = (Sheep)sheep.clone();
Sheep sheep5 = (Sheep)sheep.clone();
sheep3.friend.setColor("红色");
System.out.println("sheep1 = " + sheep1 + ",sheep1.friend = " + sheep1.friend); // 变成红色了
System.out.println("sheep2 = " + sheep2 + ",sheep2.friend = " + sheep2.friend);// 变成红色了
System.out.println("sheep3 = " + sheep3 + ",sheep3.friend = " + sheep3.friend);// 变成红色了
System.out.println("sheep4 = " + sheep4 + ",sheep4.friend = " + sheep4.friend);// 变成红色了
System.out.println("sheep5 = " + sheep5 + ",sheep5.friend = " + sheep5.friend);// 变成红色了
}
|
但是我们希望的是只有sheep3的friend变成红色,其他的还是拷贝的白色,这时候就是涉及到了我们接下来要说的深拷贝了
深拷贝
所谓的深拷贝就是,在我们修改拷贝后的数据时,对原始数据不构成影响
代码实现
武器类
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
|
import java.io.Serializable;
//武器类
public class weapon implements Cloneable , Serializable {
private String name ;//名字
private int attack ;//攻击
public weapon(){
}
public weapon(String name, int attack) {
this.name = name;
this.attack = attack;
}
@Override
public String toString() {
return "Weapon{" +
"name='" + name + '\'' +
", attack=" + attack +
'}';
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAttack() {
return attack;
}
public void setAttack(int attack) {
this.attack = attack;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
|
英雄类
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
|
import java.io.*;
public class hero implements Cloneable, Serializable {
public String name ; //英雄名
public weapon weapon ;//武器
//方式1:深拷贝 (clone)
@Override
protected Object clone() throws CloneNotSupportedException {
hero hero = null ;
hero = (hero)super.clone();//是拷贝hero类中的基本属性(name)
hero.weapon = (weapon) weapon.clone() ;//就是拷贝hero类中的weapon对象
return hero ;
}
//方式2:序列化实现深拷贝
public Object deepClone(){
//申明流对象
ByteArrayOutputStream bos = null ;
ObjectOutputStream oos = null;
ByteArrayInputStream bis = null ;
ObjectInputStream ois = null ;
try{
//序列化
bos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(bos);
oos.writeObject(this);
//返序列化
bis = new ByteArrayInputStream(bos.toByteArray());
ois = new ObjectInputStream(bis);
hero hero =(hero)ois.readObject();
return hero ;
}catch (Exception e){
e.printStackTrace();
}finally {
try{
ois.close();
bis.close();
oos.close();
bos.close();
}catch (Exception e2){
System.out.println(e2.getMessage());
}
}
return null;
}
}
|
测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
public static void main(String[] args) {
hero hero1 = new hero() ;
hero1.name= "张飞";
hero1.weapon = new weapon("丈八蛇矛",100);
//方式1:深拷贝 (clone)
// Hero hero2 = (Hero) hero1.clone();
//
// System.out.println("1英雄名:"+hero1.name+",1武器:"+hero1.weapon.hashCode());
// System.out.println("2英雄名:"+hero2.name+",2武器:"+hero2.weapon.hashCode());
// System.out.println(hero1.weapon == hero2.weapon);
//方式2:序列化实现深拷贝
hero hero2 = (hero) hero1.deepClone();
System.out.println("1英雄名:"+hero1.name+",1武器:"+hero1.weapon.hashCode());
System.out.println("2英雄名:"+hero2.name+",2武器:"+hero2.weapon.hashCode());
System.out.println(hero1.weapon == hero2.weapon);
}
|
建造者模式
结构型模式