创建型设计模式-建造者(Builder)模式

Posted 风流 少年

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了创建型设计模式-建造者(Builder)模式相关的知识,希望对你有一定的参考价值。

在了解建造者模式之前需要先了解一下重叠构造器模式和JavaBeans模式,构建者模式是为了优化前两种模式的弊端而产生的。

一:重叠构造器(telescoping constructor)模式

假如一个类中有很多字段,其中只有少数几个字段是必选的(required),其余大部分字段都是可选的(optional),那么该如何创建对象呢?

我们创建一个"营养成分表"类,营养成分在每个食物的包装盒上都有明确标明各成分的含量。我们假如能量、蛋白质这两个字段是必选的,由于每种食物可能包含的成分不同我们假如脂肪、钠、维他命、钙、铁等字段是可选的。我们认为这些成分的值一旦设置值就不能修改,所以我们将每个字段都使用final修饰符来修饰。

如何实例化对象?我们可以通过构造方法来实例化, 我们需要为所有字段生成一个最完整的构造器,只通过这一个构造器来实例化。

/**
 * 营养成分表
 *
 * @author Mengday Zhang
 * @version 1.0
 * @since 2019-08-08
 */
public class NutritionFacts 
    /** 能量 required */
    private final int energy;

    /** 蛋白质 required */
    private final double protein;

    /** 脂肪 optional */
    private final double fat;

    /** 钠 optional */
    private final int sodium;

    /** 维他命 optional */
    private final int vitamin;

    /** 钙 optional */
    private final int calcium;

    /** 铁 optional */
    private final int iron;


    public NutritionFacts(int energy, double protein, double fat, int sodium, int vitamin, int calcium, int iron) 
        this.energy = energy;
        this.protein = protein;
        this.fat = fat;
        this.sodium = sodium;
        this.vitamin = vitamin;
        this.calcium = calcium;
        this.iron = iron;
    

    public static void main(String[] args) 
        // 只初始化必要参数,其它可选参数也不得不赋一个默认值,为可选参数也要必须设置默认值显得非常不优雅
        NutritionFacts nutritionFacts = new NutritionFacts(1216, 9.2, 0.0, 0, 0, 0, 0);
        
        // 同时初始化所有必要参数和部分可选参数,其它可选参数也不得不设置一个默认值
        NutritionFacts nutritionFacts2 = new NutritionFacts(1216, 9.2, 13.9, 0, 0, 0, 0);
        
        // 同时初始化所有必要参数和部分可选参数,其它可选参数也不得不设置一个默认值
        NutritionFacts nutritionFacts3 = new NutritionFacts(1216, 9.2, 0.0, 218, 0, 0, 0);
    

重叠构造器模式的缺点

只有一个有全部字段的构造器,在使用起来非常笨拙,当我们实例化对象时想只给部分字段赋值,此时也不得不把其它不想初始化的字段也要设置默认值,这种写法非常的不优雅,而且如果类中的字段很多的话,一个构造器包含所有字段,可能在设置值的时候容易搞错顺序从而设置值错误。

为了缓解这种情况我们需要构造多种构造器,如必须的字段设置一个构造器,然后再必须字段上一个可选字段的构造器,依次类推,直到一个拥有完全字段的构造器,这样会比只有一个构造器稍微好一点。

/**
 * 营养成分表
 *
 * @author Mengday Zhang
 * @version 1.0
 * @since 2019-08-08
 */
public class NutritionFacts 
    /** 能量 required */
    private final int energy;

    /** 蛋白质 required */
    private final double protein;

    /** 脂肪 optional */
    private final double fat;

    /** 钠 optional */
    private final int sodium;

    /** 维他命 optional */
    private final int vitamin;

    /** 钙 optional */
    private final int calcium;

    /** 铁 optional */
    private final int iron;


    public NutritionFacts(int energy, double protein) 
        this(energy, protein, 0.0, 0, 0, 0, 0);
    

    public NutritionFacts(int energy, double protein, double fat) 
        this(energy, protein, fat, 0, 0, 0, 0);
    

    public NutritionFacts(int energy, double protein, double fat, int sodium) 
        this(energy, protein, fat, sodium, 0, 0, 0);
    

    public NutritionFacts(int energy, double protein, double fat, int sodium, int vitamin) 
        this(energy, protein, fat, sodium, vitamin, sodium, 0);
    

    public NutritionFacts(int energy, double protein, double fat, int sodium, int vitamin, int calcium) 
        this(energy, protein, fat, sodium, vitamin, calcium, 0);
    

    public NutritionFacts(int energy, double protein, double fat, int sodium, int vitamin, int calcium, int iron) 
        this.energy = energy;
        this.protein = protein;
        this.fat = fat;
        this.sodium = sodium;
        this.vitamin = vitamin;
        this.calcium = calcium;
        this.iron = iron;
    

    public static void main(String[] args) 
        // 只初始化必要参数
        NutritionFacts nutritionFacts = new NutritionFacts(1216, 9.2);

        // 同时初始化所有必要参数和部分可选参数
        NutritionFacts nutritionFacts2 = new NutritionFacts(1216, 9.2, 13.9);

        // 同时初始化所有必要参数和部分可选参数
        NutritionFacts nutritionFacts3 = new NutritionFacts(1216, 9.2, 0.0, 218, 0, 0, 0);

        // 同时初始化所有必要参数和部分可选参数
        NutritionFacts nutritionFacts4 = new NutritionFacts(1216, 9.2, 0.0, 218, 0, 56, 0);
    

我们虽然增加了很多个构造器,确实在实例化时只设置了自己关心的字段,看起来代码也清新不少。但是这需要写很多很多个构造器,这显然不太实际。为了解决这种问题我们继续使用JavaBeans模式来优化。

二:JavaBeans模式

JavaBeans模式就是我们一直在使用的模式,就是给一个无参构造函数,然后通过set方法为关心的字段赋值。此时可以想对哪个字段赋值就可以对哪个字段赋值。

/**
 * 营养成分表
 *
 * @author Mengday Zhang
 * @version 1.0
 * @since 2019-08-08
 */
public class NutritionFacts 
    /** 能量 required */
    private int energy;

    /** 蛋白质 required */
    private double protein;

    /** 脂肪 optional */
    private double fat;

    /** 钠 optional */
    private int sodium;

    /** 维他命 optional */
    private int vitamin;

    /** 钙 optional */
    private int calcium;

    /** 铁 optional */
    private int iron;


    public NutritionFacts() 

    


    public int getEnergy() 
        return energy;
    

    public void setEnergy(int energy) 
        this.energy = energy;
    

    public double getProtein() 
        return protein;
    

    public void setProtein(double protein) 
        this.protein = protein;
    

    public double getFat() 
        return fat;
    

    public void setFat(double fat) 
        this.fat = fat;
    

    public int getSodium() 
        return sodium;
    

    public void setSodium(int sodium) 
        this.sodium = sodium;
    

    public int getVitamin() 
        return vitamin;
    

    public void setVitamin(int vitamin) 
        this.vitamin = vitamin;
    

    public int getCalcium() 
        return calcium;
    

    public void setCalcium(int calcium) 
        this.calcium = calcium;
    

    public int getIron() 
        return iron;
    

    public void setIron(int iron) 
        this.iron = iron;
    

    public static void main(String[] args) 
        // 调用无参构造器进行实例化,调用set方法,想对哪个字段赋值就对哪个字段赋值
        NutritionFacts nutritionFacts = new NutritionFacts();
        nutritionFacts.setEnergy(1216);
        nutritionFacts.setProtein(9.2);
        nutritionFacts.setSodium(218);
        nutritionFacts.setCalcium(56);
    


JavaBeans模式这种方式可以为任意字段赋值,代码看起来也很清新,但是这种方式不能对字段使用final修饰符来修饰, 这不满足我们的需求。

三:建造者(Builder)模式

为了既能对字段使用final修饰符修饰,也达到可以优雅的对关系的字段赋值,我们继续使用建造者(Builder)模式优化。

设计模式的代码模式都是固定的(有些设计模式存在一些变体),建造者设计模式的特点有:

  • 所有字段被final修饰
  • 如果其它类需要访问属性值,可以为字段提供getter方法,设计模式中并没有提到这一点
  • 提供一个私有的构造器,构造器参数为内部静态类Builder
  • 静态内部类的类名一般起名为Builder,该类中也有一份外部类的所有属性
  • 静态内部类Builder中有每个属性的set方法,set方法的名字可以是普通的set命名方式即set作为前缀(如setName), 也可以不使用set前缀,set方法的名字和属性的名字完全一样(如name),两种做法都可以,而且每个set方法的返回类型都为当前类Builder,这样做是可以通过链式语法调用setter方法
  • 静态内部类Builder中提供一个build()方法用于返回外部类实例
/**
 * 营养成分表
 *
 * @author Mengday Zhang
 * @version 1.0
 * @since 2019-08-08
 */
public class NutritionFacts 
    /** 能量 required */
    private final int energy;

    /** 蛋白质 required */
    private final double protein;

    /** 脂肪 optional */
    private final double fat;

    /** 钠 optional */
    private final int sodium;

    /** 维他命 optional */
    private final int vitamin;

    /** 钙 optional */
    private final int calcium;

    /** 铁 optional */
    private final int iron;
    
    
    public int getEnergy() 
        return energy;
    

    public double getProtein() 
        return protein;
    

    public double getFat() 
        return fat;
    

    public int getSodium() 
        return sodium;
    

    public int getVitamin() 
        return vitamin;
    

    public int getCalcium() 
        return calcium;
    

    public int getIron() 
        return iron;
    


    private NutritionFacts(Builder builder) 
        this.energy = builder.energy;
        this.protein = builder.protein;
        this.fat = builder.fat;
        this.sodium = builder.sodium;
        this.vitamin = builder.vitamin;
        this.calcium = builder.calcium;
        this.iron = builder.iron;
    


    public static class Builder 
        private int energy;
        private double protein;
        private double fat;
        private int sodium;
        private int vitamin;
        private int calcium;
        private int iron;


        public Builder(int energy, double protein) 
            this.energy = energy;
            this.protein = protein;
        

        public Builder energy(int energy) 
            this.energy = energy;
            return this;
        

        public Builder protein(double energy) 
            this.protein = protein;
            return this;
        

        public Builder fat(double fat) 
            this.fat = fat;
            return this;
        

        public Builder sodium(int sodium) 
            this.sodium = sodium;
            return this;
        

        public Builder vitamin(int vitamin) 
            this.vitamin = vitamin;
            return this;
        

        public Builder calcium(int calcium) 
            this.calcium = calcium;
            return this;
        

        public Builder iron(int iron) 
            this.iron = iron;
            return this;
        

        public NutritionFacts build() 
        	  // 检查必要字段是否都有值,如果没有则抛出异常
            return new NutritionFacts(this);
        
    


public class Main 
    public static void main(String[] args) 
        NutritionFacts nutritionFacts = new NutritionFacts.Builder(1216, 9.2)
                .sodium(218)
                .calcium(56)
                .build();
    

建造者模式的思想:建造者模式不直接生成想要的对象,而是先创建一个Builder对象,然后再调用Builder对象的build方法来生成想要的对象。

建造者(Builder)模式适用场景

  • Java Builder模式主要是用一个内部类去实例化一个对象,避免一个类出现过多构造函数,而且构造函数如果出现默认参数的话,很容易出错。
  • 通常使用建造者模式创建的对象的属性都是不可变的,不能再次对字段进行修改
  • 一般用于配置参数

优点

建造者模式代码比较优雅,比较容易阅读,可以通过链式语法一直调用下去。

缺点

构造器需要写更多的代码,不过现在可以使用Lombok插件,只需要在类上使用一个注解(@Builder)就搞定了

四:Lombok

@Builder
public class NutritionFacts 
    /** 能量 required */
    private final int energy;

    /** 蛋白质 required */
    private final double protein;

    /** 脂肪 optional */
    private final double fat;

    /** 钠 optional */
    private final int sodium;

    /** 维他命 optional */
    private final int vitamin;

    /** 钙 optional */
    private final int calcium;

    /** 铁 optional */
    private final int iron;

@Builder编译后的.class文件

public class NutritionFacts 
    private final int energy;
    private final double protein;
    private final double fat;
    private final int sodium;
    private final int vitamin;
    private final int calcium;
    private final int iron;

    NutritionFacts(final int energy, final double protein, final double fat, final int sodium, final int vitamin, final int calcium, final int iron) 
        this.energy = energy;
        this.protein = protein;
        this.fat = fat;
        this.sodium = sodium;
        this.vitamin = vitamin;
        this.calcium = calcium;
        this.iron = iron;
    

    public static NutritionFacts.NutritionFactsBuilder builder() 
        return new NutritionFacts.NutritionFactsBuilder();
    

    public static class NutritionFactsBuilder 
        private int energy;
        private double protein;
        private double fat;
        private int sodium;
        private int vitamin;
        private int calcium;
        private int iron;

        NutritionFactsBuilder() 
        

        public NutritionFacts.NutritionFactsBuilder energy(final int energy) 
            this.energy = energy;
            return this;
        

        public NutritionFacts.NutritionFactsBuilder protein(final double protein) 
            this.protein = protein;
            return this;
        

        public NutritionFacts.NutritionFactsBuilder fat(final double fat) 
            this.fat = fat;
            return this;
        

        public NutritionFacts.NutritionFactsBuilder sodium(final int sodium) 
            this.sodium = sodium;
            return this;
        

        public NutritionFacts.NutritionFactsBuilder vitamin(final int vitamin) 
            this.vitamin = vitamin;
            return this;
        

        public NutritionFacts.NutritionFactsBuilder calcium

以上是关于创建型设计模式-建造者(Builder)模式的主要内容,如果未能解决你的问题,请参考以下文章

设计模式建造者模式Builder(创建型)

创建型模式—建造者模式

创建型模式 建造者模式

设计模式建造者模式Builder(创建型)

设计模式-创建型-5-建造者模式

06建造者模式Builder