设计模式之美笔记9

记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步

工厂模式

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

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类。

成员变量解释是否必填默认值
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版本的数据,假设只有更新和新增关键词,没有删除关键词的行为。
在这里插入图片描述

为保证系统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;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值