组合模式、代理模式(静态代理、动态代理)原理及实例

本文详细介绍了组合模式与代理模式。组合模式用于构建对象的树形结构,实现部分与整体的层次关系,使得客户端可以一致地处理单个对象和组合对象。代理模式则为对象提供一个替代品,以控制对这个对象的访问,常用于扩展对象功能。文中通过计算机组件和售票系统的例子,展示了静态代理和动态代理的实现,强调了动态代理在不修改目标对象的情况下扩展功能的优点。

组合模式

基本介绍

  1. 组合模式(Composite Pattern),又叫部分整体模式,它创建了对象组的树形结构,将对象组合成树状结构以表示“整体-部分”的层次关系。
  2. 组合模式依据树形结构来组合对象,用来表示部分以及整体层次。
  3. 这种类型的设计模式属于结构型模式。
  4. 组合模式使得用户对单个对象和组合对象的访问具有一致性,即:组合能让客
    户以一致的方式处理个别对象以及组合对象

解决的问题

  1. 组合模式解决这样的问题,当我们的要处理的对象可以生成一颗树形结构,而我们要对树上的节点和叶子进行操作时,它能够提供一致的方式,而不用考虑它是节点还是叶子
  2. 对应的示意图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DZ9Bm81V-1643880828541)(F:\StudyNotepad\img\image-20211122183230228.png)]

实例演示

现代计算机一般分为具有以下部件:键盘、显示器、机箱、鼠标。而机箱内部包括了主板、硬盘、电源等。主板是主机的心脏,其上面一般又插入了CPU,内存,显卡等设备,请用组合模式描述一台计算机,并尝试根据自己的对游戏的知识,实现一台电脑,能进行“绝地求生”游戏。

分析

我们应该先找到哪些是叶子结点,哪些是根节点。含有其他组件的是根节点(也就是能包含其他组件),最小的节点就是叶子结点,那么它只有最基本的功能。

1)UML类图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hA6k3IPk-1643880828544)(F:\StudyNotepad\img\image-20211122183510729.png)]

2)创建父类

不管它是叶子结点还是根节点它本身都是一个组件,只是能够使用的功能不同

/*
* 定义一个基本的组件,不管它其中还包含了什么首先它都是电脑的组件
* */
public abstract class OriginalComponent {
    private String name;
    private String brand;

    public OriginalComponent(String name, String brand) {
        this.name = name;
        this.brand = brand;
    }

    /*
     * 我们先以最小组件的角度来看,它可以有哪些方法,其他如CPU这些
     * 也是一个类,但是当它们继承的这个父类的时候,这些方法的使用具体
     * 还要看它们的重写。在父类中默认我们的实现是抛出异常,也就是说
     * 如果继承的类不重写这个方法,那么就是没有使用这个方法的权限。
     * */
    protected void add(OriginalComponent originalComponent){
        throw new UnsupportedOperationException();
    }

    protected void remove(OriginalComponent originalComponent){
        throw new UnsupportedOperationException();
    }

    /*
    * 输出组件的信息是基本的功能,应该是每一个组件都有的,那么我们就将这个
    * 方法定义为抽象的,每一个继承父类的子类都要实现这个方法。
    * */
    protected  abstract void print();
}
3)根节点

就如同前面所说,根节点和叶子结点的区别在于继承父类以后所能用的功能。

这里就以机箱为例。

/*
* 机箱内部也含有其他组件
* */
public class ComputerCase extends OriginalComponent{
    List<OriginalComponent> componentList = new ArrayList<>();

    public ComputerCase(String name, String brand) {
        super(name, brand);
    }

    @Override
    protected void add(OriginalComponent originalComponent) {
        componentList.add(originalComponent);
    }

    @Override
    protected void remove(OriginalComponent originalComponent) {
        componentList.remove(originalComponent);
    }

    @Override
    protected void print() {
        for (OriginalComponent originalComponent : componentList) {
            System.out.println("---机箱---");
            originalComponent.print();
        }
    }
}
4)叶子结点

叶子结点只有最基本的功能,那就是输出信息。

以其中的一个叶子结点CPU为例

public class CPU extends OriginalComponent{

    public CPU(String name, String brand) {
        super(name, brand);
    }

    @Override
    protected void print() {
        System.out.println("部件名称:"+this.getName()+"\t 部件品牌"+getBrand());
    }
}
5)组合

将每一个部件包含到管理它的类中。

public class Client {
    public static void main(String[] args) {
        Computer computer = new Computer("电脑", "pox");
        ComputerCase computerCase = new ComputerCase("机箱", "pox");
        ComputerMotherboard computerMotherboard = new ComputerMotherboard("主板", "pox");
        ComputerKeyboard computerKeyboard = new ComputerKeyboard("键盘", "pox");
        Memory memory = new Memory("内存条", "pox");

        Application application = new Application("应用", "pox");
        Game game = new Game("绝地求生", "蓝洞");

        computer.add(computerCase);
        computer.add(computerKeyboard);
        computerCase.add(computerMotherboard);
        computerMotherboard.add(memory);
        computer.add(application);
        application.add(game);

        computer.print();
    }
}
6)测试

最后一个问题,实现游戏。我们换一个想法,游戏首先是一个应用,其次它是应用中的一个组件。然而应用也是电脑的组成部分,直接将它游戏添加到组件中即可。

public class Game extends OriginalComponent{

    public Game(String name, String brand) {
        super(name, brand);
    }

    @Override
    protected void print() {
        System.out.println("游戏名称:"+this.getName()+"\t 游戏公司: "+getBrand());
        System.out.println("安装完毕~~");
        System.out.println("启动游戏....进入战场");
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sz2S1lB8-1643880828546)(F:\StudyNotepad\img\image-20211122184432580.png)]

小结

不管它包含多少组件,首先它都是这个整体的组成部分。对于包含有其他组件的组件,那么就可以实现更多的方法来包含其他组件(也就是根节点)。对于不含有其他组件的组件就是最基本的组件,那么它只需要实现最基本的功能即可。

补充

关于HashMap,源码也是一种组合模式。

首先是有一个最基本的接口Map(源码中它将我们的例子中的OriginalComponent的一些方法抽取出来创建了一个更加高的接口),在Map中就已经定义了一些如put、putAll等的方法。HashMap并不是直接实现了Map的接口,而是通过继承一个抽象类AbstractMap(就是类似我们这个例子中的OriginalComponent最原生的组件定义),在AbstractMap中实现了一些基本的方法,还有一些如put等的方法,也是默认抛出异常,如果后续继承的类要使用这些方法,那必定只有重写这些方法才能使用。

对于这个组件树中的叶子结点,在HashMap中通过内部类 - Node来实现叶子组件,当进行调用put等方法时,先会将数据包装到这个Node中然后在存放到HashMap中,实现了对组件的管理。

get()方法也是通过查找Node方式来查找。

=====HashMap部分源码,关于put======
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

=====调用putVal方法存放=====
// 部分源码
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    // 存放到Node中
    Node<K,V>[] tab; Node<K,V> p; int n, i;
}

代理模式

基本介绍

  1. 代理模式:为一个对象提供一个替身,以控制对这个对象的访问。即通过代理对象访问目标对象.这样做的好处是:可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能。
  2. 被代理的对象可以是远程对象、创建开销大的对象或需要安全控制的对象
  3. 代理模式有不同的形式, 主要有三种 静态代理、动态代理 (JDK代理、接口代理)和 Cglib代理 (可以在内存动态的创建对象,而不需要实现接口, 他是属于动态代理的范畴) 。

静态代理

静态代理在使用时,需要定义接口或者父类,被代理对象(即目标对象)与代理对象一起实现相同的接口或者是继承相同父类 。

实例演示

请使用代理模式实现一个功能:假设要实现一个去哪儿网站,其可以代理航空公司卖机票,也可以代理火车站卖火车票,还可以代理汽车站卖汽车票,分别收取票价的5%,3%, 1%作为代理费。

1)UML类图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4eiRKke1-1643880921389)(F:\StudyNotepad\img\image-20211122213327890.png)]

2)创建公共接口

代理的方法,需要相同,那么代理的对象和被代理的对象都要实现相同的接口,这里先定义接口

public interface SellTickets {
    void sell();
    // 这里getname是为了后续分辨票的类型
    String getName();
}
3)创建实现类

定义具体的票的名字和实现买票细节

public class AirTickets implements SellTickets{
    private String name = "Air";

    @Override
    public void sell() {
        System.out.println("售卖飞机票~~");
    }

    @Override
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
4)编写动态代理类

同理代理的也是同样的功能。因此也需要实现父类接口

public class GoWhereProxy implements SellTickets{
    SellTickets sellTickets;
    private String name;

    public GoWhereProxy(SellTickets sellTickets) {
        name = "去哪儿";
        this.sellTickets = sellTickets;
    }

    /*
    * 这里可以根据传入的票的类型,做出相应的功能
    * */
    @Override
    public void sell() {
        if (sellTickets.getName().equals("Air")){
            System.out.println("飞机票收取5%手续费");
            sellTickets.sell();
            System.out.println("出票~~");
        } else if (sellTickets.getName().equals("Bus")){
            System.out.println("汽车票收取1%手续费");
            sellTickets.sell();
            System.out.println("出票~~");
        } else if (sellTickets.getName().equals("Train")){
            System.out.println("火车票收取3%手续费");
            sellTickets.sell();
            System.out.println("出票~~");
        }
    }

    @Override
    public String getName() {
        return name;
    }
}
5)测试

静态代理为了实现售卖多种票的类型,则需要在公共接口中添加一个额外的属性,getName(但对于代理对象来说他实际在这个例子中是不需要这个方法的)。可不可以在不添加额外的方法的情况下实现不同票的代理售卖呢?

动态代理

public class Client {
    public static void main(String[] args) {
        AirTickets airTickets = new AirTickets();
        BusTicket busTicket = new BusTicket();
        TrainTicket trainTicket = new TrainTicket();

        /*
        * 开设三个窗口,每个窗口售卖指定类型的票
        * 分别代理飞机、汽车、火车
        * */
        GoWhereProxy goWhereProxy1 = new GoWhereProxy(airTickets);
        goWhereProxy1.sell();

        GoWhereProxy goWhereProxy = new GoWhereProxy(busTicket);
        goWhereProxy.sell();

        GoWhereProxy goWhereProxy2 = new GoWhereProxy(trainTicket);
        goWhereProxy2.sell();
    }
}
6)结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Yw6F3uCS-1643880921391)(F:\StudyNotepad\img\image-20211122214044057.png)]

小结

优点:在不修改目标对象的功能前提下, 能通过代理对象对目标功能扩展

缺点:因为代理对象需要与目标对象实现一样的接口,所以会有很多代理类

一旦接口增加方法,目标对象与代理对象都要维护

动态代理

基本介绍
  1. 代理对象,不需要实现接口,但是目标对象要实现接口,否则不能用动态代理
  2. 代理对象的生成,是利用JDK的API,动态的在内存中构建代理对象
  3. 动态代理也叫做:JDK代理、接口代理

需要实现Java底层的代理类来实现,根据接收类型来实现不同的代理。

实例测试
1)UML类图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dVgLAiQh-1643880921392)(F:\StudyNotepad\img\image-20211122215351375.png)]

2)创建公共接口

和上面的一样也需要定义一个基本接口,所有的售票类型都是售票的一种,将最基本的售票行为抽取出来为一个公共接口。

public interface SellTickets {
    void sell();
}
3)实现公共接口

这里以BusTicket为例

public class AirTicket implements SellTickets{
    @Override
    public void sell() {
        System.out.println("售卖飞机票~~");
    }
}
4)编写动态代理

底层是通过Java的代理类包来实现,具体的实现方式是通过类的反射来获取类的类型、类中的接口名、类继承的接口等一系列信息。通过获取的信息,来实现不同的传入类型,进行不同的代理。

public class DynamicProxy {
    private Object target;

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

    /*
    * 这里是在获取动态代理类,这个动态代理类会根据,传入的类的类型来进行代理
    * 就可以通过这里将票的类型不同而不同代理方式
    * 这里的判断条件就是通过类的反射来获取到类的名称。通过类的名字不同,就可以判断
    * 是不同的票的类型。
    * */
    public Object getProxyInstance(){
        return Proxy.newProxyInstance(target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        System.out.println("代理模式开始~~");
                        
                        /*
                        * target.getClass().getName().equals("dynamicproxy.AirTicket")就可以进行判断来做出不同的代理
                        * 为什么是:dynamicproxy.AirTicket,这个其实可以通过在控制台来打印获取到名称,这里我就是通过
                        * System.out.println(target.getClass().getName());来在控制台输出获取到类的名称的
                        * */
                        /*
                        * 11.30改进
                        * - 将获取类的信息改为instance判断
                        * */
                        if (target instanceof AirTicket){
                            System.out.println("收取5%的手续费");
                        } else if (target instanceof TrainTicket){
                            System.out.println("收取3%手续费");
                        } else if (target instanceof BusTicket){
                            System.out.println("收取1%手续费");
                        } else {
                            System.out.println();
                        }

                        Object invoke = method.invoke(target, args);
                        System.out.println("出票~~");
                        return invoke;
                    }
                });
    }
}
小结

就是通过反射来获取不同的代理类型,最后达到不同的代理方式。代理模式在多出源码都有使用,通过代理模式可以实现对方法的增加、扩展等,通常和反射一起使用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值