【学习笔记】设计模式之结构型模式

122 阅读8分钟

本文内容包含:装饰者模式、适配器模式、桥接模式、组合模式、外观模式、享元模式、代理模式

1 装饰者模式

  该模式的4个基本角色:

  • 基本抽象组件(Component):一个抽象类或者接口,由具体构件抽象装饰者继承或实现
  • 具体构件(ConcreteComponent):被装饰者
  • 抽象装饰者(Decorator):装饰者的抽象类,属性中包含Component对象
  • 具体装饰者(ConcreteDecorator):动态地为被装饰者添加额外功能

  UML类图如下所示:

020-01.png

1.1 例子

  需求:咖啡店的主要卖咖啡,但客户可以动态添加调料品(牛奶、糖等)

1、编写基本抽象组件

public abstract class Component {
    public abstract String show();
}

2、编写被装饰者,即咖啡类

public class Coffee extends Component {
    @Override
    public String show() {
        return "咖啡";
    }
}

3、编写抽象装饰者和具体装饰者(牛奶、糖)

public abstract class Decorator extends Component {
    private Component component;

    public Component getComponent() {
        return component;
    }

    public void setComponent(Component component) {
        this.component = component;
    }
}
public class Milk extends Decorator {
    public Milk(Component component) {
        this.setComponent(component);

    }

    @Override
    public String show() {
        return this.getComponent().show() + " 牛奶";
    }
}

public class Sugar extends Decorator {
    public Sugar(Component component) {
        this.setComponent(component);
    }

    @Override
    public String show() {
        return this.getComponent().show() + " 糖";
    }
}

4、编写测试类

public class Test {
    public static void main(String[] args) {
        Coffee coffee = new Coffee();
        Milk milk = new Milk(coffee);
        Milk milk2 = new Milk(milk);
        Sugar sugar = new Sugar(milk2);
        System.out.println(sugar.show());
    }
}

1.2 总结

  装饰者模式,可以不改动原有业务,在原有业务的基础上去扩展相关功能,达到开闭原则。Java中的IO流对象也使用了装饰者模式。

2 适配器模式

  • 将一个类的接口转换成另一种接口,让原来接口不兼容的类可以兼容
  • 用户是看不到被适配者(Source)
  • 用户是调用适配器(Adapter)转换出来的目标(Dest)接口方法,适配器再调用被适配者的相关方法
  • 可分为类适配器模式、对象适配器模式、接口(缺省)适配器模式

2.1 类适配器模式

2.1.1 例子

  需求:一般家用电压为220V,但假设某个手机的充电电压为5V,则需要适配器来转换

1、编写目标接口

public interface IVoltage5V {
    /**
     * 目标接口方法
     */
    int output5V();
}

2、编写被适配者

public class Voltage220V {
    public int output220V() {
        System.out.println("适配前电压为220V");
        return 220;
    }
}

3、编写适配器

public class Adapter extends Voltage220V implements IVoltage5V {
    @Override
    public int output5V() {
        int srcV = output220V();
        // 这里就相当于在做适配工作
        int destV = srcV / 44;
        return destV;
    }
}

4、编写用户,即手机

public class Phone {
    public void charge(IVoltage5V iVoltage5V) {
        if (5 == iVoltage5V.output5V()) {
            System.out.println("适配后电压为5V,可以充电");
        } else {
            System.out.println("适配后电压不为5V,不可以充电");
        }
    }
}

5、测试类

public class Test {
    public static void main(String[] args) {
        Phone phone = new Phone();
        phone.charge(new Adapter());
    }
}

2.1.2 总结

  • 该模式中适配器需要继承Source类,且目标是接口,有一定的局限性
  • Source类的方法会在Adapter中暴露出来,增加了使用成本

2.2 对象适配器模式

  基本思路与类适配器相同,只是修改Adapter类,让它不再继承Source类,而是在属性中拥有Source类。

  在上述例子的基础下进行修改:

1、修改适配器

public class Adapter implements IVoltage5V {
    private Voltage220V voltage220V;

    public Adapter(Voltage220V voltage220V) {
        this.voltage220V = voltage220V;
    }

    @Override
    public int output5V() {
        if (null != voltage220V) {
            int srcV = voltage220V.output220V();
            // 这里就相当于在做适配工作
            int destV = srcV / 44;
            return destV;
        }
        return -1;
    }
}

2、修改测试类

public class Test {
    public static void main(String[] args) {
        Phone phone = new Phone();
        phone.charge(new Adapter(new Voltage220V()));
    }
}

2.3 接口适配器模式

  • 目标接口中不只一个方法
  • 适配器是一个抽象类,实现目标接口,并重写所有接口方法,但都是空处理或简单处理
  • 使用适配器时,需要哪个方法才具体重写那个方法

  例子改进:

1、目标接口类

public interface IVoltage {
    int output5V();
    int output10V();
    int output15V();
}

2、被适配者

public abstract class VoltageSource {
    public abstract int output();
}

public class Voltage220V extends VoltageSource{
    @Override
    public int output() {
        System.out.println("适配前电压为220V");
        return 220;
    }
}

3、适配器

public abstract class Adapter implements IVoltage {
    private VoltageSource voltageSource;

    public Adapter(VoltageSource voltageSource) {
        this.voltageSource = voltageSource;
    }

    public VoltageSource getVoltageSource() {
        return voltageSource;
    }

    @Override
    public int output5V() { return -1; }

    @Override
    public int output10V() { return -1; }

    @Override
    public int output15V() { return -1; }
}

4、手机类

public class Phone {
    public void charge(IVoltage iVoltage) {
        if (5 == iVoltage.output5V()) {
            System.out.println("适配后电压为5V,可以充电");
        } else {
            System.out.println("适配后电压不为5V,不可以充电");
        }
    }
}

5、测试类

public class Test {
    public static void main(String[] args) {
        Phone phone = new Phone();
        phone.charge(new Adapter(new Voltage220V()) {
            @Override
            public int output5V() {
                int srcV = this.getVoltageSource().output();
                return srcV / 44;
            }
        });
    }
}

UML类图如下所示:

020-02.png

3 桥接模式

   当一个类需要在两个以上维度进行扩展时,如果采用继承方式会导致子类数量过多,于是引进桥接模式。该模式涉及的角色有:

  • 抽象化(Abstraction)角色:抽象化给出的定义,并保存一个对实现化对象的引用。充当“桥”,把抽象化和实现化结合起来
  • 修正抽象化(RefinedAbstraction)角色:扩展抽象化角色,改变和修正父类对抽象化的定义
  • 实现化(Implementor)角色:这个角色给出实现化角色的接口,但不给出具体的实现。必须指出的是,这个接口不一定和抽象化角色的接口定义相同,实际上,这两个接口可以非常不一样。实现化角色应当只给出底层操作,而抽象化角色应当只给出基于底层操作的更高一层的操作
  • 具体实现化(ConcreteImplementor)角色:这个角色给出实现化角色接口的具体实现

  UML类图如下所示:

020-03.png

3.1 例子

  需求:吃烧烤,可以分成食物和风味两个维度,如食物有鸡翅、鸡腿等;风味有微辣、特辣等。

1、编写实现化部分

public interface Taste {
    String taste();
}

public class ExtraSpicy implements Taste {
    @Override
    public String taste() {
        return "特辣";
    }
}

public class SlightlySpicy implements Taste {
    @Override
    public String taste() {
        return "微辣";
    }
}

2、编写抽象化部分

public abstract class Food {
    protected Taste taste;
    // 通过这个构造方法,把抽象化和实例化组合起来
    public Food(Taste taste) {
        this.taste = taste;
    }
    public abstract void eat();
}

public class ChickenWings extends Food {

    public ChickenWings(Taste taste) {
        super(taste);
    }

    @Override
    public void eat() {
        System.out.println(super.taste.taste() + "鸡翅");
    }
}

public class Drumsticks extends Food {

    public Drumsticks(Taste taste) {
        super(taste);
    }

    @Override
    public void eat() {
        System.out.println(super.taste.taste() + "鸡腿");
    }
}

3、测试类

public class Test {
    public static void main(String[] args) {
        new ChickenWings(new ExtraSpicy()).eat();
        new Drumsticks(new SlightlySpicy()).eat();
    }
}

3.2 总结

  当系统可能有多个角度分类,且每一种角度都可能变化,若此时仍使用继承则会造成类爆炸问题,扩展起来不灵活。这是可以使用桥接模式把这种多角度分类分离出来,让它们独立变化,减少它们之间耦合。但桥接模式的引入会增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计与编程。

4 组合模式

  组合模式在树型结构的问题中,模糊了简单元素和复杂元素的概念,客户程序可以像处理简单元素一样来处理复杂元素,从而使得客户程序与复杂元素的内部结构解耦。主要有以下角色:

  • 抽象构件(Component):可以是接口或者抽象类,是叶子构件和容器构件的接口,包含子类行为的声明和实现
  • 叶子构件(Leaf):叶子节点没有子节点,是最低级的节点
  • 容器构件(Composite):容器构件中含所有子节点,节点可以是子节点,也可以是容器节点,是一个集合,用来存储节点的。好比一个文件夹,文件夹好比一个容器,用来提供一个容器,它实现了抽象构件定义的行为方法,也可以调用子节点的方法

  UML类图如下所示:

020-04.png

4.1 例子

  需求:一个学校有多个学院,一个学院又有多个专业,用组合模式来管理

1、编写抽象构件

public abstract class Component {
    private String name;

    public String getName() {
        return name;
    }

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

    public void add(Component component) {
        // 叶子节点没有对子节点的操作,所以不写成抽象方法
    }
    public void delete() {
        // 叶子节点没有对子节点的操作,所以不写成抽象方法
    }
    public abstract void show();
}

2、编写大学类

public class University extends Component {

    private List<Component> colleges = new ArrayList<>();

    public University(String name) {
        this.setName(name);
    }

    @Override
    public void add(Component component) {
        colleges.add(component);
    }

    @Override
    public void delete() {
        colleges.remove(0);
    }

    @Override
    public void show() {
        System.out.println("-----" + getName() + "-----");
        for (Component c : colleges) {
            c.show();
        }
    }
}

2、编写学院类

public class College extends Component {
    private List<Component> majors = new ArrayList<>();

    public College(String name) {
        this.setName(name);
    }

    @Override
    public void add(Component component) {
        majors.add(component);
    }

    @Override
    public void delete() {
        majors.remove(0);
    }

    @Override
    public void show() {
        System.out.println("--" + getName() + "--");
        for (Component c : majors) {
            c.show();
        }
    }
}

3、编写专业类

public class Major extends Component {
    public Major(String name) {
        this.setName(name);
    }

    @Override
    public void show() {
        System.out.println(getName());
    }
}

4、编写客户端

public class Client {
    public static void main(String[] args) {
        University university = new University("XX大学");

        College college1 = new College("计通学院");
        college1.add(new Major("计科专业"));
        college1.add(new Major("网络专业"));
        college1.add(new Major("软件专业"));

        College college2 = new College("土木学院");
        college2.add(new Major("土木专业"));
        college2.add(new Major("材料专业"));
        college2.add(new Major("建筑专业"));

        university.add(college1);
        university.add(college2);
        university.show();
    }
}

4.2 总结

  组合模式的使用场景:

  • 想表示对象的部分-整体层次结构(树形结构)
  • 用户忽略组合对象与单个对象的不同,可以统一地使用组合结构中的所有对象

5 外观模式

  外观模式(Facade Pattern)隐藏系统的复杂性,并向客户端提供了一个客户端可以访问系统的接口。这种类型的设计模式属于结构型模式,它向现有的系统添加一个接口,来隐藏系统的复杂性。

5.1 例子

  电脑开机其实是复杂的过程,启动CPU、内存、硬盘等等,但是用户不需要知道,用户只需简单按一下电源键即可,这其实就是外观模式。

1、电脑组件类

public interface Component {
    void open();
}

public class CPU implements Component {
    @Override
    public void open() {
        System.out.println("启动CPU");
    }
}

public class Memory implements Component {
    @Override
    public void open() {
        System.out.println("启动内存");
    }
}

public class SSD implements Component {
    @Override
    public void open() {
        System.out.println("启动固体硬盘");
    }
}

2、电脑类

public class Computer {
    private CPU cpu = new CPU();
    private Memory memory = new Memory();
    private SSD ssd = new SSD();

    public void open() {
        cpu.open();
        memory.open();
        ssd.open();
    }
}

3、客户端

public class Client {
    public static void main(String[] args) {
        Computer computer = new Computer();
        computer.open();
    }
}

5.2 总结

  • 客户端不需要知道系统内部的复杂联系,整个系统只需提供一个"接待员"即可
  • web开发的MVC架构其实就使用了外观模式

6 享元模式

  享元模式是对象池的一种实现。将多个对同一对象的访问集中起来,不必为每个访问者创建一个单独的对象,以此来降低内存的消耗。

  享元模式把一个对象的状态分成内部状态和外部状态,内部状态即是不变的,外部状态是变化的;然后通过共享不变的部分,达到减少对象数量并节约内存的目的。

6.1 例子

  模拟车站买票,买票前,很多人都会查是否有票,而这些票的信息(出发地、目的地等)都相同,若频繁地创建相似对象会浪费资源,这时就可以使用享元模式。

1、票类

public interface Ticket {
    void show();
}

public class TrainTicket implements Ticket {
    private String from;
    private String to;

    public TrainTicket(String from, String to) {
        this.from = from;
        this.to = to;
    }

    @Override
    public void show() {
        System.out.println("出发地:" + from + " 目的地:" + to);
    }
}

2、车站类

public class Station {
    private Map<String, Ticket> tickets;

    private static Station instance;

    private Station() {
        this.tickets = new HashMap<>();
    }

    public static Station getInstance() {
        if (null == instance) {
            instance = new Station();
        }
        return instance;
    }

    public Ticket getTicket(String from, String to) {
        String key = from + ":" + to;
        if (tickets.containsKey(key)) {
            return tickets.get(key);
        } else {
            Ticket ticket = new TrainTicket(from, to);
            tickets.put(key, ticket);
            return ticket;
        }
    }
}

3、用户

public class Client {
    public static void main(String[] args) {
        Station station = Station.getInstance();
        Ticket ticket1 = station.getTicket("深圳", "广州");
        Ticket ticket2 = station.getTicket("深圳", "广州");
        ticket1.show();
        System.out.println(ticket1 == ticket2);
    }
}

6.2 总结

  • 享元模式可以极大减少内存中对象的数量,使得相同对象或相似对象在内存中只保存一份,降低内存占用,增强程序的性能
  • 享元模式的外部状态相对独立,而且不会影响其内部状态,从而使得享元对象可以在不同的环境中被共享

7 代理模式

  为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用,且代理对象可以在不修改目标对象方法的前提下对其方法进行扩展。可分为静态代理、动态代理、Cglib代理。

  比如:我们想听明星唱歌,一般是不能接触到明星请他唱的,经常是通过他的经理来进行预约的,这里的经理就起到代理的作用。

7.1 静态代理

1、目标类

/*
  这个接口是目标对象和代理对象都要实现的
 */
public interface Singer {
    void sing();
}

public class JayChou implements Singer {
    @Override
    public void sing() {
        System.out.println("周杰伦唱歌...");
    }
}

2、代理类

public class SingerProxy implements Singer {
    private Singer singer;

    public SingerProxy(Singer singer) {
        this.singer = singer;
    }

    @Override
    public void sing() {
        System.out.println("演唱前的预约工作...");
        singer.sing();
        System.out.println("演唱后的收尾工作...");
    }
}

3、测试类

public class Test {
    public static void main(String[] args) {
        SingerProxy singerProxy = new SingerProxy(new JayChou());
        singerProxy.sing();
    }
}

7.2 动态代理

1、目标类

public interface Star {
    void sing();
    void act();
}

public class JackyCheung implements Star{

    @Override
    public void sing() {
        System.out.println("张学友唱歌...");
    }

    @Override
    public void act() {
        System.out.println("张学友拍戏...");
    }
}

2、自定义事件处理器

public class MyInvocationHandler implements InvocationHandler {
    private Star star;

    public MyInvocationHandler(Star star) {
        this.star = star;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (method.getName().equals("sing")) {
            System.out.println("预约明星唱歌..");
            Object returnValue = method.invoke(star, args);
            System.out.println("收尾工作...");
            return returnValue;
        } else {
            System.out.println("预约明星拍戏..");
            Object returnValue = method.invoke(star, args);
            System.out.println("收尾工作...");
            return returnValue;
        }
    }
}

3、客户端

public class Client {
    public static void main(String[] args) {
        MyInvocationHandler myInvocationHandler = new MyInvocationHandler(new JackyCheung());
        /*
            Proxy.newProxyInstance的三个参数
             1.ClassLoader loader:目标对象使用的类加载器,获取加载器的方法是固定的
             2.Class<?>[] interfaces:目标对象实现的接口的类型
             3.InvocationHandler h:事件处理器,当执行目标对象的方法时,会触发事件处理器的方法
         */
        Star starProxy = (Star) Proxy.newProxyInstance(Star.class.getClassLoader(),
                new Class[]{Star.class},
                myInvocationHandler);
        starProxy.sing();
        starProxy.act();
    }
}

7.3 Cglib代理

  Cglib代理可以算作是动态代理的一种,但是它的目标对象不需要实现接口。

1、导入依赖

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.3.0</version>
</dependency>

2、目标类

public class JackyCheung {

    public void sing() {
        System.out.println("张学友唱歌...");
    }

    public void act() {
        System.out.println("张学友拍戏...");
    }
}

3、代理工厂

public class ProxyFactory implements MethodInterceptor {
    private Object target;

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

    @Override
    public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        System.out.println("cglib代理开始...");

        Object returnValue = method.invoke(target, args);

        System.out.println("cglib代理结束...");

        return returnValue;
    }

    /**
     * 返回target的代理对象
     */
    public Object getProxyInstance() {
        // 1.创建工具类
        Enhancer enhancer = new Enhancer();
        // 2.设置父类
        enhancer.setSuperclass(target.getClass());
        // 3.设置回调函数
        enhancer.setCallback(this);
        // 4.创建子类对象,即代理对象
        return enhancer.create();
    }
}

4、客户端

public class Client {
    public static void main(String[] args) {
        ProxyFactory proxyFactory = new ProxyFactory(new JackyCheung());
        JackyCheung proxyInstance = (JackyCheung) proxyFactory.getProxyInstance();
        proxyInstance.act();
        proxyInstance.sing();
    }
}

8 相关链接