Java设计模式【一】 - 创建型设计模式
参考资料
前言 - 熟能生巧(★★★)
-
关于设计模式的学习,需要结合具体的应用场景进行理解,即站在用户的角度去理解需求,目的是让自己设计的代码能够为用户提供统一的接口,并且设计的模块具有高内聚低耦合、有更好的可扩展性、便于后期的代码维护。
-
设计模式就是根据不同的需求设计出来了容易复用的框架,是工程应用中优质代码,比如观察者模式,工厂模式等,这些都和生活中的典型例子息息相关,既方便理解也方便应用。
-
有些设计模式在实现时有多种变种,比如工厂模式可以细分成简单工厂和多工厂模式,单例模式可以划分成懒汉式和饿汉式,是否需要用,具体用哪个需要结合具体场景考虑。如果功能简单,刻意使用某种设计模式可能会增加模块的复杂度。
-
这些设计模式虽然几乎尽人皆知,但不是每个人都能用得好。比如对于工厂模式,要想孰能生巧,熟练掌握该模式,需要多思考工厂方法如何应用,而且工厂方法模式还可以与其他模式混合使用(例如模板方法模式、单例模式、原型模式等)变化出无穷的优秀设计,这也正是软件设计和开发的乐趣所在。
-
关于类图中属性的可见性:
+
(public):可以被所有其他类所访问。–
(private):只能被自己类访问和修改。#
(protected) :自身,子类及同一个包中类可以访问。~
(package)default
:同一包中的类可以访问,声明时没有加修饰符。
-
Java
的特征包括:封装,多态,继承和抽象。- 为了理解
Java
的抽象,建议先根据需求,将各个实体类逐层向上抽象(由具体到抽象的过程),进而理解各种设计模式的设计逻辑和思路,分析其封装性、可扩展性、可见性(是否要使用private
,protected
或者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个产品
BlackHuman
,YellowHuman
,WhiteHuman
对用户可见; - 用户并不关心这3个人种是怎么捏出来的,他只想看看不同人种长啥样、说什么方言而已。因此作为生产商的女娲,其八卦炉(
AbstractHumanFactory
的createHuman()
)对用户可见,用于直接为用户生产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)可见性分析
- 用户关心的是男人和女人这两个产品,而不关心男人和女人的创建过程,因此用户只关心
Human
,FemaleFactory
和MaleFactory
这几个接口;
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)可见性分析(★★★)
- 用户虽然关心车辆模型是否支持对基本动作(
start
、stop
、alarm
)顺序的手动设置(setSequence
),以及车辆整体的表现结果(run
),但实际关心的是最终得到的不同类型的产品(getABenzModel()
),所以对于setSequence
和run
可以由建造者来完成,而通过导演类为用户提供一个统一的接口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
高。
因此类图修正如下:
1)浅拷贝
java浅克隆,即在实现Clonable
接口时,重写Object
的clone
方法,并转化成子类类型对象(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关键字