应用场景
主要解决参数过多导致创建多个构造器时,构造方法冗长的问题。
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模式的不足。但是也有一些缺点。
- 建造者模式并没有比重叠构造器模式简洁多少。建造者模式要在建造类中定义相同的参数,实际上就是定义两遍。并且像可序列化的类,或者数据库对应实体,或者一些特殊的方法(比如BeanUtils.copyProperties),都必须要类暴露setter方法。如果在这些类中使用建造者模式,那么setter也同样会定义两遍,导致的问题就是代码的冗长。同时因为setter方法的暴露,对于调用方没有起到拘束作用,会有调用方使用JavaBeans模式放入无效参数的风险。
- 性能方面不如直接构造方法。因为要先创建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创建不同的子类。工厂模式和建造者模式虽然都是创建型的设计模式,但没必要分的太清,可以结合使用。