微信公众号:牛奶 Yoka 的小屋
有任何问题。欢迎来撩~
最近更新:2024/10/02
[大家好,我是牛奶。]
我们所写的每一行代码,说到底其实是对真实世界的每一处细节的映射。而设计模式,就是为了能更好的映射现实世界总结出的一些设计指导思想。不使用设计模式,同样可以完成需求的开发,但是在设计模式的加持下,新的需求更容易在代码中扩展,旧的代码更容易进行维护,开发更加灵活,新旧之间不会相互牵制,促使开发的代码向艺术品的方向迈进。
设计模式共分三种,创建型模式,结构型模式,行为型。本篇文章着重讲解创建型模式,顾名思义,该设计模式旨在指导开发者如何更好的创建一个对象。比如如何只在全局创建一个对象或者固定的x个对象(单例模式);如何创建一类创建流程极其复杂的产品对象(工厂方法模式和抽象工厂模式);如何快速复制一个复杂的对象(原型模式);如何创建一个由多个部件构成的复杂产品对象(建造者模式)。
每种设计模式,不同于网上千篇一律的定义或模板代码,我都尽可能从解决工作实际问题出发,通过代码一步步演进最终的设计模式,每种设计模式都力求讲清其来龙去脉,并解剖出一些较为独特的想法和见解,望对诸君能有所帮助!
设计模式导图
单例模式
在上一篇代码坏味道中(代码坏味道有24种?我看没必要!!),我讲到了可变数据的坏味道。而单例模式也可以解决可变类的问题。它是为了保证全局有且仅有唯一的实例对象而诞生。现实生活中,需要用到唯一对象的场景很多,比如一个国家只能有一个元首,一个公司只能有一个订单号生成器,每个人都只能有一个唯一的身份id,对应到开发建模时就可以使用单例模式。在实际开发场景中,还有常常需要唯一的集中配置管理器来存储和检索配置信息(比如Windows的任务管理器,无论点击多少次启动,它都只能弹出一个);需要唯一的日志记录器实例来记录日志信息;需要唯一的管理类来管理数据库连接池或线程池等资源池信息等。所以能在代码中确保唯一的实例,也是十分重要的。
如何确保实例唯一呢?最好的办法是“自建自销”,对象自己管理自身的创建,自己管理创建的数量,而外部只能使用。总结来讲,主要有三个关键点:
-
将类的构造方法私有化
-
在类内实例化全局唯一静态类对象
-
通过公有静态方法对外暴露对象
构造方法私有化确保外部无法创建该类,保证外部唯一;将类对象又设置为静态私有成员变量,是确保内部无法创建多个对象,确保内部唯一;而静态方法是内外部唯一的情况下,唯一的对外暴露方式。
来看实际代码案例:
public class Singleton {
public static String SOME_VAL = "some val";
private static Singleton INSTANCE = new Singleton(); // 类对象设置为静态私有成员变量
// 私有化构造方法
private Singleton() {
loadConfiguration(); // 初始化时加载配置
}
// 通过公有静态方法对外暴露对象
public static Singleton getInstance() {
return INSTANCE;
}
private void loadConfiguration() {
// 从文件系统加载配置
...
// 从数据库获取数据
...
}
}
上面代码有个问题。初始化唯一类实例时,需要调loadConfiguration方法加载配置,而该方法既要读取文件又要读取数据库,非常耗时。假如某个方法中使用SOME_VAL静态变量,如下:
public class Main {
public static void main(String[] args) {
String test = EagerSingleton.SOME_VAL;
System.out.println("test end");
}
}
这个时候,明明没有获取单例对象,但是因为调用单例对象中其他静态变量或静态方法,也会触发类加载,所以仍然执行了构造函数!!这就有点无故消耗性能了,能否不使用类实例时不加载类,实际用的时候再实例化类?
可以!将类的实例化动作 Singleton INSTANCE = new Singleton() 放到方法中,取代使用静态成员变量来实例化对象。通过方法获取实例化对象!代码如下:
public class EagerSingleton {
public static String SOME_VAL = "some val";
private static EagerSingleton INSTANCE;
private EagerSingleton() {
loadConfiguration();
}
public static EagerSingleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new EagerSingleton();
}
return INSTANCE;
}
private void loadConfiguration() {
// 从文件系统加载配置
...
// 从数据库获取数据
...
}
}
新的代码案例中,在getInstance方法中实例化对象。这样,即使调用其他静态变量或静态方法,也不会执行构造函数消耗性能,只有使用该方法时,才会进行加载执行。第一种代码方案被称之为饿汉模式,这种新方案则被称之为懒汉模式。
但是!上述懒汉模式代码同样有个问题,如果此时有两个线程,同时访问getInstance方法,同时进行判空成立,同时新建一个对象,这个时候产生两个实例对象,不再符合单例模式下唯一实例的原则;还有一种情况,假如有一个线程判断为空,已经开始创建实例,但因为创建实例比较复杂,需要大量初始化操作,耗时较长,还没有完全创建完成,这个时候第二个线程过来,发现判断也是空,所以也会进行再进行另一个新实例的创建!
这种情况怎么办? 加锁!
public class EagerSingleton {
...
private static final Object LOCK = new Object();
public static EagerSingleton getInstance() {
synchronized (LOCK) {
if (INSTANCE == null) {
INSTANCE = new EagerSingleton();
}
return INSTANCE;
}
}
...
}
问题又来了,这样加完锁后,每个线程过来无论是否创建实例,都要去获取锁,获取锁操作比较消耗性能。而实际上我们只需在第一次获取锁并创建实例。怎么办?添加双检锁:
...
public static EagerSingleton getInstance() {
if (INSTANCE == null) {
synchronized (LOCK) {
if (INSTANCE == null) {
INSTANCE = new EagerSingleton();
}
}
}
return INSTANCE;
}
...
但是上述代码又还存在一个问题。多线程情况下,一些线程可能会获取到未初始化完成的不完整对象!
不是获取对象时有判空操作么?是的,但没有用,可能出现判断为非空,但是实际对象还未实例化完成的情况。原因就得从底层指令说起。创建对象底层一般分为三步:
-
memory = allocate(); // 内存分配
-
ctorInstance(memory);// 执行实例化对象的初始化,执行构造函数
-
instance = memory;// 返回引用:将内存空间的地址赋值给对应的引用,并返回
底层编译器和处理器为了优化性能,一般会对这三条指令执行顺序进行重排。假如指令2和指令3顺序互换,线程A执行完指令1,3后,线程B执行到对象判空操作。因为指令3的执行,线程B将会判断该对象已实例化完成,于是直接获取该未初始化后的对象。结果就GG了。解决办法如下:
private volatile static EagerSingleton INSTANCE;
给单例对象添加volatile修饰符,确保底层可以禁止指令重排(该方案只能在JDK 1.5及以上版本中才能正确执行)。 这样,返回一个不完整对象的问题就解决了。
又又但是! volatile关键字会屏蔽Java虚拟机所做的一些代码优化,又会导致系统运行效率降低。(能够看出问题在代码质量和性能之间来回跳跃)
那有没有一种既能解决代码质量又能解决代码性能的最优方法?有!计算机大牛Bill Pugh博士提出了Initialization on Demand Holder(IoDH)技术。看代码:
public class Singleton {
public static String SOME_VAL = "some val";
private Singleton() {
loadConfiguration();
}
private static class HolderClass {
private final static Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return HolderClass.INSTANCE;
}
private void loadConfiguration() {
// 从文件系统加载配置
...
// 从数据库获取数据
...
}
}
上述代码,它在单例类中增加一个私有静态内部类,在该内部类中创建单例对象,再将该单例对象通过getInstance()方法返回给外部使用。由于静态单例对象不是Singleton的成员变量,因此类加载时不会实例化Singleton类。第一次调用getInstance()方法时,它才会加载内部类HolderClass,并初始化静态成员变量INSTANCE;最重要的是,该方式没有加锁和volatile关键字,Java虚拟机来保证其线程安全性,确保该成员变量只能初始化一次。这也是静态局部变量的特性,在需要时才创建实例,并且在多线程环境下也能确保线程安全。
IoDH既能像之前懒汉模式一样进行懒加载,又不会存在懒汉模式情况下的多线程问题,实现了最完美的单例模式。不过IoDH与编程语言本身的特性相关,很多面向对象语言不支持IoDH。
总结
整个单例模式的演进过程如下:
单例模式演进流程
饿汉模式、加锁懒汉模式、双检锁懒汉模式+volatile修饰符都存在性能问题,而普通懒汉模式多线程情况可能会创建多个对象,双检锁懒汉模式多线程情况下又存在对象创建不完整问题,只有IoDH技术兼顾了多线程和性能,但又存在支持语言受限。没有十全十美的设计模式,但是从其演进的流程图中,能清晰到看到历代先贤们智慧的积累和技术的进步,令人感叹。
单例模式优点:
-
可对唯一实例从多种维度(如限制国家、地区年龄等条件)进行受控访问
-
无需频繁创建销毁对象,节约系统资源,提高性能
-
允许扩展为指定数目的实例对象,被称之为多例类
单例模式缺点:
-
没有抽象层,扩展困难
-
既要有创建对象方法,又要有业务方法,职责过多
简单工厂模式
这种模式诞生初是为了解决对象创建复杂的问题。我们常规的对象创建是使用new关键字,但在实际建模对象时,一个对象的创建往往伴随着多种条件的限制,创建前后又伴随着多种关联的操作行为。举个例子,在游戏开发中,你要创建角色,你不得知道创建的角色类型,是法师、战士还是刺客;得看一看你玩的游戏模式是哪种,闯关、边境还是排位;还得判断玩家等级、角色拥有权限、携带技能(是闪现、弱化、还是终结)等多个条件,来综合判断具体创建什么样的角色。角色创建完成后,带皮肤和铭文的不得提高下攻击力,默认开局不先给个“疾跑”等等。
面对如此繁琐的对象创建,如果全部放在客户使用端创建,每次扩展新的角色,客户端使用新的角色,必须修改大量代码重新创建。于是大佬们便提出,通过一个专门的类来管理对象的创建,这个类就叫做工厂类。注意,工厂类的前提是,先要有对应的角色类。见代码案例:
角色类:
public interface GameCharacter {
void display();
void fight();
}
public class Warrior implements GameCharacter {
@Override
public void display() {
System.out.println("Warrior is ready for battle!");
}
@Override
public void fight() {
System.out.println("Warrior fights with a sword.");
}
}
// 战士
public class SuperWarrior implements GameCharacter {
@Override
public void display() {
System.out.println("SuperWarrior is ready for battle!");
}
@Override
public void fight() {
System.out.println("SuperWarrior fights with a sword.");
}
}
// 法师
public class Mage implements GameCharacter {
String skin = ''; // 皮肤
Integer inscription = 0; // 铭文
pulic Mage(String skin, Integer inscription) {
tihs.skin =skin;
tihs.inscription = inscription;
}
@Override
public void display() {
System.out.println("Mage is preparing spells.");
}
@Override
public void fight() {
System.out.println("Mage casts a fireball.");
}
}
// 刺客
public class Assassin implements GameCharacter {
@Override
public void display() {
System.out.println("Assassin is hiding in the shadows.");
}
@Override
public void fight() {
System.out.println("Assassin attacks with a dagger.");
}
}
工厂类:
public class CharacterFactory {
public GameCharacter createCharacter(String characterType, int level, boolean isNightMode) {
if ("warrior".equalsIgnoreCase(characterType)) {
if (level = 50) {
return new Warrior();
} else {
return new SuperWarrior();
}
} else if ("mage".equalsIgnoreCase(characterType)) {
String skin = '',
Integer inscription = 0;
if (isNightMode) {
// Night mode affects mage's abilities
skin = 'superSkin';
inscription = 100;
...
System.out.println("Enhancing mage for night mode.");
}
return new Mage(superSkin, inscription);
} else if ("Assassin".equalsIgnoreCase(characterType)) {
if (level > 5) {
// High-level rogues get special abilities
...
System.out.println("Rogue gains stealth ability.");
}
return new Assassin();
}
throw new IllegalArgumentException("Unknown character type: " + characterType);
}
}
客户端代码:
public class Game {
public static void main(String[] args) {
CharacterFactory factory = new CharacterFactory();
GameCharacter character = factory.createCharacter("warrior", 1, false);
character.display();
character.fight();
}
}
如上代码,工厂类中承载较多且较为复杂的对象创建,客户端创建对象,只需要通过工厂类传参即可。如果需要扩展出一个新的产品类,添加角色子类后,只需要修改工厂类即可,而没有工厂类则需要修改客户端代码,这对使用方甚不友好。另外,为更加遵从开闭原则,对扩展开放,对修改关闭。工厂模式下还可将客户端创建对象入参放到XML或properties格式的配置文件中(或者一些配置平台),这样所有的修改都在配置文件中进行,替换不同的类完全不修改原有代码,扩展更加便捷。
对扩展开放,对修改关闭。 —— 开闭原则
配置方式代码如下:
gameConfig.properties 的配置文件:
character.type=warrior
character.level=1
character.isNightMode=false
读取配置文件类:
import java.util.Properties;
public class ConfigLoader {
private Properties properties;
public ConfigLoader(String filePath) {
properties = new Properties();
try (InputStream input = new FileInputStream(filePath)) {
properties.load(input);
} catch (IOException ex) {
ex.printStackTrace();
}
}
public String getCharacterType() {
return properties.getProperty("character.type");
}
public int getCharacterLevel() {
return Integer.parseInt(properties.getProperty("character.level"));
}
public boolean isNightMode() {
return Boolean.parseBoolean(properties.getProperty("character.isNightMode"));
}
}
客户端代码:
public class Game {
public static void main(String[] args) {
ConfigLoader configLoader = new ConfigLoader("gameConfig.properties");
String characterType = configLoader.getCharacterType();
int level = configLoader.getCharacterLevel();
boolean isNightMode = configLoader.isNightMode();
CharacterFactory factory = new CharacterFactory();
GameCharacter character = factory.createCharacter(characterType, level, isNightMode);
character.display();
character.fight();
}
}
简单工厂模式有一个很重要的思想是,它将对象的创建和使用分离,使用方只负责使用,不负责创建,这一思想完全契合现实场景;从另一方面看,简单工厂模式将变化集中,变化和不变进行了有效隔离,也极大降低了修改代码的风险。
有时候,创建类对象的工厂方法会设置为静态方法,来表示这个工厂方法和工厂类无关,无需先实例化再创建,同时表示该工厂方法是无状态的———不保存任何历史创建信息,每次的输出仅取决于每次的输入。因此,简单工厂方法也可以称为静态工厂方法。当然,也没必要非设置为静态方法,非静态方法的好处时能够跟踪历史创建信息,还能用来配置、缓存、延迟初始化、条件创建等。还有些时候,为了简化简单工厂模式,会将抽象产品类和工厂类合并,将静态工厂方法移至抽象产品类中。
总结
简单工厂模式优点:
1、创建类时的判断条件必不可少,以前可能放在客户端,现在放在工厂类中,将创建和使用分离。
2、客户端无须知道所创建的具体产品类的类名,只需要知道具体产品类所对应的参数即可。进一步方便使用端的对象创建。
3、配置文件的引入,实现了不修改源代码的替换。
简单工厂模式缺点:
1、工厂类中集中所有对象创建逻辑,职责过重,较大工厂类故障受影响面较大。
2、增加了系统的复杂度,简单类对象创建可不使用工厂类
工厂方法模式
简单工厂模式有个问题,替换不同的产品类使用配置文件能够符合开闭原则,但如果扩展一个新的产品或角色类,如上所述,都必须修改工厂类,多扩展一个类,就要在工厂类创建方法中多添加一个if语句。每次扩展都必须修改工厂类,明显不符合开闭原则。怎么办?提取抽象工厂类,每个产品或角色继承抽象工厂类创建子类,在各个工厂子类中创建各个产品。这样,如果需要扩展一个新的产品,就新增一个产品工厂子类,在产品工厂子类中创建新的产品。代码如下:
抽象工厂类:
public abstract class CharacterFactory {
public abstract GameCharacter createCharacter(String characterType, int level, boolean isNightMode);
}
具体工厂类:
public class WarriorFactory extends CharacterFactory {
@Override
public GameCharacter createCharacter(String characterType, int level, boolean isNightMode) {
if ("warrior".equalsIgnoreCase(characterType)) {
if (level == 50) {
return new Warrior();
} else {
return new SuperWarrior();
}
}
throw new IllegalArgumentException("Unknown character type: " + characterType);
}
}
public class MageFactory extends CharacterFactory {
@Override
public GameCharacter createCharacter(String characterType, int level, boolean isNightMode) {
if ("mage".equalsIgnoreCase(characterType)) {
String skin = "";
Integer inscription = 0;
if (isNightMode) {
skin = "superSkin";
inscription = 100;
System.out.println("Enhancing mage for night mode.");
}
return new Mage(skin, inscription);
}
throw new IllegalArgumentException("Unknown character type: " + characterType);
}
}
public class AssassinFactory extends CharacterFactory {
@Override
public GameCharacter createCharacter(String characterType, int level, boolean isNightMode) {
if ("assassin".equalsIgnoreCase(characterType)) {
if (level > 5) {
System.out.println("Assassin gains stealth ability.");
}
return new Assassin();
}
throw new IllegalArgumentException("Unknown character type: " + characterType);
}
}
客户使用类:
public class Game {
public static void main(String[] args) {
CharacterFactory factory = null;
// 根据游戏逻辑选择具体的工厂
if (/* some condition */) {
factory = new WarriorFactory();
} else if (/* some other condition */) {
factory = new MageFactory();
} else {
factory = new AssassinFactory();
}
GameCharacter character = factory.createCharacter("warrior", 50, false);
character.display();
}
}
观察客户端使用类,工厂类的创建又出现了if的情况,我们可以参考产品类如法炮制,将具体需要创建的工厂类放到配置文件中,客户端创建工厂类时只需要从配置文件中获取工厂类名来创建工厂类即可。这样,每次扩展新的产品,也不需要修改客户端代码。具体代码如下: gameConfig.properties 的配置文件:
character.type=warrior
character.factory=WarriorFactory
读取配置文件类:
import java.util.Properties;
public class ConfigLoader {
private Properties properties;
public ConfigLoader(String filePath) {
properties = new Properties();
try (InputStream input = new FileInputStream(filePath)) {
properties.load(input);
} catch (IOException ex) {
ex.printStackTrace();
}
}
public String getCharacterType() {
return properties.getProperty("character.type");
}
public String getCharacterFactory() {
return properties.getProperty("character.factory");
}
}
客户端使用类:
public class Game {
public static void main(String[] args) {
ConfigLoader configLoader = new ConfigLoader("gameConfig.properties");
String characterType = configLoader.getCharacterType();
String factoryClassName = configLoader.getCharacterFactory();
try {
Class<?> factoryClass = Class.forName(factoryClassName);
CharacterFactory factory = (CharacterFactory) factoryClass.getDeclaredConstructor().newInstance();
GameCharacter character = factory.createCharacter(characterType, 1, false);
character.display();
} catch (Exception e) {
e.printStackTrace();
}
}
}
这样就用配置替代了if条件,更方便后续的扩展。不过可以看出,代码更加复杂。客户端想要使用某个角色,本来仅仅只有角色类,使用哪个角色便创建哪个角色,一个角色父子类便可搞定。现在却用到两套父子类--角色父子类和工厂父子类。复杂的代码模式同样也主要针对复杂的场景,当某些产品或角色类的创建涉及很多复杂的初始化流程,比如需要连接数据库,需要创建文件,需要多种条件判断等,这个时候便可将这些复杂的初始流程在各个工厂子类中完成,工厂子类的定位便是承载各个产品或角色子类的创建流程。在工厂子类中,还可以设计多个构造方法来表示多种初始化方案,甚至有时为了简化客户端使用,创建完产品类后,工厂类中直接调用产品类中的方法返回。
总结
工厂方法模式在简单工厂模式的基础上增加了一层抽象,是简单工厂模式的延伸,又称为工厂模式、多态工厂模式;它是使用频率最高的设计模式之一,也是很多开源框架和API类库的核心模式,基本每个框架下都会有它的影子。
工厂模式优点:
1、在系统中加入新产品时,无须修改抽象工厂和抽象产品提供的接口,无须修改客户端,也无须修改其他的具体工厂和具体产品,而只要添加一个具体工厂和具体产品即可。
2、在各子类中添加创建细节,相对集中创建,问题风险更低,同时解决工厂类职责过重问题。
工厂模式缺点:
1、每个产品对应一个工厂类,增加系统复杂度和实现难度。
抽象工厂模式
抽象工厂模式是为了解决工厂模式的问题而诞生的。工厂模式100个产品需要创建100个工厂类,太冗余,所以抽象工厂模式提供一个创建一系列相关产品的接口,接口中拥有多个创建统一产品族的不同产品的方法,每个具体工厂中都会实现某一产品族所有相关产品的创建方法。比如海尔工厂类中会创建海尔冰箱、海尔电视、海尔空调这三种产品,同理格力冰箱、格力电视、格力空调就放到另一个格力工厂类中进行生产。格力工厂类和海尔工厂类都共同实现抽象工厂接口。抽象工厂方法总共涉及四种对象,抽象工厂接口、具体工厂类、抽象产品接口,具体产品类,其实和工厂模式大同小异,只不过工厂类中创建的不在是一个产品,而是一类产品族。举个例子:
抽象工厂类:
public interface CharacterFactory {
Warrior createWarrior(int level);
Mage createMage(boolean isNightMode);
Assassin createAssassin(int level);
}
具体工厂类:
public class FantasyCharacterFactory implements CharacterFactory {
@Override
public Warrior createWarrior(int level) {
if (level == 50) {
return new Warrior();
} else {
return new SuperWarrior();
}
}
@Override
public Mage createMage(boolean isNightMode) {
String skin = "";
Integer inscription = 0;
if (isNightMode) {
skin = "superSkin";
inscription = 100;
System.out.println("Enhancing mage for night mode.");
}
return new Mage(skin, inscription);
}
@Override
public Assassin createAssassin(int level) {
if (level > 5) {
System.out.println("Assassin gains stealth ability.");
}
return new Assassin();
}
}
这种模式把刚从简单工厂模式的if语句拆分出来的子工厂类又通过多个创建方法的形式合并到了一个类中。显而易见,这种一个工厂多个产品创建方法的模式,必定不符合开闭原则。假如在某个产品族工厂中需要添加一个新的产品,你需要修改抽象工厂类和所有具体工厂类,这种较大的修改必然会给提高后期的维护成本。因此,如果选择抽象工厂模式,一定要在一开始就要对产品族的所有产品考虑全面,尽可能减少后期对产品的增减。在实际软件开发中,在一些框架和API类库的设计有用到该模式,比如在Java语言的AWT(抽象窗口工具包)中就使用了抽象工厂模式,用来实现在不同的操作系统中,应用程序呈现与所在操作系统一致的外观界面。
总结
抽象工厂模式优点:
1、以产品族的工厂替代产品工厂,减少工厂冗余。
2、增加新的产品族很方便,无须修改已有系统,符合开闭原则。
抽象工厂模式缺点:
1、增加产品族中的新产品不符合开闭原则。
原型模式
原型模式诞生,是为了应对一些复杂对象的频繁创建!我们感知最深的例如我们常用的Crtl+C与Crtl+V,其背后便是应用原型模式的设计。再比如每次变更申请填写或者创建周报邮件,申请内容和周报内容每次填写都会有大量重复,如果每次都创建空白文档从头填写,会消耗大量精力,因此,如何能够快速创建相似的变更申请或周报邮件模板对象,便是一个重要课题。
工作周报示意图
以鄙人常规思维,克隆一个对象我首先想到直接new出一个对象,但是new的对象并无数据,它属于创建一个全新空白对象。为了确保完全相同,那给new出对象中赋完全相同的值,保证两个对象结构+数据均完全相同。举个例子:
@Data
public interface Prototype {
Prototype clone();
}
public class ConcretePrototype implements Prototype {
private String id;
private String type;
@Override
public Prototype clone() {
ConcretePrototype clonePrototype = new ConcretePrototype(); // new 新对象
clonePrototype.setId(this.id); // 属性赋值
clonePrototype.setId(this.type); // 属性赋值
return clonePrototype;
}
}
上述代码中的clone方法就实现了最简单的克隆--new对象+属性赋值。但当对象属性较多时,需要手动给每个属性赋值,此时clone代码就会变得极其繁琐。事实上,Java语言中Object类自带protected类型的clone()方法,每个类本身默认继承Object类,如果还想要支持被克隆,只需要再实现一个标识接口Cloneable即可。Java提供的克隆方法能够保证克隆对象和原型对象不为同一对象,同时又保证克隆对象和原型对象类型相同。克隆对象使用方法即x.clone()。使用Java自带克隆方法分为三步:
-
该类实现Cloneable接口
-
在类中覆盖Object类的clone()方法,并声明为public
-
在类的clone()方法中,调用super.clone()
见示例代码:
public class ConcretePrototype implements Cloneable {
private String id;
private String type;
public Prototype clone() {
try {
return (Prototype) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
这样当类中有较多属性时,就能够简化克隆流程。但是当类的属性不仅多且类型还多样化,尤其包含集合或自定义对象时,这个时候简单的克隆就会失效,因为即使Java自带的克隆也是浅克隆,而集合与自定义对象需要深克隆。
在Java语言中,数据类型分为值类型(基本数据类型)和引用类型,值类型包括int、double、byte、boolean、char等简单数据类型,引用类型包括类、接口、数组等复杂类型。浅克隆和深克隆的主要区别在于是否支持引用类型的成员变量的复制。如果原型对象的成员变量是引用类型,浅克隆底层是将引用对象的地址复制一份给克隆对象,这个时候原型对象和克隆对象的成员变量指向相同的内存地址。修改原型对象的引用类型变量,一定会影响克隆对象!
浅克隆:
深克隆:
深克隆方式有多种,最基础的深克隆是将类对象和成员类变量都实现Cloneable接口,代码如下:
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public class Person implements Cloneable {
private String name;
private int age;
private List<String> hobbies;
private Address address;
@Override
public Person clone() {
try {
Person clonedPerson = (Person) super.clone();
clonedPerson.hobbies = new ArrayList<>(this.hobbies); // 深克隆
clonedPerson.address = this.address.clone(); // 深克隆
return clonedPerson;
} catch (CloneNotSupportedException e) {
throw new AssertionError(); // Can never happen
}
}
public static class Address implements Cloneable {
private String street;
private String city;
@Override
public Address clone() {
try {
return (Address) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError(); // Can never happen
}
}
}
}
其余还有通过序列化方式实现克隆,使用第三方库如 Apache Commons Lang3 的 SerializationUtils.clone 方法实现克隆等。举个例子:
import org.apache.commons.lang3.SerializationUtils;
import java.io.Serializable;
// 注意实现序列化接口
public class Person implements Serializable {
private String name;
private int age;
private List<String> hobbies;
private Address address;
public Person deepClone() {
// 克隆
return SerializationUtils.clone(this);
}
}
// 注意实现序列化接口
public class Address implements Serializable {
private String street;
private String city;
}
另外,当需要克隆多个对象时,可以参考之前的工厂方法将同一产品族的产品对象集合到统一的工厂类中进行克隆,和工厂方法模型不同的是,工厂方法中工厂和产品一般是一对一或者一对多,且整个产品对象创建过程放在工厂类中,而原型模型的克隆方法一般都存在产品类中,因为每个产品都依赖自身进行克隆。所以一般只需要一个克隆工厂类即可,克隆工厂主要负责解决客户端通过入参克隆哪个对象的问题,通常会把这个工厂类称为原型管理器。以下示例辅助理解:
import java.io.Serializable;
public interface Prototype extends Serializable, Cloneable {
Prototype clone();
}
public class ConcreteProduct1 implements Prototype {
private String property;
@Override
public Prototype clone() {
try {
return (ConcreteProduct1) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
public class ConcreteProduct2 implements Prototype {
private int number;
@Override
public Prototype clone() {
try {
return (ConcreteProduct2) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
原型管理器:
public class PrototypeManager {
private static final Map<String, Prototype> prototypes = new HashMap<>();
private PrototypeManager() {
prototypes.put("ConcreteProduct1", new ConcreteProduct1("Value1"));
prototypes.put("ConcreteProduct2", new ConcreteProduct2(42));
}
public static Prototype clonePrototype(String key) {
Prototype prototype = prototypes.get(key);
if (prototype != null) {
return prototype.clone();
}
return null;
}
}
使用原型管理器:
public class Main {
public static void main(String[] args) {
// 克隆原型对象
Prototype ConcreteProduct1 = PrototypeManager.clonePrototype("ConcreteProduct1");
Prototype ConcreteProduct2 = PrototypeManager.clonePrototype("ConcreteProduct2");
}
}
总结
原型模式优点:
1、提高复杂实例对象的创建效率,节省创建成本。适用于初始化时间长,占用太多的CPU资源或网络资源的对象等。
2、可以保存每次克隆对象的状态。使用原型模式将对象复制一份并将其状态保存起来,以便在需要的时候使用,例如恢复到某一历史状态,可辅助实现撤销操作,git工具的历史记录便涉及该模式。
原型模式缺点:
1、深克隆较为复杂,尤其是当存在对象间多层嵌套时,需要每层对象都支持克隆。
2、当需要扩展新的克隆类时,需要修改原型管理器,违反开闭原则。
建造者模式
假如有很多不同的产品,这些产品均由一堆部件通过不同的排列组合构成。常规思路来讲,设计代码时一般会:
『创建一个部件类,里面设计多种部件属性,然后设计一个产品接口及具体产品类,在不同产品类的创建方法中,利用部件类设置各个部件属性,实现对应产品的创建。客户端使用产品时直接调用各个产品类的创建方法。』
还是以游戏为例子,假如游戏需要创建角色,部件就是每个角色的角色类型,性别,服装,发型等,产品就是每个角色:
部件类:
@Data
public class Actor {
private String type; //角色类型
private String sex; //性别
private String costume; //服装
private String hairstyle; //发型
}
抽象产品类:
public interface ActorBuilder {
//工厂方法,返同一个完整的游戏角色对象
public Actor createActor();
}
具体产品类:
public class HeroBuilder implements ActorBuilder {
@Override
public Actor createActor() (
Actor actor = new Actor();
actor.setType("英雄");
actor.setSex("男");
actor.setCostume("盔甲");
actor.setHairstyle("飘逸");
return actor;
}
}
客户端使用:
public class Main {
public static void main(String[] args) {
HeroBuilder heroBuilder = new HeroBuilder();
Actor actor = heroBuilder.createActor();
}
}
但是,第一个问题是,角色的每种属性的设置可能并非简单赋值,可能会有各种条件限制等,所以建议把每个部件的创建单独提炼出一个方法,相同类型每个产品都会创建对应部件,因此放到产品接口类中:
抽象产品类改造:
public interface ActorBuilder {
public void buildType();
public void buildSex();
public void buildCostume();
public void buildHairstyle();
//工厂方法,返同一个完整的游戏角色对象
public Actor createActor();
}
具体产品类改造:
public class HeroBuilder implements ActorBuilder {
Actor actor = new Actor();
@Override
public void buildType() {
actor.setType("英雄");
}
@Override
public void buildSex() {
actor.setSex("男");
}
@Override
public void buildCostume() {
actor.setCostume("盔甲");
}
public boolean isBareheaded(){
return true;
}
@Override
public void buildHairstyle() {
if (actor.isBareheaded()){
actor.setHairstyle("秃头");
}
actor.setHairstyle("飘逸");
}
@Override
public Actor createActor() (
buildType();
buildSex();
buildCostume();
buildHairstyle();
return actor;
}
}
上面抽象产品类和具体产品类,因为主要负责建造各个部件,因此也称为抽象建造类和具体建造类,主要负责某一角色各个部件的建造。第二问题是,当出现多个产品时,其实对于同一类型产品比如角色(英雄、刺客、法师等),每种角色的创建过程大同小异,都是创建角色类型、性别、服装、发型等,所以对于一系列同类产品的创建,可以把createActor这个创建方法中的创建流程统一移动到一个地方,替代之前放在各个产品类中,使代码更简洁。于是就有了指挥者类,指挥者类的出现是为了将产品部件的建造和产品的组装分离,所有同类型产品的组装过程统一放到指挥者类中,客户只与指挥者类交互,而在指挥者类中,调用产品类的建造方法进行产品实际的建造组装:
改造具体产品类中的建造方法:
@Override
public Actor createActor() {
return new Actor(type, sex, costume, hairstyle);
}
添加指挥者类:
public class Director {
public Actor construct(ActorBuilder builder) {
builder.buildType();
builder.buildSex();
builder.buildCostume();
builder.buildHairstyle();
return builder.createActor();
}
}
客户端使用类:
public class Client {
public static void main(String[] args) {
ActorBuilder builder = new HeroBuilder();
Director director = new Director();
Actor hero = director.construct(builder);
System.out.println(hero);
}
}
通过将产品的建造和组装分离,如果整个产品的建造流程或者顺序发生改变,只需修改指挥者类,如果产品出现新的建造方式,只需扩展一个具体建造类,无需修改其他类,使得扩展更加方便。至此,一个完整的建造者模式便成形。一个标准的建造者模式,一般包括部件类、抽象产品类(也称抽象建造类)、具体产品类(具体建造类)、指挥者类。部件类中定义产品的各个部件属性;抽象产品类中提供产品各个部件的创建方法;具体产品类中实现每种产品各个部件的建造方法;指挥者类中根据各个部件建造方法完成产品的创建。
有时候会将产品类直接放置到部件类中!还记得在之前的坏味道文章中,任何类都不推荐直接使用getter和setter方法,因为这样增大了变量可变的风险,但是你看上面对部件类各个属性赋值均为setter方法。解决该坏味道的方式,一般推荐直接在构造函数中对类变量赋值,假如角色的属性都为必填,于是创建部件类时就需要给其添加一个有参构造函数:
public class Actor {
private String type;
private String sex;
private String costume;
private String hairstyle;
// 有参构造函数
public Actor(String type, String sex, String costume, String hairstyle) {
this.type = type;
this.sex = sex;
this.costume = costume;
this.hairstyle = hairstyle;
}
}
但是,也有可能有些参数是必填,有些参数非必填,如果排列组合,就需要添加二十多种有参构造函数。就算你只使用最多参数的一种构造函数,然后将不需要的参数赋值为空,但每次使用方新建部件类时都要考虑对每一个参数进行赋值,依然非常不友好。 客户端:
Actor actor = new Actor("英雄", "男", null, null);
所以,为了能够在创建部件类时根据所需对属性变量进行赋值,直接简化建造者模型,在部件类内部添加内部产品类或者说内部建造者类:
public class Actor {
private String type;
private String sex;
private String costume;
private String hairstyle;
// 有参构造函数
public Actor(String type, String sex, String costume, String hairstyle) {
this.type = type;
this.sex = sex;
this.costume = costume;
this.hairstyle = hairstyle;
}
// 静态内部建造类
public static class Builder {
private String type = "英雄";
private String sex;
private String costume;
private String hairstyle;
public Builder setType(String type) {
this.type = type;
return this;
}
public Builder setSex(String sex) {
this.sex = sex;
return this;
}
public Builder setCostume(String costume) {
this.costume = costume;
return this;
}
public Builder setHairstyle(String hairstyle) {
this.hairstyle = hairstyle;
return this;
}
public Actor build() {
// 在这里提前给部件类赋值
return new Actor(type, sex, costume, hairstyle);
}
}
}
有了建造者内部类提前给角色的变量赋值(同时可以赋默认初始值),客户端再次使用时,就不必每次都考虑每个属性赋值:
public class Client {
public static void main(String[] args) {
Actor actor = new Actor.Builder()
.setSex("男")
.setCostume("盔甲")
.setHairstyle("飘逸")
.build();
System.out.println(actor);
}
}
当然,上述简化一般针对的是一个类中存在多个普通值类型变量,如果每个变量赋值逻辑较为复杂,一般推荐使用标准的建造者模型。
总结
建造者模式优点:
1、将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象。
2、每个具体建造者都相对独立,可以很方便地替换具体建造者或增加新的具体建造者,符合开闭原则。
3、产品对象包含多个属性时,赋值更加安全灵活。
建造者模式缺点:
1、只适用于建造流程类似的同类产品。
2、代码结构较为复杂,产品较少时无需指挥者类建造类等,可优化代码架构。
后记
至此,创建者模型讲解完毕。我在参考网上的相关文章或视频的过程中,发现几乎大多数针对设计模式的讲解,很少会分析这种模式产生的背景,以及不使用这种模式可能会出现的问题,大家几乎千篇一律的代码和定义。这也是我写这篇文章的初衷,文章里每种设计模式,我都力求能从最原始的状态讲起,确保读者能够理解这种模式诞生的来龙去脉,每种模式我都经过长久的思考,确保能提供出正确的代码案例。希望能给大家带来一些不同,带来更多的思考和启发。
在之前写代码坏味道文章时,很多解决坏味道的方式便是使用设计模式。大多数坏味道的解决其实只需要一些代码技巧,而设计模式是从更高的架构层面对可能出现的问题进行规避,一个好的代码架构离不开好的设计思想,好的设计思想也逐渐让代码成为艺术。愿每位开发者能早日成为架构大神!
历史好文:
----------------end----------------
我是牛奶,目前是一名互联网开发菜鸟,主要聚焦于互联网技术开发和个人成长的高营养价值内容分享,感兴趣的小伙伴可以在下方加个关注,大家一起共同学习和进步。