建造者模式

应用场景

主要解决参数过多导致创建多个构造器时,构造方法冗长的问题。

JavaBeans模式的不足

先通过无参构造函数创建对象,再调用setter方法设置每个必要参数以及一些可选参数。

例如:

Conference c = new Conference();
c.setOrganizer("admin");
c.setStartTime("2021-06-18 18:00");
c.setEndTime("2021-06-18 19:00");
c.setParticipantsNumber(18);

JavaBeans模式也可以解决参数过多导致的构造方法冗余的问题。但随之而来了新的问题。

1.类无处安置校验参数有效性和拘束条件

原本我们可以在构造方法中校验参数的有效性和拘束条件。

例如:

public Conference(String organizer, Date startTime, Date endTime) {
  // 校验条件:会议举办方和开始时间以及结束时间必填,不能为空
  if (StringUtils.isEmpty(organizer)) {
    throw new IllegalArgumentException("...");
  }
  if (ObjectUtils.isEmpty(startTime) || ObjectUtils.isEmpty(endTime)) {
    throw new IllegalArgumentException("...");
  }
  // 拘束条件:结束时间一定是在开始时间之后
  if (startTime > endTime) {
    throw new IllegalArgumentException("...");
  }
  this.organizer = organizer;
  this.startTime = startTime;
  this.endTime = endTime;
}

而JavaBeans模式中在最后一个setter方法前都是无效状态。类中setter方法中可以放置校验参数有效性的逻辑,但无法放置参数之间的拘束条件。并且我们不能保证调用方一定会把所有必要的setter方法写一遍,漏了很有可能导致对象是无效状态。

2.暴露setter方法导致不可变类失效

不可变类一旦赋值就不可被修改,也就是说不能暴露setter方法。而JavaBeans模式必须要暴露setter方法,这就与不可变类的要求矛盾了。JavaBeans模式不可用于参数过多的不可变类。

建造者模式可解决上述问题

建造者模式除了解决主要的构造方法冗长问题外,还可以解决JavaBeans的不足。

示例:

public class Conference {
  private String organizer;
  private Date startTime;
  private Date endTime;
  private int participantsNumber;
  
  // setter方法私有,解决不可变类问题
  private void setOrganizer(String organizer) {...}
  private void setStartTime(Date startTime) {...}
  private void setEndTime(Date endTime) {...}
  private void setParticipantsNumber(int participantsNumber) {...}
  
  private Conference(Build build) {
    this.organizer = build.organizer;
    this.startTime = build.startTime;
    this.endTime = build.endTime;
    this.participantsNumber = participantsNumber;
  }
  
  public static class Build {
    private static final int DEFAULT_PARTICIPANT_NUMBER = 0;
    
    private String organizer;
    private Date startTime;
    private Date endTime;
    private int participantsNumber = DEFAULT_PARTICIPANT_NUMBER;
    
    public Conference build() {
      // 参数校验和拘束条件校验集中放这里
      if (StringUtils.isEmpty(organizer)) {
            throw new IllegalArgumentException("...");
        }
      if (ObjectUtils.isEmpty(startTime) || ObjectUtils.isEmpty(endTime)) {
        throw new IllegalArgumentException("...");
      }
      if (startTime > endTime) {
        throw new IllegalArgumentException("...");
      }
      return new Conference(this);
    }
    
    public Build setOrganizer(String organizer) {
      if (StringUtils.isEmpty(organizer)) {
            throw new IllegalArgumentException("...");
        }
      this.organizer = organizer;
      return this;
    }
    
    public Build setStartTime(Date startTime) {
      if (ObjectUtils.isEmpty(startTime)) {
        throw new IllegalArgumentException("...");
      }
      this.startTime = startTime;
      return this;
    }
    
    public Build setEndTime(Date endTime) {
      if (ObjectUtils.isEmpty(endTime)) {
        throw new IllegalArgumentException("...");
      }
      this.endTime = endTime;
      return this;
    }
    
    public Build setParticipantsNumber(int participantsNumber) {
      if (participantsNumber < 0) {
        throw new IllegalArgumentException("...");
      }
      this.participantsNumber = participantsNumber;
      return this;
    }
  }
}


Confluence c = new Confluence.Build()
    .setOrganizer("admin")
    .setOrganizer("2021-06-18 18:00")
    .setEndTime("2021-06-18 19:00")
    .setParticipantsNumber(18)
    .build();

建造者模式的缺点

如上代码建造者模式可以解决参数过多导致的构造方法冗长问题,也可解决JavaBeans模式的不足。但是也有一些缺点。

  1. 建造者模式并没有比重叠构造器模式简洁多少。建造者模式要在建造类中定义相同的参数,实际上就是定义两遍。并且像可序列化的类,或者数据库对应实体,或者一些特殊的方法(比如BeanUtils.copyProperties),都必须要类暴露setter方法。如果在这些类中使用建造者模式,那么setter也同样会定义两遍,导致的问题就是代码的冗长。同时因为setter方法的暴露,对于调用方没有起到拘束作用,会有调用方使用JavaBeans模式放入无效参数的风险。
  2. 性能方面不如直接构造方法。因为要先创建build对象,再创建目标对象,相比于直接创建目标对象的构造方法性能方面一定不如。不过这点性能差别应该微乎其微。

因此,个人认为,建造者模式的用处应该是在一些后台调用方法的参数对象上,对于数据库对应的实体和可序列化的类来说没必要。

Calendar类

Calendar类是JDK中使用建造者模式的一个案例。

public static class Builder {
        private static final int NFIELDS = FIELD_COUNT + 1; // +1 for WEEK_YEAR
        private static final int WEEK_YEAR = FIELD_COUNT;

        private long instant;
        private int[] fields;
        private int nextStamp;
        private int maxFieldIndex;
        private String type;
        private TimeZone zone;
        private boolean lenient = true;
        private Locale locale;
        private int firstDayOfWeek, minimalDaysInFirstWeek;

        public Builder() {
        }

        public Builder setInstant(long instant) {
            if (fields != null) {
                throw new IllegalStateException();
            }
            this.instant = instant;
            nextStamp = COMPUTED;
            return this;
        }

        public Builder setInstant(Date instant) {
            return setInstant(instant.getTime()); // NPE if instant == null
        }

        public Builder set(int field, int value) {
            // Note: WEEK_YEAR can't be set with this method.
            if (field < 0 || field >= FIELD_COUNT) {
                throw new IllegalArgumentException("field is invalid");
            }
            if (isInstantSet()) {
                throw new IllegalStateException("instant has been set");
            }
            allocateFields();
            internalSet(field, value);
            return this;
        }

        public Builder setFields(int... fieldValuePairs) {
            int len = fieldValuePairs.length;
            if ((len % 2) != 0) {
                throw new IllegalArgumentException();
            }
            if (isInstantSet()) {
                throw new IllegalStateException("instant has been set");
            }
            if ((nextStamp + len / 2) < 0) {
                throw new IllegalStateException("stamp counter overflow");
            }
            allocateFields();
            for (int i = 0; i < len; ) {
                int field = fieldValuePairs[i++];
                // Note: WEEK_YEAR can't be set with this method.
                if (field < 0 || field >= FIELD_COUNT) {
                    throw new IllegalArgumentException("field is invalid");
                }
                internalSet(field, fieldValuePairs[i++]);
            }
            return this;
        }

        public Builder setDate(int year, int month, int dayOfMonth) {
            return setFields(YEAR, year, MONTH, month, DAY_OF_MONTH, dayOfMonth);
        }

        public Builder setTimeOfDay(int hourOfDay, int minute, int second) {
            return setTimeOfDay(hourOfDay, minute, second, 0);
        }

        public Builder setTimeOfDay(int hourOfDay, int minute, int second, int millis) {
            return setFields(HOUR_OF_DAY, hourOfDay, MINUTE, minute,
                             SECOND, second, MILLISECOND, millis);
        }

        public Builder setWeekDate(int weekYear, int weekOfYear, int dayOfWeek) {
            allocateFields();
            internalSet(WEEK_YEAR, weekYear);
            internalSet(WEEK_OF_YEAR, weekOfYear);
            internalSet(DAY_OF_WEEK, dayOfWeek);
            return this;
        }

        public Builder setTimeZone(TimeZone zone) {
            if (zone == null) {
                throw new NullPointerException();
            }
            this.zone = zone;
            return this;
        }

        public Builder setLenient(boolean lenient) {
            this.lenient = lenient;
            return this;
        }

        public Builder setCalendarType(String type) {
            if (type.equals("gregorian")) { // NPE if type == null
                type = "gregory";
            }
            if (!Calendar.getAvailableCalendarTypes().contains(type)
                    && !type.equals("iso8601")) {
                throw new IllegalArgumentException("unknown calendar type: " + type);
            }
            if (this.type == null) {
                this.type = type;
            } else {
                if (!this.type.equals(type)) {
                    throw new IllegalStateException("calendar type override");
                }
            }
            return this;
        }

        public Builder setLocale(Locale locale) {
            if (locale == null) {
                throw new NullPointerException();
            }
            this.locale = locale;
            return this;
        }

        public Builder setWeekDefinition(int firstDayOfWeek, int minimalDaysInFirstWeek) {
            if (!isValidWeekParameter(firstDayOfWeek)
                    || !isValidWeekParameter(minimalDaysInFirstWeek)) {
                throw new IllegalArgumentException();
            }
            this.firstDayOfWeek = firstDayOfWeek;
            this.minimalDaysInFirstWeek = minimalDaysInFirstWeek;
            return this;
        }

        public Calendar build() {
            if (locale == null) {
                locale = Locale.getDefault();
            }
            if (zone == null) {
                zone = TimeZone.getDefault();
            }
            Calendar cal;
            if (type == null) {
                type = locale.getUnicodeLocaleType("ca");
            }
            if (type == null) {
                if (locale.getCountry() == "TH"
                    && locale.getLanguage() == "th") {
                    type = "buddhist";
                } else {
                    type = "gregory";
                }
            }
            switch (type) {
            case "gregory":
                cal = new GregorianCalendar(zone, locale, true);
                break;
            case "iso8601":
                GregorianCalendar gcal = new GregorianCalendar(zone, locale, true);
                // make gcal a proleptic Gregorian
                gcal.setGregorianChange(new Date(Long.MIN_VALUE));
                // and week definition to be compatible with ISO 8601
                setWeekDefinition(MONDAY, 4);
                cal = gcal;
                break;
            case "buddhist":
                cal = new BuddhistCalendar(zone, locale);
                cal.clear();
                break;
            case "japanese":
                cal = new JapaneseImperialCalendar(zone, locale, true);
                break;
            default:
                throw new IllegalArgumentException("unknown calendar type: " + type);
            }
            cal.setLenient(lenient);
            if (firstDayOfWeek != 0) {
                cal.setFirstDayOfWeek(firstDayOfWeek);
                cal.setMinimalDaysInFirstWeek(minimalDaysInFirstWeek);
            }
            if (isInstantSet()) {
                cal.setTimeInMillis(instant);
                cal.complete();
                return cal;
            }

            if (fields != null) {
                boolean weekDate = isSet(WEEK_YEAR)
                                       && fields[WEEK_YEAR] > fields[YEAR];
                if (weekDate && !cal.isWeekDateSupported()) {
                    throw new IllegalArgumentException("week date is unsupported by " + type);
                }

                // Set the fields from the min stamp to the max stamp so that
                // the fields resolution works in the Calendar.
                for (int stamp = MINIMUM_USER_STAMP; stamp < nextStamp; stamp++) {
                    for (int index = 0; index <= maxFieldIndex; index++) {
                        if (fields[index] == stamp) {
                            cal.set(index, fields[NFIELDS + index]);
                            break;
                        }
                    }
                }

                if (weekDate) {
                    int weekOfYear = isSet(WEEK_OF_YEAR) ? fields[NFIELDS + WEEK_OF_YEAR] : 1;
                    int dayOfWeek = isSet(DAY_OF_WEEK)
                                    ? fields[NFIELDS + DAY_OF_WEEK] : cal.getFirstDayOfWeek();
                    cal.setWeekDate(fields[NFIELDS + WEEK_YEAR], weekOfYear, dayOfWeek);
                }
                cal.complete();
            }

            return cal;
        }

        private void allocateFields() {
            if (fields == null) {
                fields = new int[NFIELDS * 2];
                nextStamp = MINIMUM_USER_STAMP;
                maxFieldIndex = -1;
            }
        }

        private void internalSet(int field, int value) {
            fields[field] = nextStamp++;
            if (nextStamp < 0) {
                throw new IllegalStateException("stamp counter overflow");
            }
            fields[NFIELDS + field] = value;
            if (field > maxFieldIndex && field < WEEK_YEAR) {
                maxFieldIndex = field;
            }
        }

        private boolean isInstantSet() {
            return nextStamp == COMPUTED;
        }

        private boolean isSet(int index) {
            return fields != null && fields[index] > UNSET;
        }

        private boolean isValidWeekParameter(int value) {
            return value > 0 && value <= 7;
        }
    }

与其说是建造者模式,不如说建造者模式和工厂模式的结合。因为在build()方法中根据不同的type创建不同的子类。工厂模式和建造者模式虽然都是创建型的设计模式,但没必要分的太清,可以结合使用。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容

  • 一. 概述 建造者模式(Builder),又叫生成器模式,它将一个复杂对象的构建与它的表示分离,使得同样的构建过程...
    BrightLoong阅读 321评论 0 0
  • 概述 建造者模式(Builder Pattern):将一个复杂对象的构建与表示分离,使构建过程能可以创建出不同的表...
    jxiu阅读 698评论 0 0
  • 没有人买车会只买一个轮胎或者方向盘,大家买的都是一辆包含轮胎、方向盘和发动机等多个部件的完整汽车。如何将这些部件组...
    justCode_阅读 1,831评论 1 6
  • 引言 当遇到一个类中的参数非常多的时候,构造函数的定义会显得非常的冗长,可读性下降。并且这种时候,某些参数往往是可...
    阿堃堃堃堃阅读 351评论 0 0
  • 表情是什么,我认为表情就是表现出来的情绪。表情可以传达很多信息。高兴了当然就笑了,难过就哭了。两者是相互影响密不可...
    Persistenc_6aea阅读 124,525评论 2 7