美文网首页
创建类设计模式分享

创建类设计模式分享

作者: 明翼 | 来源:发表于2021-09-28 22:22 被阅读0次

    一 闲谈

    最近在复习设计模式,很多模式让说名字,可能记不住,但是日常开发中用的挺多的,有时候形成一种习惯。这一轮学习先把模式简化下,以前学什么总讲究完备,好像少了点很重要似的,其实很多时候应用的是基础知识,没必要死扣些定义的,也浪费时间。

    一直以来觉得设计模式可能没那么重要,一方面虽然没有记熟这些设计模式,但是遵守设计原则,自然而然开发的模式很大的可能就是特定的设计模式。这里面也分享个小技巧,判断代码的坏味道,即哪些代码是需要你重构的,比较需求连续变化,你对这点代码的修改一点也不抗拒说明你的代码可扩展性不错,如果很有情绪,不一定就是需求变化引起的,而是自己实在是不想改那个看起来一团乱麻的代码。

    设计的最重要的原则是隔离变化,将不变的和变化的部分分离开来,变化的部分被我们限定住了,这样就减轻了我们脑袋思考的容量,大脑似乎更喜欢处理这种很守规律的事情,让大脑感觉可以掌控它就不容易烦了。分离了之后,我们就可以针对接口,这种不变的抽象编程,而不要针对具体的实例编程。不变的在顶层,变化的在底层,像不像领导说话,说的很抽象,所以不会出错,越具体做事越容易出错,抽象的越狠,通用性越强,当然也不容易出错。

    举个栗子,比如我们系统里面需要定义很多灯,那么我们可以把灯最重要的方法抽出来定义成灯的接口,比如两个方法“开灯”和“关灯”,我们代码在操作灯的时候只要针对接口就信了,不用关心是声控灯,普通电灯,还是煤油灯,或者智能控制灯,我们把常变化的不同的灯和灯最常用不变化的方法抽离了。

    二 创建类设计模式概述

    创建类模式

    创建型设计模式 简单概括就是将对象的创建和对象的使用相分离:

    1. 单例模式:用于在一个进程中创建只有一个对象的类,好处节省内存,保证全局唯一等,因为创建对象比较特殊,所以形成一种模式。
    2. 工厂模式: 根据条件不同,创建相关的不同对象,有条件的创建对象。
    3. 建造者模式: 属性太多,导致构造函数的参数多,如果采用多个set方法,又缺少了安全性。
    4. 原型模式: 对象创建比较耗时,我们可以采用利用现有对象进行复制或拷贝的方式创建对象。
      可以说创建模式是从对象创建的不同角度设计不同的模式。

    三 单例模式

    我(类)只有一个对象,好单纯,ok,你就是单例。 我们放下一切已有的观点,我们来想,对象创建在java里面是通过new关键字的,我们当然可以new多个,那就无法控制一个类只有一个对象了。

    怎么办,那我们就禁止外面new的类的方式创建对象,了解java的朋友就明白了,我们可以将构造函数访问权限改成私有,作为私有方法只能通过内部的方法调用,而调用需要对象,而对象需要创建,因为私有构造函数,外部又无法创建对象,这是不是就死循环了。不是不是,类的方法还有静态方法,可以在不创建对象的时候就可以访问类的方法,致于私有构造函数只有内部可以访问,那么我们就在类的内部创建对象,返回出去不就可以了吗?
    好了,直接上代码: 最简单版本,这种写法最简单,如果sg创建不耗时,其实用这种就好了。

    public  class sg {
    // 静态方法操作静态变量
    private static sg instance  = new sg();
    
    private sg() {
     .....
    }
    
    public static sg getInstance()  {
       return instance;
    }
    }
    

    一般创建单例要注意点:1. 私有构造函数;2.考虑线程安全;3. getInstance性能是否够高;4.是否考虑延迟加载。

    什么饿汉模式,双重检查,懒汉模式懒得写。
    考虑延迟加载的最简单的单例了:

    public class sg2{ 
       private sg2() {}
    
      private static class SingletonHolder{
        private static final sg2 instance = new IdGenerator();
      }
      
      public static sg2getInstance() {
        return SingletonHolder.instance;
      }
    }
    

    不会主动生成sg2对象,直到真正调用sg2getInstance方法,而且也是线程安全的,推荐。
    我记得不错的话,在Effitive java中推荐枚举型的单例:

    
    public enum IdGenerator {
      INSTANCE;
      private AtomicLong id = new AtomicLong(0);
     
      public long getId() { 
        return id.incrementAndGet();
      }
    }
    

    与其他单例比较好处,JVM保证枚举类型和其变量在JVM中唯一的,序列化后在反序列化都可以保证是同一个对象,有这种特殊需求可以用,没有的话用起来感觉有点奇怪。

    四 工厂模式

    举个简单例子,比如我们配置文件由于特殊原因需要支持xml格式,json格式,yaml等不同格式的配置,那么我们在解析的时候很容易想到定义个解析接口,然后分别实现xml格式解析器,json格式解析器,yaml格式解析器。

    如何使用那,能想到的就是根据传入的配置文件的后缀来生成不同的解析器,然后调用解析器解析配置文件:

    
    public class RuleConfigSource {
      public RuleConfig load(String ruleConfigFilePath) {
        String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);
        IRuleConfigParser parser = RuleConfigParserFactory.createParser(ruleConfigFileExtension);
        if (parser == null) {
          throw new InvalidRuleConfigException(
                  "Rule config file format is not supported: " + ruleConfigFilePath);
        }
    
        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;
      }
    }
    

    上述就是简单工厂模式,也没干嘛,就是定义个单独的工厂类来负责解析对象的创建;将揉在一起的代码分开,对象创建是一块,使用对象是一块;其实复杂度没办法消除,只能转移或改变形式,变的容易接受点,就是我们需要的。

    好像一堆if else看起来不太好,像这个情况我们可以如下改造,这个下面的例子是我见过最多的,也最常用的代码了:

    
    public class RuleConfigParserFactory {
      private static final Map<String, RuleConfigParser> cachedParsers = new HashMap<>();
    
      static {
        cachedParsers.put("json", new JsonRuleConfigParser());
        cachedParsers.put("xml", new XmlRuleConfigParser());
        cachedParsers.put("yaml", new YamlRuleConfigParser());
        cachedParsers.put("properties", new PropertiesRuleConfigParser());
      }
    
      public static IRuleConfigParser createParser(String configFormat) {
        if (configFormat == null || configFormat.isEmpty()) {
          return null;//返回null还是IllegalArgumentException全凭你自己说了算
        }
        IRuleConfigParser parser = cachedParsers.get(configFormat.toLowerCase());
        return parser;
      }
    }
    

    至于工厂方法,抽象工厂,太麻烦,创建太多类,平时用的不太多,懒得写。

    五 建造者模式

    正如上面所说,建造者模式解决的是构造函数的参数太多,如果简化构造函数的参数,通过set方法来设置那,可能会在set之前对象就被使用的可能不够安全,开放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;
      }
      //...省略getter方法...
    
      //我们将Builder类设计成了ResourcePoolConfig的内部类。
      //我们也可以将Builder类设计成独立的非内部类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 build() {
          // 校验逻辑放到这里来做,包括必填项校验、依赖关系校验、约束条件校验等
          if (StringUtils.isBlank(name)) {
            throw new IllegalArgumentException("...");
          }
          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("...");
          }
          this.name = name;
          return this;
        }
    
        public Builder setMaxTotal(int maxTotal) {
          if (maxTotal <= 0) {
            throw new IllegalArgumentException("...");
          }
          this.maxTotal = maxTotal;
          return this;
        }
    
        public Builder setMaxIdle(int maxIdle) {
          if (maxIdle < 0) {
            throw new IllegalArgumentException("...");
          }
          this.maxIdle = maxIdle;
          return this;
        }
    
        public Builder setMinIdle(int minIdle) {
          if (minIdle < 0) {
            throw new IllegalArgumentException("...");
          }
          this.minIdle = minIdle;
          return this;
        }
      }
    }
    
    // 这段代码会抛出IllegalArgumentException,因为minIdle>maxIdle
    ResourcePoolConfig config = new ResourcePoolConfig.Builder()
            .setName("dbconnectionpool")
            .setMaxTotal(16)
            .setMaxIdle(10)
            .setMinIdle(12)
            .build();
    

    好处是规避了刚才说的几个问题,坏处是有属性的重复定义。建造者模式针对同一个类需要多个参数做定制的时候使用,与工厂模式是创建不同的类对象相比,建造者模式是创建不同参数配置的同一个类的对象。

    六 原型模式

    原型模式就是利用现有的对象通过复制或深度拷贝等方法来创建新的对象,其实用的挺少,王争老师的例子却是个有意思的用法。

    例子是这样的,我们有个统计系统,需要统计一个搜索系统中,各个关键词的搜索次数,定期统计日志更新数据库中,同时内存中也有一份统计数据; 系统要求,在更新内存中统计的数据的时候,数据要整体更新,不能更新局部,不能有不可用状态。

    简单的实现思路,定期从数据库中获取数据,创建个新的内存统计数据,创建好之后再替换老的内存统计数据即可,简单实例代码:

    
    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;
      }
    }
    

    我们可以看到我们需要将所有的数据都从数据库中捞出,然后加入到新的缓存中,这样的性能会比较差。两次轮询的如果比较短,大量的数据是没有变化的,我们可以复制一个老的内存数据,然后查询数据库的时候,只获取变化的数据(数据带有时间戳)。

    
    public class Demo {
      private HashMap<String, SearchWord> currentKeywords=new HashMap<>();
      private long lastUpdateTime = -1;
    
      public void refresh() {
        // 原型模式就这么简单,拷贝已有对象的数据,更新少量差值
        HashMap<String, SearchWord> newKeywords = (HashMap<String, SearchWord>) currentKeywords.clone();
    
        // 从数据库中取出更新时间>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;
      }
    }
    

    上述代码也存在问题,问题就在于我们clone方法只会clone基础数据类型,对于引用数据类型其实克隆的是引用本身,对象是共享的,一旦我们修改了统计的数据,会造成老内存统计数据被污染了,有的是老版本数据,有的是新版本的数据,不符合要求说的原子性。

    那我们是不是要全部进行深拷贝对象那,也不是,优秀的思路是我们开始时候仍然采用上面clone方式生成对象,如果这个对象发生改变了,那为了不破坏老的统计数据,我们移除老对象,再添加个新对象即可,方法挺好。

    
    public class Demo {
      private HashMap<String, SearchWord> currentKeywords=new HashMap<>();
      private long lastUpdateTime = -1;
    
      public void refresh() {
        // Shallow copy
        HashMap<String, SearchWord> newKeywords = (HashMap<String, SearchWord>) currentKeywords.clone();
    
        // 从数据库中取出更新时间>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())) {
            newKeywords.remove(searchWord.getKeyword());
          }
          newKeywords.put(searchWord.getKeyword(), searchWord);
        }
    
        lastUpdateTime = maxNewUpdatedTime;
        currentKeywords = newKeywords;
      }
    
      private List<SearchWord> getSearchWords(long lastUpdateTime) {
        // TODO: 从数据库中取出更新时间>lastUpdateTime的数据
        return null;
      }
    }
    

    七 备注

    文章中的代码来自极客时间的《设计模式之美》-王争。

    八 诗词欣赏

     
    
    江城子·墨云拖雨过西楼
    
    [宋] [苏轼] 
    
    墨云拖雨过西楼。水东流。晚烟收。
    柳外残阳,回照动帘钩。
    今夜巫山真个好,花未落,酒新篘。
    
    美人微笑转星眸。月花羞。捧金瓯。
    歌扇萦风,吹散一春愁。试问江南诸伴侣,谁似我,醉扬州。
    
    

    相关文章

      网友评论

          本文标题:创建类设计模式分享

          本文链接:https://www.haomeiwen.com/subject/tfbrnltx.html