设计模式读书笔记(二)设计模式之创建模式

来源:互联网 发布:差额计算法公式 编辑:程序博客网 时间:2024/05/29 15:12
  • 设计模式的分类
  • 创建模式
  • 单例模式
  • 工厂方法模式
  • 抽象工厂模式
  • 建造者模式
  • 原型模式

设计模式的分类

java设计模式中共23种模式,根据功能和特点可归为3类

今天进行分享的就是创建模式中相关的设计模式

创建模式

单例模式

单例模式: 一个相对简单的模式,目的是确保某一个类只有一个实例,而且自行实例化并向整个系统提供实例

根据以上定义中的要点,相信很多童鞋已经可以自己写出单例模式的简单实现了,下面进行简单说明

//饿汉式public class SingletonHungry {    private String name;    /**     * 构造函数私有化 不允许其他类实例化     */    private SingletonHungry(String name) {        this.name = name;    }        /**     * 私有静态常量实例化 类初始化时创建实例 自行实例化     */    private static final SingletonHungry single = new SingletonHungry("hekx");          /**     * 公有静态访问方法提供实例获取     * @return     */    public static SingletonHungry getInstance() {          return single;      }    public String getName() {        return name;    }}

public class SingletonLazy {    //懒汉式    /**     * 持有私有静态实例,防止被引用,此处赋值为null,目的是实现延迟加载     */    private SingletonLazy(){}        /**     * 私有静态全局变量初始化为空     */    private static SingletonLazy single= null;    public static SingletonLazy getInstance(){        if(single == null){            single = new SingletonLazy();        }        return single;    }}   

以上是最简单的单例模式的实现方式,在此我们简单总结一下单例模式的优点

  • 在内存中只存在一个实例,减少内存开支,特别对于一个对象需要频繁创建、销毁,而且创建或销毁时无法保证性能时,单例模式的优势就很明显了
  • 若对象的产生需要消耗较多的资源,则也应该以单例模式在应用启动时创建生成对象,永久驻留在内存中

缺点:

  • 一般没有接口,存在不利于扩展的问题
  • 并行测试中如果单例对象未能及时创建完成,可能导致严重出错
  • 单例模式与单一职责原则有冲突,因为它将"单例"与"业务"融合在一个类中

上面的栗子中,都是最简单的实现,懒汉式也存在多线程环境下的隐患,虽然可以加synchronized关键字进行锁判定,但始终会存在jvm级别的隐患,所以还是推荐使用饿汉式的单例模式,在程序启动时自动创建一次
其次,需要考虑对象复制的情况。java中,对象默认不能被复制,但如果实现了cloneable接口并实现了clone方法,则可以直接通过对象复制方式创建一个新对象,对象复制是不用调用类的构造函数,因此即便是私有构造函数,对象仍然可被复制。解决方案最好是单例类不要实现cloneable接口

而根据饿汉式的原则,在程序启动时创建实例,所以也有各种方式去实现,比如内部类等等,其原理都是一样的,在这就不一一列举了,从本质上讲设计模式本身是针对某一类问题的解决思想,而非固定的代码片段,所以学习和掌握的重点应该是各种模式的核心思想和对应解决的问题

那现在来个急转弯,单例的思想和实现已经了然,如果某实例最多可以有3个或者其他指定数量,又该如何实现呢

public class ManyObject {    //设置实例的最大数量    private static int maxNum = 2;    //实例集合    private static ArrayList<ManyObject> objs = new ArrayList<ManyObject>();    //访问下标    private static int index = 0;    //将原来实例化的部分改为实例化指定数量    static{        for(int i=0; i<maxNum; i++){            objs.add(new ManyObject());        }    }    private ManyObject(){}    //随机获取    public static ManyObject getInstance(){        Random random = new Random();        index = random.nextInt(maxNum);        return objs.get(index);    }}

单例模式相对简单,在spring中创建bean的方案,默认就是单例模式,所以各位做web的朋友应该是经常接触的,这样的优点就是Spring容器可以管理这些bean的生命周期,决定什么时候创建出来,什么时候销毁,销毁时的处理等等,如果采用非单例模式,(eg:prototype)则bean初始化后的管理交给J2EE容器,Spring容器不在跟踪管理Bean生命周期

工厂方法模式

工厂方法模式:创建一个工厂类,并对实现同一接口的类进行实例创建

下面让我们举个栗子:
场景:从工厂类中创建不同种类的人

首先我们需要一个人类的接口,以及不同的人种的实现类

public interface Human {    void getColor();    void talk();}public class BlackHuman implements Human{    @Override    public void getColor() {        System.out.println("黑色肌肤");    }    @Override    public void talk() {        System.out.println("部落语言");    }}public class WhiteHuman implements Human{    @Override    public void getColor() {        System.out.println("白色肌肤");    }    @Override    public void talk() {        System.out.println("俄罗斯语");    }}public class YellowHuman implements Human{    @Override    public void getColor() {        System.out.println("黄色肌肤");    }    @Override    public void talk() {        System.out.println("流利的川普");    }}

然后我们需要一个工厂

public abstract class AbstractHumanFactory {    public abstract Human creatHuman(String type);}public class Factory extends AbstractHumanFactory{    @Override    public Human  creatHuman(String type) {        switch (type) {        case "white":            return new WhiteHuman();        case "yellow":            return new YellowHuman();        default:            return new BlackHuman();        }    }}

现在我们可以开始通过工厂创建人的实例了

public class Main {    private static Human human = null;    public static void main(String[] args) {        AbstractHumanFactory factory = new Factory();        human  = factory.creatHuman("black");        human.talk();        human.getColor();    }}

以上就是工厂方法模式的基本使用,下面给出工厂方法模式的定义

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

工厂方法模式的通用类图

工厂方法模式的优点:

  1. 良好的封装,代码结构清晰。调用者只需传递简单的参数即可生成对象,屏蔽了创建对象的细节,降低了模块之间的耦合
  2. 良好的扩展性。增加产品类的情况下,适当地修改具体工厂类或扩展一个即可
  3. 屏蔽了产品类,调用者无需关心产品类的实现及变化,只需要关心产品的接口,只要接口不变,上层模块就不要发生变化。
  4. 典型的解耦框架。高层模块只需要知道产品的抽象类,其他的实现类都不用关心,符合迪米特法则(我不需要的就不交流)、依赖倒置原则(只依赖产品类的抽象)、里氏替换原则(子类可替换产品父类)

运用场景:

  1. 工厂方法模式是new一个对象的替代品,所以在所有需要生成对象的地方都可以使用,但也需要考虑是否要增加一个工厂类进行管理,增加代码复杂度
  2. 需要灵活的、可扩展的框架时,可以考虑工厂方法模式。
  3. 可以使用在异构项目中(例如webservice与一个非java项目交互)

工厂方法模式的扩展:

  1. 缩小为简单工厂模式

    如果一个模块只需要一个工厂,所以没有必要去实例化一个工厂类,可以直接使用静态方法去创建我们想要的对象,对之前创建人类的设计做如下改进

  2. 升级为多个工厂类

    如果初始化一个对象很费精力的情况下,所有的产品类都放到一个工厂中进行初始化会使得代码结构不清晰。依旧举个栗子,一个产品有5个具体实现,每个实现类的初始化(不仅仅是new,初始化包括new对象,并对对象设置一定的初始值)方法均不相同,如果写在一个工厂方法中,势必显得不够完美。现在我们仍以创建不同人类为栗子,每种人都拥有自己的工厂,更改为如下设计

  3. 替代单例模式

    单例模式的核心要求就是内存中只有一个对象,通过工厂方法模式也可以实现内存中只产生一个对象

        public class Singleton {        private Singleton(){}        public void doSomeThing(){            System.out.println("业务处理中");        }     }    public class SingletonFactory {        private static Singleton singleton = null;        static{          try {            Class<?> cl = Class.forName(Singleton.class.getName());            //获取无参构造            Constructor<?> constructor = cl.getDeclaredConstructor();            //设置无参构造可访问            constructor.setAccessible(true);            //实例化对象            singleton = (Singleton) constructor.newInstance();        } catch (Exception e) {            // TODO Auto-generated catch block            e.printStackTrace();        }    }    public static Singleton getSingleton(){        return singleton;    }}
  4. 延迟初始化

    一个对象被消费完毕后,并不立刻释放,工厂类保持其初始化状态,等待再次被使用。
    在下面的栗子中,如果Map中已经有对像,则直接取出返回,如果没有,则产生一个放入Map,方便下次调用。
    延迟加载还能用在对象初始化比较复杂的情况下,例如硬件访问,涉及多方面的交互,则可以通过延迟加载降低对象产生和销毁带来的复杂性。

    public class ProductFactory {    private static final Map<String,Product> prMap = new HashMap<String,Product>();    public static synchronized Product createProduct(String type) throws Exception{        Product product = null;        if(prMap.containsKey(type)){            product = prMap.get(type);        }else{            if(type.equals("methon1")){                product = new ConcreteProduct();            }            prMap.put(type, product);        }        return product;    }}

抽象工厂模式

对上面创建人类的设计进行修改,以便于更灵活的创建不同性别或者其他属性的人类。

抽象工厂模式:为创建一组相关或相互依赖的对象提供一个接口,而无需指定他们的具体类。

抽象工厂模式是工厂方法模式的升级,在有多个业务品种、业务分类时,通过抽象工程模式需要的对象是一种非常好的解决方式。

下面给出抽象工厂通用类图:

下面举一个栗子:
两个产品,分别有两种实现方式,构建抽象工厂进行对象创建。

    AbstractCreator c1 = new Creator1();    AbstractCreator c2 = new Creator2();            AbstractProductA a1 = c1.createProductA();    AbstractProductA a2 = c2.createProductA();            AbstractProductB b1 = c1.createProductB();    AbstractProductB b2 = c2.createProductB();

设计中,没有任何一个方法与实现类有关系,对于一个产品来说,我们只要知道他的工厂方法就可以直接产生一个产品对象,无需关心其实现类。


抽象工厂模式优点:

  1. 封装性,每个产品实现类不是高层需要关心的,关心的是接口、抽象,它不关心对象是如何创建出来的,因为这些都由工厂类负责,只需要知道工厂类,就能创建出需要的对象。
  2. 产品族内的约束为非公开状态,具体的产品族内的约束是在工厂内实现的。

抽象工厂模式缺点:

  1. 产品族扩展非常困难。例如上面的栗子中,如果扩展产品族,则需要增加产品C,AbstractCreator里面增加createProductC(),然后修改两个实现类。严重违反了开闭原则。所有与契约(抽象类和接口)有关系的代码都要修改,有侵害的危险。


抽象工厂模式使用场景:
一个对象族(或者一组没有任何关系的对象)都有相同的约束,则可以使用抽象工厂模式。
[对于不同操作系统下的文本编辑器和图片编辑器,拥有相同的行为,但具体实现是不同的,而相同的约束就是操作系统]


抽象工厂模式注意事项:
抽象工厂模式扩展产品族比较困难,但产品等级非常容易扩展【横向扩展容易,纵向扩展困难】

建造者模式

工厂类模式提供的是创建单个类的模式,而建造者模式则是将各个产品集中起来进行管理,用来创建复合对象(某个类具有不同属性)

下面我们来模拟一个场景:对于车模型提供不同的实现,并在客户端由客户指定顺序调用实现类的行为。对此我们做如下设计。

客户端中,我们通过sequence指定实现类的行为调用顺序,在实现类run()中进行一次调用。但客户的需求是不断变动的,如果每个实现的调用顺序不相同,这种实现就显得不足以应对了。所以对每种模型产品定义一个建造者,将需要的顺序告诉建造者由他创建。更改设计如下:

        CarBuilder benzBuild = new BenzBuilder();        benzBuild.setSequence(sequence);        CarModel car = benzBuild.getCarModel();        car.run();

若在不能预知客户需要的什么顺序的模型的情况下,再一次进行封装,指挥各个事件的先后顺序,为每种顺序指定一个代码。修改设计为如下:

增加了导演类,将各种可能出现的模型(车型及调用顺序的组合)通过方法实现

public class Director {    private ArrayList<String> sequence =  new ArrayList<String>();    private BenzBuilder benzb =  new BenzBuilder();    private BMWBuilder bmwb =  new BMWBuilder();        public BenzModel getABenzModel(){        this.sequence.clear();        this.sequence.add("start");        this.sequence.add("stop");        this.benzb.setSequence(this.sequence);        return (BenzModel) this.benzb.getCarModel();    }        public BMWModel getCBMWModel(){        this.sequence.clear();        this.sequence.add("engine");        this.sequence.add("start");        this.sequence.add("stop");        this.benzb.setSequence(this.sequence);        return (BMWModel) this.bmwb.getCarModel();    }    ...}

以上就是建造者模式的简单实现了。

建造者模式:也叫生成器模式,将一个复杂对象的构建与表示分离,使得同样的构建过程可以创建不同的表示。

建造者模式通用类图:

  • 产品类:通常实现模板方法模式,拥有模板方法和基本方法。
  • Builder抽象建造者:规范产品的组建,一般是由子类实现。
  • ConcreteBuilder具体建造者:实现抽象类定义的所有方法,并且返回一个组建好的对象。
  • 导演类:负责安排已有模块的顺序,告诉Builder开始建造

建造者模式优点:

  • 封装性:客户端不必知道产品内部组成的细节,不需要关心模型内部是如何实现的
  • 扩展性:各种模型之间相互独立,对系统扩展非常有利
  • 控制细节风险:由于建造者之间独立,因此可以对建造过程逐步细化,不对其他模块产生任何影响

使用场景:

  • 相同的方法,不同的执行顺序,产生不同的事件结果
  • 多个部件或零件,都可以装配到一个对象中,但是产生的运行结果又不相同
  • 产品类非常复杂,或者产品类中的调用顺序不同产生了不同的效能
  • 对象创建过程中会使用到系统中的一些其他对象,这些对象在产品对象的创建过程中不易得到时,可以采用建造者模式封装该对象的创建过程。

注意事项:
建造者模式关注的是零件类型的装配工艺(顺序),而工厂方法则重点是创建。这是与工厂方法模式最大不同的地方,虽然同为创建类模式,但关注点不同。

原型模式

原型模式:用原型实例指定创建对象的种类,并通过拷贝这些原型创建新的对象。【不通过new关键字产生对象,而是通过对象复制来实现的模式】


public class Mail implements Cloneable{    private String receiver;    private String subject;    private String appellation;    private String context;    private String tail;    @Override    public Mail clone(){        //Mail mail = new Mail();        Mail mail = null;        try {            mail = (Mail)super.clone();        } catch (CloneNotSupportedException e) {            // TODO Auto-generated catch block            e.printStackTrace();        }        return mail;    }}

原型模式的核心是一个clone()方法,通过该方法进行对象的拷贝,java提供了Cloneable接口来标识这个对象是可拷贝的,该接口中没有任何方法和属性,仅仅是一个标识,在JVM中具有这个标记的对象才可能被拷贝。而重写clone()方法即可从"可能被拷贝"-->"可以被拷贝"。

原型模式的优点:

  • 性能优良:原型模式是在内存二进制流的拷贝,要比直接new一个对象性能好很多,特别是在循环体内产生大量对象时,更能体现其优点
  • 逃避构造函数约束:既是优点也是缺点,直接在内存中拷贝,构造函数不会被执行。减少了约束。



使用场景:

  • 资源优化场景:类初始化需要消耗非常多的资源,包括数据、硬盘等
  • 性能和安全要求的场景:通过new产生一个对象需要非常繁琐的数据准备或访问权限,则可以使用原型模式
  • 一个对象多个修改者的场景:一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时,可以考虑原型模式拷贝多个对象供调用者使用


实际项目中很少单独使用原型模式,一般和工厂方法模式一起出现,通过clone方法创建一个对象,然后由工厂方法提供给调用者。


注意事项:

  • 构造函数不会被执行
  • 深拷贝/浅拷贝
    浅拷贝

    public class Thing implements Cloneable{private ArrayList<String> arrayList = new ArrayList<String>();@Overrideprotected Thing clone(){    Thing thing = null;    try {        thing = (Thing)super.clone();    } catch (CloneNotSupportedException e) {        // TODO Auto-generated catch block        e.printStackTrace();    }    return thing;}public void setValue(String value){    this.arrayList.add(value);}public ArrayList<String> getValue(){    return this.arrayList;}public static void main(String[] args) {    Thing thing = new Thing();    thing.setValue("张三");    Thing cloneThing = thing.clone();    cloneThing.setValue("李四");    System.out.println(thing.getValue());    //结果【张三,李四】}}

    java做了一个偷懒的拷贝动作,Object类提供的方法clone只是拷贝本对象,其对象内部的数组、引用对象等都不拷贝,还是指向原生对象内部的元素地址,这种拷贝就是浅拷贝。
    两个对象共享了一个私有变量,大家都能对其进行修改,这是非常不安全的方式。
    String类型不会带来浅拷贝的问题,内部的数组和引用对象才不拷贝,其他的原始类型(int/long/char)等都会被拷贝,但对于String,java就希望你把它认为是基本类型,它是没有clone方法的,处理机制也比较特殊,通过字符串池(stringpool)在需要的时候才在内存中创建新的字符串。
    注意:使用原型模式时,引用的成员变量必须满足两个条件才不会被拷贝,类的成员变量,而不是方法内变量必须是一个可变的引用对象,而不是一个原始类型或不可变对象

    深拷贝

      @Overrideprotected Thing clone(){    Thing thing = null;    try {        thing = (Thing)super.clone();        //增加一下一行代码        thing.arrayList = (ArrayList<String>) this.arrayList.clone();    } catch (CloneNotSupportedException e) {        // TODO Auto-generated catch block        e.printStackTrace();    }    return thing;}

    仅增加一行,对私有的类变量进行独立的拷贝。
    该方法实现了完全的拷贝,两个对象之间没有任何瓜葛了,这种拷贝就是深拷贝。
    深拷贝还有一种实现方式就是通过自己写二进制流来操作对象,然后实现对象的深拷贝。

    注意:深拷贝和浅拷贝建议不要混合使用,特别是涉及类的继承时,父类有多个引用的情况就非常复杂了,建议深/浅拷贝分开实现。

  • clonefinal
    对象的clone与对象内的final关键字是有冲突的,例如上面的栗子若做一些简单的修改
    private final ArrayList<String> arrayList = new ArrayList<String>();,则会爆出编译错误,final类型无法切换引用,所以实现clone时是不能存在final关键字的。

原创粉丝点击