超详细-七种常见结构型模式的描述总结与代码分析

Posted songjilong

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了超详细-七种常见结构型模式的描述总结与代码分析相关的知识,希望对你有一定的参考价值。

结构型模式(Structural Pattern)用于将类或对象结合在一起形成更强大的结构,就像搭积木,可以通过简单的积木组合出复杂、功能强大的模型。

结构型模式 重要程度
适配器模式(Adapter) ????
桥接模式(Bridge) ???
组合模式(Composite) ????
装饰者模式(Decorator) ???
外观模式(Facade) ?????
享元模式(Flyweight) ?
代理模式(Proxy) ????

一、适配器模式(Adapter)

生活中,充电插头有两脚的、三脚的,还有圆形的,如果想使这些插头都能工作,就需要一个多功能适配器

技术图片

基本介绍

适配器模式(Adapter Pattern)属于结构性模式,它可以将某个类的接口转换为客户端期望的另一个接口表示,主要目的是兼容性,让原本因接口不匹配不能一起工作的两个类可以协同工作,其别名为包装器(Wrapper)。适配器模式主要分为三类:类适配器模式对象适配器模式接口适配器模式

工作原理

技术图片

  1. 让原本接口不兼容的类可以兼容
  2. 从用户的角度看不到被适配者,是解耦的
  3. 用户调用适配器转化出来的目标接口方法,适配器去再调用被适配者的相关接口方法

类适配器模式

实现原理

Adapter 类继承 src 类,实现 dst 接口,完成 src 对 dst 的适配。

案例

插座(Voltage220V)的输出电压是220V,充电插头(Voltage5V)输出电压是5V,这时候就需要一个适配器(VoltageAdapter)转换电压,才能给手机(Phone)充电

代码实现

电源输出电压为220V

public class Voltage220V {
    public int output220V() {
        int src = 220;
        System.out.println("电源输出" + src + "V");
        return src;
    }
}

充电器输出电压为5V

public interface Voltage5V {
    int output5V();
}

适配器需要将220V转为5V

public class VoltageAdapter extends Voltage220V implements Voltage5V {
    @Override
    public int output5V() {
        int src = super.output220V();
        int dst = src / 44;
        System.out.println("转换为" + dst + "V");
        return dst;
    }
}

手机接收5V电压,判断电压是否为5V

public class Phone {
    public static void charging(Voltage5V voltage5V){
        int v = voltage5V.output5V();
        if(v == 5){
            System.out.println("接收电压为5V,正常充电");
        }else if(v > 5){
            System.out.println("电压高于5V,无法充电");
        }
    }
}

测试方法

@Test
public void test01(){
    System.out.println("====类适配器模式====");
    Phone.charging(new VoltageAdapter());
}

运行结果

====类适配器模式====
电源输出220V
转换为5V
接收电压为5V,正常充电

分析

  • 由于 Java 是单继承机制,所以类适配器模式有一定的局限性
  • src 类的方法再 Adapter 中都会暴露出来,增加了使用的成本
  • 由于继承了 src 类,所以它可以重写父类方法,使 Adapter 的灵活性增强了

对象适配器模式

实现原理

基本的思路和类的适配器模式相同,只是将 Adapter 类做修改,使用聚合关系替代继承关系

代码实现

沿用前面的代码,新建一个适配器,只是将原来的 Adapter 继承 src 类换为聚合的关系

public class VoltageAdapter2 implements Voltage5V {

    private Voltage220V voltage220V;

    public VoltageAdapter2(){
        this.voltage220V = new Voltage220V();
    }

    @Override
    public int output5V() {
        int src = this.voltage220V.output220V();
        int dst = src / 44;
        return dst;
    }
}

测试方法

@Test
public void test02(){
    System.out.println("====对象适配器模式====");
    Phone.charging(new VoltageAdapter2(new Voltage220V()));
}

运行结果

====对象适配器模式====
电源输出220V
转换为5V
接收电压为5V,正常充电

接口适配器模式

接口适配器模式也可称为缺省适配器模式,当不需要实现接口的全部方法时,可先设计一个抽象类实现接口,并为该接口的每个方法都提供一个默认实现,那么该抽象类的子类就可以有选择的覆盖父类的某些方法来实现需求。

适用于一个接口不想使用其所有的方法的情况

代码实现

写一个接口,里面定义一些方法

public interface InterfaceMethod {
    void m1();
    void m2();
    void m3();
    void m4();
}

一个抽象类,实现该接口

public abstract class AbstractAdapter implements InterfaceMethod {
    @Override
    public void m1() {
    }

    @Override
    public void m2() {
    }

    @Override
    public void m3() {
    }

    @Override
    public void m4() {
    }
}

测试方法

@Test
public void test(){
    //使用匿名内部类的方式
    AbstractAdapter adapter = new AbstractAdapter() {
        @Override
        public void m1() {
            System.out.println("我要用m1方法");
        }
    };
    adapter.m1();
}

运行结果

我要用m1方法

三种适配器模式总结

  • 三种命名方式是根据 src 是以怎样的形式给到 Adapter (在Adapter里的形式)来命名的。
    • 类适配器:以类给到,在 Adapter 里,就是将 src 当做类,继承

    • 对象适配器:以对象给到,在 Adapter 里, 将 src 作为一个对象,持有

    • 接口适配器:以接口给到,在 Adapter 里,将 src 作为一个接口,实现

  • Adapter模式最大的作用还是将原本不兼容的接口融合在一起工作。

二、桥接模式(Bridge)

基本介绍

  • 桥接模式是一种结构型设计模式。
  • 将实现与抽象放在两个不同的类层次中,使两个层次可以独立改变。
  • 基于类的最小设计原则,通过封装、聚合、继承等行为让不同的类承担不同的职责。
  • 它的主要特点是把抽象与行为实现分离,从而可以保持各部分的独立性以及应对它们的功能扩展。

模式结构

桥接模式包含如下角色:

  • Abstraction:抽象类
  • RefinedAbstraction:扩充抽象类
  • Implementor:实现类接口
  • ConcreteImplementor:具体实现类

技术图片

简单案例

我们以手机为例,手机有品牌(诺基亚、摩托罗拉)和样式(折叠式、直立式),我们需要生产不同的品牌和样式,比如折叠式诺基亚、直立式摩托罗拉... ...

「实现类接口」 - 手机品牌,都有开机和关机的功能

public interface PhoneBrand {
    void open();
    void close();
}

「具体实现类」 - 手机品牌 Nokia 和 Moto

public class Nokia implements PhoneBrand {
    @Override
    public void open() {
        System.out.println("诺基亚开机...");
    }

    @Override
    public void close() {
        System.out.println("诺基亚关机...");
    }
}
public class Moto implements PhoneBrand {
    @Override
    public void open() {
        System.out.println("摩托罗拉开机...");
    }

    @Override
    public void close() {
        System.out.println("摩托罗拉关机...");
    }
}

「抽象类」 - 手机类,以聚合的方式与品牌产生联系,充当着“桥”的角色

public abstract class AbsPhone{

    private PhoneBrand brand;

    public AbsPhone(PhoneBrand brand) {
        this.brand = brand;
    }

    protected void open(){
        brand.open();
    }

    protected void close(){
        brand.close();
    }
}

「扩充抽象类」 - 折叠式手机 和 直立式手机

public class FoldingPhone extends AbsPhone{

    public FoldingPhone(PhoneBrand brand) {
        super(brand);
    }

    @Override
    protected void open() {
        System.out.print("折叠式 - ");
        super.open();
    }

    @Override
    protected void close() {
        System.out.print("折叠式 - ");
        super.close();
    }
}
public class UpRightPhone extends AbsPhone{

    public UpRightPhone(PhoneBrand brand) {
        super(brand);
    }

    @Override
    protected void open() {
        System.out.print("直立式 - ");
        super.open();
    }

    @Override
    protected void close() {
        System.out.print("直立式 - ");
        super.close();
    }
}

测试

@Test
public void test(){
    AbsPhone p1 = new FoldingPhone(new Nokia());
    p1.open();
    p1.close();
    System.out.println();
    AbsPhone p2 = new UpRightPhone(new Moto());
    p2.open();
    p2.close();
}

结果

折叠式 - 诺基亚开机...
折叠式 - 诺基亚关机...

直立式 - 摩托罗拉开机...
直立式 - 摩托罗拉关机...

如果我们想创建其他类型的手机,只需要改变创建方式即可。

模式分析

  1. 实现了抽象和实现部分的分离,从而极大的提供了系统的灵活性,这有助于系统进行分层设计,从而产生更好的结构化系统。
  2. 对于系统的高层部分,只需要知道抽象部分和实现部分的接口就可以了,其它的部分由具体业务来完成。
  3. 桥接模式替代多层继承方案,可以减少子类的个数,降低系统的管理和维护成本。
  4. 桥接模式的引入增加了系统的理解和设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计和编程。
  5. 桥接模式要求正确识别出系统中两个独立变化的维度,因此其使用范围有一定的局限性,即需要有这样的应用场景。

桥接模式在 JDBC 中的应用

在 Java 中我们通常使用 JDBC 连接数据库,但是数据库的种类有很多(mysql、Oracle...),它们的连接方式、协议都不尽相同,很显然不能为每种数据库都写一个接口,这样就违背了精简设计原则,于是Java设计师就提供一套接口给厂商们自己实现,一套接口给用户调用。

我们在使用 JDBC 的时候需要写这样的代码

Class.forName("数据库驱动名");
Connection conn = DriverManager.getConnection("数据库url", "用户名", "密码");

其过程是这样的:

  • Class.forName() 的时候,通过反射机制,将 .class 文件加载进Java虚拟机内存中,Driver 类初始化,执行以下代码,向 DriverManager 中注册一个驱动。DriverManager是个 Driver 容器,管理不同的 Driver

    static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can‘t register driver!");
        }
    }
    
  • 我们获取连接时,DriverManager 就会根据驱动返回一个相应的数据库连接

    @CallerSensitive
    public static Connection getConnection(String url,
        java.util.Properties info) throws SQLException {
        return (getConnection(url, info, Reflection.getCallerClass()));
    }
    

技术图片

实际应用场景

对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。

  • 银行转账系统
    • 转账分类:网上转账,柜台转账,AMT 转账
    • 转账用户类型:普通用户,银卡用户,金卡用户...
  • 消息管理
    • 消息类型:即时消息,延时消息
    • 消息分类:手机短信,邮件消息,QQ 消息...

三、组合模式(Composite)

基本介绍

1、组合模式(Composite Pattern)又叫部分整体模式,他创建了对象组的树形结构,将对象组合成树状结构以表示「整体 - 部分」的层次关系。

2、组合模式使得用户对单个对象和组合对象的访问具有一致性,即:组合能让客户以一致的方式处理个别对象以及组合对象

模式结构

Component(抽象构件):定义参加组合对象的公有方法和属性,可以定义一些默认的行为和属性。

Composite(容器构件):树枝对象,它的作用是组合树枝结点和叶子结点形成一个树形结构。

Leaf(叶子构件):叶子构件的下面没有其他分支,也就是遍历的最小单位。


组合模式有两种实现:安全模式和透明模式,其结构如下图所示

  • 安全组合模式:在抽象构件角色中没有声明任何用于管理成员对象的方法,而是在容器构件 Composite 类中声明并实现这些方法。
  • 透明组合模式:抽象构建角色中声明了所有用于管理成员对象的方法,对其它构件公开透明。

技术图片

简单案例

要求:在页面展示出公司的部门组成(一个公司有多个部门,每个部门有多个小组);

这是一种很明显的树形结构,因此可以用组合模式解决

「抽象构件」:OrganizationComponent

public abstract class OrganizationComponent {
    private String name;

    public OrganizationComponent(String name) {
        this.name = name;
    }

    protected void add(OrganizationComponent component) {
        throw new UnsupportedOperationException("不支持添加操作");
    }

    protected void remove(OrganizationComponent component) {
        throw new UnsupportedOperationException("不支持删除操作");
    }

    protected abstract void print();


    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

「容器构件」:Company、Department

public class Company extends OrganizationComponent {
    private List<OrganizationComponent> components = new ArrayList<>();

    public Company(String name) {
        super(name);
    }

    @Override
    protected void add(OrganizationComponent component) {
        components.add(component);
    }

    @Override
    protected void remove(OrganizationComponent component) {
        components.remove(component);
    }

    @Override
    protected void print() {
        System.out.println("======="+getName()+"=======");
        for (OrganizationComponent component : components) {
            component.print();
        }
    }

    @Override
    public String getName() {
        return super.getName();
    }
}
public class Department extends OrganizationComponent {
    private List<OrganizationComponent> components = new ArrayList<>();

    public Department(String name) {
        super(name);
    }

    @Override
    protected void add(OrganizationComponent component) {
        components.add(component);
    }

    @Override
    protected void remove(OrganizationComponent component) {
        components.remove(component);
    }

    @Override
    protected void print() {
        System.out.println("======="+getName()+"=======");
        for (OrganizationComponent component : components) {
            component.print();
        }
    }

    @Override
    public String getName() {
        return super.getName();
    }
}

「叶子构件」:Group,叶子构件不没有子节点了,所以不需要添加、删除之类的方法

public class Group extends OrganizationComponent {
    public Group(String name) {
        super(name);
    }

    @Override
    protected void print() {
        System.out.println(getName());
    }

    @Override
    public String getName() {
        return super.getName();
    }
}

「测试类」:Client

public class Client {
    @Test
    public void test01(){
        OrganizationComponent company = new Company("阿里巴巴");

        OrganizationComponent department1 = new Department("市场部");
        OrganizationComponent department2 = new Department("技术部");

        OrganizationComponent group1 = new Group("市场一组");
        OrganizationComponent group2 = new Group("市场二组");
        OrganizationComponent group3 = new Group("技术一组");
        OrganizationComponent group4 = new Group("技术二组");

        //添加部门
        company.add(department1);
        company.add(department2);
        //添加小组
        department1.add(group1);
        department1.add(group2);
        department2.add(group3);
        department2.add(group4);

        //打印结果
        company.print();
    }
}

「运行结果」

=======阿里巴巴=======
=======市场部=======
市场一组
市场二组
=======技术部=======
技术一组
技术二组

在 HashMap 中的应用

在 Java(jdk 1.8为例) 的集合类 HashMap 中,抽象构件是 Map,容器构件是 HashMap,叶子构件是 Node

进入源码可以看见,在 Map 中定义了许多公共方法

技术图片

HashMap 实现了 Map,并对一些方法重写,而且 HashMap 中有一个静态内部类 Node,它就充当了叶子构件的角色,Node 中去除了 put、putAll 等方法,下面也没有子结点了

使用:

@Test
public void test02(){
    Map<String, String> map = new HashMap<>();
    map.put("k1", "v1");
    map.put("k2", "v2");
    System.out.println(map);
}

当我们 put 一个键值对的时候,在 HashMap 内部会调用 putVal 方法,将键值对封装为 Node。

总结

1、简化客户端操作。客户端只需要面对一致的对象而不用考虑整体部分或者节点叶子的问题。

2、具有较强的扩展性。当我们要更改组合对象时,我们只需要调整内部的层次关系,客户端不用做出任何改动。

3、方便创建出复杂的层次结构。客户端不用理会组合里面的组成细节,容易添加节点或者叶子从而创建出复杂的树形结构。

4、需要遍历组织机构,或者处理的对象具有树形结构时,非常适合使用组合模式。

5、要求较高的抽象性。如果节点和叶子有很多差异性的话,比如很多方法和属性都不一样,不适合使用组合模式。

四、装饰者模式(Decorator)

基本

装饰者模式属于结构型模式,它可以动态的将新功能附加到对象上,同时又不改变其结构。在对象功能扩展方面,它比继承更有弹性,装饰者模式也体现了开闭原则(OCP)。

模式结构

装饰者和被装饰者有相同的超类型,因为装饰者和被装饰者必须是一样的类型,利用继承是为了达到类型的匹配,而不是利用继承获取行为

技术图片

  • Component:装饰者和被装饰者共同的父类,是一个接口或者抽象类,用来定义基本行为
  • ConcreteComponent:定义具体对象,即被装饰者
  • Decorator:抽象装饰者,继承自 Component,从外类来扩展 ConcreteComponent。对于 ConcreteComponent来说,不需要知道Decorator的存在,Decorator 是一个接口或抽象类
  • ConcreteDecorator:具体装饰者,用于扩展 ConcreteComponent

举例说明

在咖啡店客人想点一杯加两份糖一份牛奶的摩卡咖啡,各个商品的价格如下,我们需要根据用户点的咖啡、加的配料,动态的计算价格

商品 价格
拿铁咖啡(LatteCoffee) 4.5
摩卡咖啡(MochaCoffe) 5.5
糖(Sugar) 1.0
牛奶(Milk) 2.0

「实体类」 Coffee

public abstract class Coffee{
    public String des = "咖啡"; //描述
    private float price = 0.0f; //价格

    protected abstract float cost(); //计算费用
    
    //省略getter setter方法
}

「被装饰者」LatteCoffee

public class LatteCoffee extends Coffee{
    public LatteCoffee() {
        setDes("拿铁咖啡");
        setPrice(4.5f);
    }

    @Override
    protected float cost() {
        return getPrice();
    }
}

「被装饰者」MochaCoffee

public class MochaCoffee extends Coffee {
    public MochaCoffee() {
        setDes("摩卡咖啡");
        setPrice(5.5f);
    }

    @Override
    protected float cost() {
        return getPrice();
    }
}

「抽象装饰者」Decorator

public class Decorator extends Coffee {

    private Coffee coffee;

    public Decorator(Coffee drink) {
        this.coffee = drink;
    }

    @Override
    protected float cost() {
        return getPrice() + coffee.cost();
    }

    @Override
    public String getDes() {
        return coffee.getDes() + "加" + super.getDes();
    }
}

「具体装饰者」SugarDecorator

public class SugarDecorator extends Decorator{
    public SugarDecorator(Coffee coffee) {
        super(coffee);
        setDes("糖");
        setPrice(1.0f);
    }
}

「具体装饰者」MilkDecorator

public class MilkDecorator extends Decorator{
    public MilkDecorator(Coffee coffee) {
        super(coffee);
        setDes("牛奶");
        setPrice(2.0f);
    }
}

「测试类」Client

public class Client {
    /**
     * 点一杯 加两份糖一份牛奶的摩卡咖啡
     */
    @Test
    public void test01() {
        Coffee order = new MochaCoffee();
        System.out.println(order.getDes() + ",价格:" + order.cost());
        //加两份糖
        order = new SugarDecorator(new SugarDecorator(order));
        System.out.println(order.getDes() + ",价格:" + order.cost());
        //加一份牛奶
        order = new MilkDecorator(order);
        System.out.println(order.getDes() + ",价格:" + order.cost());
    }
}

「结果」result

摩卡咖啡,价格:5.5
摩卡咖啡加糖加糖,价格:7.5
摩卡咖啡加糖加糖加牛奶,价格:9.5

在 Java IO 流中的应用

技术图片

在上图所示的关系中

  • 实体类是 InputStream
  • 被装饰者是FileInputStream、StringBufferInputStream、ByteArrayInputStream
  • 抽象装饰者是FilterInputStream
  • 具体装饰者是BufferInputStream、DataInputStream、LineNumberInputStream

具体使用如下:

BufferedInputStream bis = new BufferedInputStream(new FileInputStream("G:a.txt"));

装饰者模式总结

1、利用继承设计子类,只能在编译时静态决定,并且所有子类都会继承相同的行为;利用组合扩展对象,就可以在运行时动态的进行扩展。

2、装饰者和被装饰者对象有相同的超类型,所以在任何需要原始对象(被装饰者)的场合,都可以用装饰过的对象代替原始对象。

3、可以用一个或多个装饰者包装一个对象(被装饰者)。

4、装饰者可以在所委托的装饰者行为之前或之后加上自己的行为,以达到特定的目的。

5、被装饰者可以在任何时候被装饰,所以可以在运行时动态地、不限量地用你喜欢的装饰者来装饰对象。

6、装饰者会导致出现很多小对象,如果过度使用,会让程序变得复杂。

五、外观模式(Facade)

基本介绍

外观模式(Facade Pattern):外部与一个子系统的通信必须通过一个统一的外观对象进行,它为子系统中的一组接口提供一个统一的高层接口,使子系统更容易被使用

外观模式又称为门面模式,它是一种对象结构型模式。

模式结构

技术图片

1、Client(客户端):调用者

2、Facade(外观类):即上述所讲的高层接口

3、SubSystem(子系统):被调用者

举例说明

想要使用电脑,你只需要按一下开机键(客户端),电脑的各个部件(子系统)就开始工作了,你不需要关心硬盘如何启动的,CPU怎么运转的等等,一切都交给内部程序(外观类)处理。

编写简单的程序模拟一下

「SubSystem」:电脑的几个部件 CPU、内存、硬盘

public class Cpu {
    //使用「单例模式--饿汉式」创建对象
    private static Cpu instance = new Cpu();

    private Cpu() {
    }

    public static Cpu getInstance() {
        return instance;
    }

    public void start() {
        System.out.println("CPU启动");
    }

    public void stop() {
        System.out.println("CPU停止工作");
    }
}
public class Memory {
    private static Memory instance = new Memory();

    private Memory() {
    }

    public static Memory getInstance() {
        return instance;
    }

    public void start() {
        System.out.println("内存启动");
    }

    public void stop() {
        System.out.println("内存停止工作");
    }
}
public class HardDisk {
    private static HardDisk instance = new HardDisk();

    private HardDisk() {
    }

    public static HardDisk getInstance() {
        return instance;
    }

    public void start() {
        System.out.println("硬盘启动");
    }

    public void stop() {
        System.out.println("硬盘停止工作");
    }
}

「Facade」:电脑,统一管理开机关机中硬件的启动与停止

public class Computer {
    private Cpu cpu;
    private Memory memory;
    private HardDisk hardDisk;

    public Computer() {
        this.cpu = Cpu.getInstance();
        this.memory = Memory.getInstance();
        this.hardDisk = HardDisk.getInstance();
    }

    /**
     * 开机
     */
    public void boot(){
        cpu.start();
        memory.start();
        hardDisk.start();
    }

    /**
     * 关机
     */
    public void shutdown(){
        cpu.stop();
        memory.stop();
        hardDisk.stop();
    }
}

「Client」:电源键,可控制开机、关机

public class Client {
    Computer computer = new Computer();

    @Test
    public void boot(){
        computer.boot();
    }

    @Test
    public void shutdown(){
        computer.shutdown();
    }
}

模式分析

优点:

  • 实现了客户端与子系统的低耦合,使得子系统的变化不会影响客户端,只需要调整外观类即可。
  • 对客户端屏蔽子系统,减少了客户端处理的对象数目,操作变得更简单。
  • 降低了大型软件系统中的编译依赖性,并简化了系统在不同平台之间的移植过程,因为编译一个子系统一般不需要编译所有其他的子系统。一个子系统的修改对其他子系统没有任何影响,而且子系统内部变化也不会影响到外观对象。

缺点:

  • 不能很好的限制客户端对子系统的使用,如果对其做了太多限制会降低可变性和灵活性。
  • 在不引入「抽象外观类」的情况下,如果增加新的子系统,需要修改外观类代码,违背了「开闭原则」

适用场景

  • 当要为一个复杂子系统提供一个简单接口时可以使用外观模式。该接口可以满足大多数用户的需求,而且用户也可以越过外观类直接访问子系统。
  • 客户程序与多个子系统之间存在很大的依赖性。引入外观类将子系统与客户以及其他子系统解耦,可以提高子系统的独立性和可移植性。
  • 在层次化结构中,可以使用外观模式定义系统中每一层的入口,层与层之间不直接产生联系,而通过外观类建立联系,降低层之间的耦合度。

六、享元模式(Flyweight)

基本介绍

享元模式(Flyweight Pattern)也叫蝇量模式,运用共享技术有效地支持大量细粒度对象的复用。常用于系统底层开发,解决系统性能问题。例如数据库连接池,里面都是创建好的连接对象,如果有我们需要的,直接拿来用,避免重新创建,可以解决重复对象对内存造成浪费的问题

内部状态和外部状态

享元模式提出了细粒度和共享对象,这里就涉及了内部状态和外部状态的概念,即可以把对象的信息分为两个部分:内部状态和外部状态

内部状态(Intrinsic State):可以共享的相同内容

外部状态(Extrinsic State):需要外部环境来设置的不能共享的内容

举个栗子,围棋理论上有 361 个位置可以放棋子,每盘棋可能会产生两三百个棋子对象,由于内存有限,一台服务器很难支持更多玩家进行围棋对战,如果用享元模式来处理棋子,将棋子的颜色(黑与白)作为内部状态,棋子的位置(不确定)作为外部状态,就可以将棋子对象减少到两个实例(黑棋、白棋),这样就可以很好的解决内存开销问题。

模式结构

技术图片

  • Flyweight:抽象享元类
  • ConcreteFlyweight:具体享元类
  • UnsharedConcreteFlyweight:非共享具体享元类
  • FlyweightFactory:享元工厂类

举例说明

一个开发团队接了这样的项目,客户希望做一个产品展示网站,但网站需要有多种发布形式,每个用户可以以新闻形式发布、以博客形式发布、以微信公众号形式发布...

「抽象享元类」

public abstract class AbstractWebsite {
    public abstract void publish(User user);
}

「非共享具体享元类」

public class User {
    private String name;

    public User(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

「具体享元类」

public class ConcreteWebsite extends AbstractWebsite {
    /**
     * 发布类型
     */
    private String type = "";

    public ConcreteWebsite(String type) {
        this.type = type;
    }

    /**
     * 发布
     */
    @Override
    public void publish(User user) {
        System.out.println("用户「"+user.getName()+"」发布的网站形式为「" + type+"」");
    }
}

「享元工厂类」

public class WebsiteFactory {

    /**
     * 以 HashMap 作为对象池
     */
    private Map<String, ConcreteWebsite> pool = new HashMap<>();

    /**
     * 从对象池中返回指定类型的对象,没有则创建
     */
    public AbstractWebsite getWebsite(String type) {
        if (!pool.containsKey(type)) {
            pool.put(type, new ConcreteWebsite(type));
        }
        return pool.get(type);
    }

    /**
     * 计算对象池中对象的个数
     */
    public int count() {
        return pool.size();
    }
}

「测试类」

public class Client {
    @Test
    public void test(){
        WebsiteFactory factory = new WebsiteFactory();

        AbstractWebsite website1 = factory.getWebsite("新闻");
        website1.publish(new User("张三"));
        website1.publish(new User("李四"));

        AbstractWebsite website2 = factory.getWebsite("博客");
        website2.publish(new User("王五"));
        website2.publish(new User("赵六"));

        AbstractWebsite website3 = factory.getWebsite("公众号");
        website3.publish(new User("陈七"));
        website3.publish(new User("胡八"));

        System.out.println("对象的个数:" + factory.count());
    }
}

「运行结果」

用户「张三」发布的网站形式为「新闻」
用户「李四」发布的网站形式为「新闻」
用户「王五」发布的网站形式为「博客」
用户「赵六」发布的网站形式为「博客」
用户「陈七」发布的网站形式为「公众号」
用户「胡八」发布的网站形式为「公众号」
对象的个数:3

享元模式在Integer中的应用

首先我们看一段代码,运行结果是什么?

public class IntegerSource {
    public static void main(String[] args) {
        Integer v1 = 127;
        Integer v2 = 127;
        System.out.println("v1等于v2? " + (v1 == v2));
        Integer v3 = 128;
        Integer v4 = 128;
        System.out.println("v3等于v4? " + (v3 == v4));
    }
}
答案
v1等于v2? true
v3等于v4? false

分析:查看 Integer 源码,找到 valueOf 方法,可以看到,如果 i 在某个范围内,就不会产生新的对象,直接从缓存数组中获取,点进 IntegerCache 里就会发现 low = -128 high = 127,因此,我们可以理解为这个数组就是「内部状态」

public static Integer valueOf(int i) {
    //low = -128 , high = 127
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        //IntegerCache.cache是一个常量数组:static final Integer cache[];
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

模式分析

优点:

  • 可以极大减少内存中对象的数量,使得相同对象或相似对象在内存中只保存一份。

  • 享元模式的外部状态相对独立,而且不会影响其内部状态,从而使得享元对象可以在不同的环境中被共享。

缺点:

  • 享元模式使得系统更加复杂,需要分离出内部状态和外部状态,这使得程序的逻辑复杂化。
  • 为了使对象可以共享,享元模式需要将享元对象的状态外部化,而读取外部状态使得运行时间变长。

适用场景:

  • 一个系统有大量相同或者相似的对象,由于这类对象的大量使用,造成内存的大量耗费。

  • 对象的大部分状态都可以外部化,可以将这些外部状态传入对象中。

  • 使用享元模式需要维护一个存储享元对象的享元池,而这需要耗费资源,因此,应当在多次重复使用享元对象时才值得使用享元模式。

七、代理模式(Proxy)

代理模式介绍

代理模式提供了对目标对象额外的访问方式,即通过代理对象访问目标对象,这样可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。

技术图片

代理模式分为三类:

  • 静态代理
  • 动态代理
  • Cglib 代理

静态代理(不推荐使用)

介绍

要求目标对象和代理对象实现同一个接口,调用的时候调用代理对象的方法,从而达到增强的效果

优点:

可以在不修改目标对象的前提下,增强目标对象方法的功能(所有代理模式都可以实现,因此不推荐使用此方法)

缺点:

① 冗余。目标对象和代理对象实现同一个接口,会产生过多的代理类。

② 不易维护。当接口方法增加,目标对象与代理对象都要进行修改。

代码实现

场景:厂家生产了商品,但是没有足够的精力、人力去销售,这时候就需要一个代理商帮他售卖,但是代理商需要从中抽取 20% 的利润。

公共接口

public interface IProducer {
    void sale(float money);
}

被代理对象

public class Producer implements IProducer {
    @Override
    public void sale(float money) {
        System.out.println("卖出产品,厂家获得" + money + "元");
    }
}

代理对象

public class ProxyProducer implements IProducer{

    private IProducer producer;

    public ProxyProducer(IProducer producer) {
        this.producer = producer;
    }

    @Override
    public void sale(float money) {
        producer.sale(money * 0.8f);
    }
}

测试类

public class Client {
    @Test
    public void test(){
        IProducer producer = new Producer();
        ProxyProducer proxyProducer = new ProxyProducer(producer);
        proxyProducer.sale(1000f);
    }
}

运行结果

卖出产品,厂家获得800.0元

动态代理

介绍

动态代理也称:JDK 代理、接口代理,需要目标对象实现接口,否则不能用动态代理,利用 JDK 的 API(java.lang.reflect.Proxy),动态地在内存中构建代理对象

静态代理和动态代理的区别:

  • 静态代理在编译时就已经实现,编译完后的代理类是一个实际的 class 文件
  • 动态代理实在运行时动态生成的,编译后没有实际的 class 文件,而是在运行时动态的生成类字节码,并加载到 JVM 中

代码实现

以静态代理的情景为例,我们只需要修改代理对象的代码,代理对象不需要实现公共接口了。

public class ProxyProducer {
    /**
     * 维护一个目标对象
     */
    private Object target;

    public ProxyProducer(Object target) {
        this.target = target;
    }

    public Object getProxyInstance() {
        return Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new InvocationHandler() {
                    /**
                     * 执行被代理对象的任何接口方法都会经过这里
                     * @param proxy 代理对象的引用
                     * @param method 当前执行的方法
                     * @param args 当前执行方法的参数
                     * @return 和被代理对象具有相同的返回值
                     * @throws Throwable
                     */
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        //代理过程中执行一些方法
                        float money = (float) args[0] * 0.8f;
                        //反射机制调用目标对象的方法
                        Object invoke = method.invoke(target, money);
                        return invoke;
                    }
                });
    }
}

Cglib 代理

介绍

Cglib 代理也叫子类代理,目标对象不需要实现任何接口,它是在内存中构建一个子类对象从而实现对目标对象功能的扩展。

Cglib 是一个强大的高性能的代码生成包,它可以在运行期间扩展 Java 类与实现 Java 接口,它广泛地被许多 AOP 的框架使用,例如 Spring AOP,用于实现方法拦截。

Cglib 包底层实通过使用字节码处理框架 ASM 来转换字节码并生成新的类。

在 AOP 编程中选择哪种代理模式?

  • 目标对象需要实现接口,用 JDK 代理
  • 目标对象不需要实现接口,用 Cglib 代理

代码实现

使用之前需要导入相关 jar 包,可去 maven 仓库下载

被代理对象,无需实现接口

public class Producer {
    public void sale(float money) {
        System.out.println("卖出产品,厂家获得" + money + "元");
    }
}

代理对象

public class ProxyProducer implements MethodInterceptor {
    /**
     * 维护一个目标对象
     */
    private Object target;

    public ProxyProducer(Object target) {
        this.target = target;
    }

    /**
     * 为目标对象生成代理对象
     */
    public Object getProxyInstance(){
        //创建一个工具类
        Enhancer enhancer = new Enhancer();
        //设置父类
        enhancer.setSuperclass(target.getClass());
        //设置回调函数
        enhancer.setCallback(this);
        //创建子类对象(代理对象)
        return enhancer.create();
    }

    /**
     * 会拦截被代理对象的所有方法
     * @param obj 增强对象
     * @param method 被代理对象的方法
     * @param args 被代理对象方法的参数
     * @param methodProxy 代理对象
     */
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        System.out.println("obj:" + obj.getClass());
        Object returnValue = null;
        float money = (float) args[0] * 0.8f;
        if("sale".equals(method.getName())){
            returnValue = method.invoke(target, money);
        }
        return returnValue;
    }
}

测试类

public class Client {
    @Test
    public void test() {
        Producer producer = new Producer();
        Producer proxyInstance = (Producer) new ProxyProducer(producer).getProxyInstance();
        proxyInstance.sale(1000f);
    }
}

?? 以上所有代码和笔记均可在 我的GitHub 获取

以上是关于超详细-七种常见结构型模式的描述总结与代码分析的主要内容,如果未能解决你的问题,请参考以下文章

案例分析:设计模式与代码的结构特性

超详细总结基于比较的七大经典 排序 -- 不会的童鞋快进来补习

完整案例分析再加知识整合——艾特抽象工厂模式,超详细的

七种常见经典排序算法总结(C++)

完整案例分析再加知识整合——艾特简单工厂模式,超详细的

案例分析:设计模式与代码的结构特性