java与es8实战之一:以builder pattern开篇

打印 上一主题 下一主题

主题 932|帖子 932|积分 2796

欢迎访问我的GitHub

这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos
关于《java与es8实战》系列


  • 《java与es8实战》系列是欣宸与2022年夏季推出的原创系列,如标题所述,该系列从一个java程序员视角去学习和实践elasticsearch的8.2版本,目标是与大家一起掌握与elasticsearch开发相关的技能,以应对实际应用中的需求和挑战
本篇概览


  • 纵观欣宸过往各种系列文章,开篇无外乎两种套路

  • 第一种是对该系列的主题做重点介绍,把重点、背景说清楚
  • 第二种更加实在,就是准备工作,例如安装相关的软件,介绍对应版本,甚至写个初级的hello world


  • 那么《java与es8实战》系列的开篇应该是哪种风格?是介绍elasticsearch?还是动手部署一套es集群?亦或是用java写一套简单的增删改查代码,让大家可以快速入门?
  • 这个问题难住我了,思考良久,想到刚开始写es代码时的困惑,那时去看es的java库源码中的单元测试部分,研究如何调用java库的api,看到那里是这么写代码的,先是创建索引,创建请求对象会用到builder


  • 再随意逛到了批量操作的代码,如下图,还是builder


  • 最常用的聚合查询,如下图,也离不开builder


  • 于是我就纳闷了:以后写es相关的代码,这builder操作难道会一直伴随我?
  • 去翻阅es的官方文档,发现说的很清楚:Java客户端中的数据对象都是不可变的,这些数据对象在创建时用的是2008版本《Effective Java》中的builder模式


  • 回忆了这么多,我终于想清楚《java与es8实战》的开篇内容了:咱们不急着部署ES,也不急着写增删改查的入门级代码,今天,欣宸邀您一同去温习经典,搞清楚以下问题:

  • 直接用构造方法创建对象有什么问题?
  • 用静态方法创建对象有什么问题?
  • builder模式是什么?
  • builder模式解决了什么问题?
  • builder模式自己有啥问题?
  • es API和builder有啥关系?


  • 等咱们搞清楚这些问题,写代码操作es时遇到builder就不再疑惑,而是感受到builder带来的好处,进而养成习惯,在今后设计不可变类时自然而然的用上builder模式,那时候您不一定还在用es,然而builder模式可以长久陪伴您,因为,经典就是经典,如下图


  • 现在,咱们java程序员的es8开发之旅,就从经典的builder pattern出发
不可变对象(Immutable Objects)


  • es的API中的对象都是不可变的(immutable),关于不可变,简单的说就是:实例一旦创建后,不能改变其成员变量的值
  • 本篇文章讨论的创建对象,都是指的不可变对象
三种创建对象的常用方法


  • 这三种分别是

  • 构造方法
  • 静态工厂方法
  • builder模式
直接用构造方法创建对象有什么问题


  • 创建一个对象,最常用的方法不就是构造方法么?new Object()不香吗?
  • 成员变量很多的时候,构造方法就没那么香了,举例如下,NutritionFacts是食品包装外面显示的营养成分标签,这里面有的营养成分是必须的:每一份的含量、每一罐的含量,其他的可选
  1. public class NutritionFacts {
  2.     private final int servingSize;  // (mL)            required
  3.     private final int servings;     // (per container) required
  4.     private final int calories;     //                 optional
  5.     private final int fat;          // (g)             optional
  6.     private final int sodium;       // (mg)            optional
  7.     private final int carbohydrate; // (g)             optional
  8.     public NutritionFacts(int servingSize, int servings) {
  9.         this(servingSize, servings, 0);
  10.     }
  11.     public NutritionFacts(int servingSize, int servings,
  12.             int calories) {
  13.         this(servingSize, servings, calories, 0);
  14.     }
  15.     public NutritionFacts(int servingSize, int servings,
  16.             int calories, int fat) {
  17.         this(servingSize, servings, calories, fat, 0);
  18.     }
  19.     public NutritionFacts(int servingSize, int servings,
  20.             int calories, int fat, int sodium) {
  21.         this(servingSize, servings, calories, fat, sodium, 0);
  22.     }
  23.     public NutritionFacts(int servingSize, int servings,
  24.            int calories, int fat, int sodium, int carbohydrate) {
  25.         this.servingSize  = servingSize;
  26.         this.servings     = servings;
  27.         this.calories     = calories;
  28.         this.fat          = fat;
  29.         this.sodium       = sodium;
  30.         this.carbohydrate = carbohydrate;
  31.     }
  32. }
复制代码

  • 从上面的代码可见,为了尽量满足用户需要,NutritionFacts提供了多个构造方法给用户使用,其实相信您也明白这里面的问题:这简直是成员变量的各种排列组合呀,以后要是加字段就麻烦了
  • 再以一个使用者的视角来看看,实例化代码如下,这就有点晕了,这一眼看过去,谁知道240给了哪个字段?只能去核对构造方法的入参声明
  1. NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);
复制代码

  • 缓解上述问题的一种方法是使用JavaBeans模式,用无参构造方法,然后按照调用setXXX设置每个所需字段,示例如下所示
  1. NutritionFacts cocaCola = new NutritionFacts();
  2. cocaCola.setServingSize(240);
  3. cocaCola.setServings(8);
  4. cocaCola.setCalories(100);
  5. cocaCola.setSodium(35);
  6. cocaCola.setCarbohydrate(27);
复制代码

  • 上述方法似乎不错,哪些字段被设置一目了然,所以,成员变量多的时候,用上述方法是正确选择吗?
  • 然而,《Effective Java》原著对上述做法的评价是有着严重的弊端(the JavaBeans pattern has serious disadvantages of its own),所以,尽早放弃吧...咱们来看看具体有啥问题

  • 首先,直观的看,这种做法违背了不可变对象的定义,创建出对象后,又用setXXX方法改变了成员变量
  • 《Effective Java》的原话是在构造过程中JavaBean可能处于不一致的中的状态,我的理解如下图所示,不用颜色代表不同线程,可以看到,红色线程获取calories的值的时候,蓝色线程还没有开始设置calories的值,所以红色线程拿到的等于初始值0,这显然是不对的,正常逻辑应该是:只要cocaCola对象非空,其calories字段对外显示的值就是100

  • 经验丰富的您应该想到了这是典型的线程同步问题,应该用synchronize或ReentrantLock给蓝色代码段加锁,让红色代码先block住,直到蓝色代码执行完毕,这样就能拿到正确的值了---这种方法显然可以解决问题,然而《Effective Java》预判了您的预判:这种方式十分笨拙,在实践中很少使用,想想也是,创建和使用对象是最常见的编码了,这个思路要加多少synchronize或ReentrantLock


  • 所以构造方法不能满足我们的实际需要,再来看看静态工厂方法,它的优势在哪里
静态工厂方法的优势


  • 相比静态工厂方法,构造方法存在以下五个典型问题

  • 随着入参的不同,构造方法可以有多个,如下所示,然而都是同名的,这会给用户造成困惑,此刻用静态工厂方法,可以自由设置方法名(例如createWithName或者createWithAge),让用户更方便的选择合适的方法
  1.     public Student(String name) {
  2.         this.name = name;
  3.     }
  4.     public Student(int age) {
  5.         this.age = age;
  6.     }
复制代码

  • 使用构造方法意味着创建对象,而有时候我们只想使用,并不在乎对象本身是否是新建的,下面是Boolean.valueOf方法的源码,此处并未新建Boolean对象:
  1.     public static Boolean valueOf(String s) {
  2.         return parseBoolean(s) ? TRUE : FALSE;
  3.     }
复制代码

  • 以动物类Animal.class为例,Animal类的构造方法创建的对象Animal的实例,而静态工厂方法的返回值声明虽然是Animal,但实际返回的实例可以是Animal的子类,例如Dog
  • 静态工厂方法内部可以有灵活的逻辑来决定返回那种子类的实例,来看的静态工厂方法源码,根据底层枚举类型的大小来决定是返回RegularEnumSet实例还是JumboEnumSet实例
[code]    public static  EnumSet noneOf(Class elementType) {        Enum[] universe = getUniverse(elementType);        if (universe == null)            throw new ClassCastException(elementType + " not an enum");        if (universe.length

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

科技颠覆者

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表