快速了解Java设计模式

背景

Java二十三种设计模式的简单介绍,目的是能看懂别人写的设计模式代码,且能应用常用的设计模式。

设计模式分类

创建型模式

工厂方法(Factory)

工厂模式分为三种:简单工厂(不属于23种 设计模式,是工厂方法的特例),工厂方法,抽象工厂。
工厂模式的相关概念:

  1. 产品:类的实例
  2. 抽象产品:抽象类或接口
  3. 产品簇:多个有内在联系或者有逻辑关系的产品。比如肯德基套餐:薯条+汉堡+可乐
  4. 产品等级:相当于产品类型
    工厂模式的思想是面向接口编程。
    简单工厂是工厂方法的一个特例,先来看看简单工厂。
    工厂是一个类,工厂生产产品。

每 new 一个对象,相当于调用者多知道了一个类,增加了类与类之间的联系,不利于程序的松耦合。其实构建过程可以被封装起来,工厂模式便是用于封装对象的设计模式。
水果工厂:

public class FruitFactory {
     public Fruit create(String type) {
         switch (type){
         	case "Apple": return new Apple();
            case "Pear": return new Pear();
            default: throw new IllegalArgumentException("invalid fruit type");
     }
}

事实上,将构建过程封装的好处不仅可以降低耦合,如果某个产品构造方法相当复杂,使用工厂模式可以大大减少代码重复,这对调用者是很方便的,因为它不用关心复杂的对象构建过程。
优点:封装了对象构建过程,让客户端调用变得方便,让程序解耦。
缺点:

  1. 客户端需要记住产品的映射关系
  2. 如果具体产品多,则简单工厂变得十分臃肿
  3. 客户端需要扩展具体产品的时候,势必要修改简单工厂中的代码,这样违反了开闭原则
    工厂方法模式来解决简单工单的弊端,它规定每个产品都有一个专属工厂。

工厂方法模式定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个。工厂方法让类把实例化推迟到子类。
工厂方法模式(Factory Method pattern)是最典型的模板方法模式(Template Method pattern)应用

工厂方法有四个角色:抽象工厂,具体工厂;抽象产品,具体产品

// 工厂接口
public interface IFruitFactory {
    Fruit create();
}

// 苹果工厂
public class AppleFactory implements IFruitFactory {
	public Fruit create() {
		return new Apple();
    }
}

// 梨子工厂
public class PearFactory implements IFruitFactory {
    public Fruit create() {
        return new Pear();
	}
}

工厂将构建过程封装起来,调用者可以很方便的直接使用,同时他解决了简单工厂的两个弊端:

  1. 产品种类多时,工厂类不会臃肿。工厂方法符合单一职责原则。
  2. 需要新的产品时,不用改原来的工厂类,只需要添加新的工厂类。符合开闭原则。

与此同时,还是那个的缺点:产品等级增多时,工厂类的数量就会增多。
工厂方法UML:
在这里插入图片描述

抽象工厂(Factory)

在工厂里生产多个产品等级。

一个工厂接口,生产不同的产品。

抽象工厂模式提供一个接口,用于创建相关或依赖对象的家族,而不需要明确指定具体类。

加入现在有个商铺只卖两种水果,苹果和梨子,生产苹果和梨子的工厂也有很多。它选择的是工厂A。

public interface IFruitFactory {
    Fruit createApple();
    Fruit createPear();
}

public class AFactory implements IFruitFactory {
    @Override
    public Fruit createApple(){
        return new Apple("A");
    }
    
    @Override
    public Fruit createPear(){
        return new Pear("A");
    }
}

那么对商铺来说,卖来自A工厂的水果就是:

public class Client {
    public void main(String[] args) {
        IFruitFactory fruitFactory = new AFactory();
        Fruit apple = fruitFactory.createApple();
        Fruit pear = fruitFactory.createPear();
        
        sell(apple, pear);
	}
}

如果哪天,A工厂的水果质量不好了,商铺想换成B工厂的水果,就是这样:

public class BFactory implements IFruitFactory {
    @Override
    public Fruit createApple(){
        return new Apple("B");
    }
    
    @Override
    public Fruit createPear(){
        return new Pear("B");
    }
}

那么对于客户端而言,改动很小,只用换一个具体的工厂就可以了:

public class Client {
    public void main(String[] args) {
        IFruitFactory fruitFactory = new BFactory();
        Fruit apple = fruitFactory.createApple();
        Fruit pear = fruitFactory.createPear();
        
        sell(apple, pear);
	}
}

由于客户端只和IFruitFactory 打交道,调用的是接口中的方法,使用时根本不需要知道是在哪个具体工厂中实现的这些方法,这就使得替换工厂变得非常容易。
抽象工厂模式主要用于替换一系列方法。也就是说,客户端会使用这个接口工厂中的一系列接口,当服务端换掉这些接口的具体实现时,客户端毫不知情。
优点:1.仍然有简单工厂和工厂方法的优点;2.减少了工厂类的数量。
缺点:当产品等级(产品的种类)增多时,就要修改抽象工厂的代码,这会违反开闭原则;
因此,当产品等级比较固定时,可以考虑抽象工厂,否则不建议使用。

原型模式(Prototype)

原型模式是一种创建型模式,它允许一个对象再创建另外一个可定制的对象,根本无需知道创建的细节。当直接创建对象的代价比较大时,则采用这种模式。在Java中Prototype模式变成clone()方法的使用,由于Java的面向对象特性,使得在Java中使用原型模式变得很自然,两者已经几乎是浑然一体了。这反映在很多模式上,如Iterator遍历模式。
实现方法:

  1. 必须让目标类实现Cloneable接口
  2. 必须重写Object的clone方法,并且要把重写后的方法的访问控制修饰符改为public
public class MilkTea implements Cloneable {
    public String type;
    public boolean ice;
    
    // 这种方式是浅拷贝
    public MilkTea clone() throws CloneNotSupportedException {
        return (MilkTea) super.clone();
    }
}

这种模式要分清对象的浅拷贝与深拷贝,这是另一个知识点了。

建造者模式(Builder)

建造者模式是将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。说白了就是,将一个相似的构建对象的过程抽象出来,达到代码复用和可扩展的功能。

在这种设计模式中,有以下几个角色:

  1. builder,为创建一个产品对象的各个部件指定接口(指定建造标准,稳定建造流程)
  2. ConcreteBuilder:实现builder接口的具体建造者
  3. Director:构造一个使用builder接口的对象。它就是指导者,由他封装建造的过程。
  4. Product:具体的产品,被构建的对象。
    扩展:建造者模式在使用过程中可以演化出多种形式
    省略抽象建造者角色:如果系统中只需要一个具体的建造者的话,可以省略掉抽象建造者。
    省略指导者角色:在具体建造者只有一个的情况下,如果抽象建造者角色已经被省略掉,那么还可以省略掉指导者角色,让Builder自己扮演指导者和建造者双重角色。
    在这里插入图片描述
    建造者与工厂模式的区别在于,工厂模式只需要新建出 一个产品即可,即new出一个对象;而建造者模式更注重产品新建出来后,为产品属性赋值的过程,它强调的是构建过程。

单例模式(Singleton)

单例模式确保一个类只有一个实例,并提供一个全局访问点。
饿汉式

变量在申明时即被初始化。

public class Singleton {
	private static Singleton instance = new Singleton();
    
	public static Singleton getInstance() {
    	return instance;
	}
    
    private Singleton() { }
}   

优点:线程安全;直观
缺点:增加类初始化时间,类即使不使用,也会被实例化
懒汉式
先声明一个空变量,需要用时才初始化
方式一:双检锁方式实现的线程安全的单例模式,别漏掉volatile

public class Singleton {
	private static volatile Singleton instance = null;
	
    public static Singleton getInstance() {
    	if (instance == null) {
     		synchronized (Singleton.class) {
     			if (instance == null) {
     				instance = new Singleton();
     			}
    		}
    	}
     	return instance;
	}
    
    private Singleton() { }
}

方式二:静态内部类方式保证懒汉式单例的线程安全

public class Singleton {

    public static Singleton getInstance() {
        return Singleton.SingletonHolder.instance;
    }
    
    private static class SingletonHolder {
        public static Singleton instance = new Singleton();
    }
    
    private Singleton() {}
}

为什么这种方式能保证线程安全?来分析两个问题:

  • 静态内部类方式是怎么实现懒加载的
  • 静态内部类方式是怎么保证线程安全的

类在初始化的时候,会立即加载内部类,内部类会在使用时才加载。所以当此 Singleton 类加载时,SingletonHolder 并不会被立即加载,所以不会像饿汉式那样占用内存。另外,Java 虚拟机规定,当访问一个类的静态字段时,如果该类尚未初始化,则立即初始化此类。当调用Singleton 的 getInstance 方法时,由于其使用了 SingletonHolder 的静态变量instance,所以这时才会去初始化 SingletonHolder,在 SingletonHolder 中 new 出 Singleton 对象。这就实现了懒加载。其次,虚拟机在加载类的 clinit 方法时,会保证 clinit 在多线程中被正确的加锁、同步,即使有多个线程同时去初始化一个类,一次也只有一个线程可以执行 clinit 方法,其他线程都需要阻塞等待,从而保证了线程安全。
如何权衡两种方式?一般的建议是:对于构建不复杂,加载完成后会立即使用的单例对象,推荐使用饿汉式。对于构建过程耗时较长,并不是所有使用此类都会用到的单例对象,推荐使用懒汉式。

结构型模式

装饰者模式(Decorator)

装饰模式是在不必改变原类文件和使用继承的情况下,动态地扩展一个对象的功能。它是通过创建一个包装对象,也就是装饰来包裹真实的对象。

装饰模式:动态地给一个对象增加一些额外的职责,就增加对象功能来说,装饰模式比生成子类实现更为灵活。

提到装饰,我们先来想一下生活中有哪些装饰:

  • 女生的首饰:戒指、耳环、项链等装饰品
  • 家居装饰品:粘钩、镜子等

可以看到,一种装饰是为了让原来的更美观,一种不仅是美观,还能有新的功能(粘钩可挂东西,镜子是用来照的)。因此可以总结出装饰模式的两种功能:

  • 增强原有的特性
  • 添加新的特性

并且,装饰者并不会改变原有物品本身,只是起到一个锦上添花的作用。装饰模式也一样:

  • 增强一个类的功能
  • 为一个类添加新的功能

且装饰类不会改变原有的类。
例子:
现在有一个类要求输出一个人的颜值。
新建一个颜值接口:

public interface IBeauty {
    int getBeautyValue();
}

新建一个具体实现:

public class me implements IBeauty {
    @Override
    public int getBeautyValue() {
        return 100;
    }
}

但是一个人颜值在不同的情况下是不同的,当我们带上装饰品后,颜值会提高,且装饰品可以不断组合。例如:
我带上了戒指,很帅气。新建一个戒指装饰类

public class RingDecorator implements IBeauty {
	private final IBeauty me;

    public RingDecorator(IBeauty me) {
    	this.me = me;
    }

    @Override
    public int getBeautyValue() {
    	return me.getBeautyValue() + 20;
    }
}

测试一把:

public class Client {
   @Test
   public void show() {
       IBeauty me = new Me();
       System.out.println("我原本的颜值:" + me.getBeautyValue());

       IBeauty meWithRing = new RingDecorator(me);
       System.out.println("我带上戒指后的颜值:" + meWithRing.getBeautyValue());
   }
}

输出结果:

我原本的颜值:100
我带上戒指后的颜值:120

这就是最简单的增强功能的装饰模式。以后我们可以添加更多的装饰类,比如:
耳环装饰类:

public class EarringDecorator implements IBeauty { 
	private final IBeauty me;
    
    public EarringDecorator(IBeauty me) {
        this.me = me;
    }
    
    @Override
    public int getBeautyValue() {
        return me.getBeautyValue() + 50;
    }
}

还可以有更多的装饰类…
测试:

public class Client {
    @Test
    public void show() {
        IBeauty me = new Me();
        System.out.println("我原本的颜值:" + me.getBeautyValue());
        
        // 多次装饰、随意组合
        IBeauty meWithEarring = new EarringDecorator(new RingDecorator(me));
		System.out.println("我带上戒指、耳环后的颜值:" + meWithEarring.getBeautyValue());
    }
}

输出:

我原本的颜值:100
我带上戒指、耳环后的颜值:170

这里的装饰器也实现了IBeaty接口,并且没有添加新的方法,也就是或装饰器仅用于增强功能,没有改变Me原有的功能,这种装饰模式叫透明装饰模式,所以这种透明装饰模式可以无限装饰。

装饰模式是继承的一种替代方案。本例中如果不适用装饰模式,那么每一种装饰品都要新增一个子类,每一个装饰品之间组合又得新增一个子类,当组合增多时就会造成类爆炸。所以说这种场景下装饰模式更灵活。
下面看看新增功能的的场景:
我们用程序来模拟一下房屋装饰粘钩后,新增了挂东西功能的过程:
新建房屋接口和房屋类:

public interface IHouse {
    void live();
}

public class House implements IHouse {
    @Override
    public void live() {
         System.out.println("房屋原有的功能:居住功能");
    }
}

新建粘钩装饰器接口,继承自房屋接口:

public interface IStickyHookHouse extends IHouse {
    void hangThings();
}

粘钩装饰类:

public class StickyHookDecorator implements IStickyHookHouse {
    private final IHouse house;
    
    public StickyHookDecorator(IHouse house) {
        this.house = house;
    }
    
    @Override
    public void live() {
        house.live();
    }
    
    @Override
    public void hangThings() {
    	System.out.println("有了粘钩后,新增了挂东西功能");
    }
}

这里为什么要搞一个装饰器接口,原因是基础组件是IHouse,是一个接口,基础实现是House。装饰器是对基础组件的继承或实现,如果基础组件是一个抽象类,可以不要这个接口,直接继承这个类,本例中因为装饰器类不能直接继承IHouse,所以需要一个接口,从抽象的角度来说,是一样的,我们只关注抽象。

注意:继承抽象是为了有正确的类型,而不是继承它的行为,行为来自装饰者和基础组件,或与其他装饰者之间的组合关系。

客户端测试:

public class Client {
    @Test
    public void show() {
        IHouse house = new House();
        house.live();
        
        IHouse house = new stickyHookHouse();
        stickyHookHouse.live();
        stickyHookHouse.hangThings();
	}
}

结果:

房屋原有的功能:居住功能
房屋原有的功能:居住功能
有了粘钩后,新增了挂东西功能

这就是用于 新增功能 的装饰模式。我们并没有修改原有的功能,只是扩展了新的功能,这种模式在装饰模式中称之为 半透明装饰模式。半透明装饰模式具有不能多次装饰的特点。因为新的功能来自装饰器本身,而装饰器之间是不能有继承等依赖关系的,那样就不是装饰模式了。

UML

在这里插入图片描述

装饰模式中,面对抽象组件,有一个基本具体实现,当需要组合其他功能时,可以构建一个装饰器,且装饰器是要继承和关联抽象组件的。继承是为了跟抽象组件类型一致,这是装饰的核心,关联是为了使用基础组件的功能。

I/O流中的装饰模式

在这里插入图片描述

其中,InputStream 是一个抽象类,对应上文例子中的 IHouse,其中最重要的方法是 read 方法,这是一个抽象方法。

左边的三个类 FileInputStream、ByteArrayInputStream、ServletInputStreamInputStream 的子类,对应上文例子中实现了 IHouse 接口的 House。右下角的三个类 BufferedInputStream、DataInputStream、CheckedInputStream是三个具体的装饰者类,他们都为 InputStream 增强了原有功能或添加了新功能。

FilterInputStream 是所有装饰类的父类,它没有实现具体的功能,仅用来包装了一下InputStream

在源码中我们发现,BufferedInputStream 没有添加 InputStream 中没有的方法,所以BufferedInputStream 使用的是 透明的装饰模式DataInputStream 用于更加方便地读取 int、double 等内容,观察 DataInputStream 的源码可以发现,DataInputStream 中新增了 readIntreadLong 等方法,所以 DataInputStream 使用的是 半透明装饰模式

这就是装饰模式,注意不要和适配器模式混淆了。两者在使用时都是包装一个类,但两者的区别其实也很明显:

  • 纯粹的适配器模式 仅用于改变接口,不改变其功能,部分情况下我们需要改变一点功能以适配新接口。但使用适配器模式时,接口一定会有一个 回炉重造 的过程
  • 装饰模式 不改变原有的接口,仅用于增强原有功能或添加新功能,强调的是 锦上添花

装饰模式的优缺点:

优点: 1、装饰者是继承的有力补充,比继承灵活,不改变原有对象的情况下动态地给一个对象 扩展功能,即插即用。 2、通过使用不同装饰类以及这些装饰类的排列组合,可以实现不同效果。 3、装饰者完全遵守开闭原则。

缺点: 1、会出现更多的代码,更多的类,增加程序复杂性。 2、动态装饰时,多层装饰时会更复杂。

适配器模式(Adapter)

将一个原有的类的接口转换成期望的另一个接口,使原本的接口不兼容的类可以一起工作,这个转换过程就是适配,这个中间件就称之为适配器,属于结构型设计模式。适配器模式是作为两个不兼容的接口之间的桥梁,它结合了两个独立接口的功能。

那什么情况下可以使用适配器模式呢?

比如,现在系统已经有一个处理接口Processor,对一个字符串进行处理,Processor类有两个方法,默认方法name()返回当前类名,process()方法接受参数,返回处理后的对象。

interface Processor {
    default String name() {
        return getClass().getSimpleName();
    }

    Object process(Object input);
}

针对不同的处理情况,process方法有不同的实现,那么伴随Processor多个实现类。

class Upcase implements Processor {
    @Override 
    public String process(Object input) {
        return ((String) input).toUpperCase();
    }
}

class Downcase implements Processor {
    @Override
    public String process(Object input) {
        return ((String) input).toLowerCase();
    }
}

这里有三个处理类,分别是:字符串转大写、字符串转小写。

那么实际应用的时候,就是这样:

public class Applicator {
    public static void apply(Processor p, Object s) {
        System.out.println("Using Processor " + p.name());
        System.out.println(p.process(s));
    }

    public static void main(String[] args) {
        String s = "We are such stuff as dreams are made on";
        apply(new Upcase(), s);
        apply(new Downcase(), s);
    }
}

输出:

Using Processor Upcase
WE ARE SUCH STUFF AS DREAMS ARE MADE ON
Using Processor Downcase
we are such stuff as dreams are made on

现在,假如我们偶发现,系统中别人写的类(或者要用的类库)有跟Applicator类的apply方法相似的套路,比如现在有一个描述波段的类Waveform,以及过滤波段的过滤器类Filter

public class Waveform {
    private static long counter;
    private final long id = counter++;

    @Override
    public String toString() {
        return "Waveform " + id;
    }
}

public class Filter {
    public String name() {
        return getClass().getSimpleName();
    }

    public Waveform process(Waveform input) {
        return input;
    }
}

过滤器也是为了处理波段,它有两个方法:返回类名name()跟处理波段process()。针对不同的过滤波段的方式,伴随着不同的Filter的子类:

public class LowPass extends Filter {
    double cutoff;

    public LowPass(double cutoff) {
        this.cutoff = cutoff;
    }

    @Override
    public Waveform process(Waveform input) {
        return input; // Dummy processing 哑处理
    }
}

public class HighPass extends Filter {
    double cutoff;

    public HighPass(double cutoff) {
        this.cutoff = cutoff;
    }

    @Override
    public Waveform process(Waveform input) {
        return input;
    }
}

为了简写,子类过滤器的process()方法都做了虚假实现,实际情况是会有不同的实现的。

可以看到这个处理波段的过滤器类跟前面的Processor类很相似,处理套路几乎一样。我们想要复用Applicator的apply方法!

这个时候,我们要做的就是适配apply方法的Processor类型的参数。那如何让Filter变成Processor类型的呢?实现Processor接口时肯定不行的,这违背开闭原则,而且如果是第三方库的话也不可能修改。

这个时候,我们申明一个适配器来适配Processor接口

class FilterAdapter implements Processor {
    Filter filter;

    public FilterAdapter(Filter filter) {
        this.filter = filter;
    }

    @Override
    public String name() {
        return filter.name();
    }

    @Override
    public Waveform process(Object input) {
        return filter.process((Waveform) input);
    }
}

public class FilterProcessor {
    public static void main(String[] args) {
        Waveform w = new Waveform();
        Applicator.apply(new FilterAdapter(new LowPass(1.0)), w);
        Applicator.apply(new FilterAdapter(new HighPass(2.0)), w);
    }
}

这就是适配器模式的应用。再摘一个简单例子:

鸭子与野鸡的故事

鸭子可以飞的近,可以“呱呱叫”;野鸡飞的远,可以“咯咯叫”;

public interface Duck {
    quack();
    fly(); 
}

public class FamilyDuck implements Duck {
    @override
    public void quack() {
        System.out.Println("家鸭呱呱叫");
    }
    
    @override
    public void fly() {
        System.out.Println("家鸭飞了一小段");
    }
}
public interface WildChicken {
    gobble();
    fly(); 
}

public class WildChicken implements WildChicken {
    @override
    public void gobble() {
        System.out.Println("野鸡咯咯叫");
    }
    
    @override
    public void fly() {
        System.out.Println("野鸡飞了好远");
    }
}

假设你现在缺少鸭子对象,需要野鸡冒充,那就需要野鸡适配鸭子。

public class WildChickenAdapter implements Duck {
    private WildChicken wildChicken;
    
    public WildChickenAdapter(WildChicken wildChicken) {
        this.wildChicken = wildChicken;
    }
    
    @override
    public void quack() {
        System.out.Println(wildChicken.gobble());
    }
    
    @override
    public void fly() {
        System.out.Println(wildChicken.fly());
    }
}

这样,野鸡就适配鸭子了,需要鸭子的时候,用野鸡适配器就可以了。

总结就是:当你需要把一个对象转成另一种接口需要的类型的时候,你可以做一个对象适配器实现这个接口,这时候类型就匹配了,这个适配器有一个被适配者的引用,适配器里的行为,也就是接口的行为都委托给被适配者。

使用场景:有动机地修改一个正常运行的系统的接口,这时应该考虑使用适配器模式。

注意事项:适配器模式不是软件设计阶段考虑的设计模式,是随着软件维护,由于不同产品、不同厂家造成功能类似而接口不相同情况下的解决方案。

优点: 1、可以让任何两个没有关联的类一起运行。 2、提高了类的复用。 3、增加了类的透明度。 4、灵活性好。

缺点: 1、过多地使用适配器,会让系统非常零乱,不易整体进行把握。比如,明明看到调用的是 A 接口,其实内部被适配成了 B 接口的实现。

外观模式(Facade)

外观模式提供了一个统一的接口,用来访问子系统中的一群接口。外观定义了一个高层接口,让子系统更容易使用。外观模式又称为门面模式。

示意图:

在这里插入图片描述

外观模式非常简单,体现的就是 Java 中封装的思想。将多个子系统封装起来,提供一个更简洁的接口供外部调用。

举个简单例子:

每天上班都要做的事情有:

  1. 打开浏览器
  2. 打开IDE
  3. 打开微信

下班要做的额事情有:

  1. 关浏览器
  2. 关IDE
  3. 关微信

用程序模拟就是:

public class Browser {
    public static void open() {
        System.out.println("打开浏览器");
    }
    
    public static void close() {
        System.out.println("关闭浏览器");
    }
}

public class Ide {
    public static void open() {
        System.out.println("打开IDEA");
    }
    
    public static void close() {
        System.out.println("关闭IDEA");
    }
}

public class Wechat {
    public static void open() {
        System.out.println("打开微信");
    }
    
    public staticvoid close() {
        System.out.println("关闭微信");
    }
}

那咱们上下班就是这样

public class Client {
    @Test
	public void test() {
        System.out.println("上班:");
        Browser.open();
        Ide.open();
        Wechat.open();
        
        System.out.println("下班:");
        Browser.close();
        Ide.close();
        Wechat.close();
    }
}
上班:
打开浏览器
打开 IDEA
打开微信
下班:
关闭浏览器
关闭 IDEA
关闭微信

外观模式给我们提供了一个更合理的接口,使得一个复杂的子系统的使用变得简单。

public class WorkingFacade {
    public void startWork() {
        Browser.open();
        Ide.open();
        Wechat.open();
    }
    
    public void getOffWork() {
        Browser.close();
        Ide.close();
        Wechat.close();
    }
}

注意点:

  1. 虽说外观模式“封装”了子系统了的类,但也不是真正的封装,外观只是提供更直接的操作,并未将原来的子系统阻隔起来。如果需要子系统的更高层功能,还是可以使用原来的子系统的功能。这是外观模式的一个很好的特征:提供简化的接口的同时,依然将系统的完整功能暴露出来,以供需要的人使用。
  2. 子系统可以有多个外观。
  3. 设计模式之间的区别在于他们的设计意图,不要在形式上与其他模式混淆,例如适配器模式。

总结一下外观模式、适配器模式、装饰者模式之间的特点:

  • 当需要使用一个现有的类而其接口不符合你的需要时,就是用适配器;当需要简化并统一一个很大的接口或者一群复杂的接口时,使用外观
  • 适配器改变接口以符合客户的期望;外观将客户从一个复杂的子系统中解耦
  • 适配器将一个对象包装起来以改变其接口;装饰者将一个对象包装起来以增加新的行为和责任;而外观将一群对象“包装”起来以简化其接口

组合模式(Composite)

组合模式允许你将对象组合成树形结构来表现“整体/部分”的结构层次。组合可以让客户以一致的方式处理单个对象和组合对象。它创建了对象组的树形结构。

只有当应用的核心模型可以表示为树时,使用组合模式才有意义。

由组合模式定义的所有元素共享一个公共接口。使用此接口,客户端不必担心其使用的对象的具体类。

例子:

// TODO

UML:

在这里插入图片描述

优点:1、高层模块调用简单。 2、节点自由增加。

缺点:在使用组合模式时,其叶子和树枝的声明都是实现类,而不是接口,违反了依赖倒置原则。

使用场景:部分、整体场景,如树形菜单,文件、文件夹的管理。

注意事项:定义时为具体类。

桥接模式(Bridge)

桥接模式:将抽象部分与它的实现部分分离,使它们都可以独立地变化

实用场景:如果一个对象有两种或者多种分类方式,并且两种分类方式都容易变化。

比如说:现在有一个需求,绘制矩形、圆形、三角形这三种图案;每种颜色都有四种不同的颜色:红黄蓝绿。显然如果用排列组合来构建不同颜色的形状的话,那将产生类爆炸。

桥接模式的做法是:

public interface IShape {
    void draw();
}

public class Rectangle implements IShape {
    @override
    public void draw() {
        System.out.println("绘制矩形");
    }
}

public class Round implements IShape {
    @override
    public void draw() {
        System.out.println("绘制圆形");
    }
}

public class Triangle  implements IShape {
    @override
    public void draw() {
        System.out.println("绘制三角形");
    }
}
public interface IColor {
    void color();
}

public class Red implements IColor {
    @Override
    public String getColor() {
        return "红";
    }
}

public class Green implements IColor {
    @Override
    public String getColor() {
        return "绿";
    }
}

public class Blue implements IColor {
    @Override
    public String getColor() {
        return "蓝";
    }
}

在每个形状中桥接颜色引用:

public class Rectangle implements IShape {
    private IColor color;
    
    public void setColor(IColor color) {
        this.color = color;
    }
    
    @override
    public void draw() {
        System.out.println("绘制" + color.color() +"矩形");
    }
}
// ......

桥接模式的思想也是基于一个原则:组合优于继承原则。

设计模式不是代码的结构规范,而是设计思想。

享元模式(Cache、Flyweight)

享元模式体现的是程序可复用的特点,为了节约宝贵的内存,程序应该尽可能地复用。

适用场景:程序需要生成数量巨大的相似对象;这将耗尽目标设备的所有内存;对象间包含可共享的重复状态。

代理模式(Proxy)

给某一个对象提供一个代理,并由代理对象控制对原对象的引用。

1.静态代理

静态代理很简单,跟目标对象实现同一个接口就可以了。

比如现有个人类:

public interface IPerson {
    void eat();
}

public class Person implements IPersion {
    @override
    public void eat() {
        System.out.println("我在吃饭");
    }
}

代理类就是:

public class PersonProxy implements IPerson {
	private Person person;
    
    public PersonProxy(Person person) {
        this.person = person;
    }
    
    @override
    public void eat() {
        System.out.println("在吃饭前要先洗手");
        person.eat();
		System.out.println("吃完饭后要收拾");
    }
}

看到这个代码,我不禁想到他和装饰模式的代码结构一模一样。不过设计模式不能拘泥于代码的形式,要关注他的目的。装饰是为了增强或者增加目标功能,代理更多的是添加控制,当然也可以说是一种增强,;两者增强的目的不一样。

2.动态代理(jdk内置)

这是老生常谈的,实现InvocationHandler,使用JDK的Proxy类。

动态代理的优势就是比静态代理节省代码。

行为型模式

模板方法模式(Template)

模板方法模式定义了一个算法的步骤,并允许子类别为一个或多个步骤提供其实践方式。让子类别在不改变算法架构的情况下,重新定义算法中的某些步骤

例子:

public abstract class AbstractClass {

    public final void templateMethod(){
        // 调用基本方法,完成相关的逻辑
        this.doAnything();
        this.doSomething();
    }
    
    protected abstract void doAnything();
    protected abstract void doSomething();
}

一般情况下,模板方法被申明为final。子类去实现模板方法中某个具体步骤。特点是封装不变部分,扩展可变部分。

优点:1、提高代码的复用性。2、提高代码的扩展性。3、符合开闭原则。

缺点:1、导致类的数目增加。2、间接地增加了系统实现的复杂度。3、继承关系存在自身缺点,如果父类添加了新的抽象方法,所有子类都需要重新改一遍。

策略模式(Strategy)

策略模式指对象有某个行为,但是在不同的场景中,该行为有不同的实现算法,这组算法可以在运行时互相代替。

主要解决:在有多种算法相似的情况下,使用 if… else 所带来的复杂和难以维护。

使用场景:1、如果在一个系统里面有许多类,它们之间的区别仅在于它们的行为,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。2、一个系统需要动态地在几种算法中选择一种。 3、如果一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重的条件选择语句来实现。

注意事项:如果一个系统的策略多于四个,就需要考虑使用混合模式,解决策略类膨胀的问题。

UML:

在这里插入图片描述

举例:

加入现在模拟一下游戏角色捡到不同武器就有不同的攻击方式的场景:

// 角色有名称和打斗行为
// 打斗行为根据武器不同而改变
public class Role {
    private String name;
    private WeaponStrategy weapon;
    
    public Role(String name) {
        this.name = name;
    }
    
    public void fight() {
        weapon.attack();
    }
    
    public void setWeapon(WeaponStrategy weapon) {
        this.weapon = weapon;
    }
}

public interface WeaponStrategy {
    void attack();
}

public class Sword implements WeaponStrategy {
    @override
    public void attack() {
        System.out.println("用利剑刺");
    }
}

public class Axe implements WeaponStrategy {
    @override
    public void attack() {
        System.out.println("用斧头砍");
    }
}

public class Gun implements WeaponStrategy {
    @override
    public void attack() {
        System.out.println("用机枪射");
    }
}

测试一下:

public class Client {
    public static void main(String[] args) {
        Role role = new Role("Riven");
        
        // 捡到剑的事件
        role.setWeapon(new Sword());
        role.attack();
        
        // 捡到斧子的事件
        role.setWeapon(new Axe());
        role.attack();
       
        // 捡到机枪的事件
        role.setWeapon(new Gun());
        role.attack();
	}
}

这就是在运行时改变策略。虽然策略模式为了解决多重if else的难以维护,但是何时选择哪种策略模式依然需要判断,这是目前比较困惑的一点,先留着,后续解决吧…

观察者模式(Observer)

观察者模式定义了对象之间的一对多依赖,当一个对象改变状态时,它的所有依赖者都会受到通知并自动更新。

举一个气象站与展示牌之间的例子:气象站一有数据更新,不同的展示牌就要实时更新数据。展示牌需要订阅气象站才可以受到通知,当然,也可以取消订阅。他们之间有四个角色:Subject、Concrete Subject、Observer、Concrete Observer。

package com.learning.designpattern;

import java.util.*;

interface Subject {
    void register(Observer o);
    void remove(Observer o);
    void notifyObservers();
}

interface Observer {
    void update(DisplayData displayData);
}

interface DisplayElements {
    void display();
}

// 展示牌首先是观察者,其次他们有共同的功能:就是展示信息
class CurrentConditionDisplay implements Observer, DisplayElements  {
    DisplayData displayData;
    private WeatherData weatherData;

    public CurrentConditionDisplay(WeatherData weatherData) {
        this.weatherData = weatherData;
    }

    @Override
    public void update(DisplayData displayData) {
        this.displayData = displayData;
        display();
    }

    @Override
    public void display() {
        System.out.println("CurrentConditionDisplay:" + displayData.toString());
    }
}

class StatisticDisplay implements DisplayElements, Observer {
    private DisplayData displayData;
    private WeatherData weatherData;

    public StatisticDisplay(WeatherData weatherData) {
        this.weatherData = weatherData;
    }

    @Override
    public void update(DisplayData displayData) {
        this.displayData = displayData;
        display();
    }

    @Override
    public void display() {
        System.out.println("StatisticDisplay:" + displayData.toString());
    }
}

class DisplayData {
    private float temp;
    private float humidity;
    private float pressure;
    
    // constructor and setters and getters...
}

class WeatherData implements Subject {
    Set<Observer> observers = new HashSet<>();
    private DisplayData displayData;

    @Override
    public void register(Observer o) {
        observers.add(o);
    }

    @Override
    public void remove(Observer o) {
        observers.remove(o);
    }

    @Override
    public void notifyObservers() {
        observers.forEach(o -> o.update(displayData));
    }

    public void dataChanged() {
        notifyObservers();
    }

    public void setDisplayData(DisplayData displayData) {
        this.displayData = displayData;
    }

    public DisplayData getDisplayData() {
        return displayData;
    }
}

public class ObserverClientTest {
    public static void main(String[] args) {
        WeatherData weatherData = new WeatherData();
        weatherData.register(new CurrentConditionDisplay(weatherData));
        weatherData.register(new StatisticDisplay(weatherData));
        weatherData.setDisplayData(new DisplayData(21, 30, 40));
        
        weatherData.notifyObservers();
    }
}

UML:
在这里插入图片描述

JDK有内置的观察者模式,在java.util包下,有一个Observer接口,和一个Observable类,Observable是一个类,他就是主题,Observer就是接口。如果我们要复用JDK的观察者接口的话,我们具体主题类继承Observable就可以了。

java.util.Observable既然是一个类,那么也很明显,就是扩展性不够好。

可观察者和观察者之间用松耦合的方式结合,可观察者不知道观察者的细节,只知道观察者实现观察者接口。观察者应用广泛,例如监听器,MVC都是观察者模式的思想。

状态模式(State)

允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。

UML:
在这里插入图片描述

优点: 1、封装了转换规则。2、将所有与某个状态有关的行为放到一个类中,这让新增一个状态变得方便,只需要改变对象状态即可改变对象的行为,让维护变得方便。3、他是if else语句块的替代。

缺点: 1、必然会增加系统类和对象的个数

使用场景: 1、行为随状态改变而改变的场景。 2、条件、分支语句的代替者。

解释器模式(Interpreter)

给定一门语言,定义它的文法的一种表示,并定义一个解释器,该解释器使用该表示来解释语言中的句子

这个就不看了,设计语言用的,了解下概念,用不到。

迭代器模式(Iterator)

提供一种方法顺序的访问一个聚合对象中的各个元素,而不暴露其内部的表示。

简单点就是,不管啥集合,定义了一种遍历的标准,而不管集合内部如何实现这个标准的。

JDK已经帮我们实现了,不用自己写了,用就行了。

中介者模式(Mediator)

定义一个中介对象来封装一系列对象之间的交互,使原有对象之间的耦合松散,且可以独立地改变它们之间的交互

中介者模式是一种行为设计模式, 能让你减少对象之间混乱无序的依赖关系。 该模式会限制对象之间的直接交互, 迫使它们通过一个中介者对象进行合作。

中介者模式在 Java 代码中最常用于帮助程序 GUI 组件之间的通信。 在 MVC 模式中, 控制器是中介者的同义词。

使用场景:

  • 当一些对象和其他对象紧密耦合以致难以对其进行修改时
  • 当组件因过于依赖其他组件而无法在不同应用中复用时
  • 如果为了能在不同情景下复用一些基本行为,导致你需要被迫创建大量组件子类时

优点:单一职责;开闭原则,无需修改其他组件就就能加入新的组件;降低这组件之间的耦合;方便复用。

缺点:中介者类由于职责众多,可能变成一个超级类。

例子,模拟一下群聊中聊天室与用户,用户将要发送的消息交给聊天室显示出来。

public class ChatRoom {
    
   public void showMessage(User user, String message){
      System.out.println(new Date().toString()
         + " [" + user.getName() +"] : " + message);
   }
}

public class User {
   private String name;
   private ChatRoom chatRoom;
 
   public User(String name){
      this.name  = name;
   }
 
   public void setChatRoom(ChatRoom chatRoom) {
       this.chatRoom = chatRoom;
   }
    
   public void sendMessage(String message){
      chatRoom.showMessage(this, message);
   }
}

组件保存对于中介者对象的引用。

备忘录模式(Snapshot、Memento)

在不破坏封装的条件下(不暴露对象实现细节),通过备忘录对象存储另外一个对象内部状态的快照,在将来合适的时候把这个对象还原到存储起来的状态。

备忘录模式(Memento Pattern)又叫做快照模式(Snapshot Pattern)或Token模式,

优点:提供了一种可恢复的机制;实现了封装,

缺点:消耗资源,如果类的成员变量过多,保存势必消耗一定的内存。

总体而言,备忘录模式是利大于弊的。

备忘录模式使用三个类Originator (源对象)、Memento(备忘录对象)、和 CareTaker(管理者)。

Originator 提供一个备忘的方法,决定哪些属性是可以备忘的;CareTaker负责管理备忘录对象与源对象;

public class Originator {
   private String state;
 
   public void setState(String state){
      this.state = state;
   }
 
   public String getState(){
      return state;
   }
 
   public Memento saveStateToMemento(){
      return new Memento(state);
   }
 
   public void getStateFromMemento(Memento Memento){
      state = Memento.getState();
   }
}

public class Memento {
   private String state;
 
   public Memento(String state){
      this.state = state;
   }
 
   public String getState(){
      return state;
   }  
}

public class CareTaker {
   private List<Memento> mementoList = new ArrayList<Memento>();
 
   public void add(Memento state){
      mementoList.add(state);
   }
 
   public Memento get(int index){
      return mementoList.get(index);
   }
}

命令模式(Command)

命令模式将“请求”封装成对象,以便使用不同的请求、队列或者日志来参数化其他对象。命令模式也支持撤销的操作。

命令模式将发出请求的对象与接收请求的对象解耦。在被解耦的两者之间是通过命令对象进行沟通的。命令对象封装了接受者的一个或多个对象。

宏命令是以命令的一种简单延伸,允许调用多个命令。宏方法也支持撤销。实际操作时,也有可能命令对象直接实现了请求,而不是将工作委托给接受者。

角色:

  • ICommand:命令接口,一般会定义一个execute方法,如果需要也会有undo(撤销)方法。
  • ConcreteCommand:具体命令
  • Receiver:命令的接受者,就是执行者
  • Invoker:命令的调用者,命令入口

使用场景:认为是命令的地方都可以使用命令模式,比如: 1、GUI 中每一个按钮都是一条命令。 2、模拟 CMD。

模拟一下,遥控器按下开关按钮控制灯的开关。

灯(Receiver):

public class Light {
    
    public void on() {
        System.out.println("灯亮了");
    }
    
    public void off() {
        System.out.println("灯关了");
    }
}

开关灯命令(Command):

public interface ICommand {
	void execute();
}

public class LightOnCmd implements ICommand {
    private Light light;
    
	public LightOnCmd(Light light) {
        this.light = light;
    }
    
    @override
    public void execute() {
        light.on();
	}
}

public class LightOffCmd implements ICommand {
    private Light light;
    
	public LightOnCmd(Light light) {
        this.light = light;
    }
    
    @Override
    public void execute() {
        light.off();
	}
}

遥控器(Invoker):

public class RemoteControl {
    public ICommand cmd;
    
    public void setCmd(ICommand cmd) {
        this.cmd = cmd;
    }
    
    public void lightOnPressed() {
        System.out.println("按下了开灯按钮");
        cmd.execute();
	}
    
    public void lightOffPressed() {
        System.out.println("按下了关灯按钮");
        cmd.execute();
	}
}

客户端测试(Client):

public class Client {
    public static void main(String[] args) {
        RemoteControl remoteControl = new RemoteControl();
        Light light = new Light();

        remoteControl.setCmd(new LightOnCmd(light));
        remoteControl.lightOnPressed();

        remoteControl.setCmd(new LightOffCmd(light));
        remoteControl.lightOffPressed();
    }
}

输出:

按下了开灯按钮
灯亮了
按下了关灯按钮
灯关了

这一块,还是看下Head First那里的例子,讲的很好。

优点:解耦。符合开闭原则,很方便增加新的的命令。

缺点:可能会产生很多命令类。

责任链模式(Chain of Responsibility)

责任链模式(Chain of Responsibility)使多个对象都有机会处理请求,从而避免请求的发送者和接受者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有对象能够处理它。

你要去给某公司借款 1 万元,当你来到柜台的时候向柜员发起 “借款 1 万元” 的请求时,柜员认为金额太多,处理不了这样的请求,他转交这个请求给他的组长,组长也处理不了这样的请求,那么他接着向经理转交这样的请求。

简单java代码:

public void test(Request request) {
    int money = request.getRequestMoney();
    if(money <= 1000) {
        Clerk.response(request);	
    } else if(money <= 5000) {
        Leader.response(request);
    } else if(money <= 10000) {
        Manager.response(request);
    }
}

代码的业务逻辑就是这样:根据的借款金额来判定谁来处理这个借款请求 (request)

  • 如果请求借款金额小于 1000 元,那么柜台职员就可以直接处理这个请求(比如签字)
  • 如果请求借款金额小于 5000 元但大于 1000 元,那么职员处理不了,该请求转交给组长,组长能够处理这样的请求(比如签字)
  • 如果请求借款金额大于 5000 元但小于 10000 元,那么职员和组长都处理不了(没有权限),那么这个请求就会转交给经理,经理能够处理这样的请求(比如签字)

但是这样的代码缺点很明显:

  1. 代码臃肿,不优雅
  2. 耦合度高,新的请求类必然伴随新的ifelse,违反开闭原则

责任链模式:

UML:

在这里插入图片描述

定义一个请求的等级:

public class Level {
    private int level = 0;
    public Level(int level) {
        this.level = level;
    }
    public int getLevel() {
        return level;
    }
    
    // 判断请求等级,等级高的可以处理请求,反之不行
    public boolean above(Level level) {
        if (this.level >= level.getLevel()) {
            return true;
        } else {
            return false;
        }
    }
}

请求与相应:

//请求
class Request {
    Level level;
    public Request(Level level) {
        System.out.println("开始请求...");
        this.level = level;
    }
    public Level getLevel() {
        return level;
    }
}

//响应
class Response {
    private String message;
    public Response(String message) {
        System.out.println("处理完请求");
        this.message = message;
    }
    public String getMessage() {
        return message;
    }
}

抽象处理类和具体处理类:

//抽象处理器
abstract class Handler {
    private Handler nextHandler;
    
    public void setNextHandler(Handler handler) {
        nextHandler = handler;
    }
    
    public final Response handlerRequest(Request request) {
        Response response = null;
        if (this.getHandlerLevel().above(request.getLevel())) {
            response = this.response(request);
        } else {
            if (nextHandler != null) {
                response = this.nextHandler.handlerRequest(request);
            } else {
                System.out.println("没有合适的处理器处理该请求...");
            }
        }
        
        return response;
    }
    
    protected abstract Level getHandlerLevel();
    
    public abstract Response response(Request request); 
}
//具体的处理器 1
class ConcreteHandler1 extends Handler {
    protected Level getHandlerLevel() {
        return new Level(1);
    }
    public Response response(Request request) {
        System.out.println("该请求由 ConcreteHandler1 处理");
        return new Response("响应结果 1");
    }
}

//具体的处理器 2
class ConcreteHandler2 extends Handler {
    protected Level getHandlerLevel() {
        return new Level(2);
    }
    public Response response(Request request) {
        System.out.println("该请求由 ConcreteHandler2 处理");
        return new Response("响应结果 2");
    }
}

//具体的处理器 3
class ConcreteHandler3 extends Handler {
    protected Level getHandlerLevel() {
        return new Level(3);
    }
    public Response response(Request request) {
        System.out.println("该请求由 ConcreteHandler3 处理");
        return new Response("响应结果 3");
    }
}

客户端测试:

public class Client {
    public static void main(String[] args) {
        Handler ch1 = new ConcreteHandler1();
        Handler ch2 = new ConcreteHandler2();
        Handler ch3 = new ConcreteHandler3();

        ch1.setNextHandler(ch2);
        ch2.setNextHandler(ch3);

        Response res1 = ch1.handlerRequest(new Request(new Level(2)));
        if (res1 != null) {
            System.out.println(res1.getMessage());
		}
        Response res2 = ch1.handlerRequest(new Request(new Level(4)));
        if (res2 != null) {
            System.out.println(res2.getMessage());
        }
    } 
}

优点:与 if…else 相比,他的耦合性要低一些,因为它将条件判定分散到各个处理类中,并且这些处理类的优先处理顺序可以随意的设定,并且如果想要添加新的 handler 类也是十分简单的,这符合开放闭合原则

缺点:不能保证请求一定被接受;可能不容易观察运行时的特征,对调错不友好

注意事项:责任链模式带来了灵活性,但是在设置处理类前后关系时,一定要避免在链中出现循环引用的问题。

访问者模式(Visitor)

据说是最复杂的设计模式,后续补充。

java设计模式大体上分为三大类: 创建型模式(5种):工厂方法模式,抽象工厂模式,单例模式,建造者模式,原型模式。 结构型模式(7种):适配器模式,装饰器模式,代理模式,外观模式,桥接模式,组合模式,享元模式。 行为型模式(11种):策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。 设计模式遵循的原则有6个: 1、开闭原则(Open Close Principle)   对扩展开放,对修改关闭。 2、里氏代换原则(Liskov Substitution Principle)   只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。 3、依赖倒转原则(Dependence Inversion Principle)   这个是开闭原则的基础,对接口编程,依赖于抽象而不依赖于具体。 4、接口隔离原则(Interface Segregation Principle)   使用多个隔离的借口来降低耦合度。 5、迪米特法则(最少知道原则)(Demeter Principle)   一个实体应当尽量少的与其他实体之间发生相互作用,使得系统功能模块相对独立。 6、合成复用原则(Composite Reuse Principle)   原则是尽量使用合成/聚合的方式,而不是使用继承。继承实际上破坏了类的封装性,超类的方法可能会被子类修改。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值