从Android源码角度谈设计模式(一):创建型模式

一、前言

创建型模式对类的实例化过程进行了抽象,能够将软件模块中对象的创建和对象的使用分离。 为了使软件的结构更加清晰,外界对于这些对象只需要知道它们共同的接口,而不清楚其具体的实现细节,使整个系统的设计更加符合单一职责原则。

简单来讲,相较于另外两种类型(结构型行为型),创建型模式的侧重点在于如何创建对象

image_design_pattern_create_overview.jpg

本文会简单介绍几种常见的创建型模式

  1. 单例模式
  2. 工厂模式
  3. 建造者模式

注意,原型模式不包含在本文中,想要了解更多的可以自行冲浪搜索

二、创建型模式:单例模式

1、模式定义

单例模式的目的是保证一个类仅有一个实例,并提供一个访问它的全局访问点。 单例类拥有一个私有构造函数,确保用户无法通过new关键字直接实例化它。 除此之外,该模式中包含一个静态私有成员变量与静态公有的工厂方法。 该工厂方法负责检验实例的存在性并实例化自己,然后存储在静态成员变量中,以确保只有一个实例被创建。

image_uml_design_pattern_create_singleton.png 图片来源:自己画的

2、使用方法

单例使用方法简单,理解起来也比较容易,笔者这里就直接开门见山,介绍四种常见的使用方式

2.1 饿汉式

public class Singleton1 {

    private static final Singleton1 instance = new Singleton1();

    public Singleton1() {
    }

    public static Singleton1 getInstance() {
        return instance;
    }

    public void doSomething() {
    		//doSomething
    }

}
复制代码

饿汉式是常见使用方法中最简单的一种,JVM类加载机制得以保证类加载过程中是线程安全的,所以多线程环境下使用也是没问题的

饿汉式单例模式唯一的问题可能就是:并非懒加载,只要单例类被虚拟机加载,就必然会创建instance实例。此时,若是被提前初始化的示例工程中用不到,那的确是白白消耗了初始化的时间和内存空间

不过考虑到单例模式的使用场景,笔者认为大多数情况下使用饿汉式并不会给项目带来多余的负担。

2.2 懒汉式:静态内部类

public class Singleton2 {

    private Singleton2() {

    }

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

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

    public void doSomething() {
    		//doSomething
    }

}
复制代码

2.1的代码示例相同,静态内部类同样由JVM类加载机制保证了多线程环境下的安全性;同时,由于没有在局部变量中声明,所以就算因为非主观因素导致单例类被虚拟机加载,也无需担心创建了暂时用不到的对象导致占用内存。

静态内部类的单例模式既能保证多线程环境下的安全又实现了懒加载,代码写起来比较方便,这也是笔者个人比较喜欢用的一种方式

2.3 懒汉式:双重校验锁DCL

public class Singleton3 {

    private volatile static Singleton3 instance;

    private Singleton3() {
    }

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

    public void doSomething() {
    		//doSomething
    }

}
复制代码

DCL(DoubleCheckLock)双重校验锁可能是所有单例模式中传播最广的方式了,这也是笔者在工作早期接触最多的单例模式;DCL双重检验锁的方式把线程安全和懒加载都考虑到了,并且涉及到Java并发编程中两个比较重要的关键字:volatilesynchronized,相较于上面的两种示例,DCL可以拎出来的点比较多,笔者猜测可能这也是为什么DCL传播这么广的原因

在实际的项目开发中如果不觉得使用步骤较为繁琐,代码量可以接受的话,DCL也是比较推荐的一种使用方式

注:关于DCL双重校验锁要不要加volatile关键字取决于Java版本,在jdk8中已经确保new、初始化为原子性操作,不会出现JIT导致指令重排的情况,想了解更多的朋友请点击这里

2.4 枚举类

public enum Singleton4 {
    
    getInstance();

    public void doSomething() {
    		//doSomething
    }

}
复制代码

Java1.5以后,单例模式的实现方式终于迎来的新面孔:枚举类单例,这也是《Effective Java》作者Joshua 力荐的一种使用方式

枚举类是没有构造方法的,枚举类的创建完全由JVM内部实现,不对外开放。这样的特性也使得我们不能使用new的方式来创建枚举对象,对应的缺点就是枚举类单例模式不是懒加载的

枚举类单例除了能保证线程安全外,还加入了新功能:防止单例模式被破坏,原因有两点:

  1. 枚举类无法被反射创建

    强行使用反射创建会收到错误:java.lang.IllegalArgumentException: Cannot reflectively create enum objects

  2. 枚举类不能被反序列化

    序列化的时候,仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。

    同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。

最后,关于枚举类还有一点需要注意:枚举类不能继承任何类,枚举类在编译后会继承自java.lang.Enum,由于Java单继承的特性导致枚举类不能再继承任何其他类,但是枚举类可以实现接口

3、破坏单例模式及防止破坏单例

正如2.4小节中提到的,除了枚举类单例外,其他的三种单例模式都可以通过使用反射来破坏在进程中的唯一性

单例类由于自身的特殊性导致无法被反射反序列化创建,所以对象的唯一性暂时无法被破坏,但是可以修改枚举类的值

笔者这里写了一个简单的示例代码来破坏单例,感兴趣的朋友也可以自己在单元测试中试一试


    @Test
    public void main() {
        testSingleton1();
        testSingleton2();
        testSingleton3();
    }

    /*1.恶汉模式*/
    private static void testSingleton1() {
        Object fromInstance = Singleton1.getInstance();
        Object fromReflect = createClassWithReflect(Singleton1.class);
        System.out.println(fromInstance.equals(fromReflect));
    }

    /*2.懒汉模式-静态内部类*/
    private static void testSingleton2() {
        Object fromInstance = Singleton2.getInstance();
        Object fromReflect = createClassWithReflect(Singleton2.class);
        System.out.println(fromInstance.equals(fromReflect));
    }

    /*3.懒汉模式-DoubleCheckLock*/
    private static void testSingleton3() {
        Object fromInstance = Singleton3.getInstance();
        Object fromReflect = createClassWithReflect(Singleton3.class);
        System.out.println(fromInstance.equals(fromReflect));
    }

    private static <T> T createClassWithReflect(Class<T> clz) {
        Constructor<?> constructor = clz.getDeclaredConstructor();
        constructor.setAccessible(true);
        return (T) constructor.newInstance();
    }

复制代码

打印结果

false
false
false
复制代码

从打印结果可以看出,除枚举类外的三种实现方式全部沦陷,都可以通过反射来创建不同的实例对象来破坏单例模式的唯一性

反射创建枚举类会受到报错信息:java.lang.IllegalArgumentException: Cannot reflectively create enum objects,笔者这里就不展示了,感兴趣的同学可以自己动手试一试

最后提一句,网上其他文章提到了使用clone方法来破坏单例,笔者认为这点不成立,因为不重写clone()方法并且不将访问修饰符改为public,使用者是无法调用clone方法来创建新的实例对象

4、小结

至此,几种常见的单例模式实现方式都介绍完了,简单总结下:上述几种实现方式都可以在保证多线程环境下安全性;除了枚举单例之外,其他几种方式的单例模式都可以被破坏

此小节涉及到的代码在这里

三、创建型模式:工厂方法

1、模式定义

工厂方法模式又称为工厂模式,在工厂方法模式中,工厂父类负责定义创建产品对象的公共接口,而工厂子类则负责生成具体的产品对象,这样做的目的是将产品类的实例化操作延迟到工厂子类中完成,即通过工厂子类来确定究竟应该实例化哪一个具体产品类。

工厂方法模式包含四个角色:

  1. 抽象产品是定义产品的接口,是工厂方法模式所创建对象的超类型,即产品对象的共同父类或接口;
  2. 具体产品实现了抽象产品接口,某种类型的具体产品由专门的具体工厂创建,它们之间往往一一对应;
  3. 抽象工厂中声明了工厂方法,用于返回一个产品,它是工厂方法模式的核心,任何在模式中创建对象的工厂类都必须实现该接口;
  4. 具体工厂是抽象工厂类的子类,实现了抽象工厂中定义的工厂方法,并可由客户调用,返回一个具体产品类的实例。

image_uml_design_pattern_create_factory_method.jpg

图片来源:自己画的

2、代码示例

角色1:抽象产品类

public abstract class AbstractProduct {

    public abstract String getName();

}
复制代码

角色2:产品实现类

public class ProductA extends AbstractProduct {

    @Override
    public String getName() {
        return "A";
    }

}

public class ProductB extends AbstractProduct {

    @Override
    public String getName() {
        return "B";
    }

}
复制代码

角色3:抽象工厂类

public abstract class AbstractFactory {

    public abstract AbstractProduct createProduct();

}
复制代码

角色4:工厂实现类

public class ProductAFactory extends AbstractFactory {

    @Override
    public AbstractProduct createProduct() {
        return new ProductA();
    }
  
}

public class ProductBFactory extends AbstractFactory {

    @Override
    public AbstractProduct createProduct() {
        return new ProductB();
    }
  
}
复制代码

工厂方法使用示例

@Test
public void main() {
    AbstractFactory factoryA = new ProductAFactory();
    AbstractFactory factoryB = new ProductBFactory();

    AbstractProduct productA = factoryA.createProduct();
    AbstractProduct productB = factoryB.createProduct();

    System.out.println("工厂A生产的产品名称:" + productA.getName());
    System.out.println("工厂B生产的产品名称:" + productB.getName());

}
复制代码

打印结果

工厂A生产的产品名称:A
工厂B生产的产品名称:B
复制代码

3、源码锚点

从本小节开始,笔者将会对每种设计模式都以Java/Android源码举例说明,用锚点记忆法来辅助记忆;比如提起建造者模式就像到Android AlertDialog,提到观察者模式就想到OnClickListener一样

工厂模式在源码中的提现,我们可以记住Java容器类的迭代器:Iterator

为了加深记忆,我们这里简单剖析使用工厂方法迭代器的角色分工:

  1. 抽象工厂
    1. Iterable
  2. 工厂实现
    1. ArrayList
    2. HashMap
  3. 抽象产品
    1. Iterator
  4. 产品实现(jdk1.8)
    1. ArrayList中的Itr类
    2. HashMap中的EntryIterator/KeyIterator/ValueIterator类

从上面的结构来看,ArrayListHashMap中的iterator方法其实就相当于一个工厂方法,专为new对象而生,这里iterator方法是构造并返回一个具体的迭代器

4、小结

工厂方法模式的主要优点是增加新的产品类时无须修改现有系统,并封装了产品对象的创建细节,系统具有良好的灵活性和可扩展性;其缺点在于增加新产品的同时需要增加新的工厂,导致系统类的个数成对增加,在一定程度上增加了系统的复杂性

笔者这里没有提到简单工厂,实际上,当把工厂方法的抽象工厂类删除掉,就是简单工厂模式了,所以可以把简单工厂理解为工厂方法的简化版本;笔者这里没有给出示例,想要了解更多的可以去看阿里巴巴淘系技术在知乎上的回答:简单工厂模式、工厂方法模式和抽象工厂模式有何区别

此小节涉及到的代码在这里

四、创建型模式:抽象工厂

1、模式定义

抽象工厂模式是所有形式的工厂模式中最为抽象和最具一般性的一种形态。抽象工厂模式与工厂方法模式最大的区别在于,工厂方法模式针对的是一个产品等级结构,而抽象工厂模式则需要面对多个产品等级结构

抽象工厂模式同样包含四个角色:

  1. 抽象工厂用于声明生成抽象产品的方法;
  2. 具体工厂实现了抽象工厂声明的生成抽象产品的方法,生成一组具体产品,这些产品构成了一个产品族,每一个产品都位于某个产品等级结构中;
  3. 抽象产品为每种产品声明接口,在抽象产品中定义了产品的抽象业务方法;
  4. 具体产品定义具体工厂生产的具体产品对象,实现抽象产品接口中定义的业务方法。

虽然抽象工厂的角色数量和工厂方法相同,都是4个,但由于每个角色下还有其他角色,理解起来就会稍微有点费劲,这一点也可以从抽象工厂工厂方法UML类图看出来区别,抽象工厂明显更复杂一些

由于抽象工厂稍微有点复杂,不能再用上面的ProductA、FactoryB等无实际意义的类名来举例,为了方便理解,笔者这里使用手机组装厂来做类比,他们的角色分工如下:

  1. 抽象产品
    1. 电池组装厂
    2. 屏幕组装厂
  2. 具体产品实现
    1. 电池
      1. 比亚迪电池供应商
      2. 德赛电池供应商
    2. 屏幕
      1. 京东方屏幕供应商
      2. LG屏幕供应商
  3. 抽象工厂
    1. 手机组装厂
  4. 具体工厂实现
    1. 华为P50的组装厂
    2. 小米10的组装厂

有了上面的角色分工结构,在下面代码示例我们就直接对号入座,这样的方式可能会方便理解

image_uml_design_pattern_create_abstract_factory.jpg

图片来源:自己画的

2、代码示例

角色1-1:抽象产品类-电池

public abstract class AbstractProductBattery {

    public AbstractProductBattery() {
        createBattery();
    }

    public abstract String getBatteryName();

    protected abstract void createBattery();

}
复制代码

角色1-2:抽象产品类-屏幕

public abstract class AbstractProductScreen {

    public AbstractProductScreen() {
        createScreen();
    }

    public abstract String getScreenName();

    protected abstract void createScreen();

}
复制代码

角色2-1-1:产品实现类-电池-比亚迪

public class BYDBattery extends AbstractProductBattery {

    @Override
    public String getBatteryName() {
        return "比亚迪(BYD)";
    }

    @Override
    protected void createBattery() {
        System.out.println("加班加点生产比亚迪电池中...");
    }
}
复制代码

角色2-1-2:产品实现类-电池-德赛

public class DesayBattery extends AbstractProductBattery {

    @Override
    public String getBatteryName() {
        return "德赛(Desay)";
    }

    @Override
    protected void createBattery() {
        System.out.println("加班加点生产德赛电池中...");
    }
}
复制代码

角色2-2-1:产品实现类-屏幕-京东方

public class BOEScreen extends AbstractProductScreen {

    @Override
    public String getScreenName() {
        return "京东方(BOE)";
    }

    @Override
    protected void createScreen() {
        System.out.println("加班加点生产京东方屏幕中...");
    }

}
复制代码

角色2-2-2:产品实现类-屏幕-LG

public class LGScreen extends AbstractProductScreen {

    @Override
    public String getScreenName() {
        return "LG";
    }

    @Override
    protected void createScreen() {
        System.out.println("加班加点生产LG屏幕中...");
    }

}
复制代码

角色3:抽象工厂类

public abstract class AbstractPhoneFactory {

    protected String brand;//工厂生产的手机品牌
    protected String model;//工厂生产的手机型号
    protected AbstractProductScreen phoneScreen;//手机使用的屏幕
    protected AbstractProductBattery phoneBattery;//使用的电池

    public AbstractPhoneFactory(String brand, String model) {
        this.brand = brand;
        this.model = model;
        this.phoneScreen = createPhoneScreen();
        this.phoneBattery = createPhoneBattery();
    }

    public String getBrand() {
        return brand;
    }

    public String getModel() {
        return model;
    }

    public AbstractProductScreen getPhoneScreen() {
        return phoneScreen;
    }

    public AbstractProductBattery getPhoneBattery() {
        return phoneBattery;
    }

    protected abstract AbstractProductScreen createPhoneScreen();

    protected abstract AbstractProductBattery createPhoneBattery();
}
复制代码

角色4-1:工厂实现类-华为

public class HuaWeiPhoneFactory extends AbstractPhoneFactory {

    public HuaWeiPhoneFactory() {
        super("华为(HuaWei)", "P50");
    }

    @Override
    protected AbstractProductScreen createPhoneScreen() {
        return new LGScreen();//Lg屏幕
    }

    @Override
    protected AbstractProductBattery createPhoneBattery() {
        return new DesayBattery();//德赛的电池
    }
}
复制代码

角色4-2:工厂实现类-小米

public class XiaoMiPhoneFactory extends AbstractPhoneFactory {

    public XiaoMiPhoneFactory() {
        super("小米(XiaoMi)", "10");
    }

    @Override
    protected AbstractProductScreen createPhoneScreen() {
        return new BOEScreen();//京东方的屏幕
    }

    @Override
    protected AbstractProductBattery createPhoneBattery() {
        return new DesayBattery();//德赛电池
    }
}
复制代码

抽象工厂使用示例

    public void main() {
        AbstractPhoneFactory phoneFactory1 = new HuaWeiPhoneFactory();
        AbstractPhoneFactory phoneFactory2 = new XiaoMiPhoneFactory();
        print(phoneFactory1);
        print(phoneFactory2);
    }

    private void print(AbstractPhoneFactory phoneFactory) {
        System.out.println("产线品牌:" + phoneFactory.getBrand()
                + ",生产型号:" + phoneFactory.getModel()
                + ",电池厂商:" + phoneFactory.getPhoneBattery().getBatteryName()
                + ",屏幕厂商:" + phoneFactory.getPhoneScreen().getScreenName());
    }
复制代码

打印结果

产线品牌:华为(HuaWei),生产型号:P50,电池厂商:德赛(Desay),屏幕厂商:LG
产线品牌:小米(XiaoMi),生产型号:10,电池厂商:德赛(Desay),屏幕厂商:京东方(BOE)
复制代码

从打印结果可以看到,小米和华为都是用德赛的电池,但屏幕供应商用的不是同一家 若我们想增加一个产品线,只需要想小米和华为工厂一样,继承/实现AbstractPhoneFactory抽象工厂类即可,每个零件来自哪个供应商可以随便选择;相同的,我们也可以增加供应商来丰富我们的产品,这就是抽象工厂模式带来的好处

3、源码锚点

抽象工厂方法模式在Android源码中的实现相对来说是比较少的,在《Android 源码设计模式解析与实战》一书中提到是Android底层对MediaPlayer的创建是可以看作为抽象工厂,这一块代码笔者不熟悉,为了防止误人子弟,抽象工厂模式的源码这里笔者就不再举例,对抽象工厂源码体现感兴趣的可以自行查找其他资料

4、小结

抽象工厂模式的主要优点是隔离了具体类的生成,使得客户并不需要知道什么被创建,而且每次可以通过具体工厂类创建一个产品族中的多个对象,增加或者替换产品族比较方便,增加新的具体工厂和产品族很方便;主要缺点在于增加新的产品等级结构很复杂,需要修改抽象工厂和所有的具体工厂类,对“开闭原则”的支持呈现倾斜性。

笔者个人总结的抽象工厂工厂方法不同点:抽象工厂模式拥有多个抽象产品类,也就是本示例的电池抽象类和屏幕抽象类

抽象工厂模式适用情况包括:一个系统不应当依赖于产品类实例如何被创建、组合和表达的细节;系统中有多于一个的产品族,而每次只使用其中某一产品族;属于同一个产品族的产品将在一起使用;系统提供一个产品类的库,所有的产品以同样的接口出现,从而使客户端不依赖于具体实现。

此小节涉及到的代码在这里

五、创建型模式:建造者模式

1、模式定义

建造者模式将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。建造者模式是一步一步创建一个复杂的对象,它允许用户只通过指定复杂对象的类型和内容就可以构建它们,用户不需要知道内部的具体构建细节

下面我们以Android中创建一个弹窗类来举例,该类有以下几个特性:

  • 弹窗类有左右两个按钮,标题和内容共四个属性,弹窗类提供Builder类来组装自己
  • 一旦弹窗被创建,左右两个按钮文字不希望被更改,也就说Dialog本身不提供修改左右按钮的方法
  • Builder提供不同的创建方法,比如:创建左右两个按钮的弹窗、只有确认按钮的弹窗,创建过程由Builder来管理

建造者模式理解起来也比较容易,我们接下来康康5.2中的代码示例

2、代码示例

public class CommonDialog {

    private Params mParams;

    private CommonDialog(Params params) {
        this.mParams = params;
    }

    public void setTitleText(String title) {
        mParams.titleText = title;
    }

    public void setMessageText(String message) {
        mParams.messageText = message;
    }

    public void show() {
        //set view...
    }

    public static class Builder {

        protected Params mParams = new Params();

        public Builder setTitleText(String title) {
            mParams.titleText = title;
            return this;
        }

        public Builder setMessageText(String message) {
            mParams.messageText = message;
            return this;
        }

        public Builder setConfirmText(String confirm) {
            mParams.confirmText = confirm;
            return this;
        }

        public Builder setCancelText(String cancel) {
            mParams.cancelText = cancel;
            return this;
        }

        public CommonDialog create() {
          	//create normal dialog logic
            return new CommonDialog(mParams);
        }

        public CommonDialog createOnlyConfirm() {
          	//create only have confirm btn dialog logic
            mParams.cancelText = null;
            return new CommonDialog(mParams);
        }

    }

    private static class Params {

      	/*public to anyone*/
        private String titleText;
        private String messageText;

      	/*private field , runtime not change*/
        private String confirmText;
        private String cancelText;

    }

}
复制代码

建造者模式使用示例

public void main() {
    CommonDialog.Builder builder = new CommonDialog.Builder();
    builder.setMessageText("will you marry me");
    builder.setConfirmText("yes");
    builder.setCancelText("no");
    CommonDialog normalDialog = builder.create();//创建普通对话框
    normalDialog.show();

    builder.setMessageText("are you free now?");
    builder.setConfirmText("yes");
    CommonDialog onlyConfirmDialog = builder.createOnlyConfirm();//创建只有确认按钮的对话框
    onlyConfirmDialog.show();
    
    onlyConfirmDialog.setMessageText("Let's go to the movies?");
    onlyConfirmDialog.show();
}
复制代码

看使用示例就能明白,当在创建Dialog过程中,可以随意的更改任何属性;一旦创建了Dialog实例对象,可修改的属性就不多了。基于此,笔者总结一下Builder模式两大特点:限制、封装

3、源码锚点

建造者模式在Android源码中的实现:AlertDialog

4、小结

建造者模式的主要优点在于客户端不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象,每一个具体建造者都相对独立,而与其他的具体建造者无关,因此可以很方便地替换具体建造者或增加新的具体建造者,符合“开闭原则”,还可以更加精细地控制产品的创建过程

当然,建造者模式也是有缺点的,其主要缺点在于由于建造者模式所创建的产品一般具有较多的共同点,其组成部分相似,因此其使用范围受到一定的限制,相对应的代码量也会增加不少;如果产品的内部变化复杂,可能会导致需要定义很多具体建造者类来实现这种变化,导致系统变得很庞大。

建造者模式适用情况包括:

  1. 需要生成的产品对象有复杂的内部结构,这些产品对象通常包含多个成员属性;
  2. 需要生成的产品对象的属性相互依赖,需要指定其生成顺序;
  3. 对象的创建过程独立于创建该对象的类;
  4. 隔离复杂对象的创建和使用,并使得相同的创建过程可以创建不同类型的产品。

此小节涉及到的代码在这里

六、总结

本文介绍了常见的几种创建型设计模式,简单总结一下:

  1. 单例模式:保证一个类仅有一个实例,常见实现方式有
    1. 饿汉式:若被提前加载会占用内存
    2. 懒汉式-静态内部类
    3. 懒汉式-双重校验锁DCL
    4. 枚举类单例:由虚拟机初始化,防止破坏唯一性
  2. 工厂方法模式,通过工厂子类来确定究竟应该实例化哪一个具体产品类
    1. 简单工厂:工厂方法的简化模式
  3. 抽象工厂模式:提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类
  4. 建造者模式:将复杂对象的构造与它的表示分开,这样可以在相同的构造过程中创建不同的表示形式。

由于笔者水平有限,文中难免会出现遗漏甚至错误的地方,若您发现任何问题或者有任何建议请在这里提交Issue,感谢

全文完

七、参考资料

分类:
Android
标签: