设计模式之美笔记9 待我称王封你为后i 2022-11-28 00:41 147阅读 0赞 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 ### 文章目录 ### * * 工厂模式 * * 1. 简单工厂 * 2. 工厂方法factory method * 3. 什么时候用工厂方法,而非简单工厂模式呢 * 4. 抽象工厂 abstract factory * 5. 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(ruleConfigFilePath); 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(ruleConfigFilePath); 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类。 <table> <thead> <tr> <th>成员变量</th> <th>解释</th> <th>是否必填</th> <th>默认值</th> </tr> </thead> <tbody> <tr> <td>name</td> <td>资源名称</td> <td>是</td> <td>没有</td> </tr> <tr> <td>maxTotal</td> <td>最大总资源数量</td> <td>否</td> <td>8</td> </tr> <tr> <td>maxIdle</td> <td>最大空闲资源数量</td> <td>否</td> <td>8</td> </tr> <tr> <td>minIdle</td> <td>最小空闲资源数量</td> <td>否</td> <td>0</td> </tr> </tbody> </table> 最常见的思路如下,因为非必填的,构造函数中这几个参数传递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版本的数据,假设只有更新和新增关键词,没有删除关键词的行为。 ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dqbDMxODAy_size_16_color_FFFFFF_t_70_pic_center] 为保证系统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; } } [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dqbDMxODAy_size_16_color_FFFFFF_t_70_pic_center]: /images/20221124/2a48b117a7f247f48290e1c1f0113b21.png
相关 设计模式之美笔记16 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 文章目录 解释器模式 解释器模式的原理和实现 深藏阁楼爱情的钟/ 2022年12月01日 11:53/ 0 赞/ 135 阅读
相关 设计模式之美笔记15 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 文章目录 访问者模式 访问者模式的诞生 我就是我/ 2022年12月01日 05:16/ 0 赞/ 142 阅读
相关 设计模式之美笔记14 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 文章目录 状态模式 背景 什么 水深无声/ 2022年11月30日 15:51/ 0 赞/ 146 阅读
相关 设计模式之美笔记13 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 文章目录 策略模式 策略模式的原理和实现 忘是亡心i/ 2022年11月30日 12:27/ 0 赞/ 143 阅读
相关 设计模式之美笔记12 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 文章目录 观察者模式 原理及应用场景剖析 深碍√TFBOYSˉ_/ 2022年11月30日 04:18/ 0 赞/ 173 阅读
相关 设计模式之美笔记11 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 文章目录 门面模式 门面模式的原理和实现 ゞ 浴缸里的玫瑰/ 2022年11月28日 13:41/ 0 赞/ 155 阅读
相关 设计模式之美笔记10 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 文章目录 序言 代理模式 桥接模式 柔情只为你懂/ 2022年11月28日 10:36/ 0 赞/ 149 阅读
相关 设计模式之美笔记9 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 文章目录 工厂模式 1. 简单工厂 待我称王封你为后i/ 2022年11月28日 00:41/ 0 赞/ 148 阅读
相关 设计模式之美笔记8 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 文章目录 单例模式 1. 为什么要使用单例 柔光的暖阳◎/ 2022年11月26日 07:52/ 0 赞/ 153 阅读
相关 设计模式之美笔记7 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 文章目录 实战1:id生成器的重构 1. 需求背景 女爷i/ 2022年11月25日 13:19/ 0 赞/ 186 阅读
还没有评论,来说两句吧...