Java设计模式【一】 - 创建型设计模式

19 篇文章 3 订阅
5 篇文章 0 订阅

Java设计模式【一】 - 创建型设计模式

参考资料

前言 - 熟能生巧(★★★)

  • 关于设计模式的学习,需要结合具体的应用场景进行理解,即站在用户的角度去理解需求,目的是让自己设计的代码能够为用户提供统一的接口,并且设计的模块具有高内聚低耦合、有更好的可扩展性、便于后期的代码维护

  • 设计模式就是根据不同的需求设计出来了容易复用的框架,是工程应用中优质代码,比如观察者模式,工厂模式等,这些都和生活中的典型例子息息相关,既方便理解也方便应用

  • 有些设计模式在实现时有多种变种,比如工厂模式可以细分成简单工厂和多工厂模式,单例模式可以划分成懒汉式和饿汉式,是否需要用,具体用哪个需要结合具体场景考虑。如果功能简单,刻意使用某种设计模式可能会增加模块的复杂度

  • 这些设计模式虽然几乎尽人皆知,但不是每个人都能用得好。比如对于工厂模式,要想孰能生巧,熟练掌握该模式,需要多思考工厂方法如何应用,而且工厂方法模式还可以与其他模式混合使用(例如模板方法模式、单例模式、原型模式等)变化出无穷的优秀设计,这也正是软件设计和开发的乐趣所在

  • 关于类图中属性的可见性

    • +(public):可以被所有其他类所访问。
    • (private):只能被自己类访问和修改。
    • #(protected) :自身,子类及同一个包中类可以访问。
    • ~(package)
    • default:同一包中的类可以访问,声明时没有加修饰符
  • Java的特征包括:封装,多态,继承和抽象

    • 为了理解Java的抽象,建议先根据需求,将各个实体类逐层向上抽象(由具体到抽象的过程),进而理解各种设计模式的设计逻辑和思路,分析其封装性、可扩展性、可见性(是否要使用privateprotected或者final修饰)和可维护性如何,是否满足设计模式的六大原则。
    • 接着通过与其他设计模式的比较,从抽象到具体理解该设计模式在需求解决上有哪些优点和缺点,巩固设计模式的学习。
  • 通过的典型类图,即一个接口,多个抽象类,然后是N个实现类

文章目录

创建型设计模式

创建型设计模式的目的是根据用户简单需求,为用户提供创建好的产品,同时为用户隐藏产品的具体创建过程; 用户无法直接new产品,需要通过以下创建型设计模式来获得产品。

一、单例模式

1、需求假设

需求:皇帝每天要上朝接待臣子、处理政务,臣子每天要
叩拜皇帝,皇帝只能有一个。问臣子如何保证每天访问的皇帝是同一个人?

2、单例模式的定义和实现

1)定义(自行实例化)

确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。

2)类图

单例模式的通用类图

3)代码实现(饿汉式,线程安全 ★★★★)

皇帝类:

public  class Emperor{
  	private static final Emperor emperor = new Emperor();
  	private Emperor(){}
  	public static Emeperor getInstance(){
    	return emperor;
  	}
  	//皇帝发话了
    public static void say(){
    	System.out.println("我就是皇帝某某某....");
    }
}

臣子类:

public class Minister {
	public static void main(String[] args) {
      for(int day=0;day<3;day++){
        Emperor emperor=Emperor.getInstance();
        emperor.say();
      }
      //三天见的皇帝都是同一个人,荣幸吧!
    }
}

Note:皇帝类有两个注意点:

  • 由于单例模式下的实体类构造函数是私有的(private),要想访问到类内初始化的实例对象,需要设置静态方法static
  • 饿汉式单例模式直接在类内初始化实例对象,要想通过静态方法返回给用户,则注意用static final关键字修饰。
  • 除了构造方法,其他方法建议使用static静态方法

3、优点和适用场景

1)优点
  • 由于单例模式在内存中只有一个实例,减少了内存开支
  • 当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后用永久驻留内存的方式来解决(注意Java的GC机制)。
  • 单例模式可以避免对资源的多重占用,例如一个写文件动作,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作
2)缺点(★★)
  • 单例模式一般没有接口,扩展很困难,若要扩展,除了修改代码基本上没有第二种途径可以实现。
  • 单例模式对测试是不利的。在并行开发环境中,如果单例模式没有完成,是不能进行测试的,没有接口也不能使用mock的方式虚拟一个对象。
3)应用场景(★★★★)
  • 要求生成唯一序列号的环境
  • 整个项目中需要一个共享访问点或共享数据,例如一个Web页面上的计数器,可以不用把每次刷新都记录到数据库中,使用单例模式保持计数器的值,并确保是线程安全的
  • 创建一个对象需要消耗的资源过多,如要访问IO和数据库等资源;
  • 需要定义大量的静态常量和静态方法(如工具类)的环境,可以采用单例模式。

最佳实践:

  • Spring中,每个Bean默认就是单例的,这样做的优点是Spring容器可以管理这些Bean的生命期,决定什么时候创建出来,什么时候销毁,销毁的时候要如何处理,等等。
  • 如果采用非单例模式(Prototype类型),则Bean初始化后的管理交由J2EE容器Spring容器不再跟踪管理Bean的生命周期

4、单例模式的扩展

1)懒汉式 - 线程不安全(★★★★)
public class Singleton {
    private static Singleton singleton = null;
    //限制产生多个对象
    private Singleton(){}
    //通过该方法获得实例对象
    public static Singleton getSingleton(){
        if(singleton == null){
            singleton = new Singleton();
        }
            return singleton;
    }
}

存在的问题:如一个线程A执行到singleton=new Singleton(),但还没有获得对象(对象初始化是需要时间的),第二个线程B
也在执行,执行到(singleton==null)判断,那么线程B获得判断条件也是为真,于是继续运行下去,线程A获得了一个对象,线程B也获得了一个对象,在内存中就出现两个对象

解决方法:使用Synchronized关键字修饰方法或者代码块,或者使用Reentrantlock,但是**仍然建议使用饿汉式单例模式,静态final对象无法被修改,可以保证线程安全 **!!

2)对象复制问题

Java中,对象默认是不可以被复制的,若实现了Cloneable接口,并实现了clone方法,则可以直接通过对象复制方式创建一个新对象。

由于单例类对象很少被要求复制,因此单例类不要实现Cloneable接口

3)多例模式(应该可以配合抽象工厂实现固定容量的线程池,快速响应提交的任务 ★★★★)

固定数量的皇帝类

public class Emperor {
    //定义最多能产生的实例数量
    private static int maxNumOfEmperor = 2;
    //每个皇帝都有名字,使用一个ArrayList来容纳,每个对象的私有属性
    private static ArrayList<String> nameList=new ArrayList<String>();
    //定义一个列表,容纳所有的皇帝实例
    private static ArrayList<Emperor> emperorList=new ArrayList<Emperor>();
    //当前皇帝序列号
    private static int countNumOfEmperor =0;
    //产生所有的对象
    static{
        for(int i=0;i<maxNumOfEmperor;i++){
        	emperorList.add(new Emperor("皇"+(i+1)+"帝"));
    	}
    }
    private Emperor(){
    	//世俗和道德约束你,目的就是不产生第二个皇帝
    }
  
    //传入皇帝名称,建立一个皇帝对象
    private Emperor(String name){
    	nameList.add(name);
    }
    //随机获得一个皇帝对象
    public static Emperor getInstance(){
    	Random random = new Random();
    	//随机拉出一个皇帝,只要是个精神领袖就成
    	countNumOfEmperor = random.nextInt(maxNumOfEmperor);
    	return emperorList.get(countNumOfEmperor);
    }
    //皇帝发话了
    public static void say(){
    	System.out.println(nameList.get(countNumOfEmperor));
    }
}

这种需要产生固定数量对象的模式就叫做有上限的多例模式,它是单例模式的一种扩展,采用有上限的多例模式,我们可以在设计时决定在内存中有多少个实例,方便系统进行扩展,修正单例可能存在的性能问题,提供系统的响应速度。例如读取文件,我们可以在系统启动时完成初始化工作,在内存中启动固定数量的reader实例,然后在需要读取文件时就可以快速响应

二、工厂模式

1、需求假设(★★★)

假设女娲可以通过八卦炉来造人,如果八卦炉的火候欠佳,则造出来白人;如果火候过旺,造出来黑人;如果火候刚刚好,则造出来黄种人,而不同人种除了肤色不同之外,语言也存在不同。

玉皇大帝知道女娲会造凡人,作为天神的他很好奇凡人究竟长啥样,因此跟女娲说:“女娲,凡人我还没见过,每个人种我都要一个,你能否帮我造出来?最近你不还要忙着补天吗,当然作为报酬,我宫里的玉石你顺便挑。”,女娲听了也随即答应了。这样女娲既要帮着生产(生产商),也要帮着采购(用户)。

2、工厂模式的定义和实现

1)定义(产品单一,★★★★)

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

2)类图
3)可见性分析

先对上面的类图的可见性分析进行解释

  • 用户只关心能否给他3个不一样的人种,因此3个产品BlackHumanYellowHumanWhiteHuman对用户可见;
  • 用户并不关心这3个人种是怎么捏出来的,他只想看看不同人种长啥样、说什么方言而已。因此作为生产商的女娲,其八卦炉(AbstractHumanFactorycreateHuman())对用户可见,用于直接为用户生产3个人种。
4)代码实现(★★★★★)
a)人类接口
public interface Human {
    //每个人种的皮肤都有相应的颜色
    public void getColor();
    //人类会说话
    public void talk();
}
b)各人种实现类
public class BlackHuman implements Human {
    public void getColor(){
    	System.out.println("黑色人种的皮肤颜色是黑色的!");
    }
    public void talk() {
    	System.out.println("黑人会说话,一般人听不懂。");
    }
}

---

public class YellowHuman implements Human {
    public void getColor(){
    	System.out.println("黄色人种的皮肤颜色是黄色的!");
    }
    public void talk() {
    	System.out.println("黄色人种会说话,一般说的都是双字节。");
    }
}

---

public class WhiteHuman implements Human {
    public void getColor(){
    	System.out.println("白色人种的皮肤颜色是白色的!");
    }
    public void talk() {
    	System.out.println("白色人种会说话,一般都是但是单字节。");
    }
}
c)人类创造工厂类(★★★★)
//抽象人类创造工厂类
public abstract class AbstractHumanFactory {
    public abstract <T extends Human> T createHuman(Class<T> c);
}

//人类创造工厂类
public class HumanFactory extends AbstractHumanFactory {
    public <T extends Human> T createHuman(Class<T> c){
        //定义一个生产的人种
        Human human=null;
        try {
            //产生一个人种
            human = (Human)Class.forName(c.getName()).newInstance();
        } catch (Exception e) {
       		System.out.println("人种生成错误!");
        }
        return (T)human;
    }
}

Note

  • 在抽象工厂类中,采用了泛型(Generic)定义了createHuman(),通过定义泛型对createHuman的输入参数产生两层限制:
    • 必须是Class类型;
    • 必须Human的实现类
  • 这里的工厂实现类通过反射来创建对象
d)Main类
public class NvWa {
    public static void main(String[] args) {
        //声明阴阳八卦炉
        AbstractHumanFactory YinYangLu = new HumanFactory();
        //女娲第一次造人,火候不足,于是白人产生了
        System.out.println("--造出的第一批人是白色人种--");
        Human whiteHuman = YinYangLu.createHuman(WhiteHuman.class);
        whiteHuman.getColor();
        whiteHuman.talk();
        
        //女娲第二次造人,火候过足,于是黑人产生了
        System.out.println("\n--造出的第二批人是黑色人种--");
        Human blackHuman = YinYangLu.createHuman(BlackHuman.class);
        blackHuman.getColor();
        blackHuman.talk();
        
        //第三次造人,火候刚刚好,于是黄色人种产生了
        System.out.println("\n--造出的第三批人是黄色人种--");
        Human yellowHuman = YinYangLu.createHuman(YellowHuman.class);
        yellowHuman.getColor();
        yellowHuman.talk();
    }
}

3、优点和适合的场景

1)优点(★★★★)
  • 良好的封装性:一个对象创建是有条件约束的,如一个调用者需要一个具体的产品对象,只要**知道这个产品的类名(或约束字符串)**就可以了,不用知道创建对象的艰辛过程,降低模块间的耦合
  • 扩展性好:只要适当地修改具体的工厂类或扩展一个工厂类,就可以完成**“拥抱变化”。例如在我们的例子中,需要增加一个棕色人种,则只需要增加一个BrownHuman类**,工厂类不用任何修改就可完成系统扩展。
  • 屏蔽产品类:产品类的实现如何变化,调用者都不需要关心,它只需要关心产品的接口(如果存在多个产品,则关心多个产品的统一接口)。如果使用JDBC连接数据库,数据库从MySQL切换到Oracle,需要改动的地方就是切
    换一下驱动名称(前提条件是SQL语句是标准语句),其他的都不需要修改,这是工厂方法模式灵活性的一个直接案例。
  • 工厂方法模式是典型的解耦框架
    • 高层模块只需要知道产品的抽象类,其他的实现类
      都不用关心,符合迪米特法则(最少知道原则)。
    • 符合依赖倒置原则(基于接口编程),只依赖产品类的抽象;
    • 符合里氏替换原则,使用产品子类替换产品父类
2)场景

工厂方法模式是new一个对象的替代品,所以在所有需要生成对象的地方都可以使用,但是需要慎重地考虑是否要增加一个工厂类进行管理,增加代码的复杂度

4、工厂模式的扩展

1)简单(静态)工厂模式

如果只有一个工厂,则可以去掉AbstarctHumanFactory,直接使用工厂实现类,利用其public static <T extends Human> createHuman(Class<T> c)方法,创建并返回继承于父类Human的对象,类图如下:

简单工厂类的代码实现如下

public class HumanFactory {
	public static <T extends Human> T createHuman(Class<T> c){
      //定义一个生产出的人种
      Human human=null;
      try {
        //产生一个人种
        human = (Human)Class.forName(c.getName()).newInstance();
      } catch (Exception e) {
        System.out.println("人种生成错误!");
      }
      return (T) human;
    }
}

相应的Main类也要发生修改,这里就不附上代码了

简单(静态)工厂模式的缺点是扩展比较困难,不符合开闭原则。

2)多工厂模式(Executors ★★★★)

在使用工厂类初始化对象时,如果所有的产品类(黑/白/黄种人)都放到一个工厂方法中进行初始化会使代码结构不清晰。因此可以使用多工厂模式来分别创建。类图如下:

每个人种(具体的产品类)都对应了一个创建者,每个创建者都独立负责创建对应的产品对象,非常符合单一职责原则

工厂类代码如下:

//抽象工厂类
public abstract class AbstractHumanFactory {
	public abstract Human createHuman();
}
//工厂实现类1
public class BlackHumanFactory {
	public Human createHuman(){
       return new BlackHuman();
    }
}

//工厂实现类2
public class WhiteHumanFactory {
	public Human createHuman(){
       return new WhiteHuman();
    }
}

//工厂实现类3
public class YellowHumanFactory {
	public Human createHuman(){
       return new YellowHuman();
    }
}

Main类代码如下:

public class NvWa {
  public static void main(String[] args) {
    //女娲第一次造人,火候不足,于是白色人种产生了
    System.out.println("--造出的第一批人是白色人种--");
    Human whiteHuman = (new WhiteHumanFactory()).createHuman();
    whiteHuman.getColor();
    whiteHuman.talk();
    
    //女娲第二次造人,火候过足,于是黑色人种产生了
    System.out.println("\n--造出的第二批人是黑色人种--");
  	Human blackHuman = (new BlackHumanFactory()).createHuman();
  	blackHuman.getColor();
    blackHuman.talk();
    
    //第三次造人,火候刚刚好,于是黄色人种产生了
    System.out.println("\n--造出的第三批人是黄色人种--");
    Human yellowHuman = (new YellowHumanFactory()).createHuman();
    yellowHuman.getColor();
    yellowHuman.talk();
  }
}

优点:每一个产品类都对应了一个创建类,好处就是创建类的职责清晰,而且结构简单。

缺点

  • 增加扩展的难度:如果要扩展一个产品类,就需要建立一个相应的工厂类,这样增加了扩展的难度。
  • 增加维护的难度:因为工厂类和产品类的数量相同,维护时需要考虑两个对象之间的关系。

解决方法:在复杂应用中,对象如果和多个工厂类绑定则代码维护成本较高,这时可以增加一个协调类避免调用者与各个
子工厂交流,协调类的作用是封装子工厂类,对高层模块提供关于多个工厂类的统一访问接口

3)替代单例模式

单例模式的核心要求就是在内存中只有一个对象,通过工厂方法模式也可以只在内存中生产一个对象,类图如下所示(单例工厂聚合Singleton类(has a)):

Singleton定义了一个private无参构造函数,目的是不允许通过new的方式创建一个对象。

//单例类
public class Singleton {
  //不允许通过new产生一个对象
  private Singleton(){
  }
  public void doSomething(){
  	//业务处理
  }
}

Singleton没有提供构造器,工厂方法如何创建单例对象呢? 答案是通过反射获取private构造器。

public class SingletonFactory {
    private static Singleton singleton;
    static{
        try {
            Class cl=
            Class.forName(Singleton.class.getName());
            //获得无参构造
            Constructor
            constructor=cl.getDeclaredConstructor();
            //设置无参构造是可访问的
            constructor.setAccessible(true);
            //产生一个实例对象
            singleton = (Singleton)constructor.newInstance();
        } catch (Exception e) {
        //异常处理
        }
    }
    public static Singleton getSingleton(){
    	return singleton;
    }
}
4)延迟初始化(★★★★)

工厂类可以用来实现延迟初始化,创建对象时先从缓存中(prMap)取,如果缓存中没有,再创建并插入prMap中,延迟初始化常用于线程池和数据库连接池的实现。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VQ9lGN3J-1664638797387)(./img/Snipaste_2022-09-27_23-47-44.png)]

参考代码如下:

public class ProductFactory {
    private static final Map<String,Product> prMap = new HashMap();
    public static synchronized Product createProduct(String type) throws Exception{
        Product product =null;
        //如果Map中已经有这个对象
        if(prMap.containsKey(type)){
            product = prMap.get(type);
        }else{
            if(type.equals("Product1")){
                product = new ConcreteProduct1();
            }else{
                product = new ConcreteProduct2();
            }
        	//同时把对象放到缓存容器中
        	prMap.put(type,product);
        }
        return product;
    }
}

扩展场景:延迟加载框架是可以扩展的,例如限制某一个产品类的最大实例化数量Map<String,List<Product>> prMap),可以通过判断Map中已有的对象数量来实现,这样的处理是非常有意义的,例如JDBC连接数据库,都会要求设置一个MaxConnections最大连接数量,该数量就是内存中最大实例化的数量

三、抽象工厂模式

1、需求假设

(工厂模式中假设的需求的后续)玉皇大帝收到3个人种的产品之后,欣赏了一番,但突然有了个疑问,他问女娲:“凡人是没有性别的吗?“, 这时女娲意识到她造的人没有性别,玉皇大帝看了女娲眉头紧锁,又有了新想法,让女娲造出男人和女人,让凡人在自己的土地上繁衍后代。因此女娲在多人种的基础上,造出了男人和女人,让男人和女人之间自由配对,完成社会文明的延续。

2、抽象工厂模式的定义和实现

1)定义(产品间具有依赖关系,★★★★)

为创建一组相关或相互依赖的对象提供一个接口,而且无需指定它们的具体类(并不是用了抽象工厂类就是抽象工厂模式)。

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

2)类图(男女人类 -> 黄种人抽象类 -> 接口)

Java典型类图,即一个接口,多个抽象类,然后是N个实现
,每个人种都是一个抽象类,性别是在各个实现类中实现的。

3)可见性分析
  • 用户关心的是男人和女人这两个产品,而不关心男人和女人的创建过程,因此用户只关心HumanFemaleFactoryMaleFactory这几个接口;
4)代码实现
a)人种接口
public interface Human {
    //每个人种都有相应的颜色
    public void getColor();
    //人类会说话
    public void talk();
    //每个人都有性别
    public void getSex();
}
b)抽象人种类
//白色人种
public abstract class AbstractWhiteHuman implements Human {
    //白色人种的颜色是白色的
    public void getColor(){
    	System.out.println("白色人种的皮肤颜色是白色的!");
    }
    //白色人种讲话
    public void talk() {
    	System.out.println("白色人种会说话,一般说的都是单字节。");
    }
}
//黑色人种
public abstract class AbstractBlackHuman implements Human {
    public void getColor(){
    	System.out.println("黑色人种的皮肤颜色是黑色的!");
    }
    public void talk() {
    	System.out.println("黑人会说话,一般人听不懂。");
    }
}
//黄色人种
public abstract class AbstractYellowHuman implements Human {
    public void getColor(){
    	System.out.println("黄色人种的皮肤颜色是黄色的!");
    }
    public void talk() {
    	System.out.println("黄色人种会说话,一般说的都是双字节。");
    }
}
c)人种实现类

以黄色人种为例:包括黄色男性人种和黄色女性人种

//黄人女性
public class FemaleYellowHuman extends AbstractYellowHuman {
    public void getSex() {
    	System.out.println("黄人女性");
    }
}

//黄人男性
public class MaleYellowHuman extends AbstractYellowHuman {
    public void getSex() {
    	System.out.println("黄人男性");
    }
}
d)抽象工厂类(关于每个产品族的抽象)
public interface HumanFactory {
    //制造一个黄色人种
    public Human createYellowHuman();
    //制造一个白色人种
    public Human createWhiteHuman();
    //制造一个黑色人种
    public Human createBlackHuman();
}
e)工厂实现类(N个产品对应N个工厂实现类)

男性工厂实现类

public class MaleFactory implements HumanFactory {
    //生产出黑人男性
    public Human createBlackHuman() {
    	return new MaleBlackHuman();
    }
    //生产出白人男性
    public Human createWhiteHuman() {
    	return new MaleWhiteHuman();
    }
    //生产出黄人男性
    public Human createYellowHuman() {
    	return new MaleYellowHuman();
    }
}

女性工厂实现类

public class FemaleFactory implements HumanFactory {
    //生产出黑人女性
    public Human createBlackHuman() {
    	return new FemaleBlackHuman();
    }
    //生产出白人女性
    public Human createWhiteHuman() {
    	return new FemaleWhiteHuman();
    }
    //生产出黄人女性
    public Human createYellowHuman() {
    	return new FemaleYellowHuman();
    }
}
f)Main类
public class NvWa {
public static void main(String[] args) {
    //第一条生产线,男性生产线
    HumanFactory maleHumanFactory = new MaleFactory();
    //第二条生产线,女性生产线
    HumanFactory femaleHumanFactory = new FemaleFactory();
  
    //生产线建立完毕,开始生产人了:
    Human maleYellowHuman = maleHumanFactory.createYellowHuman();
    Human femaleYellowHuman = femaleHumanFactory.createYellowHuman();
    System.out.println("---生产一个黄色女性---");
    femaleYellowHuman.getColor();
    femaleYellowHuman.talk();
    femaleYellowHuman.getSex();
    
    System.out.println("\n---生产一个黄色男性---");
    maleYellowHuman.getColor();
    maleYellowHuman.talk();
    maleYellowHuman.getSex();
    /*
    * .....
    * 后面继续创建不同人种的男性,女性
    */
    }
}

3、优点和适合场景

1)小总结(★★★★★)

这里以女娲造男女人来解释抽象工厂方法(相比于工厂模式,抽象工厂模式有多条生产线,这里使为了让男女之间产生爱情,因此产品是男女,男女之间具有相关性,两个产品由最终的两个工厂实现类创建)

  • N个产品(男/女)则有N产品实体类;
  • 每个产品中有M个族(黑/白/黄人,产品族为3)则有M抽象产品类(不同族有不同语言);
  • N个产品(男/女)则有N工厂实现类;(不同工厂实现类对应不同的产品生产线
  • M个产品族(黑/白/黄人)则有M抽象工厂方法(1个抽象工厂类);
  • N工厂实现类中,每个工厂类可以生产1个产品族(黑男/黑女)

更加简化的抽象工厂类图如下所示:

  • 抽象产品类和抽象工厂类绑定
  • 产品实现类和工厂实现类绑定
2)优缺点(★★★)
a)优点
  • 封装性:每个产品的实现类不是高层模块要关心的,它要关心的是什么?是接口,是抽象
  • 产品族内的约束非公开状态:如果女娲在造人的时候在产品族内增加了约束,比如为了**“满足白人出生率低,黄种人出生率高“的约束**,假设生产出来的男人在黑人和黄种人的占比为1:1,则在生产出的女人在白人和黄种人的占比可以配置为1:10。这个生产过程对调用工厂类的高层模块是透明的(只是举个例子,不提倡一夫多妻)
b)缺点

抽象工厂模式的最大缺点是产品族扩展非常困难,但对于产品扩展是非常方便的

  • 如果在产品中加入一个新的产品中性人 intersex),只需要增加一个关于中性人的工厂实体类即可;
  • 如果增加一个新的产品族(蓝人 BlueHuman),则需要在抽象工厂类中增加一个新的工厂方法(createBlueHuman() ),相应在每个工厂实体类(黑/白/黄人)中去实现抽象工厂类增加createBlueHuman() ;这一点违背了开闭原则(修改无法关闭)

因此抽象工厂模式适合于横向扩展(增加产品),不适合于纵向扩展(增加产品族)

3)应用场景(★★★★)

抽象工厂模式的使用场景定义非常简单:一个对象族(或是一组没有任何关系的对象)都有相同的约束,则可以使用抽象工厂模式。

比如一个文本编辑器和一个图片处理器都是软件实体,但是Unix下的文本编辑器和Windows下的文本编辑器虽然功能和界面都相
,但是代码实现是不同的,图片处理器也有类似情况。也就是具有了共同的约束条件:操作系统类型(造人的共同约束条件是人的性别)。于是我们可以使用抽象工厂模式,产生不同操作系统下的编辑器和图片处理器,参考类图如下:

Note:不同操作系统的产品分为两个产品,每个系统产品中包含文本编辑器,图片处理器,即为两个产品族

四、生成器模式

1、需求假设

A公司与汽车模型制造商B签了一个合同,将奔驰和宝马的车辆模型交给B去做,并额外增加了一个新的需求:汽车的启动、停止、喇叭声都由客户A自己控制。其中奔驰模型A是先有引擎声音,然后再响喇叭;奔驰模型B是先启动起来,然后再有引擎声音。

2、生成器模式的定义和实现

1)定义

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

2)类图(★★★★)

Note

  • 生产出的N多个奔驰和宝马车辆模型都有run()方法,但是具体到每一个模型的run()方法中间的执行任务的顺序是不同的,A说要啥顺序,B给啥顺序。
  • CarModel中定义的setSequence方法,客户A可以通过该方法配置要执行的顺序,run()则会根据sequence定义的顺序完成指定的顺序动作。
3)可见性分析(★★★)
  • 用户虽然关心车辆模型是否支持对基本动作(startstopalarm)顺序的手动设置(setSequence),以及车辆整体的表现结果(run),但实际关心的是最终得到的不同类型的产品(getABenzModel(),所以对于setSequencerun可以由建造者来完成,而通过导演类为用户提供一个统一的接口getABenzModel()
  • 一个建造者对应一个用户的需求,对应一个车辆模型的组装顺序,为了给用户提供一个统一的接口,这里将所有建造者统一用导演类来管理
  • 对于抽象类中的run(),为了防止实现类中对其进行重写,需要对其使用final修饰。
  • 对于抽象类的基本方法,用户不可见但实现类可见,因此用protected修饰。
4)代码实现
a)车辆模型抽象类
public abstract class CarModel {
  
    //这个参数是各个基本方法执行的顺序
    private ArrayList<String> sequence = new ArrayList<String>();
	//模型是启动开始跑了
	protected abstract void start();
	//能发动,那还要能停下来,那才是真本事
	protected abstract void stop();
	//喇叭会出声音,是滴滴叫,还是哔哔叫
	protected abstract void alarm();
	//引擎会轰隆隆地响,不响那是假的
	protected abstract void engineBoom();
  
	//那模型应该会跑吧,别管是人推的,还是电力驱动,总之要会跑
	final public void run() {
        //循环一边,谁在前,就先执行谁
        for(int i=0;i<this.sequence.size();i++){
            String actionName = this.sequence.get(i);
            if(actionName.equalsIgnoreCase("start")){
                this.start(); //开启汽车
            }else if(actionName.equalsIgnoreCase("stop")){
        		this.stop(); //停止汽车
        	}else if(actionName.equalsIgnoreCase("alarm")){
        		this.alarm(); //喇叭开始叫了
        	}else if(actionName.equalsIgnoreCase("engine boom")){ //如果是engine boom关键字
        		this.engineBoom(); //引擎开始轰鸣
       		}
        }
    }
  
    //把传递过来的值传递到类内
    final public void setSequence(ArrayList<String> sequence){
    	this.sequence = sequence;
    }
}
b)奔驰和宝马模型
//Benz
public class BenzModel extends CarModel {
    protected void alarm() {
    	System.out.println("奔驰车的喇叭声音是这个样子的...");
    }
    protected void engineBoom() {
    	System.out.println("奔驰车的引擎室这个声音的...");
    }
    protected void start() {
    	System.out.println("奔驰车跑起来是这个样子的...");
    }
    protected void stop() {
    	System.out.println("奔驰车应该这样停车...");
    }
}

//BMW
public class BMWModel extends CarModel {
    protected void alarm() {
    	System.out.println("宝马车的喇叭声音是这个样子的...");
    }
    protected void engineBoom() {
    	System.out.println("宝马车的引擎室这个声音的...");
    }
    protected void start() {
    	System.out.println("宝马车跑起来是这个样子的...");
    }
    protected void stop() {
    	System.out.println("宝马车应该这样停车...");
    }
}
c)Main类生产BMW模型
public class Main {
    public static void main(String[] args) {
        /*
        * 客户告诉XX公司,我要这样一个模型,然后XX公司就告诉我老大
        * 说要这样一个模型,这样一个顺序,然后我就来制造
        */
        BenzModel benz = new BenzModel();
        //存放run的顺序
        ArrayList<String> sequence = new ArrayList<String>
        ();
        sequence.add("engine boom"); //客户要求,run的时候时候先发动引擎
        sequence.add("start"); //启动起来
        sequence.add("stop"); //开了一段就停下来
        //我们把这个顺序赋予奔驰车
        benz.setSequence(sequence);
        benz.run();
    }
}
d)抽象汽车组装类(使用建造者管理Main类,为用户提供统一的接口)
public abstract class CarBuilder {
    //建造一个模型,你要给我一个顺序要,就是组装顺序
    public abstract void setSequence(ArrayList<String> sequence);
    //设置完毕顺序后,就可以直接拿到这个车辆模型
    public abstract CarModel getCarModel();
}
e)车模型组装者
//Benz
public class BenzBuilder extends CarBuilder {
    private BenzModel benz = new BenzModel();
    public CarModel getCarModel() {
    	return this.benz;
    }
    public void setSequence(ArrayList<String> sequence) {
    	this.benz.setSequence(sequence);
    }
}

//BMW
public class BMWBuilder extends CarBuilder {
    private BMWModel bmw = new BMWModel();
    public CarModel getCarModel() {
    	return this.bmw;
    }
    public void setSequence(ArrayList<String> sequence) {
    	this.bmw.setSequence(sequence);
    }
}
f)导演类
public class Director {
      private ArrayList<String> sequence = new ArrayList();
      private BenzBuilder benzBuilder = new BenzBuilder();
      private BMWBuilder bmwBuilder = new BMWBuilder();
      /*
      * A类型的奔驰车模型,先start,然后stop,其他什么引擎了,喇叭一概没
      有
      */
      public BenzModel getABenzModel(){
          //清理场景,这里是一些初级程序员不注意的地方
          this.sequence.clear();
          //这只ABenzModel的执行顺序
          this.sequence.add("start");
          this.sequence.add("stop");
          //按照顺序返回一个奔驰车
          this.benzBuilder.setSequence(this.sequence);
          return (BenzModel)this.benzBuilder.getCarModel();
      }
      /*
      * B型号的奔驰车模型,是先发动引擎,然后启动,然后停止,没有喇叭
      */
      public BenzModel getBBenzModel(){
          this.sequence.clear();
          this.sequence.add("engine boom");
          this.sequence.add("start");
          this.sequence.add("stop");
          this.benzBuilder.setSequence(this.sequence);
          return (BenzModel)this.benzBuilder.getCarModel();
      }
  	  ...
}

Note:由于一个建造者对应一个需求,如果每个建造者都和客户绑定耦合度太高,所以可以使用导演类为用户提供一个统一的接口

3、优点和适用场景

1)优点
  • 封装性:使用建造者模式可以使客户端不必知道产品内部组成的细节,如例子中我们就不需要关心每一个具体的模型内部是如何实现的,产生的对象类型就是CarModel
  • 不同建造者独立,容易扩展
2)应用场景
  • 相同的方法,不同的执行顺序,产生不同的事件结果时,可以采用建造者模式。
  • 多个部件或零件,都可以装配到一个对象中,但是产生的运行结果又不相同时,则可以使
    用该模式。
  • 产品类非常复杂,或者产品类中的调用顺序不同产生了不同的效能,这个时候使用建造者模
    式非常合适。
  • 在对象创建过程中会使用到系统中的一些其他对象,这些对象在产品对象的创建过程中不易
    得到时,也可以采用建造者模式封装该对象的创建过程。
3)建造者和工厂方法的区别(★★★★★)
  • 建造者模式最主要的功能是基本方法的调用顺序安排,也就是这些基本方法已经实现了,通俗地说就是零件的装配,顺序不同产生的对象也不同
  • 工厂方法则重点是创建,创建零件是它的主要职责,组装顺序则不是它关心的

五、原型模式

Note:原型模式和单例模式很好理解,这里简单介绍

1、需求假设

实现个性化电子账单,为每个客户发送个性化的邮件,每个邮件中包含每个客户的个人信息。

2、定义和实现

定义

用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。

类图

如果AdvTemplate是从数据库中读取到的电子账单模板,Mail是一封邮件类,发送邮件时对该对象进行操作。

如果采用单线程发送,按照一封邮件发出去需要0.02秒(够小了,你还要到数据库中取数据呢),600万封邮件需要33个小时。

如果采用多线程发送,在产生第一封邮件对象,放到线程1中运行,还没有发送出去;线程2也启动了,直接就把邮件对象mail的收件人地址和称谓修改掉了,线程不安全了。

因此有两种解决方法:

  • new一个新的mail对象
  • 对象的复制(clone):如果有对象之间存在很多公共的信息clone效率比new高。

因此类图修正如下:

参考java重写clone()方法

1)浅拷贝

java浅克隆,即在实现Clonable接口时,重写Objectclone方法,并转化成子类类型对象(Person p = (Person) super.clone()),但如果子类对象中有引用类型(Address),clone的对象p与原型对象所指向的Address对象是相同的

public class Person{
    private Address address;
  
    @Override
    protected Person clone() {// throws CloneNotSupportedException  写到下面
        //return (Person)super.clone();
        Person p=null;
        try{
            p=(Person)super.clone();
        }catch(CloneNotSupportedException e){
            throw new RuntimeException(e);
            //e.printStackTrace();
        }
        return p;
    }
}
2)深拷贝(★★★★)

java深克隆则要求所有Person对象的所有引用类型都要重写clone方法,比如 p=(Person)super.clone();p.address = address.clone();

public class Address{
    @Override
    protected Address clone() {// throws CloneNotSupportedException  写到下面
        //return (Person)super.clone();
        Addressaddr=null;
        try{
            addr=(Address)super.clone();
        }catch(CloneNotSupportedException e){
            throw new RuntimeException(e);
            //e.printStackTrace();
        }
        return addr;
    }
}

public class Person{
    private Address address;
  
    @Override
    protected Person clone() {// throws CloneNotSupportedException  写到下面
        //return (Person)super.clone();
        Person p=null;
        try{
            p=(Person)super.clone();
            p.address = address.clone();
        }catch(CloneNotSupportedException e){
            throw new RuntimeException(e);
            //e.printStackTrace();
        }
        return p;
    }
}

3、应用场景(★★★★)

原型模式的核心是一个clone方法,通过该方法进行对象的拷贝,Java提供了一个Cloneable接口来标示这个对象是可拷贝的,Cloneable接口只是一个标记作用,在JVM中具有这个标记的对象才有可能被拷贝。

1)优点:
  • 性能优良:原型模式是在内存二进制流的拷贝,要比直接new一个对象性能好很多,特别是要在一个循环体内产生大量的对象时,原型模式可以更好地体现其优点。
  • 在调用clone()方法时,JVM会根据这个标识在堆内存中以二进制的方式拷贝对象,重新分配一个内存块,并没有执行构造函数new
2)应用场景
  • 资源优化场景:类初始化需要消化非常多的资源,这个资源包括数据、硬件资源等
  • 性能和安全要求的场景:通过new产生一个对象需要非常繁琐的数据准备或访问权限,则可以使用原型模式;
  • 一个对象多个修改者的场景:一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时可以考虑使用
    原型模式拷贝多个对象供调用者使用(JMM模型,volatile关键字)。
  • 一般是和工厂方法模式一起出现,通过clone的方法创建一个对象,然后由工厂方法提供给调用者。
3)注意事项
  • 构造函数不会被执行
  • 深拷贝和浅拷贝建议不要混合使用,特别是在涉及类的继承时,父类有多个引用的情况就非常复杂,建议的方案是深拷贝和浅拷贝分开实现(这个场景不是很明白??)
  • 对象的clone与对象内的final关键字是有冲
    突的,原因是final类型是无法赋值的,所以无法通过clone之后对其进行修改。因此要使用clone方
    法,在类的成员变量上就不要增加final关键字
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值