Java基础

设计模式3.结构型模式

2018-12-02  本文已影响1人  卢卡斯哔哔哔

点击进入我的博客

3.1 适配器模式

适配器模式把一个类的接口变换成客户端所期待的另一种接口,使得原本因接口不匹配而无法在一起工作的两个类能够在一起工作。

3.1.1 类的适配器结构

类的适配器
interface Target {
    void operation1();
    void operation2();
}

class Adaptee {
    public void operation1() {}
}

class Adapter extends Adaptee implements Target {
    @Override
    public void operation2() {}
}
类的适配器
类的适配器效果

3.1.2 对象的适配器结构

对象适配器
interface Target {
    void operation1();
    void operation2();
}

class Adaptee {
    public void operation1() {}
}

class Adapter implements Target {
    private Adaptee adaptee;

    public Adapter(Adaptee adaptee) {
        this.adaptee = adaptee;
    }

    @Override
    public void operation1() {
        adaptee.operation1();
    }

    @Override
    public void operation2() {
        // do something
    }
}
对象的适配器
对象的适配器效果

3.1.3 细节

使用场景
  1. 系统需要使用现有的类,而此类的接口不符合系统的需要
  2. 想要建立一个可以重复使用的类,用于与一些彼此间没有太大关联的一些类,包括一些可能在将来引进的类一起工作。
  3. 对对象的适配器儿模式而言,在设计里,需要改变多个已有的子类的接口,如果使用类的适配器模式,就需要针对每个子类做一个适配器类,这不太实际。
优点
  1. 可以让任何两个没有关联的类一起运行。
  2. 提高了类的复用。
  3. 增加了类的透明度。
  4. 灵活性好。
缺点
  1. 过多地使用适配器,会让系统非常零乱,不易整体进行把握。比如,明明看到调用的是 A 接口,其实内部被适配成了 B 接口的实现,一个系统如果太多出现这种情况,无异于一场灾难。因此如果不是很有必要,可以不使用适配器,而是直接对系统进行重构。
  2. 由于 JAVA 至多继承一个类,所以至多只能适配一个适配者类,而且目标类必须是抽象类。
注意点
  1. 目标接口可以忽略,此时目标接口和源接口实际上是相同的
  2. 适配器类可以是抽象类
  3. 可以有带参数的适配器模式

3.1.4 一个充电器案例

// 充电器只能接受USB接口
public class Charger {
    public static void main(String[] args) throws Exception{
        USB usb = new SuperAdapter(new TypeC());
        connect(usb);
        usb = new SuperAdapter(new Lightning());
        connect(usb);
    }

    public static void connect(USB usb) {
        usb.power();
        usb.data();
    }
}

// 充电器的接口都是USB的,假设有两个方法分别是电源和数据
interface USB {
    void power();
    void data();
}

// IOS的Lightning接口
class Lightning {
    void iosPower() {
        System.out.println("IOS Power");
    }
    void iosData() {
        System.out.println("IOS Data");
    }
}

// TYPE-C接口
class TypeC {
    void typeCPower() {
        System.out.println("TypeC Power");
    }
    void typeCData() {
        System.out.println("TypeC Data");
    }
}

// 超级适配器,可以适配多种手机机型
class SuperAdapter implements USB {
    private Object obj;

    public SuperAdapter(Object obj) {
        this.obj = obj;
    }

    @Override
    public void power() {
        if(obj.getClass() == Lightning.class) {
            ((Lightning)obj).iosPower();
        } else if(obj.getClass() == TypeC.class) {
            ((TypeC)obj).typeCPower();
        }
    }

    @Override
    public void data() {
        if(obj.getClass() == Lightning.class) {
            ((Lightning)obj).iosData();
        } else if(obj.getClass() == TypeC.class) {
            ((TypeC)obj).typeCData();
        }
    }
}

3.2 缺省适配模式

缺省适配模式为一个接口提供缺省实现,这样子类型可以从这个缺省实现进行扩展,而不必从原有接口进行扩展。

3.2.1 缺省适配模式结构

缺省适配模式
简单的例子
// 和尚
interface Monk {
    void practiceKungfu();
    void chantPrayer();
}

abstract class MonkAdapter implements Monk {
    @Override
    public void practiceKungfu() {}

    @Override
    public void chantPrayer() {}
}

class LuZhiShen extends MonkAdapter {
    @Override
    public void practiceKungfu() {
        System.out.println("拳打镇关西");
    }
}

3.2.2 细节

使用场景
作用

缺省适配器模式可以使所需要的类不必实现不需要的接口。

核心点

3.3 组合模式

组合模式,就是在一个对象中包含其他对象,这些被包含的对象可能是终点对象(不再包含别的对象),也有可能是非终点对象(其内部还包含其他对象)。
我们将对象称为节点,即一个根节点包含许多子节点,这些子节点有的不再包含子节点,而有的仍然包含子节点,以此类推。很明显,这是树形结构,终结点叫叶子节点,非终节点叫树枝节点,第一个节点叫根节点。

3.3.1 安全式的合成模式结构

安全式的合成模式要求管理集合的方法只出现在树枝结点(Composite)中,而不出现在树叶结点中。


安全式的合成模式

3.3.2 透明的合成模式结构

透明的合成模式

透明的合成模式要求所有的具体构建类,都符合一个固定的接口。

3.4 装饰器模式

装饰器模式(Decorator)允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。

3.4.1 装饰器结构

装饰器模式

3.4.2 装饰器细节

使用场景
  1. 需要扩展一个类的功能
  2. 需要动态地给一个对象增加功能,这些功能可以再动态的插销
  3. 需要增加由一些基本功能的排列组合而产生非常大量的功能
优点
  1. 更加灵活:装饰模式和继承关系的目的都是要扩展对象的功能,但是装饰模式比继承更加灵活
  2. 多样性:通过使用不同具体装饰类及其排列组合,可以创造出不同的行为
  3. 动态扩展:装饰器可以动态扩展构件类
缺点
  1. 会产生比继承关系更多的对象
  2. 比继承更加容易出错
注意点
  1. 装饰类的接口必须与被装饰类的接口相容。
  2. 尽量保持抽象构件(Component)简单。
  3. 可以没有抽象的(Component),此时装饰器(Decorator)一般是具体构件(Concrete Component)的一个子类。
  4. 装饰(Decorator)和具体装饰(Concrete Decorator)可以合并。
InputStream及其子类
InputStream
OutputStream及其子类

也用到类装饰器模式

3.4.3 例子

// Component:一个艺人
interface Artist {
    void show();
}

// Concrete Component:一个歌手
class Singer implements Artist {
    @Override
    public void show() {
        System.out.println("Let It Go");
    }
}

// 装饰后的歌手:不仅会唱歌,还会讲笑话和跳舞
class SuperSinger implements Artist {
    private Artist role;
    
    public SuperSinger(Artist role) {
        this.role = role;
    }
    @Override
    public void show() {
        System.out.println("Tell Jokes!");
        role.show();
        System.out.println("Dance!");
    }
}

3.5 代理模式

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

3.5.1 代理模式结构

代理模式
public class Test {
    public static void main(String[] args) {
        Subject subject = new RealSubject();
        Subject proxy = new ProxySubject(subject);
        proxy.request(); // 此处通过代理类来执行
    }
}

interface Subject {
    void request();
}

class RealSubject implements Subject {
    @Override
    public void request() {
        System.out.println("RealSubject");
    }
}

class ProxySubject implements Subject {
    private Subject subject;

    public ProxySubject(Subject subject) {
        this.subject = subject;
    }

    @Override
    public void request() {
        System.out.println("ProxySubject");
    }
}

3.5.2 动态代理

自从JDK 1.3以后,Java在java.lang.reflect库中提供了一下三个类直接支持代理模式:ProxyInvocationHanderMethod

动态代理步骤
  1. 创建一个真实对象
  2. 创建一个与真实对象有关的调用处理器对象InvocationHandler
  3. 创建代理,把调用处理器和要代理的类联系起来Proxy.newInstance()
  4. 在调用处理对象的invoke()方法中执行相应操作
public class Test {
    public static void main(String[] args) {
        // 创建要被代理的实例对象
        Subject subject = new RealSubject();
        // 创建一个与被代理实例对象有关的InvocationHandler
        InvocationHandler handler = new ProxySubject(subject);
        // 创建一个代理对象来代理subject,被代理的对象subject的每个方法执行都会调用代理对象proxySubject的invoke方法
        Subject proxySubject = (Subject) Proxy.newProxyInstance(Subject.class.getClassLoader(), new Class[]{Subject.class}, handler);
        // 代理对象执行
        proxySubject.request();
    }
}

interface Subject {
    void request();
}

class RealSubject implements Subject {
    @Override
    public void request() {
        System.out.println("RealSubject");
    }
}

class ProxySubject implements InvocationHandler {
    private Subject subject;

    public ProxySubject(Subject subject) {
        this.subject = subject;
    }

    /**
     * @param proxy 要代理的
     * @param method
     * @param args
     * @return
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Before Proxy");
        Object obj = method.invoke(subject, args);
        System.out.println("After Proxy");
        return obj;
    }
}

3.5.3 细节

优点
  1. 代理类和真实类分离,职责清晰。
  2. 在不改变真是累代码的基础上扩展了功能。
缺点
  1. 由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢。
  2. 实现代理模式需要额外的工作,有些代理模式的实现非常复杂。
和适配器模式的关系

适配器模式的用意是改变所考虑对象的接口,而代理模式不能改变。

和装饰模式
虚拟代理

3.6 享元模式

享元模式以共享的方式高效地支持大量的细粒度对象。

3.6.1 单纯享元模式

单纯享元模式中,所有的享元对象都是可以共享的。


单纯享元模式

3.6.2 复合享元模式

复合享元模式

3.6.3 细节

内蕴状态和外蕴状态

内蕴状态:是存储在享元对象内部的,不会随环境改变而改变的。一个享元可以具有内蕴状态并可以共享。
外蕴状态:随环境改变而改变、不可以共享的状态。享元对象的外蕴状态必须由客户端保存,并在享元对象被创建之后,在需要使用的时候再传入到享元对象内部。

不变模式

享元模式中的对象不一定非要是不变对象,但大多数享元对象的确是这么设计的。

享元工厂
  1. 使用单例模式:一般只需要一个享元工厂,可以设计成单例的。
  2. 备忘录模式:享元工厂负责维护一个表,通过这个表把很多相同的实例与它们的一个对象联系起来。
优点

减少对象的创建,降低内存消耗

缺点
  1. 提高了系统的复杂度,为了使对象可以共享,需要将一些状态外部化
  2. 需要将一些状态外部化,而读取外部状态是的运行时间稍微变长
使用场景
  1. 一个系统中有大量对象。
  2. 这些对象消耗大量内存。
  3. 这些对象的状态大部分可以外部化。
  4. 这些对象可以按照内蕴状态分为很多组,当把外蕴对象从对象中剔除出来时,每一组对象都可以用一个对象来代替。
  5. 系统不依赖于这些对象身份,换言之,这些对象是不可分辨的。
Java应用
  1. String对象,有则返回,没有则创建一个字符串并保存
  2. 数据库的连接池

3.6.4 案例

依旧是熟悉的KFC点餐为例:

public class KFC {
    public static void main(String[] args) {
        OrderFactory orderFactory = OrderFactory.getInstance();
        Order order = orderFactory.getOrder(Food.MiniBurger);
        order.operation("李雷");
        order = orderFactory.getOrder(Food.MiniBurger);
        order.operation("韩梅梅");
    }
}

enum Food {
    MiniBurger,
    MexicanTwister,
    CornSalad,
    HotWing,
    PepsiCola
}

// Flyweight角色
interface Order {
    // 传入的是外蕴对象:顾客
    void operation(String customer);
}

// ConcreteFlyweight角色
class FoodOrder implements Order {
    // 内蕴状态
    private Food food;
    // 构造方法,传入享元对象的内部状态的数据
    public FoodOrder(Food food) {
        this.food = food;
    }

    @Override
    public void operation(String customer) {
        System.out.println("顾客[" + customer + "]点的是" + food.toString());
    }
}

// FlyweightFactory角色
class OrderFactory {
    private Map<Food, Order> orderPool = new HashMap<>();
    private static OrderFactory instance = new OrderFactory();

    private OrderFactory() {}

    public static OrderFactory getInstance() {
        return instance;
    }

    // 获取Food对应的享元对象
    public Order getOrder(Food food) {
        Order order = orderPool.get(food);
        if (null == order) {
            order = new FoodOrder(food);
            orderPool.put(food, order);
        }
        return order;
    }
}

3.7 门面模式

门面模式(Facade Pattern)要求一个子系统的外部与其内部通信,必须通过一个统一的门面对象进行。

3.7.1 门面模式结构

门面模式没有一个一般化的类图描述,可以用下面的例子来说明。


门面模式

3.7.2 细节

门面数量

通常只需要一个门面类,而且只有一个实例,因此可以设计称单例模式。当然也可有多个类。

使用场景
  1. 为一个复杂的子系统提供一个简单的接口
  2. 使子系统和外部分离开来
  3. 构建一个层次化系统时,可以使使用Facade模式定义系统中每一层,实现分层。
优点
  1. 减少系统之间的相互依赖。
  2. 提高了安全性。
缺点
  1. 不符合开闭原则
  2. 如果要改东西很麻烦,继承重写都不合适。
Java例子

MVC三层结构

3.7.3 KFC例子

假如没有服务员(门面),顾客(外部系统)要点一个套餐需要知道每个套餐包含的食物(子系统)种类,这样就会非常麻烦,所以最好的方式是直接告诉服务员套餐名称就好了。

public class Customer {
    public static void main(String[] args) {
        Waiter waiter = new Waiter();
        List<Food> foodList = waiter.orderCombo("Combo1");
    }
}

abstract class Food {}
class MiniBurger extends Food {}
class MexicanTwister extends Food {}
class CornSalad extends Food {}
class HotWing extends Food {}
class PepsiCola extends Food {}

class Waiter {
    public List<Food> orderCombo(String comboName) {
        List<Food> foodList;
        switch (comboName) {
            case "Combo1" : 
                foodList = Arrays.asList(new MiniBurger(), new CornSalad(), new PepsiCola()); 
                break;
            case "Combo2":
                foodList = Arrays.asList(new MexicanTwister(), new HotWing(), new PepsiCola());
                break;
            default:
                foodList = new ArrayList<>();
        }
        return foodList;
    }
}

3.8 过滤器模式

过滤器模式使用不同的条件过滤一组对象,并通过逻辑操作以解耦方式将其链接。这种类型的设计模式属于结构模式,因为该模式组合多个标准以获得单个标准。

3.8.1 细节

步骤
  1. 创建一个要过滤的普通类,要有获得其私有属性的get方法
  2. 创建一个接口,规定过滤方法
  3. 实现接口,可以依需要重写过滤方法,参数传递的一般是存储过滤类的容器类
  4. 复杂过滤类可以通过设置传递接口参数(复用其他基础过滤类)来实现多重过滤
Java8

Java8中的lambda表达式可以更简单的实现过滤器

List<Movie> movies = Stream.of(
                new Movie("大话西游","comedy"),
                new Movie("泰囧", "comedy"),
                new Movie("禁闭岛", "suspense"))
                .filter(var -> "comedy".equals(var.getType()))
                .collect(Collectors.toList());

3.8.2 电影的例子

  1. 创建被过滤的类Movie,根据它的type属性实现过滤
  2. 创建接口Criteria,规定过滤方法
  3. 创建喜剧电影过滤器ComedyMovieCriteria,根据comedy==movie.type来过滤出需要的喜剧电影
public class Test {
    public static void main(String[] args) {
        List<Movie> movies = new ArrayList(){{
            add(new Movie("大话西游","comedy"));
            add(new Movie("泰囧", "comedy"));
            add(new Movie("禁闭岛", "suspense"));
        }};
        System.out.println(new ComedyMovieCriteria().meetCriteria(movies));
    }
}

// 被筛选的对象
class Movie {
    private String name;
    // 电影类型
    private String type;

    public Movie(String name, String type) {
        this.name = name;
        this.type = type;
    }
    // getters & setters & toString
}

// 过滤器接口
interface Criteria {
    /**
     * @param movies 要被筛选的电影
     * @return 筛选后的结果
     */
    List<Movie> meetCriteria(List<Movie> movies);
}

// 过滤喜剧电影的过滤器,要求是movie.type==comedy
class ComedyMovieCriteria implements Criteria {
    @Override
    public List<Movie> meetCriteria(List<Movie> movies) {
        List<Movie> result = new ArrayList<>();
        for (Movie movie : movies) {
            if ("comedy".equals(movie.getType())) {
                result.add(movie);
            }
        }
        return result;
    }
}

3.9 桥接模式

桥接模式是将抽象化实现化解耦,使两者可以独立地变化。桥接模式有助于理解面向对象的设计原则,包括开闭原则以及组合聚合复用原则

3.9.1 桥接模式结构

桥接模式
这个系统含有两个等级结构
桥接模式所涉及的角色

3.9.2 细节

抽象化、实现化、解耦

抽象化:存在于多个实体中的共同的概念性联系;通过忽略一些信息,把不同的实体当作相同的实体来对待。
实现化:抽象化给出的具体实现就是实现化。一个类的实例就是这个类的实现化,一个子类就是它超类的实现化。
解耦:耦合就是两个实体的某种强关联,把它们的强关联去掉就是解耦。
强关联与弱关联:所谓强关联,就是在编译期已经确定的,无法在运行期动态改变的关联;所谓弱关联,就是可以动态地确定并且可以在运行期动态地改变的关联。继承是强关联,而聚合关系是弱关联

核心理解

桥接模式中的脱耦,就是在抽象化和实现化之间使用组合关系而不是继承关系,从而使两者可以相对独立的变化。

优点
  1. 实现抽象化和实现化的分离。
  2. 提高了代码的扩展能力。
  3. 实现细节对客户透明。
缺点
  1. 桥接模式的引入会增加系统的理解与设计难度
  2. 由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计与编程。
使用场景
  1. 如果一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性,避免在两个层次之间建立静态的继承联系,通过桥接模式可以使它们在抽象层建立一个关联关系。
  2. 抽象化角色和实现化角色可以以继承的方式独立扩展而互不影响,在程序运行时可以动态将一个抽象化子类的对象和一个实现化子类的对象进行组合,即系统需要对抽象化角色和实现化角色进行动态耦合。
  3. 一个类存在两个独立变化的维度,且这两个维度都需要进行扩展。
  4. 虽然在系统中使用继承是没有问题的,但是由于抽象化角色和具体化角色需要独立变化,设计要求需要独立管理这两者。
  5. 对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统,桥接模式尤为适用
Java例子

大多数的驱动器(Driver)都是桥接模式的应用,使用驱动程序的应用系统就是抽象化角色,而驱动器本身扮演实现化角色。


JDBC驱动器

3.9.3 发送消息的案例

// Implementor角色
interface SendMsg {
    void sendMsg();
}

// Concrete Implementor角色
class EmailSendMsg implements SendMsg {
    @Override
    public void sendMsg() {
        System.out.println("Send Msg By Email");
    }
}

// Concrete Implementor角色
class WeChatSendMsg implements SendMsg {
    @Override
    public void sendMsg() {
        System.out.println("Send Msg By WeChat");
    }
}

// Abstraction 角色
abstract class Send {
    protected SendMsg sendMsg;

    public Send(SendMsg sendMsg) {
        this.sendMsg = sendMsg;
    }

    public abstract void send();
}

// Concrete Implementor角色
class ImmediatelySend extends Send {
    public ImmediatelySend(SendMsg sendMsg) {
        super(sendMsg);
    }

    @Override
    public void send() {
        sendMsg.sendMsg();
        System.out.println("Send Msg Immediately");
    }
}

// Concrete Implementor角色
class DelayedlySend extends Send {
    public DelayedlySend(SendMsg sendMsg) {
        super(sendMsg);
    }

    @Override
    public void send() {
        sendMsg.sendMsg();
        System.out.println("Send Msg DelayedlySend");
    }
}
上一篇 下一篇

猜你喜欢

热点阅读