欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Java设计模式总结(工厂模式、建造者模式、原型模式)

程序员文章站 2022-03-03 09:48:53
记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步文章目录工厂模式1. 简单工厂2. 工厂方法factory method3. 什么时候用工厂方法,而非简单工厂模式呢4. 抽象工厂 abstract factory5. DI容器1. 工厂模式和DI容器的区别2. DI容器的核心功能有哪些3. 如何实现DI容器建造者模式1. 为什么要建造者模式2. 和工厂模式的区别原型模式1. 原型模式的原理和应用2.深拷贝和浅拷贝工厂模式工厂模式一般细分为三种类型:简单工厂、工厂方法和抽象工....

文章目录


工厂模式

工厂模式一般细分为三种类型:简单工厂、工厂方法和抽象工厂。简单工厂和工厂方法原理较为简单,较为常用,抽象工厂原理稍微复杂,较少用到。

1. 简单工厂

下面代码中,根据配置文件的后缀(json、xml、yaml、properties)选择不同的解析器(JsonRuleConfigParser、XmlRuleConfigParser…)将存储在文件中的配置解析成内存对象RuleConfig。

public class RuleConfigSource { public RuleConfig load(String ruleConfigFulePath){ String ruleConfigFileExtension = getFileExtension(ruleConfigFulePath); IRuleConfigParser parser = null; if ("json".equalsIgnoreCase(ruleConfigFileExtension)){ parser = new JsonRuleConfigParser(); }else if ("xml".equalsIgnoreCase(ruleConfigFileExtension)){ parser = new XmlRuleConfigParser(); }else if ("yaml".equalsIgnoreCase(ruleConfigFileExtension)){ parser = new YamlRuleConfigParser(); }else if ("properties".equalsIgnoreCase(ruleConfigFileExtension)){ parser = new PropertiesRuleConfigParser(); }else { throw new InvalidRuleConfigException("Rule config file format is not support: "+ruleConfigFulePath); } String configText = ""; //从ruleConfigFilePath文件读取配置文本到configText中 RuleConfig ruleConfig = parser.parse(configText); return ruleConfig; } private String getFileExtension(String filePath){ //...解析文件名获取扩展名,如rule.json 返回json return "json"; } } 

为了让代码逻辑更加清晰,可读性更好,将代码中涉及到parser创建的部分逻辑剥离出来,抽象为createParser()方法。重构后:

public class RuleConfigSource { public RuleConfig load(String ruleConfigFulePath){ String ruleConfigFileExtension = getFileExtension(ruleConfigFulePath); IRuleConfigParser parser = createParser(ruleConfigFileExtension); if (parser==null){ throw new InvalidRuleConfigException("Rule config file format is not support: "+ruleConfigFulePath); } String configText = ""; //从ruleConfigFilePath文件读取配置文本到configText中 RuleConfig ruleConfig = parser.parse(configText); return ruleConfig; } private String getFileExtension(String filePath){ //...解析文件名获取扩展名,如rule.json 返回json return "json"; } private IRuleConfigParser createParser(String configFormat){ IRuleConfigParser parser = null; if ("json".equalsIgnoreCase(configFormat)){ parser = new JsonRuleConfigParser(); }else if ("xml".equalsIgnoreCase(configFormat)){ parser = new XmlRuleConfigParser(); }else if ("yaml".equalsIgnoreCase(configFormat)){ parser = new YamlRuleConfigParser(); }else if ("properties".equalsIgnoreCase(configFormat)){ parser = new PropertiesRuleConfigParser(); } return parser; } 

为了让类的职责更单一、代码更清晰,进一步将createParser()方法剥离到一个独立的类,让该类只负责对象的创建,而这个类就是要说的简单工厂模式类。

public class RuleConfigSource { public RuleConfig load(String ruleConfigFulePath){ String ruleConfigFileExtension = getFileExtension(ruleConfigFulePath); IRuleConfigParser parser =RuleConfigParserFactory.createParser(ruleConfigFileExtension); if (parser==null){ throw new InvalidRuleConfigException("Rule config file format is not support: "+ruleConfigFulePath); } String configText = ""; //从ruleConfigFilePath文件读取配置文本到configText中 RuleConfig ruleConfig = parser.parse(configText); return ruleConfig; } private String getFileExtension(String filePath){ //...解析文件名获取扩展名,如rule.json 返回json return "json"; } } public class RuleConfigParserFactory { public static IRuleConfigParser createParser(String configFormat) { IRuleConfigParser parser = null; if ("json".equalsIgnoreCase(configFormat)){ parser = new JsonRuleConfigParser(); }else if ("xml".equalsIgnoreCase(configFormat)){ parser = new XmlRuleConfigParser(); }else if ("yaml".equalsIgnoreCase(configFormat)){ parser = new YamlRuleConfigParser(); }else if ("properties".equalsIgnoreCase(configFormat)){ parser = new PropertiesRuleConfigParser(); } return parser; } } 

大部分工厂类都以Factory结尾,但不是必须的,如java的DateFormat、Calender。此外,工厂类中创建对象的方法一般都是create开头,如代码的createParser(),但也有命名为getInstance() createInstance() newInstance()的,甚至有的命名为valueOf()(如java string类的valueOf()方法)等。

上述代码中,每次调用RuleConfigParserFactory的createParser()的时候,都要创建一个新的parser。实际上,如果parser可复用,为了节省内存和对象创建的时间,可将parser事先创建好缓存起来,当调用createParser()时,从缓存取出parser对象直接用。

这种类似单例模式和简单工厂模式的结合,我们把上一种事先方法叫简单工厂的第一种实现方式,下面的叫第二种实现方式。

public class RuleConfigParserFactory { private static final Map<String,IRuleConfigParser> cachedParser = new HashMap<>(); static { cachedParser.put("json",new JsonRuleConfigParser()); cachedParser.put("xml",new XmlRuleConfigParser()); cachedParser.put("yaml",new YamlRuleConfigParser()); cachedParser.put("properties",new PropertiesRuleConfigParser()); } public static IRuleConfigParser createParser(String configFormat){ if (configFormat==null || configFormat.isEmpty()){ return null;//或者抛异常 } IRuleConfigParser parser = cachedParser.get(configFormat.toLowerCase()); return parser; } } 

对于上面两种简单工厂的实现方法,如果要添加新的parser,必须改动RuleConfigParserFactory的代码,是否违反了开闭原则呢?实际上,如果不是频繁的添加新的parser,只是偶尔改下RuleConfigParserFactory的代码,可以接受。

此外,在RuleConfigParserFactory的第一种代码实现中,有一组if分支判断逻辑,是否应用多态或其他设计模式替代呢?实际上如果if分支不多,完全可以接受。用多态虽然提高扩展性, 但增加类的个数,牺牲可读性。

2. 工厂方法factory method

如果非要去掉if分支逻辑,经典的就是利用多态,重构后:

public interface IRuleConfigParserFactory { IRuleConfigParser createParser(); } public class JsonRuleConfigParserFactory implements IRuleConfigParserFactory { @Override public IRuleConfigParser createParser() { return new JsonRuleConfigParser(); } } ... 

这就是工厂方法模式的典型代码。当新增一种parser时,只需新增一个实现了IRuleConfigParserFactory接口的Factory类即可。工厂方法比简单工厂更符合开闭原则。

上述工厂方法的实现看,很好,但是使用有些问题。

public class RuleConfigSource { public RuleConfig load(String ruleConfigFulePath){ String ruleConfigFileExtension = getFileExtension(ruleConfigFulePath); IRuleConfigParserFactory parserFactory = null; if ("json".equalsIgnoreCase(ruleConfigFileExtension)){ parserFactory = new JsonRuleConfigParserFactory(); }else if ("xml".equalsIgnoreCase(ruleConfigFileExtension)){ parserFactory = new XmlRuleConfigParserFactory(); }else if ("yaml".equalsIgnoreCase(ruleConfigFileExtension)){ parserFactory = new YamlRuleConfigParserFactory(); }else if ("properties".equalsIgnoreCase(ruleConfigFileExtension)){ parserFactory = new PropertiesRuleConfigParserFactory(); }else { throw new InvalidRuleConfigException("Rule config file format is not support: "+ruleConfigFulePath); } IRuleConfigParser parser = parserFactory.createParser(); String configText = ""; //从ruleConfigFilePath文件读取配置文本到configText中 RuleConfig ruleConfig = parser.parse(configText); return ruleConfig; } private String getFileExtension(String filePath){ //...解析文件名获取扩展名,如rule.json 返回json return "json"; } } 

工厂类对象的创建逻辑耦合进了load()函数中,引入工厂方法反而让设计更复杂了。

可以为工厂类再创建一个简单工厂,也就是工厂的工厂,用来创建工厂类对象。如下,RuleConfigParserFactoryMap是创建工厂对象的工厂类,getParserFactory()返回的是缓存好的单例工厂对象。

public class RuleConfigParserFactoryMap { private static final Map<String, IRuleConfigParserFactory> cachedFactories = new HashMap<>(); static { cachedFactories.put("json",new JsonRuleConfigParserFactory()); cachedFactories.put("xml",new XmlRuleConfigParserFactory()); cachedFactories.put("yaml",new YamlRuleConfigParserFactory()); cachedFactories.put("properties",new PropertiesRuleConfigParserFactory()); } public static IRuleConfigParserFactory getParserFactory(String type){ if (type==null || type.isEmpty()){ return null; } IRuleConfigParserFactory parserFactory = cachedFactories.get(type.toLowerCase()); return parserFactory; } } 

当需要添加新的规则配置解析器时,只需要创建新的parser类和parserFactory类,并在RuleConfigParserFactoryMap中将新的parserFactory对象添加到cachedFactories中。代码改动很少,符合开闭原则。

实际上,对于规则配置文件解析这个应用场景来说,工厂模式需要额外创建多个factory类,增加代码的复杂性。而且,每个factory类只做简单的new操作,功能单薄,没必要设计为独立的类。简单工厂模式简单好用更为合适。

3. 什么时候用工厂方法,而非简单工厂模式呢

之所以将某块代码剥离,独立为函数或类,原因是这个代码块逻辑过于复杂,剥离后更清晰,可维护。但如果本身并不复杂,没必要剥离。

基于此,当对象创建逻辑比较复杂,不是简单的new,而是要组合其他类对象,做各种初始化操作,推荐工厂方法模式,将复杂的创建逻辑拆分为多个工厂类,让每个工厂类不至于过于复杂。

此外,某些场景下,如果对象不可复用,工厂类每次都要返回不同的对象。如果用简单工厂模式,只能选择第一种包含if分支的实现方式,如果想避免if-else分支逻辑,推荐使用工厂方法模式。

4. 抽象工厂 abstract factory

应用场景较为特殊,在简单工厂和工厂方法中,类只有一个分类方式。如在规则配置解析的例子中,解析器类会根据配置文件格式(json、xml、yaml等)来分类。但,如果类有两种分类方式,如既可以按照配置文件格式来分类,也可根据解析的对象(rule规则配置还是system系统配置)来分类,会对应8个parser类:

针对规则配置的解析类:基于接口IRuleConfigParser
JsonRuleConfigParser
XmlRuleConfigParser
YamlRuleConfigParser
PropertiesRuleConfigParser

针对系统配置的解析器:基于接口ISystemConfigParser
JsonSystemConfigParser
XmlSystemConfigParser
YamlSystemConfigParser
PropertiesSystemConfigParser 

针对这种特殊的场景,如果继续按工厂方法实现,要针对每个parser都编写一个工厂类,也就是编写8个工厂类。如果未来还要增加针对业务配置的解析类(如IBizConfigParser),就要对应的增加4个工厂类。而过多的类也会让系统难以维护。如何解决?

抽象工厂就是针对这种非常特殊的场景诞生的。可以让一个工厂负责创建多个不同的类型的对象(IRuleConfigParser、ISystemConfigParser),而不是只创建一种parser对象,有效减少工厂的个数。

public interface IConfigParserFactory { IRuleConfigParser createRuleParser(); ISystemConfigParser createSystemParser(); //此处可扩展新的parser类型,如IBizConfigParser } public class JsonConfigParserFactory implements IConfigParserFactory { @Override public IRuleConfigParser createRuleParser() { return new JsonRuleConfigParser(); } @Override public ISystemConfigParser createSystemParser() { return new JsonSystemConfigParser(); } } ... 

5. DI容器

DI容器跟工厂模式有什么区别和联系?

DI容器的核心功能有哪些?如何实现一个简单的DI容器

1. 工厂模式和DI容器的区别

DI容器底层最基本的设计思路是基于工厂模式。DI容器相当于一个大的工厂类,负责在程序启动时,根据配置(要创建哪些类对象,每个类对象的创建要依赖哪些其他类对象)事先创建好对象。当应用程序要使用某个类对象的时候,直接从容器中获取即可。正因为持有一堆对象,所以被称为容器。

DI容器相对来说,处理的是更大的对象创建工程。之前的工厂模式,一个工厂类只负责某个类对象或某一组相关类对象(继承自同一抽象类或者接口的子类)的创建,而DI容器负责整个应用中所有类对象的创建。

此外,DI容器负责的事情比单纯的工厂模式要多,如配置的解析、对象生命周期的管理。

2. DI容器的核心功能有哪些

配置解析、对象创建、对象生命周期的管理

  • 配置解析

对通用的框架来说,框架代码和应用代码应该高度解耦,DI容器事先不知道应用会创建哪些对象,通过配置,应用告诉DI容器要创建哪些对象。

将需要由DI容器来创建的类对象和创建类对象的必要信息(使用哪个构造函数以及对应构造函数参数都是什么等)放到配置文件中。容器读取配置文件,根据配置文件提供的信息创建对象。

下面是典型的spring容器的配置文件,spring容器读取这个配置文件,解析出要创建的两个对象:rateLimiter和redisCounter,并得到两者的依赖关系:rateLimiter依赖redisCounter。

public class RateLimiter { private RedisCounter redisCounter; public RateLimiter(RedisCounter redisCounter){ this.redisCounter = redisCounter; } public void test(){ System.out.println("hello world"); } //... } public class RedisCounter { private String ipAddress; private int port; public RedisCounter(String ipAddress,int port){ this.ipAddress = ipAddress; this.port = port; } //... } 配置文件beans.xml: <beans> <bean id="rateLimiter" class="com.xzg.RateLimiter"> <constructor-arg ref="redisCounter" /> </bean> <bean id="redisCounter" class="com.xzg.redisCounter"> <constructor-arg type="String" value="127.0.0.1"/> <constructor-arg type="int" value="1234"/> </bean> </beans> 
  • 对象的创建

在DI容器汇总,如果给每个类都对应创建一个工厂类,那项目的类的个数会成倍增加,增加代码的维护成本。解决该问题,只需要将所有类对象的创建都放到一个工厂类中完成,如BeansFactory。

具体实现时,采用反射的机制,在程序运行中,动态加载类、创建对象,不需要事先在代码中写死要创建哪些对象,不管是创建一个对象还是十个对象,BeansFactory工厂类的代码都一样。

  • 对象的生命周期管理

简单工厂有两种实现方式,一种是每次都返回新创建的对象,另一种是每次都返回同一个事先创建好的对象,也就是单例对象。在spring框架中通过配置scope属性,来区分两种不同类型的对象。scope=prototype表示返回新创建的对象,scope=singleton表示返回单例对象。

此外,还可配置对象是否支持懒加载。还可以配置对象的init-method和destroy-method方法,如init-method=loadProperties(),destroy-method=updateConfigFile()。DI容器在创建好对象之后,会主动调用init-method方法初始化对象,对象最终销毁之前,DI容器会主动调用destroy-method方法做清理工作,如释放数据库连接池、关闭文件。

3. 如何实现DI容器

核心逻辑:配置文件解析、根据配置文件通过反射语法创建对象。

  • 最小原型设计

只实现最小原型,只支持下面配置文件中涉及到的配置语法:

配置文件beans.xml: <beans> <bean id="rateLimiter" class="com.xzg.RateLimiter"> <constructor-arg ref="redisCounter" /> </bean> <bean id="redisCounter" class="com.xzg.redisCounter"> <constructor-arg type="String" value="127.0.0.1"/> <constructor-arg type="int" value="1234"/> </bean> </beans> 

最小原型的使用方式跟spring框架类似

public class Demo { public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); RateLimiter rateLimiter = (RateLimiter) context.getBean("rateLimiter"); rateLimiter.test(); //... } } 
  • 提供执行入口

一组暴露给外部使用的接口和类

执行入口主要包含两部分:ApplicationContext和ClassPathXmlApplicationContext。其中,ApplicationContext是接口,而ClassPathXmlApplicationContext是实现类:

public interface ApplicationContext { Object getBean(String beanId); } public class ClassPathXmlApplicationContext implements ApplicationContext { private BeansFactory beansFactory; private BeanConfigParser beanConfigParser; public ClassPathXmlApplicationContext(String configLocation){ this.beansFactory = new BeansFactory(); this.beanConfigParser = new XmlBeanConfigParser(); loadBeanDefinitions(configLocation); } private void loadBeanDefinitions(String configLocation){ InputStream in = null; try{ in = this.getClass().getResourceAsStream("/"+configLocation); if (in == null){ throw new RuntimeException("Can not find config file: "+configLocation); } List<BeanDefinition> beanDefinitions = beanConfigParser.parse(in); beansFactory.addBeanDefinitions(beanDefinitions); }finally { if (in!=null){ try{ in.close(); }catch (IOException e){ //TODO log error } } } } @Override public Object getBean(String beanId) { return beansFactory.getBean(beanId); } } 

从上述代码,看出,ClassPathXmlApplicationContext负责组装BeansFactory和BeanConfigParser两个类,串联执行流程:从classpath加载xml格式的配置文件,通过BeanConfigParser解析为统一的BeanDefinition格式,然后,BeansFactory根据BeanDefinition创建对象。

  • 配置文件解析

配置文件解析主要包含BeanConfigParser接口和XmlBeanConfigParser实现类,负责将配置文件解析为BeanDefinition结构,便于BeansFactory根据该结构创建对象。

配置文件的解析较为复杂,可以参考spring中的解析。

  • 核心工厂类设计

BeansFactory是最核心的类,负责根据从配置文件解析得到的BeanDefinition创建对象。

如果对象的scope属性为singleton,对象创建后会缓存到singletonObjects这个map中,下次请求直接从map中取出返回即可。如果是prototype,每次请求,都会创建一个新的对象返回。

BeansFactory创建对象主要技术点是反射。具体实现:

public class BeansFactory { private ConcurrentHashMap<String,Object> singletonObjects = new ConcurrentHashMap<>(); private ConcurrentHashMap<String, BeanDefinition> beanDefinitions = new ConcurrentHashMap<>(); public void addBeanDefinition(List<BeanDefinition> beanDefinitionList){ for (BeanDefinition beanDefinition:beanDefinitionList){ this.beanDefinitions.putIfAbsent(beanDefinition.getId(),beanDefinition); } for (BeanDefinition beanDefinition:beanDefinitionList){ if (beanDefinition.isLazyInit() == false && beanDefinition.isSingleton()){ createBean(beanDefinition); } } } public Object getBean(String beanId){ BeanDefinition beanDefinition = beanDefinitions.get(beanId); if (beanDefinition == null){ throw new NoSuchBeanDefinitionException("Bean is not defined: "+beanId); } return createBean(beanDefinition); } @VisibleForTesting protected Object createBean(BeanDefinition beanDefinition){ if (beanDefinition.isSingleton() && singletonObjects.contains(beanDefinition)){ return singletonObjects.get(beanDefinition.getId()); } Object bean = null; try{ Class beanClass = Class.forName(beanDefinition.getBeanClassName()); List<BeanDefinition.ConstructorArg> args = beanDefinition.getConstructorArgumentValues(); if (args.isEmpty()){ bean = beanClass.newInstance(); }else{ Class[] argClasses = new Class[args.size()]; Object[] argObjects = new Object[args.size()]; for (int i = 0; i<args.size();i++){ BeanDefinition.ConstructorArg arg = args.get(i); if (!arg.getIsRef()){ argClasses[i] = arg.getType(); argObjects[i] = arg.getArg(); }else{ BeanDefinition refBeanDefinition = beanDefinitions.get(arg.getArg()); if (refBeanDefinition == null){ throw new NoSuchBeanDefinitionException("Bean is not defined: "+refBeanDefinition); } argClasses[i] = Class.forName(refBeanDefinition.getBeanClassName()); argObjects[i] = createBean(refBeanDefinition); } } bean = beanClass.getConstructor(argClasses).newInstance(argObjects); } }catch (ClassNotFoundException e){ throw new BeanCreationException("",e); } if (bean != null && beanDefinition.isSingleton()){ singletonObjects.putIfAbsent(beanDefinition.getId(),bean); return singletonObjects.get(beanDefinition.getId()); } return bean; } } 

建造者模式

builder模式,中文译为建造者模式或构建者模式。

  • 直接使用构造函数或者配合set方法就能创建对象,为什么还要建造者模式创建?
  • 建造者模式和工厂模式都可以创建对象,两者的区别在哪里?

1. 为什么要建造者模式

平时开发中,创建一个对象最常用的方式:使用new关键字调用类的构造函数完成。

什么情况下该方式不适用,需要采用建造者模式来创建对象?

假设这样一道面试题:需要定义一个资源池配置类ResourcePoolConfig。资源池可理解为线程池、连接池、对象池等。在这个资源池配置类中,有以下几个成员变量,也就是可配置项。请编写代码实现这个ResourcePoolConfig类。

成员变量 解释 是否必填 默认值
name 资源名称 没有
maxTotal 最大总资源数量 8
maxIdle 最大空闲资源数量 8
minIdle 最小空闲资源数量 0

最常见的思路如下,因为非必填的,构造函数中这几个参数传递null值,表示使用默认值

public class ResourcePoolConfig { private static final int DEFAULT_MAX_TOTAL = 8; private static final int DEFAULT_MAX_IDLE = 8; private static final int DEFAULT_MIN_IDLE = 0; private String name; private int maxTotal = DEFAULT_MAX_TOTAL; private int maxIdle = DEFAULT_MAX_IDLE; private int minIdle = DEFAULT_MIN_IDLE; public ResourcePoolConfig(String name,Integer maxIdle,Integer maxTotal,Integer minIdle){ if (StringUtils.isBlank(name)){ throw new IllegalArgumentException("name should not be empty."); } this.name = name; if (maxTotal != null){ if (maxTotal <= 0){ throw new IllegalArgumentException("maxTotal should be positive."); } this.maxTotal = maxTotal; } if (maxIdle != null){ if (maxIdle < 0){ throw new IllegalArgumentException("maxIdle should not be negative."); } this.maxIdle = maxIdle; } if (minIdle != null){ if (minIdle < 0){ throw new IllegalArgumentException("minIdle should not be negative."); } this.minIdle = minIdle; } } //...省略getter方法... } 

当前,ResourcePoolConfig只有4个可配置项,对应到构造函数,只有4个参数,但如果可配置项变成8个、10个,甚至更多,那么构造函数的参数列表会变得很长,代码的可读性和易用性都会变差。使用构造函数时,容易搞错各参数的顺序,传递错误的参数值,导致非常隐蔽的bug。

// 参数田铎,导致可读性差,参数可能传递错误 ResourcePoolConfig config = new ResourcePoolConfig("dbConnectionPool",16,null,8,8,3); 

解决方法可能也想到,就是用set()方法给成员变量赋值,替代冗长的构造函数。其中,name必填,放到构造函数中设置,强制创建类对象的时候要填写。其他配置项非必填,通过set()方法设置,让使用者自主选择是否填写。

public class ResourcePoolConfig { private static final int DEFAULT_MAX_TOTAL = 8; private static final int DEFAULT_MAX_IDLE = 8; private static final int DEFAULT_MIN_IDLE = 0; private String name; private int maxTotal = DEFAULT_MAX_TOTAL; private int maxIdle = DEFAULT_MAX_IDLE; private int minIdle = DEFAULT_MIN_IDLE; public ResourcePoolConfig(String name){ if (StringUtils.isBlank(name)){ throw new IllegalArgumentException("name should not be empty."); } this.name = name; } public void setMaxTotal(int maxTotal){ if (maxTotal <= 0){ throw new IllegalArgumentException("maxTotal should be positive."); } this.maxTotal = maxTotal; } public void setMaxIdle(int maxIdle){ if (maxIdle < 0){ throw new IllegalArgumentException("maxIdle should not be negative."); } this.maxIdle = maxIdle; } public void setMinIdle(int minIdle){ if (minIdle < 0){ throw new IllegalArgumentException("minIdle should not be negative."); } this.minIdle = minIdle; } //...省略getter方法... } 

再看新的ResourcePoolConfig类如何使用。

//ResourcePoolConfig使用举例 ResourcePoolConfig config = new ResourcePoolConfig("dbConnectionPool"); config.setMaxTotal(16); config.setMaxIdle(8); 

至此,仍没有用建造者模式,通过构造函数设置必填项,set()方法设置可选配置项,实现设计需求。如果问题难度再加大点,如还要解决下面三个问题,现有的设计思路就不能满足了。

  • 刚说name是必填的,所以把它放到构造方法中,强制创建对象的时候设置。如果必填的配置项有很多,都放到构造方法中,构造方法又会出现参数列表很长的问题。如果把必填项也通过set()设置,校验必填项是否已经填写的逻辑就无处安放了。
  • 此外,假设配置项之间有一定的依赖关系,如,如果用户设置maxTotal、maxIdle、minIdle其中一个,必须显式的设置另外两个;或者配置项之间有一定的约束条件,如maxIdle和minIdle要小于等于maxTotal,如果继续现有的设计思路,那么配置项之间的依赖关系或约束条件的校验逻辑无处安放了。

为解决这些问题,建造者模式就派上用场了。

可以把校验逻辑放到Builder类中,先创建建造者,并通过set()方法设置建造者的变量值,然后再使用build()方法真正创建对象之前,做集中的校验,校验通过才会创建对象。此外,把ResourcePoolConfig的构造函数改为private,这样只能通过建造者创建ResourcePoolConfig类对象。并且,ResourcePoolConfig没有提供任何set()方法,这样,创建出来的是不可变对象。

public class ResourcePoolConfig { private String name; private int maxTotal ; private int maxIdle ; private int minIdle ; private ResourcePoolConfig(Builder builder){ this.name = builder.name; this.maxTotal = builder.maxTotal; this.maxIdle = builder.maxIdle; this.minIdle = builder.minIdle; } //将Builder设计为ResourcePoolConfig的内部类,也可以将其设计为独立的非内部类ResourcePoolConfigBuilder public static class Builder{ private static final int DEFAULT_MAX_TOTAL = 8; private static final int DEFAULT_MAX_IDLE = 8; private static final int DEFAULT_MIN_IDLE = 0; private String name; private int maxTotal = DEFAULT_MAX_TOTAL; private int maxIdle = DEFAULT_MAX_IDLE; private int minIdle = DEFAULT_MIN_IDLE; public ResourcePoolConfig buid(){ //校验逻辑放到这里,包括必填项校验、依赖关系校验、约束条件校验等 if (StringUtils.isBlank(name)){ throw new IllegalArgumentException("name should not be empty."); } if (maxIdle > maxTotal){ throw new IllegalArgumentException("..."); } if (minIdle > maxTotal || minIdle > maxIdle){ throw new IllegalArgumentException("..."); } return new ResourcePoolConfig(this); } public Builder setName(String name){ if (StringUtils.isBlank(name)){ throw new IllegalArgumentException("name should not be empty."); } this.name = name; return this; } public Builder setMaxTotal(int maxTotal){ if (maxTotal <= 0){ throw new IllegalArgumentException("maxTotal should be positive."); } this.maxTotal = maxTotal; return this; } public Builder setMaxIdle(int maxIdle){ if (maxIdle < 0){ throw new IllegalArgumentException("maxIdle should not be negative."); } this.maxIdle = maxIdle; return this; } public Builder setMinIdle(int minIdle){ if (minIdle < 0){ throw new IllegalArgumentException("minIdle should not be negative."); } this.minIdle = minIdle; return this; } } } //这段代码会抛异常IllegalArgumentException,因为minIdle>maxIdle ResourcePoolConfig config = new ResourcePoolConfig.Builder() .setName("dbConnectionPool") .setMaxTotal(16) .setMaxIdle(10) .setMinIdle(12) .buid(); 

实际上,使用建造者模式创建对象,还能避免对象存在无效状态,如定义一个长方形,如果不适用建造者模式,而是先创建后set的方式,会导致第一个set后,对象处于无效状态

Rectangle r = new Rectangle();// r is invalid r.setWidth(2);//r is invalid r.setHeight(3);//r is valid 

为避免无效状态的存在,可以考虑使用构造函数一次性初始化好所有的成员变量,如果构造函数参数过多,采用建造者模式。

实际上,如果并不关心对象是否有短暂的无效状态,也不在意对象是否可变,如对象只是用来映射数据库读出的数据,直接暴露set()方法没问题。而且用建造者模式构建对象,代码实际上有点重复,ResourcePoolConfig类的成员变量,要在Builder类中重新定义一遍。

2. 和工厂模式的区别

工厂模式是用来创建不同但是相关类型的对象(继承同一个父类或者接口的一组子类),由给定的参数界定创建哪种类型的对象。建造者模式用来创建一种类型的复杂对象,通过设置不同的可选参数,定制化的创建不同的对象。

其实也没必要把工厂模式、建造者模式分的太清楚,知道特定场景下用哪种更合适即可。

原型模式

对js来说,很常用的开发模式。JavaScript就是基于原型的面向对象编程语言。java使用较少。通过一个clone散列表的例子搞清楚:原型模式的应用场景以及两种实现方式:深拷贝和浅拷贝。

1. 原型模式的原理和应用

如果对象的创建成本比较大,而同一个类的不同对象之间的差别不大(大部分字段都相同),这种情况下,可利用对已有的对象(原型)进行复制(或者叫拷贝)的方式创建新对象,以达到节省创建时间的目的。这种基于原型来创建对象的方式叫原型设计模式(prototype design pattern),简称原型模式。

何为“对象的创建成本比较大”?

实际上,创建对象包含的申请内存、给成员变量赋值并不会花费太多时间。但如果对象中的数据需要经过复杂的计算才能得到(如排序、计算哈希值),或者需要从RPC、网络、数据库、文件系统等非常慢速的IO中获取,可以利用原型模式,从其他已有对象中直接拷贝,而不是每次创建时,都重复执行这些耗时操作。

例如,数据库中存储大约10万条“搜索关键词”信息,每条信息包含关键词、关键词被搜索的次数、信息最近被更新的时间等。系统A在启动时会加载这份数据到内存,用于处理某些其他的业务需求。为了方便快速的查找某个关键词对应的信息,给关键词建立一个散列表索引。如java的hashmap实现。key为关键词,value为关键词详细信息。

还有另外一个系统B,专门分析搜索日志,定期(如间隔10分钟)批量更新数据库中的数据,并且标记为新的数据版本。如下面示意图,对v2版本的数据更新,得到v3版本的数据,假设只有更新和新增关键词,没有删除关键词的行为。
Java设计模式总结(工厂模式、建造者模式、原型模式)

为保证系统A的数据的实时性(不一定非常实时,但数据也不能太旧),系统A需要定期根据数据库中的数据,更新内存的索引和数据。

如何实现该需求?

其实,只需要在系统A中,记录当前数据的版本VA对应的更新时间TA,从数据库中捞出更新时间大于TA的所有搜索关键词,也就是找出VA班恩和最新版本数据的差集,针对差集中的每个关键词处理。如果已经在散列表存在,更新相应的搜索次数、更新时间等信息;如果在散列表不存在,将其插入到散列表中。

示例代码:

public class Demo { private ConcurrentHashMap<String,SearchWord> currentKeywords = new ConcurrentHashMap<>(); private long lastUpdateTime = -1; public void refresh(){ //从数据库中取出更新时间>lastUpdateTime的数据,放入currentKeywords List<SearchWord> toBeUpdatedSearchWords = getSearchWords(lastUpdateTime); long maxNewUpdatedTime = lastUpdateTime; for (SearchWord searchWord: toBeUpdatedSearchWords){ if (searchWord.getLastUpdateTime() > maxNewUpdatedTime){ maxNewUpdatedTime = searchWord.getLastUpdateTime(); } if (currentKeywords.containsKey(searchWord.getKeyword())){ currentKeywords.replace(searchWord.getKeyword(),searchWord); }else{ currentKeywords.put(searchWord.getKeyword(),searchWord); } } lastUpdateTime = maxNewUpdatedTime; } private List<SearchWord> getSearchWords(long lastUpdateTime){ //TODO 从数据库取出更新时间>lastUpdateTime的数据 return null; } } 

现在有个特殊的要求:任何时刻,系统A的所有数据都必须是同一个版本的,要么是版本a,要么是版本b。那刚才的更新方式就不能满足了。此外还要求:在更新内存数据的时候,系统A不能处于不可用状态,也就是不能停机更新数据。

如何实现?

也不难,将正在使用的数据的版本定义为“服务版本”,当要更新内存中的数据的时候,不直接在服务版本上更新,而是重新创建另一个版本数据(假设为版本b),等新的版本数据建好,再一次性将服务版本从a切换到b,既保证数据一直可用,又避免中间状态的存在。

示例代码:

public class Demo { private HashMap<String, SearchWord> currentKeywords = new HashMap<>(); public void refresh(){ HashMap<String,SearchWord> newKeywords = new LinkedHashMap<>(); //从数据库中取出所有的数据,放入newKeywords List<SearchWord> toBeUpdatedSearchWords = getSearchWords(); for (SearchWord searchWord: toBeUpdatedSearchWords){ newKeywords.put(searchWord.getKeyword(),searchWord); } currentKeywords = newKeywords; } private List<SearchWord> getSearchWords() { //TODO 从数据库总取出所有的数据 return null; } } 

不过,上述代码,newKeywords构建成本较高,需要将10万条数据从数据库中读取,计算哈希值,构建newKeywords。非常耗时。为提高效率,原型模式派上用场。

拷贝currentKeywords数据到newKeywords,从数据库只捞出新增或有更新的关键词,更新到newKeywords。相对于10万条数据,每次新增或更新的关键词个数较少,提高了数据更新的效率。

public class Demo { private HashMap<String, SearchWord> currentKeywords = new HashMap<>(); private long lastUpdateTime = -1; public void refresh(){ //原型模式就这么简单,拷贝已有对象的数据,更新少量差值 HashMap<String,SearchWord> newKeywords = currentKeywords; //从数据库中取出更新时间>lastUpdateTime的数据,放入currentKeywords List<SearchWord> toBeUpdatedSearchWords = getSearchWords(lastUpdateTime); long maxNewUpdatedTime = lastUpdateTime; for (SearchWord searchWord: toBeUpdatedSearchWords) { if (searchWord.getLastUpdateTime() > maxNewUpdatedTime) { maxNewUpdatedTime = searchWord.getLastUpdateTime(); } if (newKeywords.containsKey(searchWord.getKeyword())){ SearchWord oldSearchWord = newKeywords.get(searchWord.getKeyword()); oldSearchWord.setCount(searchWord.getCount()); oldSearchWord.setLastUpdateTime(searchWord.getLastUpdateTime()); }else{ newKeywords.put(searchWord.getKeyword(),searchWord); } } lastUpdateTime = maxNewUpdatedTime; currentKeywords = newKeywords; } private List<SearchWord> getSearchWords(long lastUpdateTime){ //TODO 从数据库取出更新时间>lastUpdateTime的数据 return null; } } 

这里是利用Java的clone()语法来复制一个对象。其实,刚刚的代码实现有问题,先要了解两个概念:深拷贝(deep copy)和浅拷贝(shallow copy)。

2.深拷贝和浅拷贝

浅拷贝只复制索引,不会复制数据背身。而深拷贝不仅复制索引,还会复制数据本身。浅拷贝得到的对象跟原始对象共享数据,而深拷贝得到的是完全独立的对象。

java中,Object类的clone()方法执行的是浅拷贝。只拷贝对象的基本数据类型的数据(int、long)以及引用对象(Searchword)的内存地址,不会递归的拷贝引用对象本身。

上述代码中,调用hashmap的clone()浅拷贝实现原型模式。当通过newKeywords更新SearchWord对象时(如更新“设计模式”这个关键词的访问次数),newKeywords和currentKeywords因为指向相同的一组SearchWord对象,导致currentKeywords指向的SearchWord,有的是老版本,有的是新版本,没法满足之前的需求:currentKeywords中的数据在任何时刻都是同一个版本的,不存在介于老版本和新版本之间的中间状态。

如何解决?

将浅拷贝替换为深拷贝。newKeywords不仅复制currentKeywords的索引,还将SearchWord对象也复制一份,这样就指向不同的SearchWord对象。不存在更新newKeywords的数据导致currentKeywords的数据也被更新的问题。

如何实现深拷贝?两种方法。

第一种:递归拷贝对象、对象的引用对象及引用对象的引用对象…直到要拷贝的对象只包含基本数据类型。

public class Demo { private HashMap<String, SearchWord> currentKeywords = new HashMap<>(); private long lastUpdateTime = -1; public void refresh(){ // deep copy HashMap<String,SearchWord> newKeywords = new HashMap<>(); for (HashMap.Entry<String,SearchWord> e: currentKeywords.entrySet()){ SearchWord searchWord = e.getValue(); SearchWord newSearchWord = new SearchWord(searchWord.getKeyword(),searchWord.getCount(),searchWord.getLastUpdateTime()); newKeywords.put(e.getKey(),newSearchWord); } //从数据库取出更新时间>lastUpdateTime的数据 放入newKeywords中 List<SearchWord> toBeUpdatedSearchWords = getSearchWords(lastUpdateTime); long maxNewUpdatedTime = lastUpdateTime; for (SearchWord searchWord: toBeUpdatedSearchWords) { if (searchWord.getLastUpdateTime() > maxNewUpdatedTime) { maxNewUpdatedTime = searchWord.getLastUpdateTime(); } if (newKeywords.containsKey(searchWord.getKeyword())){ SearchWord oldSearchWord = newKeywords.get(searchWord.getKeyword()); oldSearchWord.setCount(searchWord.getCount()); oldSearchWord.setLastUpdateTime(searchWord.getLastUpdateTime()); }else{ newKeywords.put(searchWord.getKeyword(),searchWord); } } lastUpdateTime = maxNewUpdatedTime; currentKeywords = newKeywords; } private List<SearchWord> getSearchWords(long lastUpdateTime){ //TODO 从数据库取出更新时间>lastUpdateTime的数据 return null; } } 

第二种:先将对象序列化,再反序列化为新的对象。

public Object deepCopy(Object object) throws Exception{ ByteArrayOutputStream bo = new ByteArrayOutputStream(); ObjectOutputStream oo = new ObjectOutputStream(bo); oo.writeObject(object); ByteArrayInputStream bi = new ByteArrayInputStream(bo.toByteArray()); ObjectInputStream oi = new ObjectInputStream(bi); return oi.readObject(); } 

上面两种方法,不管哪种,深拷贝都比浅拷贝耗时、耗内存。有没有更快、更省内存的实现方法?

可以先浅拷贝创建newKeywords,对需要更新的SearchWord对象,再用深拷贝创建一份新的对象,替换newKeywords中的老对象。这种既利用了浅拷贝节省时间、空间的优点,又保证currentKeywords中的数据都是老版本的数据。具体代码:

public class Demo { private HashMap<String, SearchWord> currentKeywords = new HashMap<>(); private long lastUpdateTime = -1; public void refresh(){ // shallow copy HashMap<String,SearchWord> newKeywords = currentKeywords; //从数据库中取出更新时间>lastUpdateTime的数据,放入currentKeywords List<SearchWord> toBeUpdatedSearchWords = getSearchWords(lastUpdateTime); long maxNewUpdatedTime = lastUpdateTime; for (SearchWord searchWord: toBeUpdatedSearchWords) { if (searchWord.getLastUpdateTime() > maxNewUpdatedTime) { maxNewUpdatedTime = searchWord.getLastUpdateTime(); } if (newKeywords.containsKey(searchWord.getKeyword())){ newKeywords.remove(searchWord.getKeyword()); } newKeywords.put(searchWord.getKeyword(),searchWord); } lastUpdateTime = maxNewUpdatedTime; currentKeywords = newKeywords; } private List<SearchWord> getSearchWords(long lastUpdateTime){ //TODO 从数据库取出更新时间>lastUpdateTime的数据 return null; } } 

本文地址:https://blog.csdn.net/wjl31802/article/details/108036563