zoukankan      html  css  js  c++  java
  • 【设计模式】单例模式生成器模式原型模式

    前面的几种工厂模式,主要用于选择实现,这里的三种模式:单例模式、生成器模式、原型模式,主要用于生成对象,在GoF的划分中,这是创建型的五种模式(不包括简单工厂,前面提到过,这不是一个标准意义上的设计模式)。

    1.单例模式

    2.生成器模式

    3.原型模式

     

    单例模式(Singleton

    1. 应用场景及问题描述

    在大型项目开发中,许多数据要保存在属性文件或数据库里,其中有那么一类数据,我们会频繁地访问,但是这些数据挺少而且不经常变化,那么在不同的方法、不同的时间,每次需要访问的时候都去创建一个数据连接对象,重新去数据源读取数据,开销很大而且不值得,就有这么一种解决方案,把这些数据提前取得存到某个类的变量里,让整个应用程序中只有一个该类的实例,我需要的时候就去获取这个实例。

    现在来描述一个场景,一个网站的用户有多种权限,比如普通用户、管理员、VIP1、VIP2,我在应用程序中经常需要根据权限的ID去得到对应身份的文字描述,也经常需要根据这个描述去获取这种权限的ID,访问非常频繁。

    2. 单例模式

    单例模式定义:一个类仅有一个实例,并提供一个获取它的全局访问点。

    首先,只要一个类有公有的构造函数,就不能阻挡这个类对象的多次创建,所以要将构造函数私有化,在内部创建一个本类的静态实例,并通过一个静态的方法来从外部获得这个已经创建好的实例。

    单例模式分为懒汉式和饿汉式两种实现,懒汉式是一种懒的实现方式,这个静态实例不在刚解析这个类的时候就创建,而是等真正去要这个实例的时候才创建,实现权限ID与名称双向访问的类的完整定义如下(注:这里用到了一个双向的Map类DualHashbidiMap,在commons-collections-3.2.1.jar包里):

    public class IDAssisstant {
        private IRoleDAO roleDao; //通过依赖注入获得实例,访问数据库权限表
        private static DualHashBidiMap roleMap = new DualHashBidiMap();
        private static IDAssisstant instance;
        public static synchronized IDAssisstant getInstance(){
            if(instance == null)
                instance = new IDAssisstant();
            return instance;
        }
        private IDAssisstant() {
            List<Role> rlist = roleDao.findAll();
            for (Role r : rlist) {
                roleMap.put(r.getId(), r.getName());
            }
        }
        public void reInitRoleMap() throws SeeWorldException {
            List<Role> rlist = roleDao.findAll("",false);
            roleMap.clear();
            for (Role r : rlist) {
                roleMap.put(r.getId(), r.getName());
            }
        }
        public void setRoleDao(IRoleDAO roleDao) {
            this.roleDao = roleDao;
        }
        public String getRoleName(String roleId) {
            return (String) roleMap.get(roleId);
        }
        public String getRoleID(String roleName) {
            return (String) roleMap.getKey(roleName);
        }
    }

    解释一下,这个类中的roleMap变量把数据库用户权限表(role)中的数据一次全部读取出来,每次需要根据ID来获取权限名或者根据权限名来获取ID的时候直接调用getRoleName个getRoleID这样的方法来获取,roleMap是一个双向Map,可以方便地根据键查找值或者根据值查找键。仔细看一下这两行:

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

    一开始并没有创建实例,第一次真正去调用IDAssistance.getInstance()来获取实例的时候才去创建,当然这会涉及一个并发访问的问题,并发访问时可能会多次调用IDAssistant的构造函数,所以加关键词synchronized来进行修饰。

    而饿汉式是一上来就创建,省掉了每次申请实例的时候的那个if判断,缺点是不管用不用得到这个instance实例,都会在一开始解析这个类的时候就创建它,修改一下上面这几行就得到饿汉式的实现:

    private static IDAssisstant instance = new IDAssisstant();
    public static synchronized IDAssisstant getInstance(){
         return instance;
    }

    饿汉式的实现是线程安全的,没有并发的问题。

    单例模式中变量和方法的命名,一般都是用instance和getInstance()。

    3. 延迟加载和缓存思想

    懒汉模式体现了一种延迟加载的思想,所谓延迟加载,顾名思义,就是一开始不加载,真正需要的时候才去加载。

    同时它也可以体现一种缓存的思想,这是种空间换时间的思想,在上面的实现中,instance对象其实就可以看做是缓存实例。单例模式并不一定说必须只创建一个实例,比如这个实例的访问过于频繁,一个实例忙不过来,那就可能要用两个、三个或者更多,在程序里进行调度,决定把哪个实例返回给客户,简单实现如下:

    public class Singleton {
        private final static String DEFAULT_PREFIX = "Cache";
        //Map容易用来缓存实例
        private static Map<String, Singleton> map = new HashMap<String, Singleton>();
        //用来记录当前正在使用的实例标号
        private static int current = 0;
        //定义实例的最大数目
        private final static int MAX_NUM_OF_INSTANCE = 5;
        private Singleton(){
        }
        public static Singleton getInstance(){
           String key = DEFAULT_PREFIX + current;
           Singleton s = map.get(key);
           if(s == null){
               s = new Singleton();
               map.put(key, s);
           }
           current++;
           if(current >= MAX_NUM_OF_INSTANCE)
               current = 0;
           return s;
        }
    }

    注意:这里只是提供一种思路,显而易见,这个方法是线程不安全的。

    4. Java中更好地单例实现方式

    饿汉式浪费内存,如果程序运行很久都没有访问这个单例,但这个实例早已创建,内存就会一直被占用着,而懒汉式存在线程安全问题,用synchronized关键词解决后锁住了整个方法,效率会下降很多,在Java中有这么两种实现方式,同样可以实现单例,但是却拥有更好的效率,先来看第一种,是借助类级内部类来实现。

    首先看一下类级内部类的概念:类级内部类就是有static关键词修饰的内部类,没有static修饰的叫做对象级内部类,类级内部类是外部类的静态成员,与外部类对象无依赖关系,可以定义静态方法来引用外部类中静态成员(也只能引用外部类的静态成员,不能引用非静态成员),最重要的,类级内部类相当于外部类的成员,只有在第一次被使用时才被装载。

    再来看看同步,JVM在在静态字段或静态代码块中初始化数据时隐式地为我们处理了并发控制的问题。

    现在就要利用这两个概念,我们可以采用类级内部类,只要不使用这个类级内部类,就不会创建对象实例,而且采用静态初始化器的方式可以由JVM来保证并发问题,从而实现延迟加载和线程安全。先来看代码:

    public class Singleton {
        //类级内部类,与外部类实例无关,被调用时才会装载,从而实现延迟加载
        private static class SingletonHolder{
           //静态初始化器,JVM保证线程安全
           private static Singleton instance = new Singleton();
        }
        private Singleton(){
        }
        public static Singleton getInstance(){
           return SingletonHolder.instance;
        }
    }

    当getInstance方法第一次被调用的时候,第一次读取SingletonHolder.instance,这时候SingletonHolder类才初始化,此时初始化静态域创建了Singleton的实例,因为是一个静态域,只会在虚拟机装载类的时候初始化一次,由虚拟机保证线程安全性。

    第二种单例的实现方式是使用枚举类型,这是一种更为巧妙的方式,要知道,Java的枚举类型实质上是功能齐全的类,通过共有的静态final域为每个枚举常量导出实例的类,可以看做是单例的泛型化。

    用枚举类进行实现,只需要编写一个包含单个元素的枚举类型即可,用这种方法控制简洁,无偿提供序列化机制,由JVM从根本上提供保障,绝对防止多次实例化,是更简洁、高效、安全的实现方式:

    public enum Singleton {
        //定义一个元素的枚举,代表一个Singleton的实例
        uniqueInstance;
        //定义单例自己的操作
        public void someOperation(){
           //操作
        }
    }

    5. 模式说明

    单例模式的实质就是控制实例的数目,抽象工厂中的具体工厂类就可以实现为单例,当需要控制一个类的实例数目,而且客户只能从一个全局访问点访问它时选用单例模式。

    生成器模式(Builder

    1. 场景描述

    在本模式中,我选用Java包里自带的几个类做实例说明,凡是用过Java、C#等高级编程语言的,都会知道有一个String类,而在Java中,如果要对String对象应用“+”等操作效率是很低的,JDK中提供了一个抽象类叫做AbstractStringBuilder,用这个类来构造复杂的字符串效率会高很多。可能大家对这个类比较陌生,但是对它的两个子类:StringBuilder和StringBuffer就比较熟悉了,它们用append方法来串联字符串(当然还提供有其他诸如insert的方法,先只使用用得最多的append来做例子,后面实现中会加上insert),最后用一个toString方法把生成的对象返回。

    2. 生成器模式

    生成器模式是将一个复杂对象的构建与它的表示分离开,使得同样的构建过程可以创建不同的表示,其实用AbstractStringBuilder做例子有一个不好的地方,那就是它的两个子类创建的是一种对象,对应到上面的定义中,就是“构建过程中创建了相同的表示”。

    生成器模式要把构建过程独立出来,成为一个指导者,它指导装配,但不负责具体实现,而负责具体实现的就是生成器,模式结构图如下:

    目的是构造Product对象,在具体生成器类的buildPart方法中进行生成,最后通过getResult方法返回,而在指导者内部,就使用:

    public class Director {
        private Builder builder;
        public Director(Builder builder) {
           this.builder = builder;
        }
        public void construct(){
           builder.buildPart();
        }
    }

    在生成String的例子中,实现类中的append、insert方法就相当于buildPart,toString方法相当于getResult,对应有以下结构图:

    说明:其实toString方法是在抽象类AbstractStringBuilder中定义的抽象方法,本图中放在子类里,更接近前面的一般结构图。首先不管那个指导者(也可以认为Client就是指导者),在具体的Builder实现类中,每个构建部件的方法都包含创建或组装部件的功能,在这里String类就是要构建的复杂对象,下面看一下我简化了的AbstractStringBuilder类:

    public abstract class AbstractStringBuilder {
        //实际上还实现了Appendable, CharSequence这两个接口
        char[] value;
        int count;
        public AbstractStringBuilder append(String paramString)
        {
           //做追加字符的操作
            return this;
        }
        public AbstractStringBuilder insert(int paramInt, String paramString)
        {
           //做插入字符的操作
            return this;
        }
        public abstract String toString();
    }

    以及其中一个子类StringBuilder:

    public class StringBuilder extends AbstractStringBuilder{
        //原类中实现Serializable, CharSequence这两个接口
        public StringBuilder append(String paramString)
        {
            super.append(paramString);
            return this;
        }
            public StringBuilder insert(int paramInt, String paramString)
        {
            super.insert(paramInt, paramString);
            return this;
        }
        @Override
        public String toString() {
           // TODO Auto-generated method stub
           return new String(this.value, 0, this.count);
        }
    }

    除了append方法之外,我把insert方法也加了进来,可以注意到生成器的构建部件的方法(如append)有一个不成文的特点,就是会返回本类的对象来供外部使用,而不是简单地void类型。如果需要处理细节,那么可以在toString方法中加一些约束。在客户端的调用如下:

    public class Client {
        public static void main(String[] args) {
           // TODO Auto-generated method stub
           AbstractStringBuilder builder = new StringBuilder();
           String s = builder.append("qwe").append("asd").insert(3, "zxc").toString();
           //s对象的其他操作
        }
    }

    那么这样实现有什么优势吗,使用这个生成器,可以把生成String这个复杂对象的过程分成一个个的小部分来进行构建,而不是很混乱的在Client端去拼凑这个字符串更甚至是拼凑字符数组。

    3. 带指导者的String生成器

    下面对上面的场景做一个改进,比方说,现在要求,在每一个生成的字符串末尾,需要添加一行说明,用来描述是什么类的对象(StringBuilder还是StringBuffer)生成的这个String,以及其他一些信息,既然每个要构造的字符串都有正文和最后的描述这两部分,那么还在Client里面去生成就不好了吧?那么这个任务就交由指导者去完成,首先为每个Builder的实现类添加一个appendTag方法来附加尾部信息,然后创建指导者类Director:

    public class Director {
        private AbstractStringBuilder builder;
        public Director(AbstractStringBuilder builder) {
           this.builder = builder;
        }
        public void construct(Map<Integer, String> param){
           for(Integer i : param.keySet()){
               if(i < 0)
                  builder.append(param.get(i));
               else
                   builder.insert(i, param.get(i));
           }
           builder.appendTag();
           String s = builder.toString();
        }
    }

    传入的Map为一个整数和字符串的对应,整数小于0表示追加,大于等于0表示在相应位置插入。而在客户端只需要按照需求创建一个StringBuilder或是StringBuffer的对象,传入参数就可以了:

    public class Client {
        public static void main(String[] args) {
           // TODO Auto-generated method stub
           AbstractStringBuilder builder = new StringBuilder();
           //AbstractStringBuilder builder = new StringBuffer();
           Director d = new Director(builder);
           Map<Integer, String> param = new HashMap<Integer, String>();
           param.put(-1, "qwe");
           param.put(-1, "asd");
           param.put(3, "zxc");
           d.construct(param);
        }
    }

    4. 模式说明

    生成器模式的主要功能是分步骤构建复杂的产品,重在一步一步解决构造复杂对象的问题,而且更为重要的是,这个构造过程是一致的,具体实现方式可能会不同,这些不同体现在具体生成器里,在指导者中,根据不同的生成器,使用相同的构建过程,就能创建出不同的产品(String的例子中创建的是相同的产品,如果StringBuilder和StringBuffer分别生成String的一个子类对象,那就是一般化的生成器实现了,而且,其实标准生成器模式也不需要给这些千差万别的产品提供一个统一的接口)。

    所以说,生成器主要有两部分:Builder和Director,Builder来进行部件构建和产品装配,Director来进行整体构建,Builder是变化的,而Director的构建算法是固定的。可以在客户端创造Director来进行构造,简单情况下也可以直接让客户端来充当Director。

    再注意一点,标准的生成器模式,Builder中返回最终产品的方法,应该是声明在实现里而不是接口上,这种考虑是想让Director只负责构建,甚至返回最终产品都不用Director来管,那么在哪得到最终产品呢?在客户端,因为只有客户端知道它创建的是哪一种实现,只有客户端才能调用实现里有但是接口里没有的行为,具体来说,在客户端:

    StringBuffer builder = new StringBuffer();
    Director d = new Director(builder);
    d.construct(new HashMap<Integer, String>());
    String s = builder.toString();

    使用生成器模式,可以使耦合更加松散,轻易地修改实现(Builder内部的装配过程),构建算法与具体实现相分离,使代码具有更好的复用性。

    原型模式(Prototype

    1. 场景描述

    StringBuilder和StringBuffer虽然很接近,但是也有应用场景的区别,StringBuilder更适用于单线程复杂对象的构建,而StringBuffer更适用于多线程并发访问时字符串的构建,现在需求来了,有这么一个模块,负责接收一个半成品的AbstractStringBuilder对象,复制多份,稍作修饰后分发给不同模块做进一步修饰,那应该怎么实现呢?可能一下就会想到,这很简单,创建几个新的AbstractStringBuilder实例,把对应字段都赋值到新创建的对象上,然后把这新创建的对象挨个传给对应模块就行了。但是,外部选用StringBuilder还是StringBuffer来实现是有它的道理的,我们不应该去改变这个选择,那么问题就来了,不知道外部用的到底是哪个类,怎么创建新对象?

    注:本章只是仍选用AbstractStringBuilder做例子,但已经和JDK中本来的实现没有多大关系了。

    2. 耦合度较高的实现

    一种实现方法是使用instanceof关键词来判断传入的到底是哪种类型,根据不同的判断结果创建不同的对象:

    public void forward(AbstractStringBuilder s){
           AbstractStringBuilder newString = null;
           if(s instanceof StringBuilder){
               newString = new StringBuilder();
               //将s中响应字段的值赋值给newString
           }
           else if(s instanceof StringBuffer){
               newString = new StringBuffer();
               //将s中响应字段的值赋值给newString
           }
           //对newString做修饰
           //将newString分发给特定模块
    }

    可想而知,这样的实现方式耦合度是非常高的,一来作为这个分发模块,仅仅需要知道我要复制一个AbstractStringBuilder对象,并调用特定方法进行修饰分发就可以了,还需要知道具体实现吗,而且这样也很不利于扩展。

    那么更好地实现是怎么样的呢?forward这个模块是不知道传进来的对象到底是哪种类型的,但是传进来的对象本身是知道的,所以可以通过这个“原型实例”来创建新的对象。

    3. 原型模式

    原型模式,通俗地讲就是在每个类的内部提供一个创建新对象的方法,并与this对象有相同的值。换言之,原型模式要求对象实现一个可以克隆自身的接口,调用这个方法就可以通过拷贝或者克隆自身来创建一个新对象。

    基本结构如下:

    在operating方法中,就可以通过Prototype p = prototype.clone();来创建新Prototype实例,为了要求每个子类(实现类)都指定自己的克隆实现,要在公共接口中定义该方法,或在抽象类中加入clone的抽象方法。在标准的原型实现中,客户端是持有待克隆的对象的,就像上面结构图所描述的,但在这里的例子中,改用传参的方式传入待克隆对象,思想是差不多的。

    现在在AbstractStringBuilder抽象类中添加该抽象方法cloneAsb(因为Java的Object类里是自带clone方法的,这里改个名字以示区分),在子类(实现类)覆写如下,这里给出StringBuilder中的实现,StringBuffer中同理:

    @Override
    public AbstractStringBuilder cloneAsb() {
        // TODO Auto-generated method stub
        StringBuilder builder = new StringBuilder();
        builder.count = this.count;
        builder.value = new char[this.value.length];
        for(int i = 0 ; i < this.value.length ; i++)
           builder.value[i] = this.value[i];
        return builder;
    }

    克隆出来的对象通常不仅仅是new出来的新对象,而是有值的,并且不同于简单的在clone方法中返回this,对克隆出来的新实例的修改,不影响原实例的值。

    4. Java中的克隆方法

    Java在Object类中是实现了clone方法的,访问权限为protected,我们可以不在接口或抽象类中定义自己的克隆方法,而是直接覆写该方法并调用父类实现来进行克隆,这里直接让实现类去实现Cloneable接口(这是一个标识接口,里面没有内容),并在子类中继承Object的该方法如下:

    public Object clone() throws CloneNotSupportedException {
        // TODO Auto-generated method stub
        return super.clone();
    }

    这样的实现有一个问题,就是这样的克隆方法是浅克隆,只克隆值传递的数据,而引用类型的数据克隆之后还是原引用,指向的内存空间是一样的,所以对于引用型的变量,要再去实现它的clone方法,并在当前的克隆方法中一一进行赋值:

    public Object clone() throws CloneNotSupportedException {
        // TODO Auto-generated method stub
        Object obj = super.clone();
        obj.setInnerObject(this.innerObject.clone());
        return obj;
    }

    5. 模式说明

    有时候还会创建一个原型管理器来管理可被克隆的原型,其实就是有一个Map型的变量,所有原型在第一次创建后存入到这个Map变量当中,以后再要创建该类的实例就直接向原型管理器要原型。原型可以被添加、注销或获取:

    public class PrototypeManager {
        private static Map<String, Prototype> map = new HashMap<String, Prototype>();
        //主要的方法通过静态访问就可以了,不需要让外部创建对象
        private PrototypeManager(){}
        //注册原型
        public synchronized static void setPrototype(String pid, Prototype p){
           map.put(pid, p);
        }
        //销毁注册
        public synchronized static void removePrototype(String pid){
           map.remove(pid);
        }
        //获取原型
        public synchronized static Prototype getPrototype(String pid) throws Exception{
           Prototype p = map.get(pid);
           if(p == null)
               throw new Exception("该原型未注册或已被销毁!");
           return p;
        }
    }

    原型模式的优点就是隐藏具体实现类型,不用使用类似instanceof的判断来分情况讨论,更不会错误地改变实现类型,但是必须要求原型实现clone方法,尤其在引用型变量很多的情况下会很麻烦。

  • 相关阅读:
    Ubuntu下ClickHouse安装
    redis.conf配置详解(转)
    php使用sftp上传文件
    ubuntu下安装nginx1.11.10
    cookie和session的区别
    linux下Redis主从复制
    linux-ubuntu 安装配置Redis
    php的常量
    Ubuntu防火墙配置
    技术资料
  • 原文地址:https://www.cnblogs.com/smarterplanet/p/2722429.html
Copyright © 2011-2022 走看看