七大设计原则
开闭原则
定义:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。
用抽象构建框架,用实现扩展细节。
依赖倒置原则
定义:高层模块不应该依赖低层模块,二者都应该依赖其抽象。
做到针对接口编程,不要针对实现编程。
单一职责原则
定义:不要存在多于一个导致类变更的原因。一个类/接口/方法只负责一项职责。
接口隔离原则
定义:用多个专门的接口,而不使用单一的总接口,客户端不应该依赖它不需要的接口。
建立单一接口,不要建立庞大臃肿的接口;尽量细化接口,接口中的方法尽量少;接口中不要存在子类用不到却必须实现的方法。
迪米特法则
定义:一个对象应该对其它对象保持最少的了解。又叫最少知道原则。
只与朋友说话,而不和陌生人说话。这里的朋友指的是出现在成员变量,方法输入、输出参数中的类,而出现在方法体内部的不属于朋友。
里氏替换原则
定义:如果对每一个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有的对象o1都替换成o2时,程序P的行为没有发生变化,那么类型T2是类型T1的子类型。
- 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法
- 子类中可以增加自己特有的方法
- 当子类的方法重载(注意不是覆盖)父类的方法时,方法的前置条件(即方法的输入/入参)要比父类方法的输入参数更宽松
- 当子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(即方法的输出/返回值)要比父类更严格或相等
合成复用原则
定义:尽量使用对象组合/聚合,而不是继承关系达到软件复用的目的。
创建型
简单工厂(Simple Factory)
简单工厂模式是由一个工厂对象决定创建出哪一种产品类的实例,而不向客户暴露内部细节。
简单工厂不属于23种设计模式,但是之后的工厂方法模式、抽象工厂模式都是由其演化而来,并且在实际场景中也有应用,因此有必要了解。
适用场景:工厂类负责创建的对象比较少。
优缺点
优点:只需要传入一个正确的参数,就可以获取所需要的对象而无须知道其创建细节。
缺点:工厂类的职责相对过重,增加新的产品需要修改工厂类的判断逻辑,违背开闭原则。
应用场景
创建五个类:Video、JavaVideo、PythonVideo、VideoFactory、Test:
抽象产品类Video
:1
2
3public abstract class Video {
public abstract void produce();
}
具体产品类JavaVideo
、PythonVideo
:1
2
3
4
5
6
7
8
9
10
11
12
13public class JavaVideo extends Video {
public void produce() {
System.out.println("录制Java课程视频");
}
}
public class PythonVideo extends Video {
public void produce() {
System.out.println("录制Python课程视频");
}
}
客户端类Test
,这里可以传入字符串参数或者Class类参数:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public class Test {
public static void main(String[] args) {
VideoFactory videoFactory = new VideoFactory();
Video video = videoFactory.getVideo("java");
if(video == null){
return;
}
video.produce();
VideoFactory videoFactory2 = new VideoFactory();
Video video2 = videoFactory2.getVideo(JavaVideo.class);
if(video2 == null){
return;
}
video2.produce();
}
}
在简单工厂中,客户端不应该直接创建出具体的产品类,而应交给工厂类去创建,下面看看工厂类VideoFactory
,使用了if-else判断参数或者使用使用反射技术从而决定创建哪个具体子类:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24public class VideoFactory {
public Video getVideo(Class c){
Video video = null;
try {
video = (Video) Class.forName(c.getName()).newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return video;
}
public Video getVideo(String type){
if("java".equalsIgnoreCase(type)){
return new JavaVideo();
} else if("python".equalsIgnoreCase(type)){
return new PythonVideo();
}
return null;
}
}
通过简单工厂,客户端类就不需要自己去实例化具体的产品类,做到了客户端类和产品类的解耦。
Calendar类的应用
java.util下的Calendar
类是一个抽象类,我们看看其中的getInstance
方法:
1 | public static Calendar getInstance(TimeZone zone, |
在后半段中可以看出其根据参数通过switch
和if-else
创建了相应的具体子类对象,与之前的应用场景十分类似。在这里,Calendar
既作为抽象产品类,也作为一个工厂类。
工厂方法(Factory Method)
定义一个创建对象的接口,但让实现这个接口的类来决定实例化哪个类,工厂方法让类的实例化推迟到子类中进行。
适用场景:
- 创建对象需要大量重复的代码
- 客户端不依赖于产品类示例如何被创建、实现等细节
- 一个类通过其子类来指定创建哪个对象
优缺点
优点:用户只需要关心所需产品对应的工厂,无需关心创建细节;加入新产品符合开闭原则,提高可扩展性。
缺点:类的个数容易过多,增加复杂度。
应用场景
抽象产品类Video
:
1 | public abstract class Video { |
具体产品类JavaVideo
、PythonVideo
:
1 | public class JavaVideo extends Video { |
抽象工厂类VideoFactory
:
1 | public abstract class VideoFactory { |
具体工厂类JavaVideoFactory
、PythonVideo
:
1 | public class JavaVideoFactory extends VideoFactory { |
客户端类Test
:
1 | public class Test { |
这时如果需要增加一个新的产品时,只需要添加一个新的具体产品类和具体工厂类,而无需像简单工厂一样修改工厂类里面的判断逻辑,即满足了开闭原则。
例如,如果要增加新产品FEVideo,我们需要先加入一个具体产品类:
1 | public class FEVideo extends Video { |
再增加这个具体产品所对应的具体工厂类:
1 | public class FEVideoFactory extends VideoFactory{ |
之后在应用层就可以直接使用了:
1 | public class Test { |
此时的UML类图:
Java集合接口Collection中的应用
java.util.Collection
接口下的iterator()
方法:1
2
3
4public interface Collection<E> extends Iterable<E> {
//...
Iterator<E> iterator();
//...
查看该接口的其中一个实现类ArrayList
:
1 | public Iterator<E> iterator() { |
在这里,Collection
相当于一个抽象工厂,而ArrayList
相当于一个具体工厂,这个具体工厂实现了工厂方法iterator()
实例化具体产品Itr
,而这个具体产品实现了抽象产品Iterator
。
logback中的应用
由UML可以看出ILoggerFactory
作为抽象的工厂类,实现有三个具体的工厂类,以其中的NOPLoggerFactory
为例,实现了抽象方法getLogger
来实例化具体产品类:
1 | public class NOPLoggerFactory implements ILoggerFactory { |
抽象工厂(Abstract Factory)
抽象工厂模式提供一个创建一系列相关或相互依赖对象的接口。
抽象工厂是面向产品族的,而工厂方法是面向产品等级结构的,这是两者的主要区别。
适用场景:
- 客户端不依赖于产品类实例如何被创建、实现等细节
- 强调一系列相关的产品对象(属于同一产品族)一起使用创建对象时需要大量重复的代码
- 提供一个产品类的库,所有的产品以同样的接口出现
优缺点
优点:具体产品在应用层代码隔离,无须关心创建细节;将一个系列的产品族统一到一起创建。
缺点:规定了所有可能被创建的产品集合,产品族中扩展新的产品困难,需要修改抽象工厂的接口。
应用场景
对于一个课程,既包含课程视频,也包含课程笔记:
抽象视频产品Video
:
1 | public abstract class Video { |
具体视频产品JavaVideo
、PythonVideo
:
1 | public class JavaVideo extends Video { |
同样,也有抽象笔记产品Article
和具体笔记产品JavaArticle
、PythonArticle
:
1 | public abstract class Artical { |
课程的抽象工厂CourseFactory
,生产视频和笔记两类产品:
1 | public interface CourseFactory { |
Java课程的具体工厂JavaCourseFactory
:
1 | public class JavaCourseFactory implements CourseFactory { |
Python课程的具体工厂PythonCourseFactory
:
1 | public class PythonCourseFactory implements CourseFactory { |
客户端Test
:
1 | public class Test { |
可以看出,每一个具体工厂中都只会生产同一产品族下的产品。如果要扩展新的产品族,例如要添加一个算法课程,则添加一个AlgorithmCourseFactory
工厂类即可,十分简单;但是如果要增加新的产品等级,比如在课程中除了视频和笔记外还要添加源码,那么就要修改抽象工厂中的实现,并且每一个具体工厂的实现也都要修改,抽象工厂模式在这种场景下就不适用了。
Connection中的应用
java.sql.Connection
接口定义了与指定数据库的连接:
1 | public interface Connection extends Wrapper, AutoCloseable { |
其中,Statement
、PreparedStatement
等也都为接口。我们查看Connection
的其中一个实现类ConnectionImpl
:
1 | public class ConnectionImpl extends ConnectionPropertiesImpl implements Connection { |
在createStatement
方法中实例化了Statement
接口的一个具体实现类,也就是com.mysql.jdbc.StatementImpl
。
由此可见,在这个场景中Connection
相当于一个抽象工厂,而ConnectionImpl
是一个具体工厂,抽象产品为Statement
,具体产品为StatementImpl
。在这个例子中,mysql产品族的工厂只会生产mysql的Statement、PreparedStatement等产品。
建造者(Builder)
将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
工厂方法模式注重的是整体对象的创建方法,而建造者模式注重的是部件构建的过程,旨在通过一步一步地精确构造创建出一个复杂的对象。
适用场景:
- 如果一个对象有非常复杂的内部结构(很多属性)
- 想把复杂对象的创建和使用分离
优缺点
优点:封装性好,创建和使用分离;扩展性好、建造类之间独立、一定程度上解耦。
缺点:产生多余的Builder对象;产品内部发生变化,建造者都要修改,成本较大。
应用场景
CourseBuilder
作为抽象建造者类,CourseActualBuilder
作为具体建造者类,Coach
作为教练类根据传入的建造者类安排复杂对象的建造次序(非必需),而Course
作为产品类。
抽象建造者CourseBuilder
:
1 | public abstract class CourseBuilder { |
具体建造者CourseActualBuilder
:
1 | public class CourseActualBuilder extends CourseBuilder { |
教练Coach
:
1 | public abstract class CourseBuilder { |
产品Course
:
1 | public class Course { |
客户端Test
:
1 | public class Test { |
客户端创建了一个建造者和一个教练,并将这个建造者作为参数传给教练,之后直接通过教练进行产品的创建,而对客户端隐藏了具体的创建细节。在教练内部,实际上是通过建造者一步步构造出复杂的产品的。
我们对以上的场景做进一步演化,省略了教练类,并且将建造者放在产品类的内部。这种做法在实际场景中更为常见,利于维护与扩展,并且支持链式调用。
产品类Course
以及作为建造者的内部类CourseBuilder
:
1 | public class Course { |
CourseBuilder
中的每一个构建方法都返回对象自身,使得其支持链式调用,而build()
方法将建造者作为参数传给产品类的构造函数,其根据建造者初始化产品各属性值,并将构建完毕的产品返回。
客户端Test
:
1 | public class Test { |
可以看出,演进之后的建造过程更为简洁明了。
StringBuilder中的应用
Java.util.StringBuilder
类下的append
方法:
1 | public final class StringBuilder |
可以看出,这里使用了建造者模式,append
方法总是返回建造者自身。StringBuilder
既担任建造者,又担任产品,而建造方法的实现由父类AbstractStringBuilder
完成。
StringBuffer
的实现与上面类似,区别在于StringBuffer
中的append
方法加了synchronized
关键字,因而是线程安全的。
mybatis中的应用
查看org.apache.ibatis.session
包下的SqlSessionFactoryBuilder
:
1 | public class SqlSessionFactoryBuilder { |
这里面两个参数的build
方法大多直接调用后面三个参数的build
方法,返回值都为SqlSessionFactory
,而这个方法中又有另一个建造者XMLConfigBuilder
构建出一个Configuration
对象,我们查看XMLConfigBuilder
中的相关方法:
1 | public Configuration parse() { |
构建出一个Configuration
对象的过程都在parseConfiguration
方法中,而parse
方法主要用来标记是否已经parse过并且返回构建好的Configuration
对象。
单例模式(Singleton)
保证一个类仅有一个实例,并提供一个全局访问点
适用场景:想确保任何情况下都绝对只有一个实例。
优缺点
优点:在内存里只有一个实例,减少了内存开销;可以避免对资源的多重占用;设置全局访问点,严格控制访问。
缺点:可扩展性较差。
重点
- 私有构造器
- 线程安全
- 延迟加载
- 序列化和反序列化
- 反射
懒汉式实现
线程不安全
以下实现中延迟了lazySingleton的实例化,因此如果没有使用该类,那么就不会实例化lazySingleton,从而节约了资源。
但这种实现是线程不安全的,在多线程的环境下多个线程有可能同时判断if(lazySingleton == null)
为true
而进行实例化,导致多次实例化lazySingleton。
1 | public class LazySingleton { |
synchronized关键字
要想其变为线程安全的,第一种方式是在getInstance()
方法加上synchronized
关键字,使这个方法变为同步方法:
1 | public synchronized static LazySingleton getInstance(){ |
由于这个方法是静态方法,因此这个锁将锁住这个类,等效于以下代码:
1 | public static LazySingleton getInstance(){ |
通过这种方式,虽然解决了懒汉式在多线程环境下的同步问题,但由于同步锁消耗的资源较多,且锁的范围较大,对性能有一定影响,因此还需要进行演进。
双重校验锁
当lazyDoubleCheckSingleton就算没有被实例化时,synchronized
关键字也保证了不会出现同步问题,例如,如果两个线程同时判断第一个if(lazyDoubleCheckSingleton == null)
为true
,其中一个线程会进入到第二个if(lazyDoubleCheckSingleton == null)
并开始实例化lazyDoubleCheckSingleton,而另一个线程则被阻塞直到前一个进程释放锁。一旦前一个线程实例化完并释放锁,被阻塞的线程将进入第二个if(lazyDoubleCheckSingleton == null)
且判断为false
。之后,由于lazyDoubleCheckSingleton已经被实例化过,再有线程调用此方法都会在第一个if(lazyDoubleCheckSingleton == null)
就判断为false
,不会再进行加锁操作。
1 | public class LazyDoubleCheckSingleton { |
这种实现依然存在问题,对于lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
这一行代码其实是分为以下三步执行的:
- 分配内存给这个对象
- 初始化对象
- 设置lazyDoubleCheckSingleton指向刚分配的内存地址
但是JVM为了优化指令,提高程序运行效率,会进行指令重排序,指令顺序有可能由1->2->3变为1->3->2,这在单线程下不会出现问题,但是在多线程下会导致一个线程获得还没有被初始化的实例。例如,一个线程已经执行到了lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
这一行,且完成了1->3这两步,即lazyDoubleCheckSingleton已经不为null,但还没有进行初始化,此时另一个线程在第一个if(lazyDoubleCheckSingleton == null)
判断为false
后便将还未被初始化的lazyDoubleCheckSingleton返回,从而产生问题。
要解决指令重排序导致的问题,第一种方式是使用volatile
关键字禁止JVM进行指令重排序:
1 | public class LazyDoubleCheckSingleton { |
静态内部类
另一种解决指令重排序所导致的问题的方式是使用静态内部类让其它线程看不到这个线程的指令重排序:
1 | public class StaticInnerClassSingleton { |
当StaticInnerClassSingleton类加载时,静态内部类InnerClass还不会加载进内存,只有调用getInstance()
方法使用到了InnerClass.staticInnerClassSingleton
时才会加载。在多线程环境下,只有一个线程能获得Class对象的初始化锁,从而加载StaticInnerClassSingleton类,也就是这时候完成staticInnerClassSingleton的实例化,另一个线程此时只能在这个Class对象的初始化锁上等待。因此,由于等待的线程是看不见指令重排序的过程的,所以指令重排的顺序不会有任何影响。
饿汉式实现
饿汉式即当类加载的时候就完成实例化,避免了同步问题,但同时也因为没有延迟实例化的特性而导致资源的浪费。
1 | public class HungrySingleton implements Serializable { |
以上代码与以下代码等效:
1 | public class HungrySingleton implements Serializable { |
单例模式存在的问题
序列化破坏单例模式
通过对Singleton的序列化与反序列化得到的对象是一个新的对象,这就破坏了Singleton的单例性。
1 | public class Test { |
之所以会如此,是因为序列化会通过反射调用无参数的构造方法创建一个新的对象。要解决这个问题很简单:只要在Singleton类中定义readResolve即可:
1 | public class HungrySingleton implements Serializable { |
反射攻击
通过反射可以打开Singleton的构造器权限,由此实例化一个新的对象。
1 | public class Test { |
对于饿汉式,由于是在类加载的时候就实例化对象了,因此要解决反射攻击问题,可以在构造器内部加一个判断用来防御,这样当反射调用构造器的时候hungrySingleton已经存在,不会再进行实例化并抛出异常:
1 | public class HungrySingleton implements Serializable { |
而对于懒汉式,即使加上了上面的防御代码,依然可以通过调整顺序即先使用反射创建实例,再调用getInstance()
创建实例来得到不止一个该类的对象。
枚举实现
枚举类是实现单例的最佳方式,其在多次序列化再进行反序列化之后不会得到多个实例,也可以防御反射攻击。这部分的处理是由ObjectInputStream
和Constructor
这两个类实现的。
1 | public enum EnumInstance { |
容器实现
如果系统中单例对象特别多,则可以考虑使用一个容器把所有单例对象统一管理,但是是线程不安全的。
1 | public class ContainerSingleton { |
Runtime中的应用
查看java.lang
包下的Runtime
类:
1 | public class Runtime { |
这里的currentRuntime在类加载的时候就实例化好了,属于饿汉式单例模式。
Spring中的应用
查看org.springframework.beans.factory.config
包下的AbstractFactoryBean
:
1 | public abstract class AbstractFactoryBean<T> implements FactoryBean<T>, BeanClassLoaderAware, BeanFactoryAware, InitializingBean, DisposableBean { |
在getObject()
方法中,先判断这个对象是否为单例的,如果不是则直接创建;如果是单例的,那么判断是否被初始化过,如果被初始化了则直接返回,没有的话则调用getEarlySingletonInstance()
方法获取早期的单例对象,如果早期的单例对象不存在,则通过代理来获取。
结构型
外观(Facade)
外观模式又叫门面模式,提供了一个统一的接口,用来访问子系统中的一群接口。
外观模式定义了一个高层接口,让子系统更容易使用。
适用场景:
- 子系统越来越复杂,增加外观模式提供简单调用接口
- 构建多层系统接口,利用外观对象作为每层的入口,简化层间调用
优缺点
优点:简化了调用过程,无需了解子系统,防止带来风险;减少系统依赖、松散耦合;更好的划分访问层次;符合迪米特法则,即最少知道原则。
缺点:增加子系统、扩展子系统行为容易引入风险,不符合开闭原则。
应用场景
我们考虑一个用积分兑换礼物的场景,积分兑换礼物需要校验积分是否符合资格、扣减积分以及对接物流系统三个模块,这三个模块也可以理解为三个子系统。
校验资格子系统:
1 | public class QualifyService { |
扣减积分子系统:
1 | public class PointsPaymentService { |
对接物流系统的子系统:
1 | public class ShippingService { |
积分礼物类:
1 | public class PointsGift { |
外观类:
1 | public class GiftExchangeService { |
客户端类:
1 | public class Test { |
输出:
1 | 校验衣服 积分资格通过,库存通过 |
客户端创建一个衣服作为积分商品,然后使用积分兑换系统来完成积分兑换,这个积分兑换系统作为一个外观类整合了各个子系统,而客户端无需知道具体的子系统。
Spring中的应用
查看org.springframework.jdbc.support
下的JdbcUtils
:
1 | public abstract class JdbcUtils { |
可以看出,该工具类主要是对jdbc的封装,向外提供一个隐藏了具体实现细节的接口,对访问屏蔽复杂的子系统调用。
SLF4J中的应用
SLF4J是简单的日志外观模式框架,抽象了各种日志框架例如Logback、Log4j、Commons-logging和JDK自带的logging实现接口。它使得用户可以在部署时使用自己想要的日志框架。
SLF4J没有替代任何日志框架,它仅仅是标准日志框架的外观模式。如果在类路径下除了SLF4J再没有任何日志框架,那么默认状态是在控制台输出日志。
适配器(Adapter)
将一个类的接口转换成客户期望的另一个接口。
适配器模式使原本接口不兼容的类可以一起工作。根据适配器类与适配者类的关系不同,适配器模式可分为对象适配器和类适配器两种,在对象适配器模式中,适配器与适配者之间是组合关系,使用的是委托机制;在类适配器模式中,适配器与适配者之间是继承(或实现)关系。
适用场景:
- 已经存在的类,它的方法和需求不匹配时(方法结果相同或相似)。
- 不是软件设计阶段考虑的设计模式,是随着软件维护,由于不同产品、不同厂家造功能类似而接口不相同情况下的解决方案。
优缺点
优点:能提高类的透明性和复用;目标类和适配器类解耦,提高程序扩展性;符合开闭原则。
缺点:增加了系统的复杂性;增加系统代码可读的难度。
应用场景
类适配器模式
被适配者类:
1 | public class Adaptee { |
目标接口:
1 | public interface Target { |
目标接口实现(非必需的,只是待会用来做对比):
1 | public class ConcreteTarget implements Target { |
适配器类,既实现了目标接口又继承了被适配者类,因此直接在实现的request()
中调用父类的adapteeRequest()
方法即可:
1 | public class Adapter extends Adaptee implements Target{ |
客户端类:
1 | public class Test { |
通过适配器,我们就将被适配者类Adaptee
的adapteeRequest()
方法适配成了目标接口Target
的request()
方法。
对象适配器模式
在对象适配器模式中,被适配者类Adaptee
、目标接口与实现类Target
ConcreteTarget
、客户端类Test
都不需要改变,唯一需要改变的就是适配器类Adapter
。
1 | public class Adapter implements Target { |
可以看出,对象适配器与类适配器不同之处在于类适配器是通过继承来完成适配,而对象适配器则组合被适配者并将请求委托给被适配者来完成。
变压器的例子
这里考虑一个生活中常见的变压器的场景,我们把220V交流电压适配成5V直流电压,其中220V交流电压就是被适配者类,而5V直流电压则是目标接口,我们需要一个适配器来完成这个变压操作。
被适配者类(220V交流电压):
1 | public class AC220 { |
目标接口(5V直流电压):
1 | public interface DC5 { |
适配器类,这里使用的是对象适配器模式:
1 | public class PowerAdapter implements DC5 { |
客户端类:
1 | public class Test { |
通过适配器将220V交流电压转换成了5V直流电压,此时输出:1
2输出交流电220V
使用PowerAdapter输入AC:220V 输出DC:5V
Spring AOP中的应用
在Spring的AOP中,使用的Advice (通知)
来增强被代理类的功能。Advice
的类型有:MethodBeforeAdvice
、AfterReturningAdvice
、ThrowsAdvice
,而每个类型的Advice
都有对应的拦截器MethodBeforeAdviceInterceptor
、AfterReturningAdviceInterceptor
、ThrowsAdviceInterceptor
。
Spring需要将每个Advice
都封装成对应的拦截器类型,返回给容器,所以需要使用适配器模式对 Advice
进行转换。
三个适配者类:
1 | public interface MethodBeforeAdvice extends BeforeAdvice { |
适配器接口,其中supportsAdvice
方法判断Advice
类型是否匹配,另一个是创建对应的拦截器的工厂方法:
1 | public interface AdvisorAdapter { |
三个适配器类:
1 | class MethodBeforeAdviceAdapter implements AdvisorAdapter, Serializable { |
客户端类:
1 | public class DefaultAdvisorAdapterRegistry implements AdvisorAdapterRegistry, Serializable { |
上面的代码在while循环里逐个取出注册的适配器,调用supportsAdvice()
方法来判断Advice
对应的类型,然后调用 getInterceptor() 创建对应类型的拦截器。
这里应该属于对象适配器模式,不过这里的Advice
对象是从外部传进来,而不是成员属性。
组合(Composite)
将对象组合成树形结构来表示“整体-部分”层次关系,允许用户以相同的方式处理单独对象和组合对象。
适用场景:
- 希望客户端可以忽略组合对象与单个对象的差异时。
- 处理一个树形结构时。
优缺点
优点:客户端不必关心处理的是单个对象还是整个组合结构,简化了客户端代码;增加新的构件无须对现有类库进行任何修改,符合开闭原则。
缺点:限制类型时会较为复杂;使设计变得更加抽象。
应用场景
一个在线学习网站下有许多目录以及学习视频,而目录下可能还会存在子目录,这里就可以使用组合模式。
抽象构件:
1 | public abstract class CatalogComponent { |
叶子构件(课程视频):
1 | public class Course extends CatalogComponent{ |
容器构件(课程目录):
1 | public class CourseCatalog extends CatalogComponent { |
客户端类:
1 | public class Test { |
组合模式的关键是定义了一个抽象构件类,它既可以代表叶子,又可以代表容器,而客户端针对该抽象构件类进行编程,无须知道它到底表示的是叶子还是容器,可以对其进行统一处理。同时容器对象与抽象构件类之间还建立一个聚合关系,在容器对象中既可以包含叶子,也可以包含容器,以此实现递归组合,形成一个树形结构。
装饰者(Decorator)
在不改变原有对象的基础之上,将功能附加到对象上。
提供了比继承更有弹性的替代方案(扩展原有对象功能)。
适用场景:
- 扩展一个类的功能或给一个类添加附加职责。
- 动态的给一个对象添加功能,这些功能可以再动态的撤销。
优缺点
优点:继承的有力补充,比继承灵活,不改变原有对象的情况下给一个对象扩展功能;通过使用不同装饰类以及这些装饰类的排列组合,可以实现不同效果;符合开闭原则。
缺点:会出现更多的代码,更多的类,增加程序复杂性;动态装饰时、多层装饰时会更复杂。
应用场景
我们考虑一个买煎饼的例子,人们可以自由地选择是否要在煎饼上加鸡蛋或者火腿,每次要加多少个,而总共价格是多少。
煎饼抽象类:
1 | public abstract class ABattercake { |
煎饼类:
1 | public class Battercake extends ABattercake { |
抽象装饰类(并不是真正的抽象类,因为这个场景中不需要抽象方法),这个类将抽象煎饼类作为成员属性,并且也继承了抽象煎饼类:
1 | public class AbstractDecorator extends ABattercake { |
加鸡蛋的装饰类,继承了抽象装饰类:
1 | public class EggDecorator extends AbstractDecorator { |
加火腿的装饰类,继承了抽象装饰类:
1 | public class SausageDecorator extends AbstractDecorator { |
客户端类:
1 | public class Test { |
输出:
1 | 煎饼 加一个鸡蛋 加一个鸡蛋 加一根香肠 销售价格:12 |
装饰类和具体组件类都继承了抽象组件类。所谓装饰,就是把这个装饰者套在被装饰者之上,从而动态扩展被装饰者的功能,装饰者的方法有一部分是自己的,这属于它的功能,然后调用被装饰者的方法实现,从而也保留了被装饰者的功能。
Java I/O中的应用
在Java中应用程序通过输入流(InputStream)的Read方法从源地址处读取字节,然后通过输出流(OutputStream)的Write方法将流写入到目的地址。
流的来源主要有三种:本地的文件(File)、控制台、通过socket实现的网络通信。
下面查看其中InputStream的类图,而关于OutputStream、Reader、Writer等都与此类似:
由上图可以看出只要继承了FilterInputStream的类就是装饰者类,可以用于包装其他的流,装饰者类还可以对装饰者和类进行再包装。以下是对其中部分类的简要介绍:
流名称 | 简介 |
---|---|
ByteArrayInputStream | 字节数组输入流在内存中创建一个字节数组缓冲区,从输入流读取的数据保存在该字节数组缓冲区中 |
PipedInputStream | 访问管道,主要在线程中使用,一个线程通过管道输出流发送数据,而另一个线程通过管道输入流读取数据,这样可实现两个线程间的通讯 |
FileInputStream | 访问文件,把一个文件作为 InputStream ,实现对文件的读取操作 |
PushBackInputStream | 推回输入流,可以把读取进来的某些数据重新回退到输入流的缓冲区之中 |
BufferedInputStream | 带缓冲的输入流一次读很多字节先放到内存中,等缓冲区满的时候一次性写入磁盘,这种方式可以减少磁盘操作次数,因此效率很高 |
DataInputStream | 允许应用程序以与机器无关方式从底层输入流中读取基本Java数据类型 |
Spring中的应用
查看org.springframework.cache.transaction
下的TransactionAwareCacheDecorator
:
该类实现了Cache
接口,同时将Cache
组合到类中成为了成员属性,所以可以大胆猜测TransactionAwareCacheDecorator
是一个装饰类,不过这里并没有抽象装饰类,且TransactionAwareCacheDecorator
没有子类,这里的装饰类关系并没有Java I/O中的装饰关系那么复杂。
实际上,Spring cache是对缓存使用的抽象,通过它我们可以在不侵入业务代码的基础上让现有代码即刻支持缓存。通过Spring的TransactionSynchronizationManager
将其缓存操作与Spring管理的事务同步,仅在成功事务的提交之后执行实际的缓存操作。
MyBatis中的应用
查看包org.apache.ibatis.cache
:
代理(Proxy)
代理模式为其它对象提供一种代理,以控制对这个对象的访问,代理对象在客户端和目标对象之间起到中介的作用。
我们有多种不同的方式来实现代理。如果按照代理创建的时期来进行分类的话, 可以分为两种:静态代理、动态代理。静态代理是由程序员创建或特定工具自动生成源代码,再对其编译,在运行之前,代理类.class文件就已经被创建了。动态代理是在程序运行时通过反射机制动态创建的。
适用场景:
- 保护目标对象
- 增强目标对象
优缺点
优点:能将代理对象与真实被调用的目标对象分离;保护目标对象;增强目标对象。
缺点:会造成系统设计中类的数目增加;在客户端和目标对象增加一个代理对象,会造成请求处理速度变慢;增加系统的复杂度。
应用场景
静态代理
使用静态代理可以做到在符合开闭原则的情况下对目标对象进行功能扩展,但我们得为每一个服务都创建代理类,工作量太大,不易管理。
服务接口:
1 | public interface BuyHouse { |
实现服务接口:
1 | public class BuyHouseImpl implements BuyHouse { |
代理类:
1 | public class BuyHouseProxy implements BuyHouse { |
客户端类:
1 | public class Test{ |
动态代理
在动态代理中我们不再需要手动的创建代理类,我们只需要编写一个动态处理器就可以了。真正的代理对象由JDK再运行时为我们动态的来创建。
动态处理器,实现了InvocationHandler
接口:
1 | public class DynamicProxyHandler implements InvocationHandler { |
客户端类:
1 | public class Test { |
其中Proxy.newProxyInstance()
方法接受三个参数:
ClassLoader loader
:指定当前目标对象使用的类加载器,获取加载器的方法是固定的Class<?>[] interfaces
:指定目标对象实现的接口的类型,使用泛型方式确认类型InvocationHandler
:指定动态处理器,执行目标对象的方法时,会触发事件处理器的方法
动态代理虽然不需要自己手动实现代理类和目标方法,但动态代理目标对象必须有接口,没有接口不能实现JDK版动态代理。
CGLIB代理
JDK实现动态代理需要实现类通过接口定义业务方法,对于没有接口的类,如何实现动态代理呢,这就需要CGLib了。CGLib采用了非常底层的字节码技术,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。但因为采用的是继承,所以不能对final修饰的类进行代理。JDK动态代理与CGLib动态代理均是实现Spring AOP的基础。
CGLIB创建的动态代理对象比JDK创建的动态代理对象的性能更高,但是CGLIB创建代理对象时所花费的时间却比JDK多得多。所以对于单例的对象,因为无需频繁创建对象,用CGLIB合适,反之使用JDK方式要更为合适一些。
Spring的代理选择
- 当Bean有实现接口时,Spring就会用JDK的动态代理。
- 当Bean没有实现接口时,Spring使用CGlib。
- 可以强制使用CGLib。
桥接(Bridge)
将抽象部分与它的具体实现部分分离,使它们都可以独立地变化。
通过组合的方式建立两个类之间联系,而不是继承。
适用场景:
- 抽象和具体实现之间增加更多的灵活性。
- 一个类存在两个(或多个)独立变化的维度,且这两个(或多个)维度都需要独立进行扩展。
- 不希望使用继承,或因为多层继承导致系统类的个数剧增。
优缺点
优点:分离抽象部分及其具体实现部分;提高了系统的可扩展性;符合开闭原则与合成复用原则。
缺点:增加了系统的设计难度;需要正确地识别出系统中两个独立变化的维度。
应用场景
画图时可以画正方形、长方形、圆形三种形状,而每种形状又可以画白色、灰色、黑色三种颜色,因此我们可以很自然地想出以下的继承关系:
对于这种方案,假如我们要添加一个椭圆形状,我们又要增加三种颜色,也就是白椭圆、灰椭圆和黑椭圆。假如我们要添加一个绿色,我们就要增加绿正方形、绿椭圆和绿长方形。每次增加都会增加若干个类(如果增加颜色则会增加形状个数个类,若增加形状则会增加颜色个数个类),这会导致系统类的个数剧增,且不利于系统的扩展。
对于这种有几个变化维度的场景,我们就可以使用桥接模式来减少系统中的类个数。这里提供两个父类一个是颜色、一个形状,颜色父类和形状父类两个类都包含了相应的子类,然后根据需要对颜色和形状进行组合:
形状抽象类,将颜色接口设为其成员变量:
1 |
|
具体形状类:
1 | public class Circle extends Shape{ |
颜色接口:
1 | public interface Color { |
具体颜色类:
1 | public class White implements Color{ |
客户端类:
1 | public class Test{ |
这里在实现时要注意,抽象类的方法要调用组合的实现类的方法(如类Square
中的color.bepaint("正方形")
这行代码),这样才能体现出桥接的意义。
JDBC的应用
jdbc的类族设计是由sun公司设计了一套接口,再由各个数据库公司实现接口,我们在调用的过程中只需要使用接口去定义,然后在加载Driver的过程中底层代码会给我们选择好接口真正的实现类,以此来实现真正的数据库连接,此后所有的方法,包括获取statement等等,都是由接口声明调用,但是底层返回的是接口实现类。用这种桥接的模式,我们可以很轻松地在不同的数据库连接中进行转化,只需要修改Driver加载的类,如果把加载类的声明放入配置文件中,更是不需要重新去编译,可以很方便地在不同数据库间进行转化。
享元(Flyweight)
享元模式提供了减少对象数量从而改善应用所需的对象结构的方式。
适用场景:
- 常常应用于系统底层的开发,以便解决系统的性能问题。
- 系统有大量相似对象、需要缓冲池的场景。
优缺点
优点:减少对象的创建,降低内存中对象的数量,降低系统的内存,提高效率;减少内存之外的其它资源占用(比如创建对象所需的时间)。
缺点:关注内外部状态以及线程安全问题;使系统的逻辑复杂化。
应用场景
在年底的时候公司的老总、副总等许多高层经常需要将部门经理叫去办公室汇报工作,而汇报的往往都是同样的内容,部门经理没有必要每次汇报前都准备一份全新的相同报告,而可以直接使用之前的报告,在这里可以应用享元模式。
雇员接口:
1 | public interface Employee { |
部门经理类,在创建部门经理的时候需要指定部门department
,这个部门就是外部状态,而职位title
是固定设为部门经理
的,因此就是内部状态:
1 | public class Manager implements Employee { |
雇员工厂类,用于创建并管理享元对象。这里使用了一个HashMap
作为缓存池,如果某个部门经理没有做过汇报,那么就创建这个部门经理,并且设置汇报内容,然后将其加入到缓存池中。如果以后再叫到了这个部门经理,就直接从缓存池中取出而不必再创建一遍了:
1 | public class EmployeeFactory { |
客户端类,随机取出一个部门的部门经理做汇报:
1 | public class Test { |
输出:
1 | 创建部门经理: BD 创建报告: BD部门汇报: 此次报告的主要内容是... |
可以发现每个部门经理只创建过一次报告,之后都使用的同一份报告做汇报。
Integer中的应用
我们查看jdk中Integer
类下的valueOf
方法:
1 | //... |
可以看出,在使用valueOf
方法时,如果传入的参数在缓存范围内(这个范围在IntegerCache
中设置为-128~127),那么直接从缓存中读取并返回,否则就创建一个新的对象返回。
我们可以通过一个实验来加以验证:1
2
3
4
5
6
7
8
9
10
11
12
13public static void main(String[] args) {
Integer t1 = new Integer(100);
Integer t2 = new Integer(100);
System.out.println(t1 == t2);
Integer t3 = 100;
Integer t4 = 100;
System.out.println(t3 == t4);
Integer t5 = 128;
Integer t6 = 128;
System.out.println(t5 == t6);
}
输出:
1 | false |
使用new创建的Integer对象用==
比较的是对象地址,因为对象不同所以地址也不相同,故输出false
;而像Integer t = xxx
这种形式的定义实际会变成Integer t = Integer.valueOf(xxx)
,先判断是否能直接从缓存中取出,100是在缓存范围内的也加入过缓存,因此可以直接取出;而128超出了缓存范围,所以在valueOf
方法中会创建一个新的对象返回。
行为型
模板方法(Template Method)
定义了一个算法的骨架,并允许子类为一个或多个步骤提供实现。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法的某些步骤。
适用场景:
- 一次性实现一个算法的不变的部分,并将可变的行为留给子类来实现。
- 各子类中公共的行为被提取出来并集中到一个公共父类中,从而避免代码重复。
优缺点
优点:提高复用性;提高扩展性;符合开闭原则。
缺点:类数目增加,增加了系统实现的复杂度;继承关系自身缺点,即如果父类添加新的抽象方法,所有子类都要改一遍。
应用场景
制作一节网课的步骤可以简化为4个步骤:制作PPT;录制视频;编写笔记;提供课程资料。
所有课程都需要制作PPT、录制视频,但不是每个课程都需要编写笔记,而提供的课程资料在每个课程都不尽不同(有些课程需要提供源代码,有些需要提供图片文件等)。
我们可以在抽象父类中确定整个流程的模板,并实现固定不变的步骤,而把不固定的步骤留给子类实现。除此之外,对于类似编写笔记这个不一定有的步骤,我们可以通过一个钩子方法,让子类来决定流程中其执行与否。
抽象父类,由于制作PPT、录制视频对于每节课都是必须且相同的,因此声明为final
使得子类无法对其修改,而编写笔记虽然可有可无,但是具体的操作对于所有课程也是相同的因此不需要修改,所以也声明为final
,而提供课程资料(packageCourse
方法)这一步骤则交由具体子类实现:
1 | public abstract class ACourse { |
前端课程:
1 | public class FECourse extends ACourse { |
设计模式课程,覆盖了钩子方法,让其可以编写笔记:
1 | public class DesignPatternCourse extends ACourse { |
客户端类:
1 | public class Test { |
JDK中的应用
我们查看java.util
下的AbstractList
抽象类:
1 | public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> { |
这里面的addAll
方法就相当于一个模板方法,它定义了这个算法的整体流程,而其具体的步骤如rangeCheckForAdd
、add
则交由子类如ArrayList
等来完成。
Servlet中的应用
Servlet是用Java编写的服务器端程序,主要功能在于交互式地浏览和修改数据,生成动态Web内容。狭义的Servlet是指Java语言实现的一个接口,广义的Servlet是指任何实现了这个Servlet接口的类,一般情况下,人们将Servlet理解为后者。
每一个Servlet都必须要实现Servlet
接口,GenericServlet
是个通用的、不特定于任何协议的Servlet,它实现了Servlet
接口,而HttpServlet
继承于GenericServlet
,实现了Servlet
接口,为Servlet
接口提供了处理HTTP协议的实现,所以我们定义的Servlet
只需要继承HttpServlet
即可。
在HttpServlet
的service
方法中,首先获得到请求的方法名,然后根据方法名调用对应的doXXX
方法,比如说请求方法为GET,那么就去调用doGet
方法;请求方法为POST,那么就去调用doPost
方法。
HttpServlet
相当于定义了一套处理HTTP请求的模板。service
方法为模板方法,定义了处理HTTP请求的基本流程,doXXX
等方法为基本步骤,根据请求方法做相应的处理,编写自定义的Servlet
时可以重写这些方法。
1 | public abstract class HttpServlet extends GenericServlet { |
策略模式(Strategy)
定义了算法家族,分别封装起来,让它们之间可以互相替代,此模式让算法的辩护权啊不会影响到使用算法的用户。
适用场景:
- 系统有很多类,而他们的区别仅仅在于他们的行为不同。
- 一个系统需要动态地在几种算法中选择一种。
优缺点
- 优点:符合开闭原则;避免使用多重条件转移语句;提高算法的保密性和安全性。
- 缺点:客户端必须知道所有的策略类,并自行决定使用哪一个策略类。
应用场景
在促销期间商家有不同的促销策略:返现、立减和满减。
策略抽象类:
1 | public interface PromotionStrategy { |
返现策略:
1 | public class FanXianPromotionStrategy implements PromotionStrategy { |
立减策略:
1 | public class LiJianPromotionStrategy implements PromotionStrategy{ |
满减策略:
1 | public class ManJianPromotionStratehy implements PromotionStrategy { |
促销活动,将促销策略作为成员变量:
1 | public class PromotionActivity { |
客户端类:
1 | public class Test { |
每当要新增一个促销策略的时候,直接增加一个策略实现即可,十分方便。
命令模式(Command)
命令模式允许请求的一方和接收的一方独立开来,使得请求的一方不必知道接收请求的一方的接口,更不必知道请求是怎么被接收,以及操作是否被执行、何时被执行、是怎么被执行的。
命令允许请求的一方和接收请求的一方能够独立演化,从而具有以下的优点:
- 命令模式使新的命令很容易地被加入到系统里。
- 能较容易地设计一个命令队列。
- 可以容易地实现对请求的撤销和恢复。
- 在需要的情况下,可以较容易地将命令记入日志。
命令模式涉及到五个角色,它们分别是:
- 客户端(Client)角色:创建一个具体命令(ConcreteCommand)对象并确定其接收者。
- 命令(Command)角色:声明了一个给所有具体命令类的抽象接口。
- 具体命令(ConcreteCommand)角色:实现
execute()
方法,负责调用接收者的相应操作。 - 调用者(Invoker)角色:负责调用命令对象执行请求。
- 接收者(Receiver)角色:负责具体实施和执行一个请求。
应用场景
设计一个遥控器,可以控制电灯开关。
命令与具体命令: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
29public interface Command {
void execute();
}
public class LightOnCommand implements Command {
Light light;
public LightOnCommand(Light light) {
this.light = light;
}
public void execute() {
light.on();
}
}
public class LightOffCommand implements Command {
Light light;
public LightOffCommand(Light light) {
this.light = light;
}
public void execute() {
light.off();
}
}
接收者:1
2
3
4
5
6
7
8
9public class Light {
public void on() {
System.out.println("Light is on!");
}
public void off() {
System.out.println("Light is off!");
}
}
调用者: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
26public class Invoker {
private Command[] onCommands;
private Command[] offCommands;
private final int slotNum = 7;
public Invoker() {
this.onCommands = new Command[slotNum];
this.offCommands = new Command[slotNum];
}
public void setOnCommand(Command command, int slot) {
onCommands[slot] = command;
}
public void setOffCommand(Command command, int slot) {
offCommands[slot] = command;
}
public void onButtonWasPushed(int slot) {
onCommands[slot].execute();
}
public void offButtonWasPushed(int slot) {
offCommands[slot].execute();
}
}
客户端:1
2
3
4
5
6
7
8
9
10
11
12public class Client {
public static void main(String[] args) {
Invoker invoker = new Invoker();
Light light = new Light();
Command lightOnCommand = new LightOnCommand(light);
Command lightOffCommand = new LightOffCommand(light);
invoker.setOnCommand(lightOnCommand, 0);
invoker.setOffCommand(lightOffCommand, 0);
invoker.onButtonWasPushed(0);
invoker.offButtonWasPushed(0);
}
}
与策略模式的区别
命令模式与策略模式的区别主要在于目的上。策略模式是通过不同的算法做同一件事情,而命令模式则是通过不同的命令做不同的事情。
Runnable中的应用
命令模式最典型的一个应用就是Runnable
。Thread
是一个调用者,它提供了start()
,join()
,interrupt()
等方法让我们来控制命令(也就是Runnable
)的执行。并且,常常会将Runnable
的实现类直接当作接收者。
观察者模式(Observer)
定义了对象之间的一对多依赖,让多个观察者对象同时监听某一个主题对象,当主题对象发生变化时,它的所有依赖者(观察者)都会收到通知并更新。
优缺点
- 优点:观察者和被观察者之间建立一个抽象的耦合;支持广播通信。
- 缺点:观察者之间有过多的细节依赖、提高时间消耗及程序复杂度。
应用场景
每个课程有一名老师,而课程的学生可能提出许多问题,因此创建三个类Course
、Question
、Teacher
。其中课程应该作为被观察者,而老师由于要回答学生们的问题,因此作为观察者时刻观察着。
课程类Course
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21public class Course extends Observable {
private String courseName;
public Course(String courseName) {
this.courseName = courseName;
}
public String getCourseName() {
return courseName;
}
public void setCourseName(String courseName) {
this.courseName = courseName;
}
public void produceQuestion(Course course, Question question){
System.out.println(question.getUserName() + "在" + course.getCourseName() + "提交了一个问题");
setChanged();
notifyObservers(question);
}
}
问题类Question
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public class Question {
private String userName;
private String questionContent;
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getQuestionContent() {
return questionContent;
}
public void setQuestionContent(String questionContent) {
this.questionContent = questionContent;
}
}
教师类Teacher
:1
2
3
4
5
6
7
8
9
10
11
12
13public class Teacher implements Observer {
private String teacherName;
public Teacher(String teacherName) {
this.teacherName = teacherName;
}
public void update(Observable o, Object arg) {
Course course = (Course)o;
Question question = (Question) arg;
System.out.println(teacherName + "老师的" + course.getCourseName() + "课程接收到一个" + question.getUserName() + "提交的问答:" + question.getQuestionContent());
}
}
可以看出,以上代码通过继承Observable
类和实现Observer
接口实现了观察者模式。
Observable
中有两个方法对Observer
特别重要,一个是setChanged()
方法用来设置一个内部标志位表示数据发生了变化,一个是notifyObservers()
方法会去调用一个列表中所有的Observer
的update()
方法,通知它们数据发生了变化。
客户端类Test
:
1 | public class Test { |
Observable
通过addObserver()
方法把任意多个Observer
添加到这个列表中。
运行结果:
1 | cenjie在Java设计模式课程提交了一个问题 |
可以看出,当被观察者course
对象发生变化时,teacher1
和teacher2
这两个观察者都得到了通知。
责任链模式(Chain Of Responsibility)
使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链发送该请求,直到有一个对象处理它为止。
应用场景
抽象处理器,组合有一个同类型的成员变量:
1 | public abstract class Handler { |
具体处理器一:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public class ConcreteHandler1 extends Handler {
public ConcreteHandler1(Handler successor) {
super(successor);
}
protected void handleRequest(Request request) {
if (request.getType() == RequestType.TYPE1) {
System.out.println(request.getName() + " is handle by ConcreteHandler1");
return;
}
if (successor != null) {
successor.handleRequest(request);
}
}
}
具体处理器二:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public class ConcreteHandler2 extends Handler {
public ConcreteHandler2(Handler successor) {
super(successor);
}
protected void handleRequest(Request request) {
if (request.getType() == RequestType.TYPE2) {
System.out.println(request.getName() + " is handle by ConcreteHandler2");
return;
}
if (successor != null) {
successor.handleRequest(request);
}
}
}
要处理的请求:
1 | public class Request { |
客户端:1
2
3
4
5
6
7
8
9
10
11
12
13
14public class Client {
public static void main(String[] args) {
Handler handler1 = new ConcreteHandler1(null);
Handler handler2 = new ConcreteHandler2(handler1);
Request request1 = new Request(RequestType.TYPE1, "request1");
handler2.handleRequest(request1);
Request request2 = new Request(RequestType.TYPE2, "request2");
handler2.handleRequest(request2);
}
}
可以看出,handler2
中包含了一个handler1
,当发送一个request
给handler2
时,handler2
可以将request
继续传递给handler1
,由handler1
完成处理并结束。
备忘录模式(Memento)
保存一个对象的某个状态,以便在适当的时候恢复对象。 为用户提供一种可恢复机制,并对存档信息进行封装,但如果使用不当的话会造成资源的浪费。
应用场景
可以通过备忘录模式对用户的文章进行修改保存与回滚。
文章类:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56public class Article {
private String title;
private String content;
private String imgs;
public Article(String title, String content, String imgs) {
this.title = title;
this.content = content;
this.imgs = imgs;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getImgs() {
return imgs;
}
public void setImgs(String imgs) {
this.imgs = imgs;
}
public ArticleMemento saveToMemento() {
ArticleMemento articleMemento = new ArticleMemento(this.title,this.content,this.imgs);
return articleMemento;
}
public void undoFromMemento(ArticleMemento articleMemento) {
this.title = articleMemento.getTitle();
this.content = articleMemento.getContent();
this.imgs = articleMemento.getImgs();
}
public String toString() {
return "Article{" +
"title='" + title + '\'' +
", content='" + content + '\'' +
", imgs='" + imgs + '\'' +
'}';
}
}
文章备忘录,属性与文章类一致,当没有setter
方法,防止他人的修改: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
32public class ArticleMemento {
private String title;
private String content;
private String imgs;
public ArticleMemento(String title, String content, String imgs) {
this.title = title;
this.content = content;
this.imgs = imgs;
}
public String getTitle() {
return title;
}
public String getContent() {
return content;
}
public String getImgs() {
return imgs;
}
public String toString() {
return "ArticleMemento{" +
"title='" + title + '\'' +
", content='" + content + '\'' +
", imgs='" + imgs + '\'' +
'}';
}
}
文章备忘录管理器,聚合了之前保存的管理器,可以随时调取上一个备忘录,也可以加入一个新的备忘录:1
2
3
4
5
6
7
8
9
10
11
12
13
14public class ArticleMementoManager {
private final Stack<ArticleMemento> ARTICLE_MEMENTO_STACK = new Stack<ArticleMemento>();
public ArticleMemento getMemento()
{
ArticleMemento articleMemento= ARTICLE_MEMENTO_STACK.pop();
return articleMemento;
}
public void addMemento(ArticleMemento articleMemento)
{
ARTICLE_MEMENTO_STACK.push(articleMemento);
}
}
客户端: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
45public class Test {
public static void main(String[] args) {
ArticleMementoManager articleMementoManager = new ArticleMementoManager();
Article article= new Article("如影随行的设计模式A","手记内容A","手记图片A");
ArticleMemento articleMemento = article.saveToMemento();
articleMementoManager.addMemento(articleMemento);
System.out.println("标题:"+article.getTitle()+" 内容:"+article.getContent()+" 图片:"+article.getImgs()+" 暂存成功");
System.out.println("手记完整信息:"+article);
System.out.println("修改手记start");
article.setTitle("如影随行的设计模式B");
article.setContent("手记内容B");
article.setImgs("手记图片B");
System.out.println("修改手记end");
System.out.println("手记完整信息:"+article);
articleMemento = article.saveToMemento();
articleMementoManager.addMemento(articleMemento);
article.setTitle("如影随行的设计模式C");
article.setContent("手记内容C");
article.setImgs("手记图片C");
System.out.println("暂存回退start");
System.out.println("回退出栈1次");
articleMemento = articleMementoManager.getMemento();
article.undoFromMemento(articleMemento);
System.out.println("回退出栈2次");
articleMemento = articleMementoManager.getMemento();
article.undoFromMemento(articleMemento);
System.out.println("暂存回退end");
System.out.println("手记完整信息:"+article);
}
}
中介者模式(Mediator)
用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显示的相互引用,从而降低耦合。
应用场景
用户(同事类):1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public class User {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public User(String name) {
this.name = name;
}
public void sendMessage(String message) {
StudyGroup.showMessage(this, message);
}
}
学习小组:1
2
3
4
5
6public class StudyGroup {
public static void showMessage(User user, String message){
System.out.println(new Date().toString() + " [" + user.getName() + "] : " + message);
}
}
客户端:1
2
3
4
5
6
7
8
9public class Test {
public static void main(String[] args) {
User geely = new User("Geely");
User tom= new User("Tom");
geely.sendMessage(" Hey! Tom! Let's learn Design Pattern");
tom.sendMessage("OK! Geely");
}
}
参考资料
- 弗里曼. Head First 设计模式 [M]. 中国电力出版社, 2007.
- 慕课网java设计模式精讲 Debug 方式+内存分析
- SLF4J和Logback日志框架详解
- 设计模式—代理模式
- 设计模式读书笔记–桥接模式
- JDBC源码分析&桥接模式
- 模板方法模式及典型应用