设计模式
设计模式
设计模式是⼀套被反复使⽤的、多数⼈知晓的、经过分类编⽬的、代码设计经验的总结。
一、单例模式
⼀个单例类在任何情况下都只存在⼀个实例,构造⽅法必须是私有的、由⾃⼰创建⼀个静态变量存储实例,对外提供⼀个静态公有⽅法获取实例。
优点是内存中只有⼀个实例,减少了开销,尤其是频繁创建和销毁实例的情况下并且可以避免对资源的多重占⽤。缺点是没有抽象层,难以扩展,与单⼀职责原则冲突。
1、饿汉式,线程安全
饿汉式单例模式,顾名思义,类⼀加载就创建对象,这种⽅式⽐较常⽤,但容易产⽣垃圾对象,浪费内存空间。
- 优点:线程安全,没有加锁,执⾏效率较⾼
- 缺点:不是懒加载,类加载时就初始化,浪费内存空间
懒加载 (lazy loading):使⽤的时候再创建对象
/**
* 饿汉式单例测试
*
* @className: Singleton
* @date:
*/
public class Singleton {
// 1、私有化构造⽅法
private Singleton(){}
// 2、定义⼀个静态变量指向⾃⼰类型
private final static Singleton instance = new Singleton();
// 3、对外提供⼀个公共的⽅法获取实例
public static Singleton getInstance() {
return instance;
}
}
public class Test {
public static void main(String[] args) throws Exception{
// 使⽤反射破坏单例
// 获取空参构造⽅法
Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor(null);
// 设置强制访问
declaredConstructor.setAccessible(true);
// 创建实例
Singleton singleton = declaredConstructor.newInstance();
System.out.println("反射创建的实例" + singleton);
System.out.println("正常创建的实例" + Singleton.getInstance());
System.out.println("正常创建的实例" + Singleton.getInstance());
}
}
2、懒汉式,线程不安全
这种⽅式在单线程下使⽤没有问题,对于多线程是⽆法保证单例的,这⾥列出来是为了和后⾯使⽤锁保证线程安全的单例做对⽐。
- 优点:懒加载
- 缺点:线程不安全
/**
* 懒汉式单例,线程不安全
*
* @className: Singleton
* @date: 2021/6/7 14:32
*/
public class Singleton {
// 1、私有化构造⽅法
private Singleton(){ }
// 2、定义⼀个静态变量指向⾃⼰类型
private static Singleton instance;
// 3、对外提供⼀个公共的⽅法获取实例
public static Singleton getInstance() {
// 判断为 null 的时候再创建对象
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
3、懒汉式,线程安全
通过 synchronized 关键字加锁保证线程安全, synchronized 可以添加在⽅法上⾯,也可以添加在代码块上⾯,这⾥演示添加在⽅法上⾯,存在的问题是 每⼀次调⽤ getInstance 获取实例时都需要加锁和释放锁,这样是⾮常影响性能的。
/**
* 懒汉式单例,⽅法上⾯添加 synchronized 保证线程安全
*
* @className: Singleton
* @date: 2021/6/7 14:32
*/
public class Singleton {
// 1、私有化构造⽅法
private Singleton(){ }
// 2、定义⼀个静态变量指向⾃⼰类型
private static Singleton instance;
// 3、对外提供⼀个公共的⽅法获取实例
public synchronized static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
双重检查锁(DCL, 即 double-checked locking**)**
/**
* 双重检查锁(DCL, 即 double-checked locking) *
* @className: Singleton
* @date:
*/
public class Singleton {
// 1、私有化构造⽅法
private Singleton() {
}
// 2、定义⼀个静态变量指向⾃⼰类型
private volatile static Singleton instance;
// 3、对外提供⼀个公共的⽅法获取实例
public static Singleton getInstance() {
// 第⼀重检查是否为 null
if (instance == null) {
// 使⽤ synchronized 加锁
synchronized (Singleton.class) {
// 第⼆重检查是否为 null
if (instance == null) {
// new 关键字创建对象不是原⼦操作,所以使用volatile禁止指令重排序
instance = new Singleton();
}
}
}
return instance;
}
}
优点:懒加载,线程安全,效率较⾼
缺点:实现较复杂
这⾥的双重检查是指两次⾮空判断,锁指的是 synchronized 加锁,为什么要进⾏双重判断,其实很简单,第⼀重判断,如果实例已经存在,那么就不再需要进⾏同步操作,⽽是直接返回这个实例,如果没有创建,才会进⼊同步块,同步块的⽬的与之前相同,⽬的是为了防⽌有多个线程同时调⽤时,导致⽣成多个实例,有了同步块,每次只能有⼀个线程调⽤访问同步块内容,当第⼀个抢到锁的调⽤获取了实例之后,这个实例就会被创建,之后的所有调⽤都不会进⼊同步块,直接在第⼀重判断就返回了单例。
关于内部的第⼆重空判断的作⽤,当多个线程⼀起到达锁位置时,进⾏锁竞争,其中⼀个线程获取锁,如果是第⼀次进⼊则为 null,会进⾏单例对象的创建,完成后释放锁,其他线程获取锁后就会被空判断拦截,直接返回已创建的单例对象。
双重检查锁中使⽤ volatile 的两个重要特性:
可⻅性、禁⽌指令重排序
静态内部类
/**
* 静态内部类实现单例
*
* @className: Singleton
* @date: 2021/6/7 14:32
*/
public class Singleton {
// 1、私有化构造⽅法
private Singleton() {
}
// 2、对外提供获取实例的公共⽅法
public static Singleton getInstance() {
return InnerClass.INSTANCE;
}
// 定义静态内部类
private static class InnerClass{
private final static Singleton INSTANCE = new Singleton();
}
}
优点:懒加载,线程安全,效率较⾼,实现简单
二、⼯⼚模式
1、简单⼯⼚模式
简单⼯⼚模式指由⼀个⼯⼚对象来创建实例,客户端不需要关注创建逻辑,只需提供传⼊⼯⼚的参数。
简单工厂模式其实并不算是一种设计模式,更多的时候是一种编程习惯。简单工厂的实现思路是,定义一个工厂类,根据传入的参数不同返回不同的实例,被创建的实例具有共同的父类或接口。简单工厂的适用场景是:
- 需要创建的对象较少。
- 客户端不关心对象的创建过程。
Calendar 抽象类的 getInstance ⽅法,调⽤ createCalendar ⽅法根据不同的地区参数创建不同的⽇历对象。
Spring 中的 BeanFactory 使⽤简单⼯⼚模式,根据传⼊⼀个唯⼀的标识来获得 Bean 对象。
2、⼯⼚⽅法模式
和简单工厂对比一下,最根本的区别在于,简单工厂只有一个统一的工厂类,而工厂方法是针对每个要创建的对象都会提供一个工厂类,这些工厂类都实现了一个工厂基类。
下面总结一下工厂方法的适用场景:
- 客户端不需要知道它所创建的对象的类。
- 客户端可以通过子类来指定创建对应的对象。
示例:
现在需要设计一个这样的图片加载类,它具有多个图片加载器,用来加载jpg,png,gif格式的图片,每个加载器都有一个read()方法,用于读取图片。下面我们完成这个图片加载类。
首先完成图片加载器的设计,编写一个加载器的公共接口:
public interface Reader {
void read();
}
然后完成各个图片加载器的代码:
// jpg图片加载器
class JpgReader implements Reader {
@Override
public void read() {
System.out.print("read jpg");
}
}
// png图片加载器
class PngReader implements Reader {
@Override
public void read() {
System.out.print("read png");
}
}
// gif图片加载器
class GifReader implements Reader {
@Override
public void read() {
System.out.print("read gif");
}
}
现在我们按照定义所说定义一个抽象的工厂接口ReaderFactory:
interface ReaderFactory {
Reader getReader();
}
里面有一个getReader()方法返回我们的Reader 类,接下来我们把上面定义好的每个图片加载器都提供一个工厂类,这些工厂类实现了ReaderFactory 。
// jpg加载器工厂
class JpgReaderFactory implements ReaderFactory {
@Override
public Reader getReader() {
return new JpgReader();
}
}
// png加载器工厂
class PngReaderFactory implements ReaderFactory {
@Override
public Reader getReader() {
return new PngReader();
}
}
// gif加载器工厂
class GifReaderFactory implements ReaderFactory {
@Override
public Reader getReader() {
return new GifReader();
}
}
在每个工厂类中我们都通过重写的getReader()方法返回各自的图片加载器对象。
3、抽象⼯⼚模式
下面总结一下抽象工厂的适用场景:
- 和工厂方法一样客户端不需要知道它所创建的对象的类。
- 需要一组对象共同完成某种功能时。并且可能存在多组对象完成不同功能的情况。
- 系统结构稳定,不会频繁的增加对象。
4、简单工厂模式和抽象工厂模式有什么区别?
- 简单工厂模式其实并不算是一种设计模式,更多的时候是一种编程习惯。简单工厂的实现思路是,定义一个工厂类,根据传入的参数不同返回不同的实例,被创建的实例具有共同的父类或接口。
- 工厂方法模式是简单工厂的仅一步深化, 在工厂方法模式中,我们不再提供一个统一的工厂类来创建所有的对象,而是针对不同的对象提供不同的工厂。也就是说每个对象都有一个与之对应的工厂。工厂方法的实现思路是,定义一个用于创建对象的接口,让子类决定将哪一个类实例化。工厂方法模式让一个类的实例化延迟到其子类。
- 抽象工厂模式是工厂方法的仅一步深化,在这个模式中的工厂类不单单可以创建一个对象,而是可以创建一组对象。这是和工厂方法最大的不同点。抽象工厂的实现思路是,提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。(同一类对象用同一个工厂)
- 工厂方法模式 VS 抽象工厂模式
工厂方法模式:一个抽象产品类,可以派生出多个具体产品类。每个具体工厂类只能创建一个具体产品类的实例。
抽象工厂模式:多个抽象产品类,每个抽象产品类可以派生出多个具体产品类。一个抽象工厂类可以派生出多个具体工厂类。每个具体工厂类可以创建多个具体产品的实例。
区别:工厂方法模式只有一个抽象产品类,而抽象工厂模式有多个。工厂方法模式的具体工厂类只能创建一个具体产品类的实例,而抽象工厂模式可以创建多个。
三、适配器模式
适配器模式就是将⼀个类的接⼝,转换成客户期望的另⼀个接⼝。它可以让原本两个不兼容的接⼝能够⽆缝完成对接。作为中间件的适配器将⽬标类和适配者解耦,增加了类的透明性和可复⽤性。
适配器不是在详细设计时添加的,而是解决正在服役的项目的问题。
步骤一、为媒体播放器和更高级的媒体播放器创建接口。
MediaPlayer.java
public interface MediaPlayer {
public void play(String audioType, String fileName);
}
AdvancedMediaPlayer.java
public interface AdvancedMediaPlayer {
public void playVlc(String fileName);
public void playMp4(String fileName);
}
步骤 2、创建实现了 AdvancedMediaPlayer 接口的实体类。
VlcPlayer.java
public class VlcPlayer implements AdvancedMediaPlayer{
@Override
public void playVlc(String fileName) {
System.out.println("Playing vlc file. Name: "+ fileName);
}
@Override
public void playMp4(String fileName) {
//什么也不做
}
}
Mp4Player.java
public class Mp4Player implements AdvancedMediaPlayer{
@Override
public void playVlc(String fileName) {
//什么也不做
}
@Override
public void playMp4(String fileName) {
System.out.println("Playing mp4 file. Name: "+ fileName);
}
}
步骤 3、创建实现了 MediaPlayer 接口的适配器类。
MediaAdapter.java
public class MediaAdapter implements MediaPlayer {
AdvancedMediaPlayer advancedMusicPlayer;
public MediaAdapter(String audioType){
if(audioType.equalsIgnoreCase("vlc") ){
advancedMusicPlayer = new VlcPlayer();
} else if (audioType.equalsIgnoreCase("mp4")){
advancedMusicPlayer = new Mp4Player();
}
}
@Override
public void play(String audioType, String fileName) {
if(audioType.equalsIgnoreCase("vlc")){
advancedMusicPlayer.playVlc(fileName);
}else if(audioType.equalsIgnoreCase("mp4")){
advancedMusicPlayer.playMp4(fileName);
}
}
}
步骤 4、创建实现了 MediaPlayer 接口的实体类。
AudioPlayer.java
public class AudioPlayer implements MediaPlayer {
MediaAdapter mediaAdapter;
@Override
public void play(String audioType, String fileName) {
//播放 mp3 音乐文件的内置支持
if(audioType.equalsIgnoreCase("mp3")){
System.out.println("Playing mp3 file. Name: "+ fileName);
}
//mediaAdapter 提供了播放其他文件格式的支持
else if(audioType.equalsIgnoreCase("vlc")
|| audioType.equalsIgnoreCase("mp4")){
mediaAdapter = new MediaAdapter(audioType);
mediaAdapter.play(audioType, fileName);
}
else{
System.out.println("Invalid media. "+
audioType + " format not supported");
}
}
}
步骤 5、使用 AudioPlayer 来播放不同类型的音频格式。
AdapterPatternDemo.java
public class AdapterPatternDemo {
public static void main(String[] args) {
AudioPlayer audioPlayer = new AudioPlayer();
audioPlayer.play("mp3", "beyond the horizon.mp3");
audioPlayer.play("mp4", "alone.mp4");
audioPlayer.play("vlc", "far far away.vlc");
audioPlayer.play("avi", "mind me.avi");
}
}
步骤 6、执行程序,输出结果:
Playing mp3 file. Name: beyond the horizon.mp3
Playing mp4 file. Name: alone.mp4
Playing vlc file. Name: far far away.vlc
Invalid media. avi format not supported
适配器模式的优缺点
- 优点:
- 提⾼了类的复⽤;
- 组合若⼲关联对象形成对外提供统⼀服务的接⼝;
- 扩展性、灵活性好。
- 缺点:
- 过多使⽤适配模式容易造成代码功能和逻辑意义的混淆。
- 部分语⾔对继承的限制,可能⾄多只能适配⼀个适配者类,⽽且⽬标类
- 必须是抽象类。
四、代理模式(proxy pattern)
代理模式的本质是⼀个中间件,主要⽬的是解耦合服务提供者和使⽤者。使⽤者通过代理间接的访问服务提供者,便于后者的封装和控制。是⼀种结构性模式。
静态代理和动态代理的区别
- 灵活性 :动态代理更加灵活,不需要必须实现接⼝,可以直接代理实现类,并且可以不需要针对每个⽬标类都创建⼀个代理类。另外,静态代理中,接⼝⼀旦新增加⽅法,⽬标对象和代理对象都要进⾏修改,这是⾮常麻烦的!
- JVM 层⾯ :静态代理在编译时就将接⼝、实现类、代理类这些都变成了⼀个个实际的 class ⽂件。⽽动态代理是在运⾏时动态⽣成类字节码,并加载到 JVM 中的。
五、观察者模式
观察者模式主要⽤于处理对象间的⼀对多的关系,是⼀种对象⾏为模式。该模式的实际应⽤场景⽐较容易确认,当⼀个对象状态发⽣变化时,所有该对象的关注者均能收到状态变化通知,以进⾏相应的处理。
观察者模式的优缺点
- 优点:
- 被观察者和观察者之间是抽象耦合的;
- 耦合度较低,两者之间的关联仅仅在于消息的通知;
- 被观察者⽆需关⼼他的观察者;
- ⽀持⼴播通信;
- 缺点:
- 观察者只知道被观察对象发⽣了变化,但不知变化的过程和缘由;
- 观察者同时也可能是被观察者,消息传递的链路可能会过⻓,完成所有通知花费时间较多;
- 如果观察者和被观察者之间产⽣循环依赖,或者消息传递链路形成闭环,会导致⽆限循环;
你的项⽬是怎么⽤的观察者模式?
在⽀付场景下,⽤户购买⼀件商品,当⽀付成功之后三⽅会回调⾃身,在这个时候系统可能会有很多需要执⾏的逻辑(如:更新订单状态,发送邮件通知,赠送礼品…),这些逻辑之间并没有强耦合,因此天然适合使⽤观察者模式去实现这些功能,当有更多的操作时,只需要添加新的观察者就能实现,完美实现了对修改关闭,对扩展开放的开闭原则。
步骤 1、创建 Subject 类。
Subject.java
import java.util.ArrayList;
import java.util.List;
public class Subject {
private List<Observer> observers
= new ArrayList<Observer>();
private int state;
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
notifyAllObservers();
}
public void attach(Observer observer){
observers.add(observer);
}
public void notifyAllObservers(){
for (Observer observer : observers) {
observer.update();
}
}
}
步骤 2、创建 Observer 类。
Observer.java
public abstract class Observer {
protected Subject subject;
public abstract void update();
}
步骤 3、创建实体观察者类。
BinaryObserver.java
public class BinaryObserver extends Observer{
public BinaryObserver(Subject subject){
this.subject = subject;
this.subject.attach(this);
}
@Override
public void update() {
System.out.println( "Binary String: "
+ Integer.toBinaryString( subject.getState() ) );
}
}
OctalObserver.java
public class OctalObserver extends Observer{
public OctalObserver(Subject subject){
this.subject = subject;
this.subject.attach(this);
}
@Override
public void update() {
System.out.println( "Octal String: "
+ Integer.toOctalString( subject.getState() ) );
}
}
HexaObserver.java
public class HexaObserver extends Observer{
public HexaObserver(Subject subject){
this.subject = subject;
this.subject.attach(this);
}
@Override
public void update() {
System.out.println( "Hex String: "
+ Integer.toHexString( subject.getState() ).toUpperCase() );
}
}
步骤 4、使用 Subject 和实体观察者对象。
ObserverPatternDemo.java
public class ObserverPatternDemo {
public static void main(String[] args) {
Subject subject = new Subject();
new HexaObserver(subject);
new OctalObserver(subject);
new BinaryObserver(subject);
System.out.println("First state change: 15");
subject.setState(15);
System.out.println("Second state change: 10");
subject.setState(10);
}
}
步骤 5、执行程序,输出结果:
First state change: 15
Hex String: F
Octal String: 17
Binary String: 1111
Second state change: 10
Hex String: A
Octal String: 12
Binary String: 1010
六、装饰器模式
装饰器模式主要对现有的类对象进⾏包裹和封装,以期望在不改变类对象及其类定义的情况下,为对象添加额外功能。是⼀种对象结构型模式。需要注意的是,该过程是通过调⽤被包裹之后的对象完成功能添加的,⽽不是直接修改现有对象的⾏为,相当于增加了中间层。
实现
我们将创建一个 Shape 接口和实现了 Shape 接口的实体类。然后我们创建一个实现了 Shape 接口的抽象装饰类 ShapeDecorator,并把 Shape 对象作为它的实例变量。
RedShapeDecorator 是实现了 ShapeDecorator 的实体类。
DecoratorPatternDemo 类使用 RedShapeDecorator 来装饰 Shape 对象。
步骤 1、创建一个接口:
Shape.java
public interface Shape {
void draw();
}
步骤 2、创建实现接口的实体类。
Rectangle.java
public class Rectangle implements Shape {
@Override
public void draw() {
System.out.println("Shape: Rectangle");
}
}
Circle.java
public class Circle implements Shape {
@Override
public void draw() {
System.out.println("Shape: Circle");
}
}
步骤 3、创建实现了 Shape 接口的抽象装饰类。
ShapeDecorator.java
public abstract class ShapeDecorator implements Shape {
protected Shape decoratedShape;
public ShapeDecorator(Shape decoratedShape){
this.decoratedShape = decoratedShape;
}
public void draw(){
decoratedShape.draw();
}
}
步骤 4、创建扩展了 ShapeDecorator 类的实体装饰类。
RedShapeDecorator.java
public class RedShapeDecorator extends ShapeDecorator {
public RedShapeDecorator(Shape decoratedShape) {
super(decoratedShape);
}
@Override
public void draw() {
decoratedShape.draw();
setRedBorder(decoratedShape);
}
private void setRedBorder(Shape decoratedShape){
System.out.println("Border Color: Red");
}
}
步骤 5、使用 RedShapeDecorator 来装饰 Shape 对象。
DecoratorPatternDemo.java
public class DecoratorPatternDemo {
public static void main(String[] args) {
Shape circle = new Circle();
ShapeDecorator redCircle = new RedShapeDecorator(new Circle());
ShapeDecorator redRectangle = new RedShapeDecorator(new Rectangle());
//Shape redCircle = new RedShapeDecorator(new Circle());
//Shape redRectangle = new RedShapeDecorator(new Rectangle());
System.out.println("Circle with normal border");
circle.draw();
System.out.println("\nCircle of red border");
redCircle.draw();
System.out.println("\nRectangle of red border");
redRectangle.draw();
}
}
步骤 6、执行程序,输出结果:
Circle with normal border
Shape: Circle
Circle of red border
Shape: Circle
Border Color: Red
Rectangle of red border
Shape: Rectangle
Border Color: Red
七、模板模式
- 设计模式
- 一、单例模式
- 1、饿汉式,线程安全
- 2、懒汉式,线程不安全
- 3、懒汉式,线程安全
- 双重检查锁(DCL, 即 double-checked locking**)**
- 静态内部类
- 二、⼯⼚模式
- 1、简单⼯⼚模式
- 2、⼯⼚⽅法模式
- 3、抽象⼯⼚模式
- 4、简单工厂模式和抽象工厂模式有什么区别?
- 三、适配器模式
- 步骤一、为媒体播放器和更高级的媒体播放器创建接口。
- 步骤 2、创建实现了 AdvancedMediaPlayer 接口的实体类。
- 步骤 3、创建实现了 MediaPlayer 接口的适配器类。
- 步骤 4、创建实现了 MediaPlayer 接口的实体类。
- 步骤 5、使用 AudioPlayer 来播放不同类型的音频格式。
- 步骤 6、执行程序,输出结果:
- 适配器模式的优缺点
- 四、代理模式(proxy pattern)
- 静态代理和动态代理的区别
- 五、观察者模式
- 观察者模式的优缺点
- 你的项⽬是怎么⽤的观察者模式?
- 步骤 1、创建 Subject 类。
- 步骤 2、创建 Observer 类。
- 步骤 3、创建实体观察者类。
- 步骤 4、使用 Subject 和实体观察者对象。
- 步骤 5、执行程序,输出结果:
- 六、装饰器模式
- 实现
- 步骤 1、创建一个接口:
- 步骤 2、创建实现接口的实体类。
- 步骤 3、创建实现了 Shape 接口的抽象装饰类。
- 步骤 4、创建扩展了 ShapeDecorator 类的实体装饰类。
- 步骤 5、使用 RedShapeDecorator 来装饰 Shape 对象。
- 步骤 6、执行程序,输出结果:
- 七、模板模式