Android 设计模式

设计模式不是起点,而是终点

一、设计模式六大原则

1、单一职责原则

定义:就一个类而言,应该仅有一个引起它变化的原因。

一个方法只做一件事。
一个类不要承担过多的职责。

2、开放封闭原则

定义:类、模块、函数等应该是可以拓展的,但是不可修改。

设计程序时,面对需求的改变要尽可能地保证相对稳定,尽量通过扩展的方式来实现变化,而不是通过修改原有的代码来实现。程序应该对扩展开放、对修改关闭:当程序需要扩展时,不应该修改原来的代码,而是可以添加一个类来扩展。

比如商品打折,可以更改原来的商品类的 getPrice 方法,也可以增加一个子类,重写 getPrice 方法,通过高层模块,在打折时使用子类。

开闭原则的思想其实是将不变的地方封装起来,将可变的部分暴露出去,增加扩展性。

开闭原则的有点在于:原来的代码不用更改,而且还可以复用原来的代码。

3、里氏替换原则

定义:所有引用基类(父类)的地方必须能透明地使用其子类的对象。

里氏替换原则是实现开放封闭原则的重要方式之一。由于使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型来对对象进行定义,而在运行时在确定其子类类型,用子类对象来替换父类对象。使用里氏替换原则需要注意以下问题。

子类的所有方法必须在父类中声明,或子类必须实现父类中声明的所有方法,根据里氏替换原则,为了保证系统的扩展性,在程序中通常使用父类来进行定义。如果一个方法只存在于子类中,在父类中不提供相应的声明,则无法在以父类定义的对象中使用该方法。

在运用里氏替换原则时,尽量把父类设计为抽象类或者接口,让子类继承父类或实现父接口,并实现在父类中声明的方法。运行时,子类实例替换父类实例,可以方便扩展系统的功能,同时无须修改原有子类的代码;增加新的功能可以通过增加一个新的子类来实现。里氏替换原则是开放封闭原则的具体实现手段之一。

4、依赖倒置原则

定义:高层模块不应该依赖底层模块,两者都应该依赖于抽象。抽象不 该依赖于细节,细节应该依赖于抽象。

在 Java 中,抽象指接口或者抽象类,两者都是不能直接被实例化的;细节就是实现类,实现接口或者继承抽象类而产生的就是细节,也就是可以加上一个关键字 new 产生的对象。高层模块就是调用端,低层模块就是具体实现类。

依赖倒置原则在 Java 中的表现就是,模块间的依赖通过抽象发生,实现类之间不发生直接依赖关系,其依赖关系是通过接口或抽象类产生的。如果类与类直接依赖细节,那么就会直接耦合,如此一来当修改时,就会同时修改依赖者代码,这样限制了可扩展性。

5、迪米特原则

定义:一个软件实体应当尽可能少地与其他实体发生相互作用。

也被称为最少知识原则。如果一个系统符合迪米特原则,那么当其中某一个模块发生修改时,就会尽量少地影响其他模块。迪米特原则要求我们在设计系统时,应该尽量减少对象之间的交互。如果两个对象之间不必彼此直接通信,那么这两个对象就不应当发生任何直接的相互作用。如果其中的一个对象需要调用另一个对象的某一个方法,则可以通过第三者转发这个调用。简言之,就是通过引入一个合理的第三者来降低现有对象之间的耦合度。

  • 在类的划分上,应当尽量创建松耦合的类。类之间的耦合度越低,就越有利于复用。一个处在松耦合中的类一旦被修改,则不会对关联的类造成太大波及。

  • 在类的结构上,每一个类都应当尽量降低其成员变量和成员函数的访问权限。

  • 在对其他类的引用上,一个对象对其他对象的引用应当降到最低。

6、接口隔离原则

定义:一个类对另一个类的依赖应该建立在最小的接口上。

建立单一接口,不要建立庞大臃肿的接口;尽量细化接口,接口中的方法尽量少。也就是说,我们要为各个类建立专用的接口,而不要试图建立一个很庞大的接口供所有依赖它的类调用。

  • 接口尽量小,但是要有限度。对接口进行细化提高程序设计的灵活性;但是如果过小,则会造成接口数量过多,使设计复杂化。所以,一定要适度。

  • 为依赖接口的类定制服务,只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。只有专注地为一个模块提供定制服务,才能建立最小的依赖关系。

  • 提高内聚,减少对外交互。接口方法尽量少用 public 修饰。接口是对外的承诺,承诺越少对系统的开发越有利,变更风险也会越少。

二、设计模式分类

GoF 提出的设计模式共有 23 种,根据目的准则分类,分为三大类。

  • 创建型设计模式,共 5 种:单例模式、工厂方法模式、抽象工厂模式、建造者模式、原型模式。
  • 结构型设计模式,共 7 种:适配器模式、装饰模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
  • 行为型设计模式,共 11 种:策略模式、模板方法模式、观察者模式、迭代器模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。

三、创建型设计模式

3.1、单例模式

定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

使用场景

  • 整个项目需要一个共享访问点或共享数据。
  • 创建一个对象需要耗费的资源过多,比如访问 I/O 或者数据库等资源。
  • 工具类对象。

3.1.1、饿汉模式

public class Singleton {
    private static Singleton instance = new Singleton();

    private Singleton() {
    }

    public static Singleton getInstance() {
        return instance;
    }
}

这种模式在类加载时就完成了初始化,所有类加载较慢,但获取对象的速度快。这种方式基于类加载机制,避免了多线程的同步问题。在类加载的时候就完成实例化,没有达到懒加载的效果。如果从始至终未使用过这个实例,则会造成内存的浪费。

3.1.2、懒汉模式(线程不安全)

public class Singleton {
    private static Singleton instance;

    private Singleton() {
    }

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

懒汉模式声明了一个静态对象,在用户第一次调用时初始化。这虽然节约了资源,但第一次加载时需要实例化,反应稍慢一些,而且在多线程时不能正常工作。

3.1.3、懒汉模式(线程安全)

public class Singleton {
    private static Singleton instance;

    private Singleton() {
    }

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

这种写法能够在多线程中很好的工作,但是每次调用 getInstance 方法时都需要进行同步。这回造成不必要的同步开销,而且大部分时候我们是用不到同步的。所以,不建议用这种模式。

3.1.4、双重检查模式(DCL)

public class Singleton {
    private static volatile Singleton instance;

    private Singleton() {
    }

    public static synchronized Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

这种写法在 getInstance 方法中对 Singleton 进行了两次判空:第一次是为了不必要的同步,第二次是在 Singleton 等于 null 的情况下才创建实例。在这里使用 volatile 会或多或少地影响性能,但考虑到程序的正确性,牺牲这点性能还是值得的。DCL 的优点是资源利用率高。第一次执行 getInstance 时单例对象才被实例化,效率高。其缺点是第一次加载时反应稍慢一些,在高并发环境下也有一定的缺陷。DCL 虽然在一定程度上解决了资源的消耗和多余的同步、线程安全等问题,但其还是在某些情况会出现失效的问题,也就是 DCL 失效。这里建议用静态内部类单例模式来替代 DCL。

3.1.5、静态内部类单例模式

public class Singleton {

    private Singleton() {
    }

    public static synchronized Singleton getInstance() {
        return SingletonHolder.instance;
    }

    private static class SingletonHolder {
        private static final Singleton instance = new Singleton();
    }
}

第一次加载 Singleton 类时并不会初始化 instance,只有第一次调用 getInstance 方法时虚拟机加载 SingletonHolder 并初始化 instance。这样不仅能确保线程安全,也能保证 Singleton 类的唯一性。所以,推荐使用静态内部类单例模式。

3.1.6、枚举单例

public enum Singleton {
    INSTANCE;

    public void doSomeThing() {
    }
}

默认枚举实例的创建是线程安全的,并且在任何情况下都是单例。枚举单例的优点就是简单,但是大部分应用开发很少用枚举,其可读性并不是很高。

3.2、简单工厂模式

简单工厂模式(又叫做静态工厂方法模式),其属于创建型设计模式,但是并不属于 23 种 GoF 设计模式之一。

定义:简单工厂模式属于创建型模式,其又被称为静态工厂方法模式,这是由一个工厂对象决定创建出哪一种产品类的实例。

根据参数的不同返回不同的产品,这就是简单工厂模式。简单工厂模式中有如下角色。

  • Factory:工厂类,这是简单工厂模式的核心,它负责实现创建所有实例的内部逻辑。工厂类的创建产品类的方法可以被外界直接调用,创建所需的产品对象。
  • IProduct:抽象产品类,这是简单工厂模式所创建的所有对象的父类,它负责描述所有实例所共有的公共接口。
  • Product:具体产品类,这是简单工厂模式的创建目标。

简单实现

假设有一个计算机的代工生产商,它目前已经可以生产联想计算机了。随着业务的拓展,还需要生产惠普和华硕的计算机。这时候我们就创建一个单独的类来专门生产计算机,这就是简单工厂模式。

  1. 抽象产品类
public abstract class Computer {
    public abstract void start();
}
  1. 具体产品类
public class LenovoComputer extends Computer {
    @Override
    public void start() {
        Log.d("TAG", "联想计算机");
    }
}
public class HpComputer extends Computer {
    @Override
    public void start() {
        Log.d("TAG", "惠普计算机");
    }
}
public class AsusComputer extends Computer {
    @Override
    public void start() {
        Log.d("TAG", "华硕计算机");
    }
}
  1. 工厂类
public class ComputerFactory {
    public static Computer makeComputer(String type) {
        Computer computer = null;
        switch (type) {
            case "Lenovo":
                computer = new LenovoComputer();
                break;
            case "Hp":
                computer = new HpComputer();
                break;
            case "Asus":
                computer = new AsusComputer();
                break;
        }
        return computer;
    }
}
  1. 客户端调用
        ComputerFactory.makeComputer("Hp").start();

使用场景

  • 工厂类负责创建的对象比较少
  • 客户只需要知道传入工厂类的参数,而无需关系创建对象的逻辑。

优缺点

  • 优点:使用户根据参数获得对应的类实例,避免了直接实例化,降低了耦合性。
  • 缺点:可实例化的类型在编译期间已经被确定。如果增加新类型,则需要修改工厂,这违背了开放封闭原则。简单工厂需要知道所有要生成的类型,其当子类过多或者子类层次过多是不宜使用。

3.3、工厂方法模式

定义:定义一个用于创建对象的接口,让子类决定实例化哪个类。工厂方法使一个类的实例化延迟到其子类。

工厂方法模式有如下角色。

  • Product 抽象产品类。
  • ConcreteProduct:具体产品类,实现 Product 接口。
  • Factory:抽象工厂类,该方法返回一个 Product 类型的对象。
  • ConcreteFactory:具体工厂类,返回 ConcreteProduct 实例。

简单实现

  1. 抽象工厂类
public abstract class ComputerFactory {
    public abstract <T extends Computer> T makeComputer(Class<T> tClass);
}
  1. 具体工厂类
public class GDCComputerFactor extends ComputerFactory {
    @Override
    public <T extends Computer> T makeComputer(Class<T> tClass) {
        Computer computer = null;
        String className = tClass.getName();
        try {
            // 通过反射来生产不同厂家的计算机
            computer = (Computer) Class.forName(className).newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return (T) computer;
    }
}
  1. 客户端调用
        ComputerFactory computerFactory = new GDCComputerFactor();
        HpComputer hpComputer = computerFactory.makeComputer(HpComputer.class);
        hpComputer.start();

相对于简单工厂模式,当需要生产新的计算机时,就不用修改工厂类,直接创建产品即可,符合开放封闭原则。

3.4、建造者模式

定义:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

建造者模式中有如下角色。

  • Director:导演类,负责安排已有模块的顺序,然后通知 Builder 开始建造。
  • Builder:抽象 Builder 类,规范产品的组建,一般由子类实现。
  • ConcreteBuilder:具体建造者,实现抽象 Builder 类定义的所有方法,并且返回一个组建好的对象。
  • Product:产品类。

简单实现

建造者模式也被称为生成器模式,它是创建一个复杂对象的创建型模式,其将构建复杂对象的过程和它的部件解耦,使得构建过程和部件表示分离开来。例如我们要组装一台电脑,我们找到 DIY 商家。这时我们可以要求这台计算机的 CPU、主板或者其他部件需要的品牌,配置,根据我们的需求来变化。但是组装计算机的过程是统一的,我们无需知道这些部件是如何组装成一台计算机的。对于这种情况我们可以采用建造者模式,将部件和组装过程分离,使得构建过程和部件都可以自由拓展,两者之间的耦合也降到最低。

  1. 产品类
public class Computer {
    private String cpu;
    private String mainboard;
    private String ram;

    public void setCpu(String cpu) {
        this.cpu = cpu;
    }

    public void setMainboard(String mainboard) {
        this.mainboard = mainboard;
    }

    public void setRam(String ram) {
        this.ram = ram;
    }
}
  1. Builder 类用来规范产品的组建
public abstract class Builder {
    public abstract void buildCpu(String cpu);

    public abstract void buildMainboard(String mainboard);

    public abstract void buildRam(String ram);

    /**
     * 组装方法
     */
    public abstract Computer assemble();
}
  1. 具体建造者
public class ComputerBuilder extends Builder {
    private Computer mComputer = new Computer();

    @Override
    public void buildCpu(String cpu) {
        mComputer.setCpu(cpu);
    }

    @Override
    public void buildMainboard(String mainboard) {
        mComputer.setMainboard(mainboard);
    }

    @Override
    public void buildRam(String ram) {
        mComputer.setRam(ram);
    }

    @Override
    public Computer assemble() {
        return mComputer;
    }
}
  1. 导演类统一组装过程,先安装主板,再安装 CPU,最后安装内存并组装成计算机。
public class Director {
    private Builder mBuilder = null;

    public Director(Builder builder) {
        this.mBuilder = builder;
    }

    public Computer assembleComputer(String cpu, String mainboard, String ram) {
        // 规范建造流程
        this.mBuilder.buildMainboard(mainboard);
        this.mBuilder.buildCpu(cpu);
        this.mBuilder.buildRam(ram);
        return mBuilder.assemble();
    }
}
  1. 客户端调用

    最后商家用导演类组装计算机,我们只需要提供自己想要的 CPU、主板和内存。

        Builder builder = new ComputerBuilder();
        Director director = new Director(builder);
        // 组装计算机
        director.assembleComputer("i7-6700", "华硕主板", "金士顿内存");

使用场景

  • 当创建复杂对象的算法应该独立于该对象的组成部分以及它们的装配方法时。
  • 相同的方法,不同的执行顺序,产生不同的事件结果时。
  • 多个部件或零件都可以被装配到一个对象中,但是产生的运行结果又不相同时。
  • 产品类非常复杂,或者产品类中的调用顺序不同而产生了不同的效能。
  • 在创建一些复杂的对象时,这些对象的内部组成构件间的建造顺序是稳定的,但是对象的内部组成构件面临着复杂的变化。

优点

  • 使用建造者模式可以使客户端不必知道产品内部组成的细节。
  • 具体的建造者类之间是相互独立的,容易扩展。
  • 由于具体的建造者是独立的,因此可以对建造过程逐步细化,而不对其他的模块产生任何影响。

缺点

  • 产生多余的 Build 对象以及导演类。

四、结构型设计模式

4.1、代理模式

定义:为其他对象提供一种代理以控制对这个对象的访问。

代理模式中有如下角色。

  • Subject:抽象主题类,声明真实主题与代理的共同接口方法。
  • RealSubject:真实主题类,代理类所代表的真实主题。客户端通过代理类间接地调用真实主题类的方法。
  • Proxy:代理类,持有对真实主题类的引用,在其所实现的接口方法中调用真实主题类中相应的接口方法执行。
  • Client:客户端类。

简单实现

我现在需要买一台电脑,由于我不懂电脑,就找了一个代理来帮我买。

  1. 抽象主题类。我要买电脑,代理帮我买电脑,我们有共同的方法,买。
public interface IShop {
    void buy();
}
  1. 真实主题类,就是我。
public class Me implements IShop {
    @Override
    public void buy() {
        Log.d("TAG", "我买电脑");
    }
}
  1. 代理类。也要实现 IShop 接口,同时代理是在帮我买电脑,所以在 buy 方法中调用的是被代理者我的 buy 方法。
public class ProxyPeople implements IShop {
    private IShop mShop;

    public ProxyPeople(IShop mShop) {
        this.mShop = mShop;
    }

    @Override
    public void buy() {
        mShop.buy();
    }
}
  1. 客户端调用。客户端就是代理类包含了真实主题类(被代理者),最终调用的都是真实主题类(被代理者)中实现的方法。
        IShop me = new Me();
        IShop proxyPeople = new ProxyPeople(me);
        proxyPeople.buy();

动态代理的简单实现

从编码的角度来说,代理模式分为静态代理和动态代理,上面的例子是静态代理,在代码运行前就已经存在了代理类的 class 编译文件;而动态代理则是在代码运行时通过反射来动态生成代理类的对象,并确定来代理谁。Java 提供了动态的代理接口 InvocationHandler,实现该接口需要重写 invoke 方法。

  • 首先修改上面的代理类。
public class ProxyPeople implements InvocationHandler {
    private Object mObject;

    public ProxyPeople(Object mObject) {
        this.mObject = mObject;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object object = method.invoke(mObject, args);
        if (method.getName().equals("buy")) {
            Log.d("TAG", "代理在帮我买电脑了");
        }
        return object;
    }
}
  • 客户端调用,调用 Proxy.newProxyInstance 来生成动态代理类,调用 proxy 的 buy 方法会调用 ProxyPeople 的 invoke 方法。
        IShop me = new Me();
        ProxyPeople proxyPeople = new ProxyPeople(me);
        ClassLoader classLoader = me.getClass().getClassLoader();
        // 动态创建代理类
        IShop proxy = (IShop) Proxy.newProxyInstance(classLoader, new Class[]{IShop.class}, proxyPeople);
        proxy.buy();

代理模式的类型

代理模式从编码的角度来说分为静态代理和动态代理,而从适用范围来说则可分为 4 种。

  • 远程代理:为一个对象在不同的地址空间提供局部代表,这样系统可以将 Server 部分的实现隐藏。
  • 虚拟代理:使用一个代理对象表示一个十分耗费资源的对象并在真正需要时才创建。
  • 安全代理:当调用真实的对象时,代理处理另外一些事,比如计算真实对象的引用计数,当该对象没有引用时,可以自动释放它:或者访问一个实际对象时,检查是否已经能够锁定它,以确保其他对象不能改变它。

代理模式的优点

  • 真实主题类就是实现实际的业务逻辑,不用关心其他非本职的工作。
  • 真实主题类随时都会发生变化;但是因为它实现了公共的接口,所以代理类可以不做任何修改就能够使用。

4.2、装饰模式

装饰模式是结构型设计模式之一,其在不必改变类文件和使用继承的情况下,动态的扩展一个对象的功能,是继承的替代方案之一。它通过创建一个包装对象,也就是装饰来包裹真实的对象。

定义:动态地给一个对象添加一些额外的职责,就增加功能来说,装饰模式比生成子类更为灵活。

装饰模式有如下角色。

  • Component:抽象组件,可以是接口或是抽象类,被装饰的最原始的对象。
  • ConcreateComponent:组件具体实现类。Component 的具体实现类,被装饰的具体对象。
  • Decorator:抽象装饰者,从外类来拓展 Component 类的功能,但对于 Component 来说无须知道 Decorator 的存在。在它的属性中必然有一个 private 变量指向 Component 抽象组件。
  • ConcreateDecorator:装饰者的具体实现类。

简单实现

参考博客

一个纯煎饼 8 元,一根香肠 2 元,一个鸡蛋 1 元。根据顾客的需求会有各种各样的搭配,煎饼只加一根香肠,煎饼只加一个鸡蛋,煎饼加一根香肠一个鸡蛋,煎饼加两根香肠一个鸡蛋等等。

  1. 抽象组件类
public abstract class ABattercake {
    protected abstract String getDesc();

    protected abstract int cost();
}
  1. 组件的具体实现类
public class Battercake extends ABattercake {
    @Override
    protected String getDesc() {
        return "煎饼";
    }

    @Override
    protected int cost() {
        return 8;
    }
}
  1. 抽象装饰者类
public class ADecorator extends ABattercake {
    private ABattercake mBattercake;

    public ADecorator(ABattercake battercake) {
        this.mBattercake = battercake;
    }

    @Override
    protected String getDesc() {
        return mBattercake.getDesc();
    }

    @Override
    protected int cost() {
        return mBattercake.cost();
    }
}
  1. 装饰者的具体实现类
public class EggDecorator extends ADecorator {

    public EggDecorator(ABattercake battercake) {
        super(battercake);
    }

    @Override
    protected String getDesc() {
        return super.getDesc() + " 加一个鸡蛋";
    }

    @Override
    protected int cost() {
        return super.cost() + 1;
    }
}
public class SausageDecorator extends ADecorator {

    public SausageDecorator(ABattercake battercake) {
        super(battercake);
    }

    @Override
    protected String getDesc() {
        return super.getDesc() + " 加一根香肠";
    }

    @Override
    protected int cost() {
        return super.cost() + 2;
    }
}
  1. 客户端调用,如果现在有人想买加两个鸡蛋和一根香肠的煎饼。
        ABattercake aBattercake = new Battercake();
        aBattercake = new EggDecorator(aBattercake);
        aBattercake = new EggDecorator(aBattercake);
        aBattercake = new SausageDecorator(aBattercake);
        // 煎饼 加一个鸡蛋 加一个鸡蛋 加一根香肠 销售价格为: 12
        System.out.println(aBattercake.getDesc() + " 销售价格为: " + aBattercake.cost());

使用场景

  • 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
  • 需要动态的给一个对象增加功能,这些功能可以动态的撤销。
  • 当不能采用继承的方式对系统进行扩充或者采用不利于系统扩展和维护时。

优点

  • 通过组合而非继承的方式,动态的扩展一个对象的功能,在运行时选择不同的装饰器,从而实现不同的行为。
  • 有效避免了使用继承的方式扩展对象功能而带来的灵活性差、子类无限制扩张的问题。
  • 具体组件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体组件类和具体装饰类,在使用时再对其进行组合,原有代码无须改变,符合“开放封闭原则”。

缺点

  • 因为所有对象均继承于 Component,所以如果 Component 内部结构发生改变,则不可避免地影响所有子类(装饰者类和被装饰者类)。如果基类改变,则势必影响对象的内部。
  • 比继承更加灵活机动的特性,也同时意味着装饰者模式比继承更加易于出错,排错也很困难。对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为烦琐,所以,只在必要的时候使用装饰者模式。
  • 装饰层数不能过多,否则影响效率。

4.3、外观模式

外观模式也称门面模式。当我们开发 Android 的时候,无论是做 SDK 还是封装 API,大多都会用到外观模式,它通过一个外观类使得整个系统的结构只有一个统一的高层接口,这样能降低用户的使用成本。

定义:要求一个子系统的外部与内部的通信必须通过一个统一的对象进行。此模式提供一个高层的接口,使得子系统更易于使用。

外观模式有如下角色。

  • Facade:外观类,知道哪些子系统负责处理请求,将客户端的请求代理给适当的子系统对象。
  • Subsystem:子系统类,可以有一个或者多个子系统。实现子系统的功能,处理外观类指派的任务,注意子系统类不含有外观类的引用。

简单实现

我们把武侠张无忌当做一个系统,张无忌作为一个武侠,他本身有 3 个子系统,分别是招式、内功和经脉。比武时,为了威力最大化,就需要搭配使用,比如要使用七伤拳,就需要先开启经脉并使用九阳神功。而这些子系统的搭配作为对手也不会知道,对于子系统,张无忌也可以随时更改。外观模式本身就是将子系统的逻辑和交互隐藏起来,为用户提供一个高层次的接口,使得系统更加易用,同时也隐藏了具体的实现,这样即使具体的子系统发生了变化,用户也不会感知到。

  1. 子系统类
public class ZhaoShi {
    public void taiJIQuan() {
        Log.d("TAG", "招式:太极拳");
    }

    public void qiShangQuan() {
        Log.d("TAG", "招式:七伤拳");
    }

    public void shengHuo() {
        Log.d("TAG", "招式:圣火令");
    }
}
public class NeiGong {
    public void jiuYang() {
        Log.d("TAG", "内功:九阳神功");
    }

    public void QianKun() {
        Log.d("TAG", "内功:乾坤大挪移");
    }
}
public class JingMai {
    public void jingMai() {
        Log.d("TAG", "开启经脉");
    }
}
  1. 外观类,在这里就是张无忌本身,用来合理搭配子系统类。
public class ZhangWuJi {
    private ZhaoShi zhaoShi;
    private NeiGong neiGong;
    private JingMai jingMai;

    public ZhangWuJi() {
        zhaoShi = new ZhaoShi();
        neiGong = new NeiGong();
        jingMai = new JingMai();
    }

    /**
     * 使用乾坤大挪移
     */
    public void qianKun() {
        jingMai.jingMai();// 开启经脉
        neiGong.QianKun();// 使用内功乾坤大挪移
    }

    /**
     * 使用七伤拳
     */
    public void qiShang() {
        jingMai.jingMai();// 开启经脉
        neiGong.jiuYang();// 使用内功九阳神功
        zhaoShi.qiShangQuan();// 使用招式七伤拳
    }
}
  1. 客户端调用
        ZhangWuJi zhangWuJi = new ZhangWuJi();
        // 张无忌使用乾坤大挪移
        zhangWuJi.qianKun();
        // 张无忌使用七伤拳
        zhangWuJi.qiShang();

使用场景

  • 构建一个有层次结构的子系统时,使用外观模式定义子系统中每层的入口点。如果子系统之间是相互依赖的,则可以让其通过外观接口进行通信,减少子系统之间的依赖关系。
  • 子系统往往会因为不断地重构演化而变得越来越复杂,大多数的模式使用时也会产生很多很小的类,这给外部调用它们的用户程序带来了使用上的困难,我们可以使用外观类提供一个简单的接口,对外隐藏子系统的具体实现并隔离变化。
  • 当维护一个遗留的大型系统时,可能这个系统已经非常难以维护和拓展;但因为它含有重要的功能,所以新的需求必须依赖于它,这时可以使用外观类,为设计粗糙或者复制的遗留代码提供一个简单的接口,让新系统和外观类交互,而外观类负责与遗留的代码进行交互。

优点

  • 减少系统的相互依赖,所有的依赖都是对外观类的依赖,与子系统无关。
  • 对用户隐藏了子系统的具体实现,减少用户对子系统的耦合,这样即使具体的子系统发生了变化,用户也不会感知到。
  • 加强了安全性,子系统中的方法如果不在外观类中开通,就无法访问到子系统中的方法。

缺点

  • 不符合开放封闭原则。如果业务出现变更,则可能要直接修改外观类。

4.4、享元模式

享元模式是池技术的重要实现方式,它可以减少应用程序创建的对象,降低程序内存的占用,提高程序的性能。

定义:使用共享对象有效地支持大量细粒度的对象。

要求细粒度对象,那么不可避免地使用对象数量多且性质相近。这些对象分为两个部分:内部状态和外部状态。内部状态是对象可共享出来的信息,存储在享元对象内部并且不会随环境的改变而改变;而外部状态是对象依赖的一个标记,它是随环境改变而改变的并且不可共享的状态。

享元模式有如下角色。

  • Flyweight:抽象享元角色,同时定义出对象的外部状态和内部状态的接口或者实现。
  • ConcreteFlyweight:具体享元角色,实现抽象享元角色定义的业务。
  • FlyweightFactory:享元工厂,负责管理对象池和创建享元对象。

简单实现

一个网上商城卖商品,如果每个用户下单都生成一个商品对象,这显然会耗费很多资源。如果赶上 “双 11”,那恐怖的订单量会生成很多商品对象,再加上商品本身种类繁多,就极易产生 “Out Of Memory”。因此,我们采用享元模式来对商品的创建进行优化。

  1. 抽象享元角色。定义 showGoodsPrice 方法来展示商品的价格。
public interface IGoods {
    void showGoodsPrice(String name);
}
  1. 具体享元角色。实现 IGoods 接口,并实现展示价格的方法。其中 name 为内部状态,type 为外部状态。
public class Goods implements IGoods {
    private String name; // 名称

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

    @Override
    public void showGoodsPrice(String type) {
        if (name.equals("iphone7") && type.equals("32G")) {
            Log.d("TAG", type + " " + this.name + "价格为7032");
        } else if (name.equals("iphone7") && type.equals("128G")) {
            Log.d("TAG", type + " " + this.name + "价格为7128");
        } else if (name.equals("iphone5") && type.equals("32G")) {
            Log.d("TAG", type + " " + this.name + "价格为5128");
        } else if (name.equals("iphone5") && type.equals("128G")) {
            Log.d("TAG", type + " " + this.name + "价格为5128");
        }
    }
}
  1. 享元工厂。用来创建 Goods 对象。通过 Map 容器来存储 Goods 对象,将内部状态 name 作为 Map 的 key,以便标识 Goods 对象,如果 Map 容器中包含此 key,则使用 Map 容器中存储的 Good 对象;否则就新创建 Goods 对象,并放入 Map 容器中。
public class GoodsFactory {
    private static Map<String, Goods> pool = new HashMap<>();

    public static Goods getGoods(String name) {
        if (pool.containsKey(name)) {
            Log.d("TAG", "使用缓存,key 为:" + name);
            return pool.get(name);
        } else {
            Goods goods = new Goods(name);
            pool.put(name, goods);
            Log.d("TAG", "创建商品,key 为:" + name);
            return goods;
        }
    }
}
  1. 客户端调用。调用 GoodsFactory 工厂中的 getGoods 方法来创建 Goods 对象,调用 showGoodsPrice 方法来显示商品的价格。
        Goods goods1 = GoodsFactory.getGoods("iphone7");
        goods1.showGoodsPrice("32G");
        Goods goods2 = GoodsFactory.getGoods("iphone7");
        goods2.showGoodsPrice("32G");
        Goods goods3 = GoodsFactory.getGoods("iphone7");
        goods3.showGoodsPrice("128G");
        Goods goods4 = GoodsFactory.getGoods("iphone5");
        goods4.showGoodsPrice("128G");
  1. 运行结果
    在这里插入图片描述
    在这个例子中,name 作为内部状态是不变的,并且作为 Map 的 key 值是可以共享的。而 showGoodsPrice 方法中需要传入的 type 值则是外部状态,它的值是变化的。

使用场景

  • 系统中存在大量的相似对象。
  • 需要缓冲池的场景。

五、行为设计模式

5.1、策略模式

写代码时总会遇到一种情况,就是我们会有很多的选择,由此衍生出很多的 if…else,或者 case。如果每个条件语句中包含了一个简单的逻辑,那还比较容易处理;但如果在一个条件语句中又包含了多个条件语句,就会使得代码变得臃肿,维护的成本也会加大,这显然违背了开放封闭原则。

定义:定义一系列的算法,把每一个算法封装起来,并且使它们可相互替换。策略模式使得算法可独立于使用它的客户而独立变化。

策略模式有如下角色。

  • Context:上下文角色,用来操作策略的上下文环境,起到承上启下的作用,屏蔽高层模块对策略、算法的直接访问。
  • Strategy:抽象策略角色,策略、算法的抽象,通常为接口。
  • ConcreteStrategy:具体的策略实现。

简单实现

张无忌作为一个大侠会遇到很多对手,面对不同程度的对手,不会都使用最强的武功,于是张无忌想出了 3 种应战的策略,分别对付 3 个实力层次的对手。

  1. 抽象策略角色。定义一个接口策略用来战斗。
public interface AStrategy {
    void fighting();
}
  1. 具体策略实现。分别定义 3 个策略来实现策略接口,用来对付 3 个实力层次的对手。
public class WeakStrategy implements AStrategy {
    @Override
    public void fighting() {
        Log.d("TAG", "遇到较弱的对手,使用太极剑");
    }
}
public class CommonStrategy implements AStrategy {
    @Override
    public void fighting() {
        Log.d("TAG", "遇到普通的对手,使用圣火令神功");
    }
}
public class StrongStrategy implements AStrategy {
    @Override
    public void fighting() {
        Log.d("TAG", "遇到强大的对手,使用乾坤大挪移");
    }
}
  1. 上下文角色。构造方法包含了具体策略实现,通过传入不同策略来调用不同的策略 fighting 方法。
public class Context {
    private AStrategy mStrategy;

    public Context(AStrategy aStrategy) {
        this.mStrategy = aStrategy;
    }

    public void fighting() {
        mStrategy.fighting();
    }
}
  1. 客户端调用。可以看出把条件语句中的逻辑抽离,使代码逻辑更加清晰了。
        Context context;
        if (false) {
            // 遇到宋青书,采用较弱对手策略
            context = new Context(new WeakStrategy());
        } else if (true) {
            // 遇到灭绝师太,采用普通对手策略
            context = new Context(new CommonStrategy());
        } else if (false) {
            // 遇到成昆,采用强大对手策略
            context = new Context(new StrongStrategy());
        }
        context.fighting();

使用场景

  • 对客户隐藏具体策略(算法)的实现细节,彼此完全独立。
  • 针对同一类型问题的多种处理方式,仅仅是具体行为有差别时。
  • 在一个类中定义了很多行为,而且这些行为在这个类里的操作以多个条件语句的形式出现。策略模式将相关的条件分支移入它们各自的 Strategy 类中,以代替这些条件语句。

优点

  • 使用策略模式可以避免多重添加语句。多重条件语句不易维护,而且易出错。
  • 易于拓展。当需要添加一个策略时,只需要实现接口就可以了。

缺点

  • 每一个策略都是一个类,复用性小。如果策略过多,类的数量会增多。
  • 上层模块必须知道有哪些策略,才能够使用这些策略,这与迪米特原则相违背。

5.2、模板方法模式

在软件开发中,有时会遇到类似的情况:某个方法的实现需要多个步骤,其中有些步骤是固定的;而有些步骤并不固定,存在可变性。为了提高代码的复用性和系统的灵活性,可以使用模板方法模式来应对这类情况。

定义:定义一个操作中的算法框架,而将一些步骤延迟到子类中,使得子类不改变一个算法的结构即可重定义算法的某些特定步骤。

在模板方法模式中有如下角色。

  • AbstractClass:抽象类,定义了一套算法框架。
  • ConcreteClass:具体实现类。

简单实现

参考博客

现在要炒蒜蓉菜心和手撕包菜两盘菜,它们有一个共同的模板,热油,倒作料,倒菜,炒熟。

  1. 创建抽象类,定义一个算法框架,也可以说公用模板。这个抽象类包含了 3 种类型的方法,分别是抽象方法、具体方法和钩子方法。抽象方法交由子类实现,倒佐料、倒菜。具体方法直接由父类实现,热油、翻炒。钩子方法又分为了两类:一是返回值为 boolean 的 needOther 方法,有的人需要加点鸡精、味精等,如果不需要则返回 false。二是 hook 方法,有的人最后需要加点葱花、香菜等,这是一个空实现方法,由子类视是否需要来实现。
public abstract class ACook {
    // 模板方法为 final,防止被篡改
    public final void cook() {
        heatOil(); // 热油
        pourSeasoning(); // 倒佐料
        pourVegetable(); // 倒菜
        if (needOther()) { // 是否需要其他佐料,例如鸡精、味精。
            pourOther(); // 倒其他佐料
        }
        fry(); // 翻炒
        hook(); // 钩子方法
    }

    protected void heatOil() {
        Log.d("TAG", "热油");
    }

    protected abstract void pourSeasoning();

    protected abstract void pourVegetable();

    protected boolean needOther() {
        return true;
    }

    protected abstract void pourOther();

    protected void fry() {
        Log.d("TAG", "翻炒");
    }

    /**
     * 空实现方法
     */
    protected void hook() {
    }
}
  1. 具体实现类。
public class CaiXin extends ACook {
    @Override
    protected void pourSeasoning() {
        Log.d("TGA", "倒蒜蓉");
    }

    @Override
    protected void pourVegetable() {
        Log.d("TGA", "倒菜心");
    }

    @Override
    protected void pourOther() {
        Log.d("TGA", "倒味精");
    }
}
public class BaoCai extends ACook {
    @Override
    protected void pourSeasoning() {
        Log.d("TGA", "倒辣椒");
    }

    @Override
    protected void pourVegetable() {
        Log.d("TGA", "倒包菜");
    }

    @Override
    protected void pourOther() {
    }

    @Override
    protected boolean needOther() {
        return false;
    }

    @Override
    protected void hook() {
        Log.d("TGA", "加葱花");
    }
}
  1. 客户端调用。
        CaiXin caiXin = new CaiXin();
        caiXin.cook();
        BaoCai baoCai = new BaoCai();
        baoCai.cook();

使用场景

  • 多个子类共有的方法,并且逻辑基本相同时。
  • 面对重要,复杂的算法,可以把核心算法设计为模板方法,周边相关细节功能则由各个子类实现。
  • 需要通过子类来决定父类算法中的某个步骤是否执行,实现子类对父类的反向控制。

优点

  • 模板方法通过把不变的行为搬移到超类,去除了子类中的重复代码。
  • 子类实现算法的某些细节,有助于算法的扩展。

缺点

  • 每个不同的实现都需要定义一个子类,这会导致类的个数增加,设计更加抽象。

5.3、观察者模式

观察者模式又被称为发布-订阅模式,属于行为型设计模式的一种,是一个在项目中经常使用的模式。

定义:定义对象间一种一对多的依赖关系,每当一个对象改变状态时,则所有依赖于它的对象都会得到通知并被自动更新。

观察者模式有如下角色。

  • Subject:抽象主题(抽象被观察者)。抽象主题角色把所有观察者对象保存在一个集合里,每个主题都可以有任意数量的观察者。抽象主题提供一个接口,可以增加和删除观察者对象。
  • ConcreteSubject:具体主题(具体被观察者)。该角色将有关状态存入具体观察者对象,在具体主题的内部状态发生改变时,给所有注册过的观察者发送通知。
  • Observer:抽象观察者,是观察者的抽象类。它定义了一个更新接口,使得在得到主题更改通知时更新自己。
  • ConcreteObserver:具体观察者,实现抽象观察者定义的更新接口,以便在得到主题更改通知时更新自身的状态。

简单实现

参考博客

拿微信公众号举例,有很多微信用户订阅了一个公众号,那么这个公众号就是被观察者,微信用户就是观察者。当这个公众号有动态更新时就会通知这些微信用户。

  1. 抽象观察者。
public interface Observer {
    void update(String message);
}
  1. 具体观察者。
public class WeixinUser implements Observer {
    private String name;

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

    @Override
    public void update(String message) {
        Log.d("TAG", name + "-" + message);
    }
}
  1. 抽象被观察者。
public interface Subject {
    /**
     * 增加订阅者
     */
    void attach(Observer observer);

    /**
     * 删除订阅者
     */
    void detach(Observer observer);

    /**
     * 通知订阅者更新消息
     */
    void notify(String message);
}
  1. 具体被观察者,公众号。
public class ConcreteSubject implements Subject {
    private List<Observer> userList = new ArrayList<>();

    @Override
    public void attach(Observer observer) {
        userList.add(observer);
    }

    @Override
    public void detach(Observer observer) {
        userList.remove(observer);
    }

    @Override
    public void notify(String message) {
        for (Observer observer : userList) {
            observer.update(message);
        }
    }
}
  1. 客户端调用。
        // 创建微信用户
        WeixinUser user1 = new WeixinUser("张三");
        WeixinUser user2 = new WeixinUser("李四");
        WeixinUser user3 = new WeixinUser("王五");
        // 订阅公众号
        subject.attach(user1);
        subject.attach(user2);
        subject.attach(user3);
        // 更新动态通知微信用户
        subject.notify("动态更新了");

使用场景

  • 关联行为场景。需要注意的是,关联行为是可拆分的,而不是“组合”关系。
  • 事件多级触发场景。
  • 跨系统的消息交换场景,如消息队列、事件总线的处理机制。

优点

  • 观察者和被观察者之间是抽象耦合,容易扩展。
  • 方便建立一套触发机制。

缺点

  • 在应用观察者模式时需要考虑一下开发效率和运行效率的问题。程序中包括一个被观察者、多个观察者,开发、调试等内容会比较复杂,而且在 Java 中消息的通知一般是顺序执行的,那么一个观察者卡顿,会影响整体的执行效率,在这种情况下,一般会采用异步方式。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值