设计模式③ :生成实例
文章目录
一、前言
有时候不想动脑子,就懒得看源码又不像浪费时间所以会看看书,但是又记不住,所以决定开始写"抄书"系列。本系列大部分内容都是来源于《 图解设计模式》(【日】结城浩 著)。该系列文章可随意转载。
二、Singleton 模式
Singleton 模式 :只有一个实例
1. 介绍
何为Singleton 模式 :
- 想确保任何情况下都绝对只有一个实例
- 想在程序上表现出“只存在一个实例”
像这种确保只生成一个实例的模式被称为Singleton 模式。
Singleton 模式登场的角色:
- Singleton :在 Singleton 模式中只有这一个角色。Singleton 角色中有一个返回唯一实例的 static 方法,该方法总是会返回同一个实例。
类图如下:
Demo 如下:
public class Singleton {
private static volatile Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
2. 应用
- Spring 容器中 bean 默认的就是单例的。实际上是每个 Bean在创建后都会注册到一个 Map集合中。但实际上,Spring中的单例模式完成了保证一个类仅有一个实例,但没有从构造器级别去控制单例,这是因为spring管理的是任意的java对象。
个人使用:该部分内容是写给自己看的,帮助自身理解,因此就不交代项目背景了,读者请自行忽略(???):
- 目前的单例模式除了自定义一些工具类或者资源类之外,大部分情况下都是通过将 Bean 实例注入到Spring容器中,依赖于Spring的特性来保证实例的唯一性 。
3. 总结
单例模式有多种实现 饱汉式、饿汉式、DCL等等,这里不做过多赘述,如有需要,可以参考:设计模式-单例模式介绍和使用场景及Spring单例模式的使用
相关设计模式:以下设计模式中,多数情况下都只会生成一个实例。
- AbstractFactory模式
- Builder 模式
- Facade 模式
- Prototype 模式
三、Prototype 模式
Prototype 模式 :通过复制生成实例
1. 介绍
在开发过程中会存在 在不指定类名的前提下生成实例 的需求,如:
- 对象种类繁多,无法将它们整合到一个类中 :该种情况是需要处理的对象太多,如果将它们分别作为一个类,必须要编写很多个类文件。
- 难以根据类生成实例时 :该种情况是生成实例的过程太过复杂,很难根据类来生成实例。
- 想解耦框架与生成的实例时 :该种情况是想让生成实例的框架不依赖具体的类。这时,不能指定类名来生成实例,而要实现“注册”一个“原型”实例,然后通过复制该实例来生成新的实例。
Prototype 模式中登场的角色
- Prototype(原型):该角色负责定义用于复制现有实例来生成新实例的方法。在下面Demo中由 Product 接口扮演该角色
- ConcretePrototype(具体的原型):该角色负责实现复制现有实例并生成新的实例方法。在下面Demo中由 MessageBox 和 UnderlinePen 接口扮演该角色
- Client(使用者):该角色负责使用复制实例的方法生成新的实例。在下面Demo中由 Manger 接口扮演该角色
类图如下:
Demo 如下:
- Manager : 调用 createClone 方法复制实例的类
- Product :声明了抽象方法 use 和 createClone 的接口
- MessageBox :将字符串放入方框中并使其显示出来的类。Product 的子类
- UnderlinePen :给字符串加上下划线并使其显示出来的类。Product 的子类
public class Manager {
private Map<String, Product> registeredProducts = Maps.newHashMap();
/**
* 注册 Product
* @param name
* @param product
*/
public void register(String name, Product product) {
registeredProducts.put(name, product);
}
/**
* 创建 Product
* @param productName
* @return
*/
public Product create(String productName) {
return registeredProducts.get(productName).createClone();
}
}
// Product 接口
public interface Product extends Cloneable {
/**
* 使用该 Product
* @param msg
*/
void user(String msg);
/**
* 通过 clone 创建出一个 Product实例
* @return
*/
Product createClone();
}
public class MessageBox implements Product {
private char msgChar;
public MessageBox(char msgChar) {
this.msgChar = msgChar;
}
@Override
public void user(String msg) {
for (int i = 0; i < msg.length() + 4; i++) {
System.out.print(msgChar);
}
System.out.println();
System.out.println(msgChar + " " + msg + " " + msgChar);
for (int i = 0; i < msg.length() + 4; i++) {
System.out.print(msgChar);
}
System.out.println();
}
@Override
public Product createClone() {
try {
return (Product) clone();
} catch (Exception e) {
return null;
}
}
}
public class UnderlinePen implements Product {
private char msgChar;
public UnderlinePen(char msgChar) {
this.msgChar = msgChar;
}
@Override
public void user(String msg) {
System.out.println(msg);
for (int i = 0; i < msg.length(); i++) {
System.out.print(msgChar);
}
System.out.println();
}
@Override
public Product createClone() {
try {
return (Product) clone();
} catch (Exception e) {
return null;
}
}
}
// 测试用main 方法
public class DemoMain {
public static void main(String[] args) {
Manager manager = new Manager();
UnderlinePen underlinePen = new UnderlinePen('~');
MessageBox messageBox = new MessageBox('*');
manager.register("underlinePen", underlinePen);
manager.register("messageBox", messageBox);
manager.create("underlinePen").user("Hello World");
System.out.println();
manager.create("messageBox").user("Hello World");
}
}
Main 方法执行后输出如下
Hello World
~~~~~~~~~~~
***************
* Hello World *
***************
2. 应用
- 如利用 org.springframework.beans.BeanUtils#copyProperties 方法对 Bean 进行拷贝复制时则是通过已有 Bean 拷贝复制出新的 Bean。
个人使用:该部分内容是写给自己看的,帮助自身理解,因此就不交代项目背景了,读者请自行忽略(???):
-
在某项目中,当遇到异常情况时需要发送告警信息,告警信息多种多样,但是标题信息不同,这时使用 Prototype 模式来 拷贝告警信息的基础信息。(不过实际上即是不依靠 Prototype 模式也可以实现该需求,不过这里通过 WarnMsgManager 的存在使其与 WarnMsg 解耦)如下:
// 告警消息基础类 @Slf4j @Data public class WarnMsg implements Cloneable { /** * 消息标题 */ private String title; /** * 消息内容 */ private String content; /** * 环境 */ private String env; /** * 需要的话可以重写该方法 * * @return */ public WarnMsg clone(String content) { try { final WarnMsg clone = (WarnMsg) clone(); clone.setContent(content); return clone; } catch (Exception e) { log.warn("[告警信息][克隆失败]", e); } return null; } } // 上线告警 public class OnlineWarnMsg extends WarnMsg { public OnlineWarnMsg() { setTitle("上线"); setEnv("AAA"); } } // 离线告警 public class OfflineWarnMsg extends WarnMsg { /** * 设置消息的初始属性,假设这里有很多属性 */ public OfflineWarnMsg() { setTitle("下线"); setEnv("AAA"); } } // 告警消息管理类 public class WarnMsgManager { /** * 通过 warnMsgMap 可以将 WarnMsg 与 WarnMsgManager 解耦 */ private Map<String, WarnMsg> warnMsgMap = Maps.newHashMap(); /** * 注册消息类型 * @param name * @param warnMsg */ public void register(String name, WarnMsg warnMsg) { warnMsgMap.put(name, warnMsg); } /** * 创建对应消息类型 * @param name * @return */ public WarnMsg create(String name, String content) { return warnMsgMap.get(name).clone(content); } } public class Main { public static void main(String[] args) { // 初始化数据 WarnMsgManager warnMsgManager = new WarnMsgManager(); warnMsgManager.register("offline", new OfflineWarnMsg()); warnMsgManager.register("online", new OnlineWarnMsg()); // 创建告警消息实例 final WarnMsg online = warnMsgManager.create("online", "上线信息"); final WarnMsg offline = warnMsgManager.create("online", "下线信息"); System.out.println("offline = " + offline); System.out.println("online = " + online); } }
3. 总结
Prototype 模式可以用于 对象的构造成本比较高或是较为麻烦时,其是对象构建时需要频繁获取链接或锁的情况时,通过 clone 的效率会更高,否则可以使用new的形式。
相关设计模式
- Flyweight 模式:使用 Prototype 模式可以生成一个与当前实例的状态完全相同的实例。而使用 Flyweight 模式可以在不同的地方使用同一个实例。
- Memento 模式:使用 Prototype 模式可以生成一个与当前实例的状态完全相同的实例。而使用 Memento 模式可以保存当前实例的状态,实现快照和撤销功能。
- Composite 模式以及 Decoratort 模式 :经常使用Composite 模式以及 Decoratort 模式 时,需要能够动态地创建复杂结构实例。这时可以使用 Prototype 模式,以方便生成实例。
- Command 模式:想要复制 Command 模式中出现的命令时,可以使用 Prototype 模式。
扩展思路:
- 在 Java 对象中,我们可以通过 clone 方法来复制实例。但需要注意,如果需要调用 clone 方法,则对象需要实现 java.lang.Cloneable 接口(被复制对象的父类或父接口实现都可以)。另外 clone 方法的定义是在 java.lang.Object 中,并非在 java.lang.Cloneable 接口中, java.lang.Cloneable 接口仅仅用来标记 “可以使用 clone 方法”。
- 实现了 java.lang.Cloneable 接口 调用 clone 方法进行复制时, clone 方法的返回值是复制出的新的实例(clone 方法内部所进行的处理是分配与要复制的实例同样大小的内存空间,接着将要复制的实例中的字段的值复制到所分配的内存空间中)
- clone 方法所进行的只是浅拷贝,如果需要实现深拷贝则需要调用者自身重写。需要注意是 clone 在复制对象的时候并不会调用对象的构造函数,因此如果对象在构造函数中有一些特殊处理,则需要调用者自身去处理。
四、Builder 模式
Builder 模式 :组装复杂的实例
1. 介绍
Builder 模式中登场的角色
- Builder (建造者):该角色负责定义用于生成实例的接口 ,其中定义了准备用于生成实例的方法。
- ConcreteBuilder (具体的建造者):该角色负责实现 Builder 角色的接口的类,这里定义了在生成实例时实际被调用的方法,此外,该角色还定义了获取最终生成结果的方法。
- Director(监工):该角色负责使用Builder 角色的接口来生成实例。它并不依赖于 ConcreteBuilder 角色。他只会调用在Builder 角色中定义的方法。
- Client (使用者):该角色使用了Builder 模式。在示例程序中由 Main 类扮演该角色。
类图如下:
Demo 如下:
// 抽象构建器类
public abstract class Builder {
/**
* 构建 title
* @param title
*/
public abstract void builderTitle(String title);
/**
* 构建 content
* @param content
*/
public abstract void builderContent(String content);
/**
* 构建结束 :可以做最后的整合或者一些必填参数的校验
*/
public abstract void close();
}
// 使用 Builder 生成实例
public class Director {
private Builder builder;
public Director(Builder builder) {
this.builder = builder;
}
/**
* 定义生成过程,并且可以做一些额外的操作
*/
public void construct() {
builder.builderTitle("title");
builder.builderContent("content");
builder.close();
}
}
public class ConcreteBuilder extends Builder {
private String fileContent = "";
@Override
public void builderTitle(String title) {
fileContent += title + "\n";
}
@Override
public void builderContent(String content) {
fileContent += content;
}
@Override
public void close() {
// TODO : 可以用于校验一些必须填充的条件,或其他处理
}
public String getResult() {
return fileContent;
}
}
public class Main {
public static void main(String[] args) {
// 实际使用中会可能会由多种 Builder
ConcreteBuilder concreteBuilder = new ConcreteBuilder();
final Director director = new Director(concreteBuilder);
director.construct();
final String result = concreteBuilder.getResult();
System.out.println(result);
}
}
2. 应用
- Spring Security 中的 HttpSecurity 的构建个人认为就是对复杂对象构建的应用,如下:
@Override protected void configure(HttpSecurity http) throws Exception { // 自定义登陆拦截器 JwtLoginFilter jwtLoginFilter = new JwtLoginFilter(); jwtLoginFilter.setAuthenticationManager(authenticationManagerBean()); jwtLoginFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler); jwtLoginFilter.setAuthenticationFailureHandler(authenticationFailureHandler); JwtTokenFilter jwtTokenFilter = new JwtTokenFilter(); // 使用自定义验证实现器 JwtAuthenticationProvider jwtAuthenticationProvider = new JwtAuthenticationProvider(userDetailsService, passwordEncoder); // 登陆验证信息 http.authenticationProvider(jwtAuthenticationProvider) .authorizeRequests() .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() { @Override public <O extends FilterSecurityInterceptor> O postProcess(O object) { object.setSecurityMetadataSource(filterInvocationSecurityMetadataSource); object.setAccessDecisionManager(accessDecisionManager); return object; } }) .anyRequest().authenticated() .and() .formLogin(); // jwt 拦截器配置 http.sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) //禁用session .and() .csrf().disable() .addFilterAt(jwtLoginFilter, UsernamePasswordAuthenticationFilter.class) // 添加拦截器 .addFilterAfter(jwtTokenFilter, JwtLoginFilter.class); // 权限处理信息 http.exceptionHandling() // 用来解决认证过的用户访问无权限资源时的异常 .accessDeniedHandler(accessDeniedHandler) // 用来解决匿名用户访问无权限资源时的异常 .authenticationEntryPoint(authenticationEntryPoint); }
个人使用:该部分内容是写给自己看的,帮助自身理解,因此就不交代项目背景了,读者请自行忽略(???):
-
在某项目中,某一个接口返回的数据结果集相同,但是其内部会根据 Region 的不同通过不同的途径去获取数据,最终构建出来一个完成的数据集,如果对象的构建过程非常耗时可以将对象的部分内容异步构建。这里可以使用Builder。如下:
// 顶层抽象类,protected 防止子类滥用方法 public abstract class Builder { protected abstract String buildTitle(String msg); protected abstract String buildAuthor(String msg); protected abstract String buildContent(String msg); protected abstract boolean close(); } // 增加获取数据集的方法 public abstract class DataBuilder extends Builder { /** * 构建并校验 * @return */ public String getResult() { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append(buildTitle("")).append("\n"); stringBuilder.append(buildAuthor("")).append("\n"); stringBuilder.append(buildContent("")).append("\n"); close(); return stringBuilder.toString(); } } // 一个数据来源的实现,简单实现 public class OneDataBuilder extends DataBuilder { @Override public String buildTitle(String msg) { return "title"; } @Override public String buildAuthor(String msg) { return "author"; } @Override public String buildContent(String msg) { return "content"; } @Override public boolean close() { // TODO : 检验数据或对数据做最后的处理 return true; } } // main 方法调用 public class Main { public static void main(String[] args) { // 实际应用会有多个 DataBuilder。 DataBuilder dataBuilder = new OneDataBuilder(); // 获取结果。不同的Region数据来源不同,最后拼接的数据集 result。 // 在某些 Region 中,DataBuilder 不同部分的数据甚至可以可以开启异步来获取。最终在 getResult 方法中拼接 final String result = dataBuilder.getResult(); System.out.println("result = " + result); } }
3. 总结
- 在示例程序中, Main 并不知道有没有调用 Builder ,他仅仅是调用了 Director 的 construct 方法,这样 Director 就会开始工作完成了文档的编写,而 Main 对此一无所知。而对于 Director 知道 Builder ,并调用Builder 的方法来完成文档编写。但是他并不知道具体是 Builder 的哪个子类,他仅仅知道这是个 Builder 类。这样就可以完成类子类的传递,无论是哪个 Builder 的子类都可以借用 Director 来完成方法调用。
- Builder 模式 可以用于复杂对象的创建,尤其是多个实现的复杂对象,在设想中,设置可以将部分耗时以及与主体数据无太多关联的数据异步构建,最后通过 close 或者其他方法来等待所有同步和异步数据构建完成,提高对象的创建效率。
相关设计模式 :
- Template Method 模式:在 Builder 模式中,Director 角色控制 Builder 角色。在 Template Method 模式中,父类控制子类。
- Composite 模式:有些情况下 Builder 模式生成的实例构成了 Composite 模式。
- Abstract Factory 模式 :Abstract Factory 模式 和 Builder 模式都用于生成复杂实例。
- Facade 模式:在 Builder 模式中,Director 角色通过组合 Builder 角色中的复杂方法向外部提供可以简单生成实例的接口,Facade 模式中的 Facade 角色则是通过组合内部模块向外部提供可以简单调用的接口。
五、Abstract Factory 模式
Abstract Factory 模式 :将关联零件组装成产品
本部分内容推荐阅读 : 抽象工厂模式在spring源码中的应用,以下部分内容参考该文。
1. 介绍
抽象工厂会将抽象零件组装成抽象产品。也就是说,我们并不关心零件的具体实现,而是只关心接口。我们仅使用该接口欧将零件组装成为产品。
在 Template Method 模式和 Builder 模式中,子类这一层负责方法的具体实现。在 Abstract Factory 模式中也是一样的。在子类这一层中有具体的工厂,它负责将具体的零件组装成为具体的产品。
Abstract Factory 模式 登场的角色如下:
- AbstractProduct (抽象产品):该角色负责定义 AbstractFactory角色所生成的抽象零件和产品的接口。
- AbstractFactory(抽象工厂):该角色负责定义用于生成抽象产品的接口
- Client(委托者):该角色仅会调用 AbstractFactory 角色和 AbstractProduct 角色的接口来进行工作,对于具体的零件、产品和工厂一无所知。
- ConcreteProduct (具体产品):该角色负责实现 AbstractProduct 角色的接口。
- ConcreteFactory (具体工厂):该角色负责实现 AbstractFactory 角色的接口。
类图如下:
Demo 如下:
// 抽象罐头工厂类
public abstract class CanFactory {
/**
* 获取工厂实例
* @param className
* @return
* @throws Exception
*/
public CanFactory getCanFactory(String className) throws Exception {
final Class<?> factoryClass = Class.forName(className);
return (CanFactory) factoryClass.newInstance();
}
/**
* 包装苹果罐头
*/
public abstract AppleCan packAppleCan();
/**
* 包装牛肉罐头
*/
public abstract BeefCan packBeefCan();
}
// 互联网罐头工厂
public class InternetCanFactory extends CanFactory {
// 包装苹果罐头
@Override
public AppleCan packAppleCan() {
return new BaiduAppleCan();
}
// 包装牛肉贯通
@Override
public BeefCan packBeefCan() {
return new TencentBeefCan();
}
}
// 牛肉罐头抽象类
public abstract class BeefCan {
/**
* 闻起来如何
* @return
*/
public abstract String smell();
}
// 苹果罐头抽象类
public abstract class AppleCan {
/**
* 吃起来如何
* @return
*/
public abstract String taste();
}
// 罐头具体实现类
public class BaiduAppleCan extends AppleCan {
@Override
public String taste() {
return "牛肉真香";
}
}
public class TencentBeefCan extends BeefCan {
@Override
public String smell() {
return "苹果真甜";
}
}
在上面的 Demo 中:CanFactory、AppleCan 和 BeefCan 的关系已经确定, CanFactory 用来生产 AppleCan 和 BeefCan ,而 InternetCanFactory 作为 CanFactory 的实现类,可以生产 百度苹果罐头 和 腾讯牛肉罐头 。即抽象层定义工厂框架,实现层进行具体的实现。
2. 应用
在 Spring 中,BeanFactory 是用于管理 Bean 的一个工厂,所有工厂都是 BeanFactory 的子类。这样我们可以通过 IOC 容器来管理访问 Bean,根据不同的策略调用 getBean() 方法,从而获得具体对象。
个人使用:该部分内容是写给自己看的,帮助自身理解,因此就不交代项目背景了,读者请自行忽略(???):
-
在项目A中,需要对多种文件类型资料进行解析,而解析后的结果是相同的,则可以通过抽象工厂模式为每种文件资料实现一个工厂类。如下:
public class FileHandlerFactory { /** * pdf */ public static final String FILE_PDF = "pdf"; /** * excel */ public static final String FILE_EXCEL = "excel"; /** * 获取处理器 * @param fileType * @return */ public static FileHandler getFileHandler(String fileType) { if (FILE_PDF.equalsIgnoreCase(fileType)) { return new PdfFileHandler(); } else if (FILE_EXCEL.equalsIgnoreCase(fileType)) { return new ExcelFileHandler(); } return null; } } public abstract class FileHandler { /** * 处理 */ abstract void handle(); } public class ExcelFileHandler extends FileHandler { @Override void handle() { System.out.println("ExcelFileHandler.handle"); } } public class PdfFileHandler extends FileHandler{ @Override void handle() { System.out.println("PdfFileHandler.handle"); } } public class DemoMain { public static void main(String[] args) { FileHandler pdfHandler = FileHandlerFactory.getFileHandler(FileHandlerFactory.FILE_PDF); FileHandler excelHandler = FileHandlerFactory.getFileHandler(FileHandlerFactory.FILE_EXCEL); pdfHandler.handle(); excelHandler.handle(); } }
3. 总结
优点 : 抽象工厂模式隔离了具体类的生成, 使得客户并不需要知道什么被创建。 由于这种隔离,更换一个具体工厂就变得相对容易, 所有的具体工厂都实现了抽象工厂中定义的那些公共接口, 因此只需改变具体工厂的实例, 就可以在某种程度上改变整个软件系统的行为。当一个族中的多个对象被设计成一起工作时, 它能够保证客户端始终只使用同一个族中的对象。增加新的族很方便, 无须修改已有系统, 符合“开闭原则”。
缺点 : 增加新的等级结构麻烦, 需要对原有系统进行较大的修改, 甚至需要修改抽象层代码,这显然会带来较大的不便, 违背了“开闭原则”。
使用场景 : 一个系统不应当依赖于具体类实例如何被创建、 组合和表达的细节, 这对于所有类型的工厂模式都是很重要的, 用户无须关心对象的创建过程, 将对象的创建和使用解耦;系统中有多于一个的族, 而每次只使用其中某一族。 可以通过配置文件等方式来使得用户可以动态改变族, 也可以很方便地增加新的族。属于同一个族的对象将在一起使用, 这一约束必须在系统的设计中体现出来。 同一个族中的对象可以是没有任何关系的对象, 但是它们都具有一些共同的约束, 如同一操作系统下的按钮和文本框, 按钮与文本框之间没有直接关系, 但它们都是属于某一操作系统的, 此时具有一个共同的约束条件: 操作系统的类型。等级结构稳定, 设计完成之后, 不会向系统中增加新的等级结构或者删除已有的等级结构。
相关设计模式:
- Builder 模式 : Abstract Factory 模式是通过调用抽象产品的接口来组装抽象产品,生成具有复杂结构的实例。Builder 模式 则是分阶段地制作复杂的实例。
- Factory Method 模式:Abstract Factory 模式在零件和产品生成时可能会使用到该模式
- Composite 模式:Abstract Factory 模式在制作产品时可能会使用该模式
- Singleton 模 :Abstract Factory 模式的具体工厂时可能会使用该模式
扩展思路:
- Abstract Factory 模式非常方便增加具体的工厂,即是修复具体工厂的Bug,都无需修改抽象工厂和调用部分。但是相对的,该模式难以增加新工厂方法,如果要增加新的工厂方法则需要将已经实现的具体工厂进行同步改造。
参考内容
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!