创建销毁对象(第二条:当面临多个构造器参数的时候考虑使用建造者)

第二条:当面临多个构造器参数的时候考虑使用建造者


静态工厂和构造器都有一个不足:需要很多可选的参数时他们的扩展性不好。考虑下面一个类的情况,这个类代表了打包食物上面营养元素的标签。这些标签有一些必须的条目­-一份的大小,一罐多少份,一份多少卡,-还有超过20种的可选条目-总热量,饱和脂肪,反义脂肪,胆固醇,钠等等。大部分商品只会为一小部分条目设置为非零。

这种类应该写什么样的构造器或者静态方法呢?传统地,开发人员使用telescoping constructor pattern,这种模式需要提供一个仅仅包含必选参数的构造器,还有第二个只有一个可选参数的构造器,还有第三个有两个可选参数的构造器等等。直到最终的一个包含所有可选参数的构造器。这儿有一个实践过的例子。为了简单起见,只写了4个可选参数:

// Telescoping constructor pattern - does not scale well!
public class NutritionFacts {
    private final int servingSize; // (mL) required
    private final int servings; // (per container) required
    private final int calories; // (per serving) optional
    private final int fat; // (g/serving) optional
    private final int sodium; // (mg/serving) optional
    private final int carbohydrate; // (g/serving) optional

    public NutritionFacts(int servingSize, int servings) {
        this(servingSize, servings, 0);
    }

    public NutritionFacts(int servingSize, int servings,
                          int calories) {
        this(servingSize, servings, calories, 0);
    }

    public NutritionFacts(int servingSize, int servings,
                          int calories, int fat) {
        this(servingSize, servings, calories, fat, 0);
    }

    public NutritionFacts(int servingSize, int servings,
                          int calories, int fat, int sodium) {
        this(servingSize, servings, calories, fat, sodium, 0);
    }

    public NutritionFacts(int servingSize, int servings,
                          int calories, int fat, int sodium, int carbohydrate) {
        this.servingSize = servingSize;
        this.servings = servings;
        this.calories = calories;
        this.fat = fat;
        this.sodium = sodium;
        this.carbohydrate = carbohydrate;
    }
}

创建一个对象的时候,当选择最短的包含多有参数的构造器:

NutritionFacts cocaCola = newNutritionFacts(240, 8, 100, 0, 35, 27);

通常调用这个构造方法的时候都不需要设置大部分参数,但是不管怎样你都要传值给它们。在这个例子中,我们给fat传递了0。仅仅只有6个参数可能看起来并不是很糟糕,当时参数在稍微多一些,就可能玩不下去了。

简而言之,telescoping constructorpattern这种模式可以用,但是当有很多参数时,很难去写客户端代码,而且读这种代码更加困难。读者只会一脸懵逼的找到底这些参数代表什么意思,而且需要他们仔细的数参数才能找到答案。除此之外,大量的类型相同的参数还会引起隐晦的bug。如果客户端不小心把两个参数传反了,编辑器是不会报错的,但是程序在跑的时候就会出错(item 51)。

当构造一个类存在多个可选的参数的另一种选择是JavaBeans pattern,使用这种方式,可以调用一个无参的构造方法去创建对象,然后为每个必须的参数或者需要的可选参数去调用set方法就行了:

// JavaBeans Pattern - allows inconsistency, mandates
mutability

public class NutritionFacts {
    // Parameters initialized to default values (if any)
    private int servingSize = -1; // Required; no default value
    private int servings = -1; // Required; no default value
    private int calories = 0;
    private int fat = 0;
    private int sodium = 0;
    private int carbohydrate = 0;

    public NutritionFacts() {
    }

    // Setters
    public void setServingSize(int val) {
        servingSize = val;
    }

    public void setServings(int val) {
        servings = val;
    }

    public void setCalories(int val) {
        calories = val;
    }

    public void setFat(int val) {
        fat = val;
    }

    public void setSodium(int val) {
        sodium = val;
    }

    public void setCarbohydrate(int val) {
        carbohydrate = val;
    }
}

这种方法没有telescoping constructor pattern的缺点,除了有点长以外,用它创建对象或者读这种代码都挺简单的:

NutritionFacts cocaCola=newNutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);

然而不幸的是,JavaBeans模式自身存在很严重的弊端。因为整个对象构建被分成了多次调用,JavaBean这种构造方式在构造中途可能会存在不一致的状态。类并不能仅仅通过检查构造参数的有效性来强行做到一致性。尝试使用这种不在一致状态的对象可能会导致错误,可是这种产生bug的对象却早都不见了,因此想要调试就有难度了。与之相关的一个缺点是,JavaBeans模式将不可能把类设置为不可变的(Item 17),还需要开发人员为了线程安全去做额外的工作。

有方式可以减少这种缺陷的:当对象构造完毕后,可以人为的通过“开始冻结”这个对象,并且直到“完全冻结”才允许使用。但是这种变量就显得不方便,也很少用于实践中。并且,他可能会引起运行时错误,因为编辑器并不会保证开发人员在使用这种对象前先去调用freeze方法。

幸运的是,有第三种选择,它结合了telescoping constructor pattern的安全性和JavaBeanspattern的易读性。它是Builder模式的一种形式。与直接构造所需的对象不同的是,客户端带着必传参数调用构造器(或者静态工厂方法)然后得到一个builder对象。然后客户端调用这个builder对象上长得像setter的方法去设置每个可选参数。最终,客户端调用这个builder对象的无参构造方法去构建出想要的对象,这种对象通常是不可变的。这个builder是他所要构建的类的静态成员类(Item 24)。下面是实践中它的样子:

// Builder Pattern
public class NutritionFacts {
    private final int servingSize;
    private final int servings;
    private final int calories;
    private final int fat;
    private final int sodium;
    private final int carbohydrate;

    public static class Builder {
        // Required parameters
        private final int servingSize;
        private final int servings;
        // Optional parameters - initialized to default values
        private int calories = 0;
        private int fat = 0;
        private int sodium = 0;
        private int carbohydrate = 0;

        public Builder(int servingSize, int servings) {
            this.servingSize = servingSize;
            this.servings = servings;
        }

        public Builder calories(int val) {
            calories = val;
            return this;
        }

        public Builder fat(int val) {
            fat = val;
            return this;
        }

        public Builder sodium(int val) {
            sodium = val;
            return this;
        }

        public Builder carbohydrate(int val) {
            carbohydrate = val;
            return this;
        }

        public NutritionFacts build() {
            return new NutritionFacts(this);
        }
    }

    private NutritionFacts(Builder builder) {
        servingSize = builder.servingSize;
        servings = builder.servings;
        calories = builder.calories;
        fat = builder.fat;
        sodium = builder.sodium;
        carbohydrate = builder.carbohydrate;
    }
}

NutritionFacts是不可变的,所有的参数的默认值都在一个地方。Builder的setter方法返回它自己,这是为了能够实现链式调用,让API看起来很顺畅。下面是客户端调用的代码:

NutritionFacts cocaCola = newNutritionFacts.Builder(240, 8).calories(100).sodium(35).carbohydrate(27).build();

客户端代码很容易写,更容易读。Builder模式模拟了Python和Scala里面的指定可选参数。为了简洁起见,删除了有效性检查。想要一开始就检查出无效参数,就在builder的构造方法和setter方法里面做去参数校验。检查通过build方法调用有多个构造参数的构造器产生对象的不变性。为了确保这些不变量具有抵御攻击的能力,从builder中拷贝完参数后检查参数(Item 50)。如果某个验证失败了,抛出异常IllegalArgumentException(Item 72),它的详细信息可以表明哪个参数是无效的(Item 75)。

Builder模式对于层次结构也适应的很好。对于使用同一层的builders,每一个builder嵌在相应的类里面。抽象类有抽象的builder;具体的类有具体的builder。例如,考虑下面一个在根层次上代表不同种类披萨的抽象类:

// Builder pattern for class hierarchies

// Builder pattern for class hierarchies
public abstract class Pizza {
    final Set<Topping> toppings;

    Pizza(Builder<?> builder) {
        toppings = builder.toppings.clone(); // See Item 50
    }

    abstract static class Builder<T extends Builder<T>> {
        EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);

        public T addTopping(Topping topping) {
            toppings.add(Objects.requireNonNull(topping));

            return self();
        }

        abstract Pizza build();

        // Subclasses must override this method to return "this"
        protected abstract T self();
    }
    public enum Topping {HAM,
        MUSHROOM,
        ONION,
        PEPPER,
        SAUSAGE;
    }
}

注意Pizza.Builder是一个带着递归类型参数的泛型类型(Item 30),它加上这个抽象的self方法,就允许了在不必进行类型转换的情况下,能够在子类中合理的做链式调用。这种对于java缺少自身类型这种事实的替代办法被称为simulated self-typeidiom。

这里有两个Pizza的具体子类,一个代表了标准的纽约风格的披萨,另一个是馅饼。前者有一个必须的大小参数,然而后者让你选择酱汁应该被放进去还是不放进去:

public class NyPizza extends Pizza {
    private final Size size;

    private NyPizza(Builder builder) {
        super(builder);
        size = builder.size;
    }

    public static class Builder extends Pizza.Builder<Builder> {
        private final Size size;

        public Builder(Size size) {
            this.size = Objects.requireNonNull(size);
        }

        @Override
        public NyPizza build() {
            return new NyPizza(this);
        }

        @Override
        protected Builder self() {
            return this;
        }
    }
    public enum Size {SMALL,
        MEDIUM,
        LARGE;
    }
}

public class Calzone extends Pizza {
    private final boolean sauceInside;

    private Calzone(Builder builder) {
        super(builder);
        sauceInside = builder.sauceInside;
    }

    public static class Builder extends Pizza.Builder<Builder> {
        private boolean sauceInside = false; // Default

        public Builder sauceInside() {
            sauceInside = true;

            return this;
        }

        @Override
        public Calzone build() {
            return new Calzone(this);
        }

        @Override
        protected Builder self() {
            return this;
        }
    }
}

可以看到每个子类的build方法可以声明返回正确的子类:NyPizza.Builderbuild方法返回NyPizzaCalzone.Builderbuild方法返回Calzone。这种在父类方法中声明的返回某一类型,而在子类方法中声明返回这个类型的子类,被称为covariant return typing。它允许了客户端使用这些builder而不用去做类型转化。这种“有层次关系的builders”的客户端代码在本质上与那个简单的NutritionFacts builder是一样的。为了简洁起见,例子中展示的客户端代码假设以静态的方式引入了枚举常量:

NyPizzapizza = new NyPizza.Builder(SMALL)

.addTopping(SAUSAGE).addTopping(ONION).build();

Calzonecalzone = new Calzone.Builder()

.addTopping(HAM).sauceInside().build();

Builders相比于构造方法的小小优势是builders可以有多个可变参数,因为每个这种参数都为自己声明一个方法。或者,builders可以通过多次调用一个方法将参数传递到一个类参数中,正如之前addTopping方法所强调的。Builder模式是相当灵活的。一个builder是可以重复的使用来构建多个对象的。调用build方法的builder的参数是可以通过调整来创建另一个对象的。Builder在创建对象的时候可以自动赋上某些值,比如序列号,它就是在对象每次创建的时候增加的。

Builder模式也是存在缺陷的。为了创建对象,你必须首先去创建它的builder。尽管创建builder的消耗在实践中可能不被注意,但是它却可以成为性能要求严格的场景的一个问题。另外,Builder模式比起telescoping constructorpattern是更加的冗长的,所以只有在参数足够多的情况下去使用它才会体现builder的意义,一般4个以上。但是需要熟记于心的是你可能要在将来加入更多的参数。但是如果你一开始用构造方法或者静态工厂方法去构造一个对象,然后后来因为这个类的参数多到不容易处理了才换成了builder,老的构造器还有静态工厂方法就像一个受伤的大拇指暴露在外面一样。因此,一开始就用builder一般是更好的选择。

总的来说,当设计一个无论静态工厂方法或者构造器都需要一堆参数的这种类的时候, Builder模式是一个好的选择 ,尤其当很多参数都是可选的或者参数类型都是一样的时候。使用 builders 的客户端代码比起 telescoping constructors 更容易读或者写,而且 builders JavaBeans 也安全一些。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
提供的源码资源涵盖了小程序应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 适合毕业设计、课程设计作业。这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。 所有源码均经过严格测试,可以直接运行,可以放心下载使用。有任何使用问题欢迎随时与博主沟通,第一时间进行解答!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值