本文旨在快速梳理常用的设计模式,了解每个模式主要针对的是哪些情况以及其基础特征,每个模式前都有列举出一个或多个可以深入阅读的参考网页,以供读者详细了解其实现。
快速回忆
一、创建型
- 单例(Singleton)
- 工厂模式
- 简单工厂(Simple Factory)
- 工厂方法(Factory Method)
- 抽象工厂(Abstract Factory)
- 生成器(Builder)
- 原型模式(Prototype)
二、行为型
- 责任链(Chain Of Responsibility)
- 命令(Command)
- 解释器(Interpreter)
- 迭代器(Iterator)
- 中介者(Mediator)
- 备忘录(Memento)
- 观察者(Observer)
- 状态(State)
- 策略(Strategy)
- 模板方法(Template Method)
- 访问者(Visitor)
- 空对象(Null)
三、结构型
- 适配器(Adapter)
- 装饰器(Decorator)
- 代理模式(Proxy)
- 外观模式/门面模式(Facade)
- 桥接模式(Bridge Pattern)
- 组合模式(Composite)
- 享元模式(Flyweight)
理念
首先搞清楚一点,设计模式不是高深技术,不是奇淫技巧。设计模式只是一种设计思想,针对不同的业务场景,用不同的方式去设计代码结构,其最最本质的目的是为了解耦,延伸一点的话,还有为了可扩展性和健壮性,但是这都是建立在解耦的基础之上。
高内聚低耦合
高内聚:系统中A、B两个模块进行交互,如果修改了A模块,不影响模块B的工作,那么认为A有足够的内聚。
低耦合:就是A模块与B模块存在依赖关系,那么当B发生改变时,A模块仍然可以正常工作,那么就认为A与B是低耦合的。
创建型
单例模式
请参考Github详细解释,下面的一点仅供快速复习。Github写的很好。
同时参考:
http://blog.jobbole.com/109449/
意图
确保一个类只有一个实例,并提供该实例的全局访问点。
类图
使用一个私有构造函数、一个私有静态变量以及一个公有静态函数来实现。
私有构造函数保证了不能通过构造函数来创建对象实例,只能通过公有静态函数返回唯一的私有静态变量。
实现
懒汉式——线程安全/双重校验
单例是为了保证系统中只有一个实例,其关键点有
一.私有化构造函数
二.声明静态单例对象
三.构造单例对象之前要加锁(lock一个静态的object对象)或者方法上加synchronized。
四.需要两次检测单例实例是否已经被构造,分别在锁之前和锁之后
使用lock(obj)
public class Singleton {
private Singleton() {} //关键点0:构造函数是私有的
private volatile static Singleton single; //关键点1:声明单例对象是静态的
private static object obj= new object();
public static Singleton GetInstance() //通过静态方法来构造对象
{
if (single == null) //关键点2:判断单例对象是否已经被构造
{
lock(obj) //关键点3:加线程锁
{
if(single == null) //关键点4:二次判断单例是否已经被构造
{
single = new Singleton();
}
}
}
return single;
}
}
使用synchronized (Singleton.class)
public class Singleton {
private Singleton() {}
private volatile static Singleton uniqueInstance;
public static Singleton getUniqueInstance() {
if (uniqueInstance == null) {
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
可能提问
0.为何要检测两次?
如果两个线程同时执行 if 语句,那么两个线程就会同时进入 if 语句块内。虽然在if语句块内有加锁操作,但是两个线程都会执行 uniqueInstance = new Singleton(); 这条语句,只是先后的问题,也就是说会进行两次实例化,从而产生了两个实例。因此必须使用双重校验锁,也就是需要使用两个 if 语句。
1.构造函数能否公有化?
不行,单例类的构造函数必须私有化,单例类不能被实例化,单例实例只能静态调用。
2.lock住的对象为什么要是object对象,可以是int吗?
不行,锁住的必须是个引用类型。如果锁值类型,每个不同的线程在声明的时候值类型变量的地址都不一样,那么上个线程锁住的东西下个线程进来会认为根本没锁。
3.uniqueInstance 采用 volatile 关键字修饰
uniqueInstance = new Singleton(); 这段代码其实是分为三步执行。
分配内存空间
初始化对象
将 uniqueInstance 指向分配的内存地址
但是由于 JVM 具有指令重排的特性,有可能执行顺序变为了 1>3>2
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton(){}
public static Singleton getInstance(){
if(uniqueInstance == null){
// B线程检测到uniqueInstance不为空
synchronized(Singleton.class){
if(uniqueInstance == null){
uniqueInstance = new Singleton();
// A线程被指令重排了,刚好先赋值了;但还没执行完构造函数。
}
}
}
return uniqueInstance;// 后面B线程执行时将引发:对象尚未初始化错误。
}
}
饿汉式-线程安全
线程不安全问题主要是由于 uniqueInstance 被实例化了多次,如果 uniqueInstance 采用直接实例化的话,就不会被实例化多次,也就不会产生线程不安全问题。但是直接实例化的方式也丢失了延迟实例化带来的节约资源的优势。
private static Singleton uniqueInstance = new Singleton();
静态内部类实现
当 Singleton 类加载时,静态内部类 SingletonHolder 没有被加载进内存。只有当调用 getUniqueInstance()
方法从而触发 SingletonHolder.INSTANCE
时 SingletonHolder 才会被加载,此时初始化 INSTANCE 实例。
这种方式不仅具有延迟初始化的好处,而且由虚拟机提供了对线程安全的支持。
public class Singleton {
private Singleton() {
}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getUniqueInstance() {
return SingletonHolder.INSTANCE;
}
}
枚举实现
这是单例模式的最佳实践,它实现简单,并且在面对复杂的序列化或者反射攻击的时候,能够防止实例化多次。
public enum Singleton {
uniqueInstance;
}
考虑以下单例模式的实现,该 Singleton 在每次序列化的时候都会创建一个新的实例,为了保证只创建一个实例,必须声明所有字段都是 transient,并且提供一个 readResolve() 方法。
public class Singleton implements Serializable {
private static Singleton uniqueInstance;
private Singleton() {
}
public static synchronized Singleton getUniqueInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
如果不使用枚举来实现单例模式,会出现反射攻击,因为通过 setAccessible() 方法可以将私有构造函数的访问级别设置为 public,然后调用构造函数从而实例化对象。如果要防止这种攻击,需要在构造函数中添加防止实例化第二个对象的代码。
从上面的讨论可以看出,解决序列化和反射攻击很麻烦,而枚举实现不会出现这两种问题,所以说枚举实现单例模式是最佳实践。
使用场景
- Logger Classes
- Configuration Classes
- Accesing resources in shared mode
- Factories implemented as Singletons
简单/静态工厂(Simple Factory)
https://www.jianshu.com/p/d1b6731c1c0e
定义
在创建一个对象时不向客户暴露内部细节,并提供一个创建对象的通用接口。
在简单工厂模式中,可以根据参数的不同返回不同类的实例。
简单工厂模式专门定义一个类来负责创建其他类的实例
结构
简单工厂模式包含如下角色:
-
Factory:工厂角***r> 工厂角色负责实现创建所有实例的内部逻辑
-
Product:抽象产品角***r> 抽象产品角色是所创建的所有对象的父类,负责描述所有实例所共有的公共接口
-
ConcreteProduct:具体产品角***r> 具体产品角色是创建目标,所有创建的对象都充当这个角色的某个具体类的实例。
实现
public class Test {
public static void main(String[] args) {
String loginType = "password";
String name = "name";
String password = "password";
Login login = LoginManager.factory(loginType);
boolean bool = login.verify(name, password);
if (bool) {
/**
* 业务逻辑
*/
} else {
/**
* 业务逻辑
*/
}
}
}
优缺点
优点
构造容易,逻辑简单。
缺点
1.简单工厂模式中的if else判断非常多,完全是Hard Code,如果有一个新产品要加进来,就要同时添加一个新产品类,并且必须修改工厂类,再加入一个 else if 分支才可以, 这样就违背了 “开放-关闭原则“中的对修改关闭的准则了。
2.一个工厂类中集合了所有的类的实例创建逻辑,违反了高内聚的责任分配原则,将全部的创建逻辑都集中到了一个工厂类当中,所有的业务逻辑都在这个工厂类中实现。什么时候它不能工作了,整个系统都会受到影响。因此一般只在很简单的情况下应用,比如当工厂类负责创建的对象比较少时。
3.简单工厂模式由于使用了静态工厂方法,造成工厂角色无法形成基于继承的等级结构。
适用环境
- 工厂类负责创建的对象比较少:由于创建的对象较少,不会造成工厂方法中的业务逻辑太过复杂。
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);
②Java加密技术
获取不同加密算法的密钥生成器:
KeyGenerator keyGen=KeyGenerator.getInstance("DESede");
创建密码器:
Cipher cp = Cipher.getInstance("DESede");
工厂方法(Factory Method)
https://www.jianshu.com/p/1cf9859e0f7c
意图
又称为工厂模式/虚拟构造器(Virtual Constructor)模式/多态工厂(Polymorphic Factory)模式
即通过工厂子类来确定究竟应该实例化哪一个具体产品类。
使用动机
不再设计一个按钮工厂类来统一负责所有产品的创建,而是将具体按钮的创建过程交给专门的工厂子类去完成。
我们先定义一个抽象的按钮工厂类,再定义具体的工厂类来生成圆形按钮、矩形按钮、菱形按钮等,它们实现在抽象按钮工厂类中定义的方法。这种抽象化的结果使这种结构可以在不修改具体工厂类的情况下引进新的产品,如果出现新的按钮类型,只需要为这种新类型的按钮创建一个具体的工厂类就可以获得该新按钮的实例,这一特点无疑使得工厂方法模式具有超越简单工厂模式的优越性,更加符合“开闭原则”。
角色
-
Product:抽象产品,工厂方法模式所创建的对象的超类,也就是所有产品类的共同父类或共同拥有的接口。在实际的系统中,这个角色也常常使用抽象类实现。
-
ConcreteProduct:具体产品,这个角色实现了抽象产品(Product)所声明的接口,工厂方法模式所创建的每一个对象都是某个具体产品的实例。
-
Factory:抽象工厂,担任这个角色的是工厂方法模式的核心,任何在模式中创建对象的工厂类必须实现这个接口。在实际的系统中,这个角色也常常使用抽象类实现。
-
ConcreteFactory:具体工厂,担任这个角色的是实现了抽象工厂接口的具体Java类。具体工厂角色含有与业务密切相关的逻辑,并且受到使用者的调用以创建具体产品对象。
实现
看链接内
客户端调用
static void Main(string[] args)
{
//先给我来个灯泡
ICreator creator = new BulbCreator();
ILight light = creator.CreateLight();
light.TurnOn();
light.TurnOff();
//再来个灯管看看
creator = new TubeCreator();
light = creator.CreateLight();
light.TurnOn();
light.TurnOff();
}
优缺点
优点
①在工厂方法模式中,工厂方法用来创建客户所需要的产品,同时还向客户隐藏了哪种具体产品类将被实例化这一细节,用户只需要关心所需产品对应的工厂,无须关心创建细节,甚至无须知道具体产品类的类名。
②工厂方法模式之所以又被称为多态工厂模式,是因为所有的具体工厂类都具有同一抽象父类。
③使用工厂方法模式的另一个优点是在系统中加入新产品时,无须修改抽象工厂和抽象产品提供的接口,无须修改客户端,也无须修改其他的具体工厂和具体产品,而只要添加一个具体工厂和具体产品就可以了。这样,系统的可扩展性也就变得非常好,完全符合“开闭原则”,这点比简单工厂模式更优秀。
缺点
①在添加新产品时,需要编写新的具体产品类,而且还要提供与之对应的具体工厂类,系统中类的个数将成对增加,在一定程度上增加了系统的复杂度,有更多的类需要编译和运行,会给系统带来一些额外的开销。
②由于考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增加了系统的抽象性和理解难度,且在实现时可能需要用到DOM、反射等技术,增加了系统的实现难度。
JDK
- JDBC中的工厂方法:
Connection conn=DriverManager.getConnection("jdbc:microsoft:sqlserver://localhost:1433; DatabaseName=DB;user=sa;password=");
Statement statement=conn.createStatement();
ResultSet rs=statement.executeQuery("select * from UserInfo");
- java.util.Calendar
- java.util.ResourceBundle
- java.text.NumberFormat
- java.nio.charset.Charset
- java.net.URLStreamHandlerFactory
- java.util.EnumSet
- javax.xml.bind.JAXBContext
抽象工厂(Abstract Factory)
https://www.jianshu.com/p/d6622f3e71ed
定义
产品等级结构 :产品等级结构即产品的继承结构,如一个抽象类是电视机,其子类有海尔电视机、海信电视机、TCL电视机,则抽象电视机与具体品牌的电视机之间构成了一个产品等级结构,抽象电视机是父类,而具体品牌的电视机是其子类。
产品族 :在抽象工厂模式中,产品族是指由同一个工厂生产的,位于不同产品等级结构中的一组产品,如海尔电器工厂生产的海尔电视机、海尔电冰箱,海尔电视机位于电视机产品等级结构中,海尔电冰箱位于电冰箱产品等级结构中。
抽象工厂模式是所有形式的工厂模式中最为抽象和最具一般性的一种形态。
抽象工厂模式与工厂方法模式最大的区别
工厂方法模式针对的是一个产品等级结构,而抽象工厂模式则需要面对多个产品等级结构,一个工厂等级结构可以负责多个不同产品等级结构中的产品对象的创建 。
角色
抽象工厂模式包含如下角色:
- AbstractFactory:抽象工厂
- ConcreteFactory:具体工厂
- AbstractProduct:抽象产品
- Product:具体产品
实现
抽象产品: 苹果系列
public interface Apple
{
void AppleStyle();
}
具体产品:iphone
public class iphone implements Apple
{
public void AppleStyle()
{
Console.WriteLine("Apple's style: iPhone!");
}
}
抽象工厂
public interface Factory
{
Apple createAppleProduct();
Sumsung createSumsungProduct();
}
手机工厂
public class Factory_Phone implements Factory
{
public Apple createAppleProduct()
{
return new iphone();
}
public Sumsung createSumsungProduct()
{
return new note2();
}
}
调用
public static void Main(string[] args)
{
//采购商要一台iPad和一台Tab
Factory factory = new Factory_Pad();
Apple apple = factory.createAppleProduct();
apple.AppleStyle();
Sumsung sumsung = factory.createSumsungProduct();
sumsung.BangziStyle();
//采购商又要一台iPhone和一台Note2
factory = new Factory_Phone();
apple = factory.createAppleProduct();
apple.AppleStyle();
sumsung = factory.createSumsungProduct();
sumsung.BangziStyle();
}
优缺点
优点
①应用抽象工厂模式可以实现高内聚低耦合的设计目的,因此抽象工厂模式得到了广泛的应用。
②增加新的具体工厂和产品族很方便,因为一个具体的工厂实现代表的是一个产品族,无须修改已有系统,符合“开闭原则”。
缺点
开闭原则的倾斜性(增加新的工厂和产品族容易,增加新的产品等级结构麻烦)
适用环境
在以下情况下可以使用抽象工厂模式:
①一个系统不应当依赖于产品类实例如何被创建、组合和表达的细节,这对于所有类型的工厂模式都是重要的。
②系统中有多于一个的产品族,而每次只使用其中某一产品族。(与工厂方法模式的区别)
③属于同一个产品族的产品将在一起使用,这一约束必须在系统的设计中体现出来。
④系统提供一个产品类的库,所有的产品以同样的接口出现,从而使客户端不依赖于具体实现。
JDK
- javax.xml.parsers.DocumentBuilderFactory
- javax.xml.transform.TransformerFactory
- javax.xml.xpath.XPathFactory
生成器(Builder)
https://blog.csdn.net/c275046758/article/details/50540789
意图
封装一个对象的构造过程,并允许按步骤构造。
实现
JDK
- java.lang.StringBuilder
- java.nio.ByteBuffer
- java.lang.StringBuffer
- java.lang.Appendable
- Apache Camel builders
原型模式(Prototype)
https://www.cnblogs.com/lwbqqyumidi/p/3746821.html
意图
通过一个已经存在的对象,复制出更多的具有与此对象具有相同类型的新的对象。
浅复制 clone():
我们知道,一个类的定义中包括属性和方法。属性用于表示对象的状态,方法用于表示对象所具有的行为。其中,属性既可以是Java中基本数据类型,也可以是引用类型。
Java中的浅复制通常使用**clone()**方式完成。
当进浅复制时,clone函数返回的是一个引用,指向的是新的clone出来的对象,此对象与原对象分别占用不同的堆空间。同时,复制出来的对象具有与原对象一致的状态。
此处对象一致的状态是指:复制出的对象与原对象中的属性值完全相等==。
深复制 deepclone():
Java中的深复制一般是通过对象的序列化和反序列化得以实现。序列化时,需要实现Serializable接口。
从输出结果中可以看出,深复制不仅在堆内存上开辟了空间以存储复制出的对象,甚至连对象中的引用类型的属性所指向的对象也得以复制,重新开辟了堆空间存储。
JDK
- java.lang.Object#clone()
- deepclone()
行为型
责任链(Chain Of Responsibility)
https://blog.csdn.net/maoyuanming0806/article/details/80183494
意图
使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链发送该请求,直到有一个对象处理它为止。
在这种模式中,通常每个接收者都包含对另一个接收者的引用。如果一个对象不能处理该请求,那么它会把相同的请求传给下一个接收者,依此类推。
职责和角色
- Handler:处理类的抽象父类
- concreteHandler:具体的处理类
设计使用责任链的基本流程
- 组织对象链:将某人物的所有职责执行对象以链的形式加以组织起来
- 消息或请求的传递:将消息或请求沿着对象链传递,让处于对象链中的对象得到处理机会
- 对象链中对象的职责分配:不同对象完成不同职责
- 任务的完成:对象链末尾的对象结束任务并停止消息或请求的继续传递。
应用实例
红楼梦中的"击鼓传花"。 JS 中的事件冒泡。 JAVA WEB 中 Apache Tomcat 对 Encoding 的处理,Struts2 的拦截器,jsp servlet 的 Filter,springMVC的拦截器
何时使用:在处理消息的时候以过滤很多道。
如何解决:拦截的类都实现统一接口。
关键代码:Handler 里面聚合它自己,在 HandlerRequest 里判断是否合适,如果没达到条件则向下传递,向谁传递之前 set 进去。
实现举例
处理器的抽象类
package com.mym.designmodel.CoRModel;
/**
* 职责:Handler 职责类的抽象父类
*/
public abstract class AbstractCarHandler {
AbstractCarHandler carHandler = null;
public abstract void carHandler();
public AbstractCarHandler setNextCarHandler(AbstractCarHandler nextCarHandler){
this.carHandler = nextCarHandler;
return this.carHandler;
}
/**职责下传*/
protected void doChain(){
if(this.carHandler != null){
this.carHandler.carHandler();
}
}
}
责任链一个执行者1
package com.mym.designmodel.CoRModel;
/**
* 职责:concreteHandler 具体的处理类
*/
public class CarHeadHandler extends AbstractCarHandler {
@Override
public void carHandler() {
System.out.println("处理车的head!");
//下传
this.doChain();
}
}
责任链一个执行者2
package com.mym.designmodel.CoRModel;
/**
* 职责:concreteHandler 具体的处理类
*/
public class CarBodyHandler extends AbstractCarHandler {
@Override
public void carHandler() {
System.out.println("处理车的body!");
//下传
this.doChain();
}
}
责任链一个执行者3
package com.mym.designmodel.CoRModel;
/**
* 职责:concreteHandler 具体的处理类
*/
public class CarTailHandler extends AbstractCarHandler {
@Override
public void carHandler() {
System.out.println("处理车的tail!");
//下传
this.doChain();
}
}
客户端client
package com.mym.designmodel.CoRModel;
/**
* 测试
*/
public class MainClass {
public static void main(String[] args) {
AbstractCarHandler carheadHandle = new CarHeadHandler();
AbstractCarHandler carbodyHandle = new CarBodyHandler();
AbstractCarHandler carTailHandler = new CarTailHandler();
//组装责任链
carheadHandle.setNextCarHandler(carbodyHandle).setNextCarHandler(carTailHandler);
//链头部开始执行
carheadHandle.carHandler();
}
}
JDK
命令(Command)
https://www.cnblogs.com/konck/p/4199907.html
意图
命令模式是为了解决命令的请求者和命令的实现者之间的耦合关系。
解决了这种耦合的好处我认为主要有两点:
1.更方便的对命令进行扩展(注意:这不是主要的优势,后面会提到)
2.对多个命令的统一控制(这种控制包括但不限于:队列、撤销/恢复、记录日志等等)
应用实例
struts 1 中的 action 核心控制器 ActionServlet 只有一个,相当于 Invoker,而模型层的类会随着不同的应用有不同的模型类,相当于具体的 Command。
类图
- Command:命令
- Receiver:命令接收者,也就是命令真正的执行者
- Invoker:通过它来调用命令
- Client:可以设置命令与命令的接收者
JDK
解释器(Interpreter)
https://www.cnblogs.com/chenssy/p/3346427.html
意图
所谓解释器模式就是定义语言的文法,并且建立一个解释器来解释该语言中的句子。
这种模式实现了一个表达式接口,该接口解释一个特定的上下文。这种模式被用在 SQL 解析、符号处理引擎等。
应用实例
编译器、运算表达式计算。
JDK
- java.util.Pattern
- java.text.Normalizer
- All subclasses of java.text.Format
- javax.el.ELResolver
迭代器(Iterator)
https://www.jianshu.com/p/3d0406a01b73
意图
提供一种顺序访问聚合对象元素的方法,并且不暴露聚合对象的内部表示。
迭代器模式的优缺点
优点
①简化了遍历方式,对于对象集合的遍历,还是比较麻烦的,对于数组或者有序列表,我们尚可以通过游标来取得,但用户需要在对集合了解很清楚的前提下,自行遍历对象,但是对于hash表来说,用户遍历起来就比较麻烦了。而引入了迭代器方法后,用户用起来就简单的多了。
②可以提供多种遍历方式,比如说对有序列表,我们可以根据需要提供正序遍历,倒序遍历两种迭代器,用户用起来只需要得到我们实现好的迭代器,就可以方便的对集合进行遍历了。
③封装性良好,用户只需要得到迭代器就可以遍历,而对于遍历算法则不用去关心。
缺点
对于比较简单的遍历(像数组或者有序列表),使用迭代器方式遍历较为繁琐,大家可能都有感觉,像ArrayList,我们宁可愿意使用for循环和get方法来遍历集合。
JDK
中介者(Mediator)
http://www.runoob.com/design-pattern/mediator-pattern.html
意图
集中相关对象之间复杂的沟通和控制方式。
实现
Alarm(闹钟)、CoffeePot(咖啡壶)、Calendar(日历)、Sprinkler(喷头)是一组相关的对象,在某个对象的事件产生时需要去操作其它对象,形成了下面这种依赖结构:
使用中介者模式可以将复杂的依赖结构变成星形结构:
JDK
- All scheduleXXX() methods of java.util.Timer
- java.util.concurrent.Executor#execute()
- submit() and invokeXXX() methods of java.util.concurrent.ExecutorService
- scheduleXXX() methods of java.util.concurrent.ScheduledExecutorService
- java.lang.reflect.Method#invoke()
备忘录(Memento)
https://blog.csdn.net/o279642707/article/details/60767258
意图
在不违反封装的情况下获得对象的内部状态,从而在需要时可以将对象恢复到最初状态。
主要解决
所谓备忘录模式就是在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态。
应用实例
1、后悔药。
2、打游戏时的存档。
3、Windows 里的 ctri + z。
4、IE 中的后退。
5、数据库的事务管理。
JDK
- java.io.Serializable
观察者(Observer)
https://www.jianshu.com/p/fc4554cda68d
意图
观察者模式是对象的行为模式,又叫发布-订阅(Publish/Subscribe)模式、模型-视图(Model/View)模式、源-***(Source/Listener)模式或从属者(Dependents)模式。
观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态上发生变化时,会通知所有观察者对象,使它们能够自动更新自己。
何时使用
一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知,进行广播通知。
实现举例
天气数据布告板会在天气信息发生改变时更新其内容,布告板有多个,并且在将来会继续增加。
JDK
状态(State)
https://www.cnblogs.com/java-my-life/archive/2012/06/08/2538146.html
意图
允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它所属的类。
应用实例
考虑一个在线投票系统的应用,要实现控制同一个用户只能投一票,如果一个用户反复投票,而且投票次数超过5次,则判定为恶意刷票,要取消该用户投票的资格,当然同时也要取消他所投的票;如果一个用户的投票次数超过8次,将进入黑名单,禁止再登录和使用系统。
策略(Strategy)
https://www.jianshu.com/p/7fa8ad000a97
https://www.cnblogs.com/wenjiang/p/3352041.html
意图
定义一系列算法,封装每个算法,并使它们可以互换。
策略模式和状态模式的区别:
之所以说状态模式是策略模式的孪生兄弟,是因为它们的UML图是一样的,但意图却完全不一样,**策略模式是让用户指定更换的策略算法,而状态模式是状态在满足一定条件下的自动更换,用户无法指定状态,最多只能设置初始状态。 **
策略模式可以让算法独立于使用它的客户端。
具体场景实现
假设现在要设计一个贩卖各类书籍的电子商务网站的购物车系统。一个最简单的情况就是把所有货品的单价乘上数量,但是实际情况肯定比这要复杂。比如,本网站可能对所有的高级会员提供每本20%的促销折扣;对中级会员提供每本10%的促销折扣;对初级会员没有折扣。
根据描述,折扣是根据以下的几个算法中的一个进行的:
算法一:对初级会员没有折扣。
算法二:对中级会员提供10%的促销折扣。
算法三:对高级会员提供20%的促销折扣。
public static void main(String[] args) {
//选择并创建需要使用的策略对象
MemberStrategy strategy = new AdvancedMemberStrategy();
//创建环境
Price price = new Price(strategy);
//计算价格
double quote = price.quote(300);
System.out.println("图书的最终价格为:" + quote);
}
策略模式对多态的使用
通过让环境类持有一个抽象策略类(超类)的引用,在生成环境类实例对象时,让该引用指向具体的策略子类。再对应的方法调用中,就会通过Java的多态,调用对应策略子类的方法。从而可以相互替换,不需要修改环境类内部的实现。同时,在有新的需求的情况下,也只需要修改策略类即可,降低与环境类之间的耦合度。
策略模式和工厂方法的异同
工厂模式和策略模式的区别在于实例化一个对象的位置不同,对工厂模式而言,实例化对象是放在服务端的,即放在了工厂类里面;
而策略模式实例化对象的操作在客户端
工厂模式要求服务端的销售部门足够灵敏,而策略模式由于对策略进行了封装,所以他的销售部门比较傻,需要客户提供足够能区分使用哪种策略的参数,而这最好的就是该策略的实例了。
JDK
- java.util.Comparator#compare()
- javax.servlet.http.HttpServlet
- javax.servlet.Filter#doFilter()
模板方法(Template Method)
https://www.jianshu.com/p/cc391b56bd0e
典型用例:Spring
定义
模板方法模式是类的行为模式。
准备一个抽象类,将部分逻辑以具体方法以及具体构造函数的形式实现,然后声明一些抽象方法来迫使子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些抽象方法,从而对剩余的逻辑有不同的实现。这就是模板方法模式的用意。
结构
模板方法中的方法可以分为两大类:模板方法和基本方法。
模板方法
一个模板方法是定义在抽象类中的,把基本操作方法组合在一起形成一个总算法或一个总行为的方法。
一个抽象类可以有任意多个模板方法,而不限于一个。每一个模板方法都可以调用任意多个具体方法。
基本方法
基本方法又可以分为三种
-
抽象方法:一个抽象方法由抽象类声明,由具体子类实现。在Java语言里抽象方法以abstract关键字标示。
-
具体方法:一个具体方法由抽象类声明并实现,而子类并不实现或置换。
-
钩子方法:一个钩子方法由抽象类声明并实现,而子类会加以扩展。通常抽象类给出的实现是一个空实现,作为方法的默认实现。
默认钩子方法
一个钩子方法常常由抽象类给出一个空实现作为此方法的默认实现。这种空的钩子方法叫做“Do Nothing Hook”。具体模版类中可以选择是否重写钩子方法,通常重写钩子方法是为了对模版方法中的步骤进行控制,判断钩子方法中的状态,是否进行下一步操作。
使用场景
模板方法模式是基于继承的代码复用技术,它体现了面向对象的诸多重要思想,是一种使用较为频繁的模式。模板方法模式广泛应用于框架设计中,以确保通过父类来控制处理流程的逻辑顺序(如框架的初始化,测试流程的设置等)。
JDK
- java.util.Collections#sort()
- java.io.InputStream#skip()
- java.io.InputStream#read()
- java.util.AbstractList#indexOf()
访问者(Visitor)
https://www.jianshu.com/p/80b9cd7c0da5
什么是访问者模式?
比如我有一个账单,账单有收入,支出两个固定方法。但是访问账单的人不确定,有可能是一个或者多个。
访问者模式有两个特点
一般被访问的东西所持有的方法是固定的,就像账单只有收入和支出两个功能。而访问者是不固定的。
数据操作与数据结构相分离:频繁的更改数据,但不结构不变。比如:虽然每一天账单的数据都会变化(数据变化),但是只有两类数据,就是支出和收入(结构不变)。
代码
见参考网页
空对象(Null)
意图
使用什么都不做的空对象来代替 NULL。
一个方法返回 NULL,意味着方法的调用端需要去检查返回值是否是 NULL,这么做会导致非常多的冗余的检查代码。并且如果某一个调用端忘记了做这个检查返回值,而直接使用返回的对象,那么就有可能抛出空指针异常。
结构型
适配器(Adapter)
https://www.jianshu.com/p/93821721bf08
定义
客户类调用适配器的方法时,在适配器类的内部将调用适配者类的方法,而这个过程对客户类是透明的,客户类并不直接访问适配者类。因此,适配器可以使由于接口不兼容而不能交互的类可以一起工作。这就是适配器模式的模式动机。
角色
-
目标(Target)角色:这就是所期待得到的接口。注意:由于这里讨论的是类适配器模式,因此目标不可以是类。
-
源(Adapee)角色:现在需要适配的接口。
-
适配器(Adaper)角色:适配器类是本模式的核心。适配器把源接口转换成目标接口。显然,这一角色不可以是接口,而必须是具体类。
类适配器
创建新类,继承源类,并实现新接口
class adapter extends oldClass implements newFunc{}
对象适配器
创建新类持源类的实例,并实现新接口
class adapter implements newFunc { private oldClass oldInstance ;}
-
类适配器使用对象继承的方式,是静态的定义方式
-
而对象适配器使用对象组合的方式,是动态组合的方式。
接口适配器
创建新的抽象类实现旧接口方法
abstract class adapter implements oldClassFunc { void newFunc();}
总结
建议尽量使用对象适配器的实现方式,多用合成/聚合、少用继承。当然,具体问题具体分析,根据需要来选用实现方式,最适合的才是最好的。
优点
- 更好的复用性
- 更好的扩展性
缺点
过多的使用适配器,会让系统非常零乱,不易整体进行把握。比如,明明看到调用的是A接口,其实内部被适配成了B接口的实现,一个系统如果太多出现这种情况,无异于一场灾难。因此如果不是很有必要,可以不使用适配器,而是直接对系统进行重构。
装饰模式(Decorator)
给一类对象增加新的功能,装饰方法与具体的内部逻辑无关。
实现
设计不同种类的饮料,饮料可以添加配料,比如可以添加牛奶,并且支持动态添加新配料。每增加一种配料,该饮料的价格就会增加,要求计算一种饮料的价格。
下图表示在 DarkRoast 饮料上新增新添加 Mocha 配料,之后又添加了 Whip 配料。DarkRoast 被 Mocha 包裹,Mocha 又被 Whip 包裹。它们都继承自相同父类,都有 cost() 方法,外层类的 cost() 方法调用了内层类的 cost() 方法。
代码
interface Source{ void method();}
public class Decorator implements Source{
private Source source ;
public void decotate1(){
System.out.println("decorate");
}
@Override
public void method() {
decotate1();
source.method();
}
}
装饰模式与代理模式的区别
装饰器模式关注于在一个对象上动态的添加方法,然而代理模式关注于控制对对象的访问。
-
用代理模式,代理类(proxy class)可以对它的客户隐藏一个对象的具体信息。因此,当使用代理模式的时候,我们常常在一个代理类中创建一个对象的实例。
-
当我们使用装饰器模 式的时候,我们通常的做法是将原始对象作为一个参数传给装饰者的构造器。
代理模式(Proxy)
详细代码实例:https://www.cnblogs.com/daniels/p/8242592.html
简介
代理模式的定义:代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。
通俗的来讲代理模式就是我们生活中常见的中介。
为什么要用代理模式
-
中介隔离作用
在某些情况下,一个客户类不想或者不能直接引用一个委托对象,而代理类对象可以在客户类和委托对象之间起到中介的作用,其特征是代理类和委托类实现相同的接口。
-
开闭原则,增加功能
真正的业务功能还是由委托类来实现,但是可以在业务功能执行的前后加入一些公共的服务。例如我们想给项目加入缓存、日志这些功能,我们就可以使用代理类来完成,而没必要打开已经封装好的委托类。
有哪几种代理模式
静态代理
是由程序员创建或特定工具自动生成源代码,在对其编译。在程序员运行之前,代理类.class文件就已经被创建了。
代码:静态代理创建代理类:
package main.java.proxy.impl;
import main.java.proxy.BuyHouse;
public class BuyHouseProxy implements BuyHouse {
private BuyHouse buyHouse;
public BuyHouseProxy(final BuyHouse buyHouse) {
this.buyHouse = buyHouse;
}
@Override
public void buyHosue() {
System.out.println("买房前准备");
buyHouse.buyHosue();
System.out.println("买房后装修");
}
}
静态代理总结
-
优点:可以做到在符合开闭原则的情况下对目标对象进行功能扩展。
-
缺点:我们得为每一个服务都得创建代理类,工作量太大,不易管理。同时接口一旦发生改变,代理类也得相应修改。
动态代理:JDK反射机制(接口代理)
-
是在程序运行时通过反射机制动态创建的。
-
为需要拦截的接口生成代理对象以实现接口方法拦截功能。
代码:编写动态处理器
package main.java.proxy.impl;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class DynamicProxyHandler implements InvocationHandler {
private Object object;
public DynamicProxyHandler(final Object object) {
this.object = object;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("买房前准备");
Object result = method.invoke(object, args);
System.out.println("买房后装修");
return result;
}
}
代码:编写测试类
package main.java.proxy.test;
import main.java.proxy.BuyHouse;
import main.java.proxy.impl.BuyHouseImpl;
import main.java.proxy.impl.DynamicProxyHandler;
import java.lang.reflect.Proxy;
public class DynamicProxyTest {
public static void main(String[] args) {
BuyHouse buyHouse = new BuyHouseImpl();
BuyHouse proxyBuyHouse = (BuyHouse) Proxy.newProxyInstance(BuyHouse.class.getClassLoader(), new
Class[]{BuyHouse.class}, new DynamicProxyHandler(buyHouse));
proxyBuyHouse.buyHosue();
}
}
动态代理总结
-
优势:虽然相对于静态代理,动态代理大大减少了我们的开发任务,同时减少了对业务接口的依赖,降低了耦合度。
-
劣势:只能对接口进行代理
动态代理:CGLIB代理
-
其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。
-
但因为采用的是继承,所以不能对final修饰的类进行代理。
-
JDK动态代理与CGLib动态代理均是实现Spring AOP的基础。
代码见网页
CGLIB代理总结:(与JDK代理区别:)
CGLIB创建的动态代理对象比JDK创建的动态代理对象的性能更高,但是CGLIB创建代理对象时所花费的时间却比JDK多得多。
- 所以对于单例的对象,因为无需频繁创建对象,用CGLIB合适,反之使用JDK方式要更为合适一些。
- 同时由于CGLib由于是采用动态创建子类的方法,对于final修饰的方法无法进行代理。
外观模式/门面模式(Facade)
https://www.cnblogs.com/lthIU/p/5860607.html
简单来说,该模式就是把一些复杂的流程封装成一个接口供给外部用户更简单的使用。这个模式中,涉及到3个角色。
角色
1)门面角色:外观模式的核心。它被客户角色调用,它熟悉子系统的功能。内部根据客户角色的需求预定了几种功能的组合。
2)子系统角色: 实现了子系统的功能。它对客户角色和Facade时未知的。它内部可以有系统内的相互交互,也可以由供外界调用的接口。
3)客户角色: 通过调用Facede来完成要实现的功能。
代码
package com.huawei.facadeDesign.facade;
import org.apache.log4j.Logger;
import com.huawei.facadeDesign.children.CPU;
import com.huawei.facadeDesign.children.Disk;
import com.huawei.facadeDesign.children.Memory;
/**
* 门面类(核心)
* @author Administrator
*
*/
public class Computer
{
public static final Logger LOGGER = Logger.getLogger(Computer.class);
private CPU cpu;
private Memory memory;
private Disk disk;
public Computer()
{
cpu = new CPU();
memory = new Memory();
disk = new Disk();
}
public void start()
{
LOGGER.info("Computer start begin");
cpu.start();
disk.start();
memory.start();
LOGGER.info("Computer start end");
}
public void shutDown()
{
LOGGER.info("Computer shutDown begin");
cpu.shutDown();
disk.shutDown();
memory.shutDown();
LOGGER.info("Computer shutDown end...");
}
}
优点
-
松散耦合:使得客户端和子系统之间解耦,让子系统内部的模块功能更容易扩展和维护;
-
简单易用:客户端根本不需要知道子系统内部的实现,或者根本不需要知道子系统内部的构成,它只需要跟Facade类交互即可。
-
更好的划分访问层次:有些方法是对系统外的,有些方法是系统内部相互交互的使用的。子系统把那些暴露给外部的功能集中到门面中,这样就可以实现客户端的使用,很好的隐藏了子系统内部的细节。
桥接模式(Bridge Pattern)
http://www.cnblogs.com/houleixx/archive/2008/02/23/1078877.html
含义
在软件系统中,某些类型由于自身的逻辑,它具有两个或多个维度的变化,那么如何应对这种“多维度的变化”?
如何利用面向对象的技术来使得该类型能够轻松的沿着多个方向进行变化,而又不引入额外的复杂度?这就要使用Bridge模式。
由上图变为下图
代码
详细代码见参考网页
static void Main(string[] args){
//男人开着公共汽车在高速公路上行驶;
Console.WriteLine("=========================");
AbstractRoad Road3 = new SpeedWay();
Road3.Car = new Bus();
people p = new Man();
p.Road = Road3;
p.Run();
Console.Read();
}
组合模式(Composite)
https://www.cnblogs.com/lfxiao/p/6816026.html
组合模式是为了表示那些层次结构,同时部分和整体也可能是一样的结构,常见的如文件夹或者树.
定义
组合模式定义了如何将容器对象和叶子对象进行递归组合,使得客户在使用的过程中无须进行区分,可以对他们进行一致的处理。
在使用组合模式中需要注意一点也是组合模式最关键的地方:叶子对象和组合对象实现相同的接口。这就是组合模式能够将叶子节点和对象节点进行一致处理的原因。
角色
1.Component :组合中的对象声明接口,在适当的情况下,实现所有类共有接口的默认行为。声明一个接口用于访问和管理Component子部件。
2.Leaf:叶子对象。叶子结点没有子结点。
3.Composite:容器对象,定义有枝节点行为,用来存储子部件,在Component接口中实现与子部件有关操作,如增加(add)和删除(remove)等。
适用场景
1、需要表示一个对象整体或部分层次,在具有整体和部分的层次结构中,希望通过一种方式忽略整体与部分的差异,可以一致地对待它们。
2、让客户能够忽略不同对象层次的变化,客户端可以针对抽象构件编程,无须关心对象层次结构的细节。
享元模式(Flyweight)
https://www.cnblogs.com/chenssy/p/3330555.html
定义
所谓享元模式就是运行共享技术有效地支持大量细粒度对象的复用,所以享元模式要求能够共享的对象必须是细粒度对象。
-
内部状态:在享元对象内部不随外界环境改变而改变的共享部分。
-
外部状态:随着环境的改变而改变,不能够共享的状态就是外部状态。
代码
享元工厂类FlyweightFactory:
利用了HashMap保存已经创建的颜色
public class FlyweightFactory{
static Map<String, Shape> shapes = new HashMap<String, Shape>();
public static Shape getShape(String key){
Shape shape = shapes.get(key);
//如果shape==null,表示不存在,则新建,并且保持到共享池中
if(shape == null){
shape = new Circle(key);
shapes.put(key, shape);
}
return shape;
}
public static int getSum(){
return shapes.size();
}
}
客户端程序:Client.java
public class Client {
public static void main(String[] args) {
Shape shape1 = FlyweightFactory.getShape("红色");
shape1.draw();
Shape shape2 = FlyweightFactory.getShape("灰色");
shape2.draw();
Shape shape3 = FlyweightFactory.getShape("绿色");
shape3.draw();
Shape shape4 = FlyweightFactory.getShape("红色");
shape4.draw();
Shape shape5 = FlyweightFactory.getShape("灰色");
shape5.draw();
Shape shape6 = FlyweightFactory.getShape("灰色");
shape6.draw();
System.out.println("一共绘制了"+FlyweightFactory.getSum()+"中颜色的圆形");
}
}
参考
简书大牛:https://www.jianshu.com/nb/10186551
Github:https://github.com/CyC2018/Interview-Notebook/blob/master/notes/设计模式.md
菜鸟网:http://www.runoob.com/design-pattern/design-pattern-tutorial.html
补充:
23种设计模式总结: