【55-90】结构型模式

news2024/11/15 8:31:21

目录

一.结构型模式概述

二.代理模式

2.1 概述

2.2 结构

2.3 静态代理

2.4 JDK动态代理

2.5 CGLIB动态代理

2.6 三种代理的对比

2.7 优缺点

三.适配器模式

3.1 概述

3.2 结构

3.3 类适配器模式

3.4 对象适配器模式

3.5 应用场景

四.装饰者模式

4.1 概述

4.2 结构【既继承又聚合】

4.3 案例

4.4 使用场景

4.5 JDK源码解析

五.桥接模式

5.1 概述

5.2 结构

5.3 案例

5.4. 使用场景

六.外观模式

6.1 概述

6.2 结构

6.3 案例

6.4 使用场景

6.5 源码解析

七.组合模式

7.1 概述

7.2 结构

7.3 案例

7.4 组合模式的分类

7.5 优点

7.6 应用场景

八.享元模式

8.1 概述

8.2 结构

8.3 案例

8.4 优缺点和使用场景

8.5 JDK源码解析


一.结构型模式概述

结构型模式描述如何将类或对象按照某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者采用组合或聚合来组合对象。

由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式更加灵活。

结构型模式分为以下7种:

  • 代理模式
  • 适配器模式
  • 装饰着模式
  • 桥接模式
  • 外观模式
  • 组合模式
  • 享元模式

二.代理模式

2.1 概述

由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介。

JAVA中的代理按照代理类生成时机不同又分为静态代理和动态代理。静态代理代理类再编译期就生成,而动态代理代理类则是再JAVA运行时动态生成。动态代理又有Jdk代理和CGLib代理两种。

2.2 结构

代理(Proxy)模式分为三种角色:

  • 抽象代理(Subject)类:通过接口或抽象类声明真实主题和代理对象实现的业务方法。
  • 真实主题(Real Subject)类:实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。
  • 代理(Proxy)类:提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能。

2.3 静态代理

我们通过案例来感受一下静态代理。

【例】火车站卖票

如果要买火车票的话,需要去火车站买票,坐车到火车站,排队等一系列的操作,显然比较麻烦,而火车站在多个地方都有代售点,我们去代售点买票就方便很多了。这个例子其实就是典型的代理模式,火车站是目标对象,代售点是代理对象。类图如下:

SellTickets接口类【抽象主题】

//卖火车票的接口【抽象主题类】
public interface SellTickets {

    void sell();
}

TrainStation具体实现类【具体主题】

//火车站【具体主题类】
public class TrainStation implements SellTickets{

    public void sell(){
        System.out.println("火车站卖票");
    }

}

ProxyPoint类【代理类】

//代售点【代理类】
public class ProxyPoint implements SellTickets{

    //声明火车站类对象
    private TrainStation trainStation=new TrainStation();

    public void sell(){
        System.out.println("代理点收取一些服务费用");
        trainStation.sell();
    }

}

Client测试类

public class Client {

    public static void main(String[] args) {
        //创建代售点类对象
        ProxyPoint proxyPoint = new ProxyPoint();
        //调用方法进行买票
        proxyPoint.sell();
    }
}

从上面的代码中可以看出测试类直接访问的是ProxyPoint类对象,也就是说ProxyPoint作为访问对象和目标对象的中介。同时也对sell方法进行了增强(代理点收取了一些服务费用)。

2.4 JDK动态代理

接下来我们使用动态代理实现上面案例,先说说JDK提供的动态代理。JAVA中提供了一个动态代理类Proxy,Proxy并不是我们上述所说的代理对象类,而是提供一个创建代理对象的静态方法(newProxyInstance方法)来获取代理对象。

ProxyFactory类

//获取代理对象的工厂类
//代理类也实现了对应的接口
public class ProxyFactory {

    //声明目标对象,这里使用接口类型是因为代理类也是接口的实现
    private TrainStation station=new TrainStation();

    public SellTickets getProxyObject(){
        //返回代理对象即可
        /*
            CLassLoader loader : 类加载器,用于加载代理类。可以通过目标对象获取类加载器。
            Class<?>[] interfaces : 代理类实现的接口的字节码对象
            InvocationHandler h : 代理对象的调用处理程序
         */
        //获取代理对象的方法
        SellTickets proxyObject = (SellTickets) Proxy.newProxyInstance(
             station.getClass().getClassLoader(),
             station.getClass().getInterfaces(),
              new InvocationHandler(){
                 @Override
                    /*
                        Object proxy : 代理对象,和proxyObject对象是同一个对象,在invoke方法中基本不用
                        Method method : 对接口中的方法进行封装的method对象
                        Object[] args : 调用方法的实际参数
                     */
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//                        System.out.println("invoke方法执行了");
                        System.out.println("代售点收取一定的服务费用(JDK动态代理)");
                        //执行目标对象中的方法 sell方法,因为没有返回值,所以obj=null
                        Object obj = method.invoke(station, args);
                        return obj;
                    }
              }
        );

        return proxyObject;
    }

}
public class Client {
    public static void main(String[] args) {
        //获取代理对象
        //1.创建代理工厂对象
        ProxyFactory factory = new ProxyFactory();
        //2.使用代理工厂的方法获取代理对象
        SellTickets proxyObject = (SellTickets) factory.getProxyObject();
        //3.调用类调用方法
        proxyObject.sell();
    }
}

使用了动态代理,我们现在思考下面问题:
ProxyFactory是代理类吗?

ProxyFactory不是代理模式中所说的代理类,而代理类是程序运行过程中动态的在内存中生成的类。

  • 代理类($Proxy0)实现了SellTickets。这也就印证了我们之前说的真实类和代理类实现了同样的接口。
  • 代理类($Proxy0)将我们提供了的匿名内部类对象传递给了父类。

执行流程如下:

  1. 在测试类中通过代理对象调用sell()方法
  2. 根据多态的特性,执行的是代理类($Proxy0)中的sell()方法
  3. 代理类($Proxy0)中的sell()方法中又调用了InvocationHandler接口的子实现类对象的invoke方法
  4. invoke方法通过反射执行了真实对象所属类(TrainStation)中的sell()方法

2.5 CGLIB动态代理

同样是上面的案例,我们再次使用CGLIB代理实现。

如果没有定义SellTickets接口,只定义了TrainStation(火车站类)。很显然JDK代理是无法使用的了,应为JDK动态代理要求必须定义接口,对接口进行处理。

CGLI是一个功能强大,高性能的代码生成包。它为没有实现接口的类提供代理,为JDk动态代理提供了很好的补充。

CFLIB是第三方提供的包,所以需要引入jar包的坐标。

2.6 三种代理的对比

JDK代理和CGLIB代理

使用CGLIB实现动态代理,CGLIB底层采用ASM字节码生成框架,使用字节码技术生成代理类,在JDK.6之前比使用Java反射效率要高。唯一需要注意的是,CGLIB不能对声明为final的类或者方法进行代理,因为CGLIB原理是动态生成代理类的子类。

在JDK.6、JDK.7、JDK.8逐步对JDK动态代理优化之后,在调用次数较少的情况下,JDK代理效率要高于CGLIB代理效率,只有当进行大量调用的时候,JDK.6和JDk.7比CGLIB代理效率低一点,但是到JDK.8的时候,JDK代理效率高于CGLIB代理。所以如果有接口使用JDK代理,如果没有接口使用CGLIB代理。

动态代理和静态代理

动态代理和静态代理相比较,最大好处就是接口声明的所有方法都被转移到调用处理器的一个集中的方法中处理。这样,在接口方法数量比较多的时候,我们可以进行灵活处理,而不用像静态代理那样每个方法进行中转。

如果接口增加一个方法,静态代理模式除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。而动态代理不会出现该问题。

2.7 优缺点

优点:

  • 代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用。
  • 代理对象可以扩展目标对象的功能。
  • 代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度。

缺点:

  • 增加了系统的复杂度。

2.8 使用场景

远程代理

本地服务通过网络请求远程服务。为了实现本地到远程通信,我们需要实现网络通信,处理其中肯可能的异常。为良好的代码设计和可维护性,我们将网络通信部分隐藏起来,只暴露给本地一个接口,通过该接口即可访问远程服务提供的功能,而不必过多关心通信部分的细节。

防火墙代理

当你的浏览器配置成使用代理功能是,防火墙就将你的浏览器的请求转给互联网;当互联网返回响应时,代理服务器再把它转给你的浏览器,

保护代理

控制对一个对象的访问,如果需要,可以给不同的用户提供不同级别的使用权限。

三.适配器模式

3.1 概述

如果去欧洲国家旅行的话,他们的插座如下图最左边,是欧洲标准。而我们使用的插头如下图最右边的。因此我们的笔记本电脑,手机再当地不能直接充电。所以需要一个插座转换器,转换器第1面插入当地的插座;第2面供我们充电,这样使得我们的插头在当地可以使用。

定义:

        将一个类的接口转换成客户希望的另外一个接口,使得原本由于不兼容而不能一起工作的那些类能一起工作。

        适配器模式分为类适配器模式和对象适配器模式,前者类之间的耦合度比后者高,且要求程序员了解现有组件库中的相关组件的内部结构,所以应用相对较少。

3.2 结构

适配器模式(Adapter)包含以下主要角色:

  • 目标(Target)接口:当前系统业务所期待的接口,它可以时抽象类或接口。
  • 适配者(Adaptee)类:它是适配的现存组件库中的组件接口。
  • 适配器(Adapter)类:它是一个转换器,通过继承或引用适配者对象,把适配者接口转换成目标接口,让客户按目标接口的格式访问适配者。

3.3 类适配器模式

实现方式:定义一个适配器类来实现当前系统的业务接口,同时又继承现有组件库中已经存在的组件

【例】读卡器

现在又一台电脑只能读取SD卡,而读取TF卡中的内容的话就需要使用到适配器模式。创建一个读卡器,将TF卡中的内容取出来。

类图如下:

类适配器模式违背了合成复用原则。类适配器是客户类有一个接口规范的情况下可用的,反之不可用。

3.4 对象适配器模式

实现方式:对象适配器模式可采用将现有组件库中已经实现的组件引入适配器类中,该类同时实现当前系统的业务接口。

【例】读卡器

我们使用对象适配器模式将读卡器的案例进行改写。类图如下:

注意:还有一个适配器模式是接口适配器模式。当不希望实现一个接口中所有方法时,可以创建一个抽象类Adapter,实现所有方法。而此时我们只需要继承该对象即可。

3.5 应用场景

  • 以前开发的系统存在满足新系统功能需求的类,但其接口同新系统接口不一致。
  • 使用第三方提供的组件,但组件接口定义和自己的要求接口定义不同。

四.装饰者模式

4.1 概述

先看一个例子。

快餐店有炒面、炒饭这些快餐,可以额外附加鸡蛋、火腿、培根这些配菜,当然加配菜需要额外加钱,每个配菜的价钱通常不太一样,那么计算总价就会显得比较麻烦。

使用继承的方式存在的问题:

  • 扩展性不好

如果要再加一种配料(火腿肠),我们就会发现需要给FriedRice和FriedNoodles分别定义一个子类。如果要新增一个快餐品类(炒河粉)的话,就需要定义更多的子类。

  • 产生过多的子类

定义:

指在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外的功能)的模式。

4.2 结构【既继承又聚合】

装饰者模式中的角色:

  • 抽象构件(Component)角色:定义一个抽象接口以规范准备接收附加责任的对象。
  • 具体构建(Concrete Component)角色:实现抽象构件,通过装饰者角色为其添加一些职责。
  • 抽象装饰(Decorator)角色:继承或实现抽象构件,并包含具体构建实例(聚合),可以通过其子类扩展具体构件的功能。
  • 具体装饰(ConcreteDecorator)角色:实现抽象装饰的相关方法,并给具体构件对象添加附加的责任。

4.3 案例

我们使用装饰者模式对快餐店案例进行改进,体会装饰者模式的精髓。类图如下:

FastFood抽象构件类

//快餐类 抽象构件角色
public abstract class FastFood {

    private float price; // 价格
    private String desc; // 描述
    public FastFood(float price, String desc) {
        this.price = price;
        this.desc = desc;
    }
    public float getPrice() {
        return price;
    }
    public void setPrice(float price) {
        this.price = price;
    }
    public String getDesc() {
        return desc;
    }
    public void setDesc(String desc) {
        this.desc = desc;
    }
    public abstract float cost();

}

FriedRice具体构件类

// 炒饭 具体构件角色
public class FiredRice extends FastFood{

    public FiredRice(){
        super(10,"炒饭");
    }

    public float cost(){
        return getPrice();
    }

}

FriedNoodles具体构件类

//炒面 具体构件类
public class FriedNoodles extends FastFood{

    public FriedNoodles(){
        super(12, "炒面");
    }

    public float cost(){
        return getPrice();
    }

}

Ganish抽象装饰类

public abstract class Garnish extends FastFood{

    //声明快餐类的变量
    private FastFood fastfood;

    public FastFood getFastfood() {
        return fastfood;
    }
    public void setFastfood(FastFood fastfood) {
        this.fastfood = fastfood;
    }

    public Garnish(FastFood fastfood,float price, String desc) {
        super(price, desc);
        this.fastfood = fastfood;
    }

}

Egg具体装饰类

//鸡蛋类 具体装饰者类
public class Egg extends Garnish{

    public Egg(FastFood fastFood){
        super(fastFood,1,"鸡蛋");
    }
    public float cost(){
        //计算价格
        return getPrice()+getFastfood().cost();
    }

    public String getDesc(){
        return super.getDesc()+getFastfood().getDesc();
    }

}

Bacon具体装饰类

public class Bacon extends Garnish{

    public Bacon(FastFood fastFood){
        super(fastFood,2,"培根");
    }

    public float cost(){
        return getPrice()+getFastfood().cost();
    }

    public String getDesc(){
        return super.getDesc()+getFastfood().getDesc();
    }

}

Client测试类

public class Client {
    public static void main(String[] args) {
        //点一份炒饭
        FastFood food = new FiredRice();
        System.out.println(food.getDesc()+" "+food.cost());
        System.out.println("=====================");
        //在炒饭中加一个鸡蛋
        food=new Egg(food);
        System.out.println(food.getDesc()+" "+food.cost());
        System.out.println("=====================");
        //再加一个鸡蛋
        food=new Egg(food);
        System.out.println(food.getDesc()+" "+food.cost());
        System.out.println("=====================");
        //再加一个培根
        food=new Bacon(food);
        System.out.println(food.getDesc()+" "+food.cost());
    }
}

 好处:

  • 装饰者模式可以带来比继承更加灵活的扩展功能,使用更加方便,可以通过组合不同的装饰者对象来获取具有不同行为状态的的多样化的结果。装饰者模式比继承更具有良好的扩展性,完美的遵循开闭原则,继承是静态附加责任,装饰者则是动态附加责任。
  • 装饰者和被装饰者可以独立发展,不会相互耦合,装饰者模式是继承的一个替代模式,装饰者模式可以动态扩展一个实现类的功能。

4.4 使用场景

  • 当不能采用继承的方式对系统进行扩充或者采用继承不利于系统扩展和维护时。

        不能采用继承的情况主要有两类:

        第一类时系统中存在大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长。

        第二类是因为类定义不能继承(final类)

  • 在不影响其他对象的情况下,以动态、透明的方式给对象添加职责
  • 当对象的功能要求可以动态地添加,也可以动态的撤销时。

4.5 JDK源码解析

4.6 代理和装饰者模式的区别

静态代理和装饰者模式的区别:
相同点:

  • 都要实现与目标类相同的业务接口
  • 在两个类中都要声明目标对象
  • 都可以在不修改目标类的前提下增强目标方法

不同点:

  • 目标不同:装饰者时为了增强目标对象。静态代理时为了保护和隐藏目标对象。
  • 获取目标对象的构件地方不同:装饰者是由外界传进来,可以通过构造方法传递。静态代理是在代理类内部创建,以此来隐藏目标对象。

五.桥接模式

5.1 概述

现在有一个需求,需要创建不同的图形,并且每个图形都可以会有不同的颜色。完美可以利用继承方式来设计类的关系:

完美可以发现有很多的类,假如我们再增加一个形状或者再增加一种颜色,就需要创建很多的类。

试想,在一个有很多种可能会变化的维度的系统中,用继承的方式会造成类爆炸,扩展起来很不灵活。每次在一个维度上增加一个具体的实现都要增加很多个子类。为了更加灵活的设计系统,我们此时可以考虑使用桥接模式。

定义:

将抽象和实现分离,使它们可以独立变化,它是用组合关系替代继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。

5.2 结构

桥接模式包含以下主要角色:

  • 抽象化(Abstraction)角色:定义抽象类,并包含一个对实现化对象的引用。
  • 扩展抽象化(Refined Abstraction)角色:是抽象化角色的子类,实现父类中的业务方法,并通过组合关系调用实现化角色中的业务方法。
  • 实现化(Implementor)角色:定义实现化角色接口,供扩展抽象角色调用。
  • 具体实现化(Concrete Implementor)角色:给出实现化角色接口的具体实现。

5.3 案例

【例】视频播放器

需要开发一个跨平台的视频播放器,可以在不同的操作平台(如Windows、MAC、Linux等)上播放多种格式的视频文件,常见视频格式包括RMVB、AVI、WMV等。该播放器包含了两个维度,适合使用桥接模式。

类图如下:

OperatingSystem抽象化角色

//抽象的操作系统类(抽象化角色)
public abstract class OpratingSystem {

    //声明VideoFile
    protected VideoFile videoFile;

    public OpratingSystem(VideoFile videoFile) {
        this.videoFile = videoFile;
    }

    public abstract void play(String fileName);

}

Mac扩展抽象化角色

public class Mac extends OpratingSystem{

    public Mac(VideoFile videoFile){
        super(videoFile);
    }

    public void play(String fileName) {
        videoFile.decode(fileName);
    }
}

Windows扩展抽象化角色

//扩展抽象化角色(windwos操作系统)
public class Windows extends OpratingSystem{

    public Windows(VideoFile videoFile) {
        super(videoFile);
    }

    public void play(String fileName) {
        videoFile.decode(fileName);
    }

}

VideoFile实现化角色

//视频文件(实现化角色)
public interface VideoFile {

    //解码功能
    void decode(String filename);

}

AviFile具体实现化角色

//Avi视频文件 (具体的实现化角色)
public class AviFile implements VideoFile{

    public void decode(String filename){
        System.out.println("avi视频文件:"+filename);
    }

}

RmvbFile具体实现化角色

//rmvb视频文件 (具体的实现化角色)
public class RmvbFile implements VideoFile{
    public void  decode(String fileName) {
        System.out.println("rmvb视频文件:"+fileName);
    }
}

Client测试类

public class Client {
    public static void main(String[] args) {
        //创建Mac系统对象
        OpratingSystem system=new Mac(new AviFile());
        //使用操作系统播放视频文件
        system.play("战狼3");

    }
}

好处:

  • 桥接模式提高了系统的可扩充性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统。如:如果现在有一种视频文件类型wmv,我们只需要再定义一个类实现VideoFile接口即可,其他类不需要发生变化。
  • 实现细节对客户透明。

5.4. 使用场景

  • 当一个类存在两个独立变化的维度,且这两个维度都需要进行扩展时。
  • 当一个系统不希望使用继承或因为多层次继承导致系统类的个数急剧增加时。
  • 当一个系统需要在构件的抽象化角色和具体角色之间增加更多的灵活性时。避免在两个层次之间建立静态的继承联系,通过桥接模式可以使它们在抽象建立一个关联关系。

六.外观模式

6.1 概述

有些人可能炒过股票,但其实大部分人都不太懂,这种没有足够了解证券知识的情况下做股票时很容易亏钱的,刚开始炒股肯定都会想,如果懂行的帮帮手就好了,其实基金就是个好帮手,支付宝里就是许多的基金,它将投资者分散的资金集中起来,交由专业的经理人进行管理,投资于股票、债券、外汇等领域,而基金投资的收益归持有者所有,管理机构收取一定比例的托管管理费用。

定义:
又名门面模式,是一种通过为多个复杂的子系统提供一个一致的接口。而使这些子系统更加容易被访问的模式。该模式对外有一个统一的接口,外部应用程序不用关心内部系统的具体的细节,这样会大大价降低应用程序的复杂度,提高了程序的可维护性。

外观(Facade)模式“迪米特法则”的典型应用。

6.2 结构

外观(Facade)模式包含以下角色:

  • 外观(Facade)角色:为多个子系统对外提供一个共同的接口。
  • 子系统(Sub System)角色:实现系统的部分功能,各户可以通过外观角色访问它。

6.3 案例

【例】智能家电控制

小明的爷爷已经60岁了,一个人在家生活:每次都需要打开灯、打开电视、打开空调;睡觉时关闭灯、关闭电视、关闭空调;操作起来比较麻烦。所以小明给爷爷买了智能音箱,可以通过语音直接控制这些智能家电的开启和关闭。类图如下:

代码如下:

Light类

//电灯类
public class Light {

    //开灯
    public void on(){
        System.out.println("Light on");
    }

    //关灯
    public void off(){
        System.out.println("Light off");
    }

}

TV类

//电视机类
public class TV {

    //打开电视机
    public void on(){
        System.out.println("TV is on");
    }

    //关闭电视机
    public void off(){
        System.out.println("TV is off");
    }

}

AirCondition类

//空调类
public class AirCondition {

    //打开空调
    public void on(){
        System.out.println("Air condition is on");
    }

    //关闭空调
    public void off(){
        System.out.println("Air condition is off");
    }

}

SmartAppliancesFacade类

//外观类 -智能音箱- 用户主要和该类对象进行交互
public class SmartAppliancesFacade {
    //聚合电灯对象,电视机对象,空调对象
    private Light light;
    private  TV tv;
    private AirCondition airCondition;

    public SmartAppliancesFacade() {
        light=new Light();
        tv=new TV();
        airCondition=new AirCondition();
    }

    //通过语音控制
    public void say(String msg){
        if(msg.contains("on")){
            on();
        }
        else if(msg.contains("off")){
            off();
        }
        else{
            System.out.println("Sorry, but I don't know what to do with");
        }
    }

    //一键打开
    private void on(){
        light.on();
        tv.on();
        airCondition.on();
    }

    //一键关闭
    private void off(){
        light.off();
        tv.off();
        airCondition.off();
    }


}

Client测试类

public class Client {
    public static void main(String[] args) {
        //创建智能音信对象
        SmartAppliancesFacade smartAppliancesFacade=new SmartAppliancesFacade();
        //控制家电
        smartAppliancesFacade.say("on家电");
        System.out.println("===================");
        smartAppliancesFacade.say("off家电");
    }
}

好处:

  • 降低了子系统和客户端之间的耦合度,使得子系统的变化不会影响调用它的客户类。
  • 对客户屏蔽了子系统的组件,减少了客户处理的对象数目,并使得子系统使用起来更加容易。

缺点:

  • 不符合开闭原则,修改很麻烦。

6.4 使用场景

  • 对分层结构系统构建时,使用外观模式定义子系统中每层的入口电可以简化子系统之间的依赖关系。
  • 当一个复杂系统的子系统很多时,外观模式可以为系统设计一个简单的接口供外界访问。
  • 当客户端与多个子系统之间存在很大的联系时,引入外观模式可以将它们分离,从而提高子系统的独立性和可移植性。

6.5 源码解析

七.组合模式

7.1 概述

对于这个图片肯定会非常熟悉,上图我们可以看做一个文件系统,对于这样的结构我们称之为树形结构。在树形结构中可以通过调用某个方法来遍历整个树,当我们找到某个叶子节点后,就可以对叶子节点进行相关的操作。可以将这棵树理解成一个很大的容器,容器里面包含很多的成员对象,这些成员对象即可是容器对象也可以是叶子对象。但是由于容器对象和叶子对象在功能上面的区别,使得我们在使用的过程中必须要区分容器对象和叶子对象,但是这样就给客户带来了不必要的麻烦,作为客户而已,它始终希望能够一致的对待容器对象和叶子对象。

定义:

又名部分整体模式,是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次,这种类型的设计模式属于结构模式,它创建了对象族的树形结构。

7.2 结构

组合模式主要包含三种角色:

  • 抽象根节点(Component):定义系统各层次对象的共有方法和属性,可以预先定义一些默认的行为和属性。
  • 树枝节点(Composite):定义树枝节点的行为,组合树枝节点和叶子节点形成一个属性结构。
  • 叶子节点(leaf):叶子节点对象,其下再无分支,是系统层次遍历的最小单位。

7.3 案例

如下图,我们在访问一个管理系统时,经常可以看到类似的菜单。一个菜单可以包含菜单项(菜单项是指不再包含其他内容的菜单条目),也可以包含带有其他菜单项的菜单,因此使用组合模式描述菜单就很恰当,我们需求是针对一个菜单,打印出其包含的所有菜单以及菜单项的名称。

要实现该案例,我们先画出类图:

不管是菜单还是菜单项,都应该继承自统一的接口。

MenuComponent抽象根节点

//菜单组件(抽象根节点)
public abstract class MenuComponent {

    //菜单组件名称
    protected String name;
    //菜单组件的层级
    protected int level;

    //添加子菜单
    public void add(MenuComponent component) {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    //移除子菜单
    public void remove(MenuComponent component) {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    //获取指定的子菜单
    public MenuComponent getChild(int index) {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    //获取菜单或者菜单项的名称
    public String getName() {
        return name;
    }

    //打印菜单名称的方法(包含子菜单和子菜单项)
    public abstract void print();

}

MenuItem树枝节点

public class MenuItem extends MenuComponent {

    public MenuItem(String name, int level){
        this.name=name;
        this.level=level;
    }

    public void print(){
        //打印菜单项名称
        for(int i=0;i<level;i++){
            System.out.print("--");
        }
        System.out.println(name);
    }
}

Menu树枝节点

public class Menu extends MenuComponent {

    //菜单可以有多个子菜单或者子菜单项
    private List<MenuComponent> meauComponentList=new ArrayList<MenuComponent>();

    //构造方法
    public Menu(String name, int level){
        this.name=name;
        this.level=level;
    }

    public void add(MenuComponent comp){
        meauComponentList.add(comp);
    }

    public void remove(MenuComponent comp){
        meauComponentList.remove(comp);
    }

    public MenuComponent getChild(int index){
        return meauComponentList.get(index);
    }

    public void print(){
        //打印菜单名称
        for(int i=0;i<level;i++){
            System.out.print("--");
        }
        System.out.println(name);
        //打印子菜单或子菜单项名称
        for(MenuComponent comp : meauComponentList){
            comp.print();
        }
    }

}

Client测试类

public class Client {
    public static void main(String[] args) {
        //创建菜单树
        MenuComponent menu1=new Menu("菜单管理",2);
        menu1.add(new MenuItem("页面访问",3));
        menu1.add(new MenuItem("展开菜单",3));
        menu1.add(new MenuItem("编辑菜单",3));
        menu1.add(new MenuItem("删除菜单",3));
        menu1.add(new MenuItem("新增菜单",3));

        MenuComponent menu2=new Menu("权限管理",2);
        menu2.add(new MenuItem("页面访问",3));
        menu2.add(new MenuItem("提交保存",3));

        MenuComponent menu3=new Menu("角色管理",2);
        menu3.add(new MenuItem("页面访问",3));
        menu3.add(new MenuItem("新增角色",3));
        menu3.add(new MenuItem("修改角色",3));

        //创建一级菜单
        MenuComponent component=new Menu("系统管理",1);
        //将二级菜单添加到一级菜单中
        component.add(menu1);
        component.add(menu2);
        component.add(menu3);

        //打印菜单名称(如果有子菜单一块打印)
        component.print();
    }
}

7.4 组合模式的分类

在使用组合模式时,根据抽象构件类的定义形式,我们可以将组合模式分为透明组合模式和安全组合模式两种形式。

透明组合模式:

透明组合模式中,抽象根节点角色中声明了所有用于管理成员对象的方法,比如在示例中MenuComponent声明了add、getChild方法,这样做的好处时确保所有的构件类都有相同的接口。透明组合模式也是组合模式的标准形式。

透明组合模式的缺点是不够安全,因为叶子对象和容器对象在本质上是有区别的,叶子对象不可能有下一个层次对象,即不可能包含成员对象,因此为其提供add、remove等方法是没有意义的,这在编译阶段不会出错,但运行阶段如果调用这些方法可能会出错(如果没有提供相应的错误处理代码)

安全组合模式:

在安全组合模式中,在抽象构件角色中没有声明任何用于管理成员对象的方法,而是在树枝节点Menu类中声明并实现这类方法。安全组合模式的缺点是不够透明,因为叶子构件和容器构件具有不同的方法,且容器构件中那些用于管理成员对象的方法没有在抽象构件中定义,因此客户端不能完全针对抽象编程,必须有区别对待叶子构件和容器构件。

7.5 优点

7.6 应用场景

组合模式正是应用树形结构而生,所以组合模式的使用场景就是出现树形结构的地方。比如:文件目录显示,多继目录呈现等树形数据结构操作。

八.享元模式

8.1 概述

定义:运用共享技术来有效支持大量细粒度对象的复用。它通过共享意境存在的对象来大幅度减小需要创建的对象数量,避免大量相似对象的开销,从而提高系统资源的利用率。

8.2 结构

享元(Flyweiht)模式中存在以下两种状态:

  1. 内部状态,即不会随着环境的改变而改变的可共享部分。
  2. 外部状态,指随环境改变而改变的不可以共享部分。享元模式的实现要领就是区分应用中的两种状态,并将外部状态外部化。

享元模式的主要有以下角色:

  • 抽象享元角色(Flyweight):通常是一个接口或者抽象类,在抽象享元类中声明了具体享元类公共的方法,这些方法可以向外界提供享元对象的内部数据(内部状态),同时也可以通过这些方法来设置外部数据(外部状态)
  • 具体享元(Concrete Flyweight)角色:它实现了抽象享元类,称为享元对象,在具体享元类中为内部状态提供了存储空间。通常我们可以结合单例模式来设计具体享元类,为每一个具体享元类提供唯一的享元模式。
  • 非享元(Unsharable Flyweight)角色:并不是所有的抽象享元类的子类都需要被共享,不能被共享的子类可以设计为非共享具体享元类,当需要一个非共享具体享元类的对象时可以直接通过实例化创建。
  • 享元工厂(Flyweight Factory)角色:负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检查系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在的话,则创建一个新的享元对象。

8.3 案例

【例】俄罗斯方块

下面的图片时众所周知的俄罗斯方块中的一个个方块,如果在俄罗斯方块这个游戏中,每个不同的方块都是一个实例对象,这些对象就要占用很多的内存空间,下面利用享元模式进行实现。

先来看类图:

AbstractBox抽象享元角色

//抽象享元角色
public abstract class AbstractBox {

    //获取图形的方法
    public abstract String getShape();
    //显示图形及其颜色
    public void display(String color){
        System.out.println("方块的形状:"+getShape()+" 颜色:"+color);
    }
}

IBox具体享元角色

//I图形类(具体享元角色)
public class IBox extends AbstractBox{
    public String getShape(){
        return "I";
    }
}

LBox具体享元角色

//L图形类(具体享元角色)
public class LBox extends AbstractBox{
    public String getShape(){
        return "L";
    }
}

OBox具体享元角色

//O图形类(具体享元角色)
public class OBox extends AbstractBox{
    public String getShape(){
        return "O";
    }
}

BoxFactory享元工厂类

//工厂类(该类设计为单例)
public class BoxFactory {

    public HashMap<String, AbstractBox> map;

    //在构建方法中进行初始化
    private BoxFactory(){
        map=new HashMap<String, AbstractBox>();
        map.put("I",new IBox());
        map.put("L",new LBox());
        map.put("O",new OBox());
    }

    //提供一个方法获取该工厂类对象
    public static BoxFactory getInstance(){
        return factory;
    }

    private static BoxFactory factory=new BoxFactory();

    //根据图形名称获取图形对象
    public AbstractBox getShape(String name){
        return map.get(name);
    }

}

Client测试类

public class Client {

    public static void main(String[] args) {
        //获取I图形对象
        AbstractBox box1= BoxFactory.getInstance().getShape("I");
        box1.display("灰色");

        //获取L图形对象
        AbstractBox box2= BoxFactory.getInstance().getShape("L");
        box2.display("灰色");

        //获取L图形对象
        AbstractBox box3= BoxFactory.getInstance().getShape("O");
        box3.display("灰色");

        //获取L图形对象
        AbstractBox box4= BoxFactory.getInstance().getShape("O");
        box4.display("蓝色");

        System.out.println("两次获取到的O图形是否为同一个对象:" + (box3==box4));

    }

}

8.4 优缺点和使用场景

优点:

  • 极大减少内存中相似或相同对象数量,节约系统资源,提高系统性能
  • 享元模式中的外部状态相对独立,且不影响内部状态

缺点:

为了使对象可以共享,需要将享元对象的部分状态外部化,分离内部状态和外部状态,使程序逻辑复杂

使用场景:

  • 一个系统有大量相同或相似的对象,造成内存的大量浪费
  • 对象的大部分状态都可以外部化,可以将这些外部状态传入对象中
  • 在使用享元模式时需要维护一个存储享元对象的享元池,而这需要耗费一定的系统资源,因此,应当在需要多次重复使用享元对象时才值得使用享元模式。
     

8.5 JDK源码解析

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2070909.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

从并发20到并发120之laravel性能优化

调优成果 遇到问题 单台服务并发20&#xff0c;平均响应时间1124ms&#xff0c;通过htop观察&#xff0c;发现cpu占用率达到100%&#xff08;包括sleep的进程&#xff09;&#xff0c;内存几乎没怎么用。 调优后 单机最大吞吐量达到120 响应时长不超过1000ms 硬件信息 …

数学建模----线性回归分析(引入热力图的绘制方法)

目录 0.直击重点 1.一元线性回归分析 1.1散点图的绘制 1.2相关性的分类 1.3计算相关系数 1.4模型的检验 1.5模型的预测 2.多重线性回归分析&#xff08;上&#xff09; 2.1多重线性的概念 2.2散点图的分类 2.3热力图的绘制 2.4根据结果确定新的变量 3.多重线性…

【开端】 如何判断手机号码属于哪个国家(手机号判断正则)汇总

import org.apache.commons.lang3.StringUtils; /** * 手机号判断正则 */ public enum MobileRegularExp { /** * 国家 正则 */ CN("中国", 86, "^(\\?0?86\\-?)?1[3456789]\\d{9}$"), TW("中国台湾", 886, "…

第七节 循环结构;goto语句

目录 7.1 while循环 7.1.1 if 和 while的对⽐ 7.1.2 while的执行流程 7.1.3 while的练习 7.2 for循环 7.2.1 语法形式 7.2.2 for循环的执⾏流程 7.2.3 for 循环的练习 7.3 while 和 for 循环的对比 7.4 do while 循环 7.4.1 do while 的语法形式 7.4.2 do while循…

Jamba前生今世:1.5开源来袭

AI21服务于企业&#xff0c;为企业构建基础模型和AI系统以加速GenAI在生产中的使用。AI21 成立于2017年&#xff0c;已从NVIDIA、Intel、Google等公司共筹集了3.36亿美元。它是最早将生成式AI推向大众的公司之一&#xff0c;借助AI21平台&#xff0c;企业可以构建自己的生成式A…

菲菲更名宝贝:批量处理,文件命名不再繁琐

你是否有这样的经历&#xff1f;曾几何时&#xff0c;在堆积如山的文件中迷失方向&#xff0c;为了一个个手动重命名文件而加班到深夜&#xff1f;是否渴望有一种魔法&#xff0c;能瞬间让你的文件整理得井井有条&#xff0c;让繁琐的命名工作变得轻松愉快&#xff1f;那么&…

大数据毕业设计开题报告100例

文章目录 &#x1f6a9; 1 前言1.1 选题注意事项1.1.1 难度怎么把控&#xff1f;1.1.2 题目名称怎么取&#xff1f; 1.2 开题选题推荐1.2.1 起因1.2.2 核心- 如何避坑(重中之重)1.2.3 怎么办呢&#xff1f; &#x1f6a9;2 选题概览&#x1f6a9; 3 项目概览题目1 : 深度学习社…

前端网站优化-Brotli 压缩

杨绛先生说:“岁不声不响&#xff0c;你且不慌不忙。在凡俗的烟火里&#xff0c;愿以素心&#xff0c;阅来日方长。生活总是一地鸡毛&#xff0c;繁杂琐碎的日常&#xff0c;无力掌控的局面&#xff0c;以及猝不及防的变化&#xff0c;让日子多了几分慌张”。 市井长巷&#xf…

ssrf漏洞复现

环境搭建 zhuifengshaonianhanlu/pikachu: 一个好玩的Web安全-漏洞测试平台 (github.com) 直接将其复制到linux环境下拉取docker就行 我这里已经拉去过了&#xff0c;如果拉去速度慢话&#xff0c;可以在/etc/docker下的daemon.json中配置镜像加速 vim /etc/docker/daemon.js…

大模型学习笔记 - LLM 对齐优化算法 DPO

LLM - DPO LLM - DPO DPO 概述DPO 目标函数推导DPO 目标函数梯度的推导 DPO 概述 大模型预训练是从大量语料中进行无监督学习&#xff0c;语料库内容混杂&#xff0c;训练的目标是语言模型损失&#xff0c;任务是next token prediction&#xff0c;生成的token 不可控&…

MyBatis-Plus分页插件使用详解

一、简述 在使用mybatis开发项目的时候我们通常使用pagehelper来进行分页操作&#xff0c; 但是我们在使用MyBatis-Plus 开发时&#xff0c;MyBatis-Plus内置已经有分页功能了&#xff0c;其实不需要在额外引入pagehelper依赖了&#xff0c;而且两者同时引入有时候还会导致分页…

主流商品API接口在电商跨境电商企业应用/项目中的重要作用

618狂欢已经开启&#xff0c;为了获取更大利益&#xff0c;电商商家应使用价格接口系统。价格接口对电商商家有多方面的好处&#xff0c;主要体现在以下几个方面&#xff1a; 1、价格接口系统可以帮助品牌和商家实现更加科学和精准的定价策略。通过实时获取多个主流电商平台&a…

公众号里面的试卷怎么打印

经过我们的观察发现&#xff0c;微信公众号中的试卷通常有两种形式&#xff1a;图片和文档。如果试卷是以图片的形式嵌入在文章中作为配图&#xff0c;您只需点击图片并长按&#xff0c;选择“保存图片”到手机中。之后&#xff0c;您可以选择任何方便的方式完成打印。 不过&am…

ELK企业级日志分析系统(分布式文件系统与企业级应用)

一、ELK 概述 1、ELK简介 ELK平台是一套完整的日志集中处理解决方案&#xff0c;将 ElasticSearch、Logstash 和 Kiabana 三个开源工具配合使用&#xff0c; 完成更强大的用户对日志的查询、排序、统计需求。 ElasticSearch 是基于Lucene&#xff08;一个全文检索引擎的架构…

笔记整理—uboot启动过程(4)BL2干了什么及内存排布

uboot的第一阶段结束于start_armboot&#xff0c;第二阶段的uboot代码主要负责soc外部硬件&#xff08;inand、网卡、......&#xff09;、uboot本身构建&#xff08;uboot指令、环境变量、......&#xff09;最后进入命令行&#xff0c;等待命令然后倒数&#xff0c;等待bootc…

Pytest框架环境切换实战教程

测试人员每天都跟不同的环境打交道&#xff0c;比如线上环境&#xff0c;测试环境&#xff0c;预上线环境等等&#xff0c;那么作为自动化测试人员写的代码&#xff0c;我们也要具备能自由切换环境的能力&#xff0c;那么今天小编就给大家聊一下&#xff0c;如何能让我们python…

linux 安装kafaka单体服务

1.下载kafka的linux安装包 前往Apache Kafka官方网站下载页面&#xff08;Apache Kafkahttps://kafka.apache.org/downloads&#xff09;&#xff0c;选择最新稳定版的Kafka二进制分发文件&#xff0c;通常是以.tgz结尾的文件。 手动下载kafka_2.13-3.8.0.tgz到本地&#xff0…

Spring Boot 与 Spring Security 的集成及 OAuth2 实现

我的主页&#xff1a;2的n次方_ 在现代 Web 应用开发中&#xff0c;安全性是至关重要的。无论是保护用户的敏感数据&#xff0c;还是确保 API 只允许经过授权的请求访问&#xff0c;开发者都需要一个强大且灵活的安全框架来实现这些需求。Spring Security 作为 Spring 框架的…

MATLAB 生成指定范围、角度、厚度的含噪平面点云(77)

模拟生成点云并可视化显示,可以验证算法有效性,尤其是针对验证算法的某方面 MATLAB 生成指定范围、角度、厚度的含噪平面点云(77) 一、算法介绍二、使用步骤1.代码2.效果一、算法介绍 如题,模拟生成一组平面点云,含有噪声点,确定算法稳定性,可以指定生成平面的范围,厚…

混合A*算法

混合A*算法是一种改进版的A*算法&#xff0c;特别针对车辆动力学进行了优化。这种算法在经典A*的基础上引入了新的维度和概念&#xff0c;以生成更加实际可行的路径。 首先&#xff0c;混合A*算法不仅考虑x和y的位置&#xff0c;还引入了θ维度来表示车辆的朝向。这意味着搜索…