1. 程式人生 > >設計模式學習筆記4:建造者模式

設計模式學習筆記4:建造者模式

定義

將一個複雜的物件與它的表示分離,使得同樣的構建過程可以建立不同的表示。

​ 使用者只需要指定需要建造的型別就可以得到它們,建造過程及細節不需要知道。

​ 【就是如何一步步構建一個包含多個元件的物件,相同的構建過程可以建立不同的產品,比較適用於流程固定但是順序不一定固定】

型別

建立型

使用場景

如果一個物件有非常複雜的內部結構(很多屬性);

想把複雜的物件的建立和使用進行分離

優點

封裝性好,建立和使用分離;

擴充套件性好、建造類之間獨立,一定程度上解耦

缺點

產生多餘的Builder物件;

產品內部發生變化,建造者都要修改

和工廠模式的區別

建造者模式和工廠模式比較相近,但還是有區別的:

不同點 建造者模式 工廠模式
著重點 更注重方法的呼叫順序 注重建立產品
建立力度 可以建立一些複雜的產品 建立的都是一個模樣
關注點 不止創造這個產品,還要知道產品的組成部件 創建出想要的物件就行

程式碼實現

業務場景:建造線上學習網站的視訊教學課程,就比如建造Java課程。

首先新建builder包:

建立課程實體類Course,給這個課程設定些屬性,設定get/set方法以及toString:

package com.ljm.design.pattern.creational.builder;

/**
 * 課程實體類
 */
public class Course {
    //為了方便全使用String,因為這不是重點
    private String courseName;//名字
    private String coursePPT;//PPT
    private String courseVideo;//視訊
    private String courseArticle;//文章
    private String courseQA;//問答
    
	//為節省空間,set/get/toString省略
} 複製程式碼

接下來建立一個抽象類。這個類是課程的建造者,然後根據課程類Course的屬性宣告他們的建造方法,最後再宣告一個構造課程整體的抽象方法:

package com.ljm.design.pattern.creational.builder;

//抽象的課程建造者
public abstract class CourseBuilder {

    public abstract void builderCourseName(String courseName);
    public abstract void builderCoursePPT(String coursePPT);
    public abstract void builderCourseVideo(String courseVideo);
    public abstract void builderCourseArticle(String courseArticle);
    public abstract void builderCourseQA(String courseQA);

    //屬性就建造完成後,建造課程並返回
    public abstract Course makeCourse();

}
複製程式碼

接下有了抽象建造者,就要來實現一個真正的課程建造者CourseActualBuilder繼承CourseBuilder。,實現裡面的方法:

package com.ljm.design.pattern.creational.builder;

//課程的實際建造者
public class CourseActualBuilder extends CourseBuilder {
    //這裡簡單實現以下,直接設定屬性,最後返回
    private Course course = new Course();
    
    @Override
    public void builderCourseName(String courseName) {
        course.setCourseName(courseName);
    }
    @Override
    public void builderCoursePPT(String coursePPT) {
        course.setCoursePPT(coursePPT);
    }
    @Override
    public void builderCourseVideo(String courseVideo) {
        course.setCourseVideo(courseVideo);
    }
    @Override
    public void builderCourseArticle(String courseArticle) {
        course.setCourseArticle(courseArticle);
    }
    @Override
    public void builderCourseQA(String courseQA) {
        course.setCourseQA(courseQA);
    }

    @Override
    public Course makeCourse() {
        return course;
    }
}
複製程式碼

在這裡引入一個課程助理Assistant ,課程講師和學習網站合作時,網站老闆肯定不會和講師談業務,而是會指派一個業務人員和講師對接,那這個人可以稱之為課程助理。網站老闆在下達課程任務,會告訴課程助理,然後助理和對應的講師進行對接,然後來共同製作這個課程。

這裡可以認為助理是一個指揮官,講師負責課程(提交課程屬性),課程助理通過講師提交的資料拼接成一個完整的課程。

接下來完成Assistant 類:

package com.ljm.design.pattern.creational.builder;

//課程助理類
public class Assistant {
    //助理負責組裝課程,可定有CourseBuilder
    private CourseBuilder courseBuilder;
    //通過set注入
    public void setCourseBuilder(CourseBuilder courseBuilder) {
        this.courseBuilder = courseBuilder;
    }

    //宣告組裝行為,返回課程
    public Course makeCourse(String courseName, String coursePPT,
                             String courseVideo, String courseArticle,
                             String courseQA){
        this.courseBuilder.builderCourseName(courseName);
        this.courseBuilder.builderCoursePPT(coursePPT);
        this.courseBuilder.builderCourseVideo(courseVideo);
        this.courseBuilder.builderCourseArticle(courseArticle);
        this.courseBuilder.builderCourseQA(courseQA);
        
        return this.courseBuilder.makeCourse();
    }
}
複製程式碼

現在來看看這幾個類的UML圖:

指揮官也就是助理和課程建造者組合,一個助理包含一個(抽象)課程建造者;實際的建造者包含(持有)一個課程;都是1:1關係。

最後來建立測試類Test:

public class Test {
    public static void main(String[] args) {
        //抽象的父類引用來建立子類實現
        CourseBuilder courseBuilder = new CourseActualBuilder();
        //new一個助理
        Assistant assistant = new Assistant();
        //注入builder
        assistant.setCourseBuilder(courseBuilder);

        //呼叫助產生課程的方法
        Course course = assistant.makeCourse("JavaEE高階","JavaEE高階PPT",
                "JavaEE高階視訊","JavaEE高階文章","JavaEE高階問答");
        System.out.println(course);
    }
}
複製程式碼

再來看一下UML類圖:

這裡主要看Test,他和抽象的builder和具體的課程都沒有關係,但是和助理有關係,Test來建立助理,助理通過組合的方式使用CourseBuilder類,但是著這個例子中實際使用的是實際的建造者CourseActualBuilder來建立Course。最後Test通過這個助理拿到具體的課程類。

​ 現在對於CourseBuilder有一個繼承的實現類,而Test負責建立具體的Builder,那麼就可以有很多不同的Builder,每個Builder他們特點都不一樣,就比如還有個前端課程的Builder,裡面還要builder一個前端資源(圖片等),所有可以再應用層根據實際需求的不同new出實際建造者傳給助理,也就是說注入具體建造者到助理的職責現在交給Test。

​ 還有一種方式,就比如這個教學就是一個後端課程的教學(就比如JavaEE高階),完全不需要前端課程的圖片等資源,那麼就可以把後端課程的builder預設注入負責後端課程的教學助理當中,這樣應用層就不用關心具體的建造者(不用 new CourseActualBuilder),應用層只和具體的課程助理有關。

程式碼實現演進

​ 上面程式碼實現中引入了一個助理類,但這個助理類不是必須的

​ 在builder包建立一個資料夾,com.ljm.design.pattern.creational.builder.v2,表示版本2.


先來建立一個課程類Course,這裡要使用靜態內部類,這個內部類就是建造者,收先還是設定跟前面一樣的屬性,重寫toString方便測試,然後宣告一個靜態內部類CourseBuilder,靜態內部類還是有一樣的五個屬性,直接寫建造每個屬性的方法,

package com.ljm.design.pattern.creational.builder.v2;

/**
 * 課程實體類
 * v2
 */
public class Course {
    private String courseName;//名字
    private String coursePPT;//PPT
    private String courseVideo;//視訊
    private String courseArticle;//文章
    private String courseQA;//問答

    @Override
    public String toString() {
        return "Course{" +
                "courseName='" + courseName + '\'' +
                ", coursePPT='" + coursePPT + '\'' +
                ", courseVideo='" + courseVideo + '\'' +
                ", courseArticle='" + courseArticle + '\'' +
                ", courseQA='" + courseQA + '\'' +
                '}';
    }

    //靜態內部類:建造者
    public static class CourseBuilder{
        private String courseName;//名字
        private String coursePPT;//PPT
        private String courseVideo;//視訊
        private String courseArticle;//文章
        private String courseQA;//問答

        /**
         * 建造屬性
         */
        public void builderCourseName(String courseName){
        	this.courseName = courseName;
        }
    }
}
複製程式碼

演進班的核心在於鏈式呼叫 ,所以要把建造屬性的方法的返回改成這個靜態內部類本身,所以上面的建造屬性的方法應該這樣寫:

/**
 * 建造屬性
 */
public CourseBuilder builderCourseName(String courseName){
    this.courseName = courseName;
    return this;//返回的是本身
}
複製程式碼

這樣,返回本身之後就還可以呼叫其他的builder方法。

接下來完成剩下的建造屬性的方法:

我們是要通過CourseBuilder返回一個Course,那麼在Course類中寫一個(空)構造器,但是構造器的引數改為CourseBuilder,而這個引數正式Course的靜態內部內CourseBuilder建立的物件:

所以這樣我們還要在CourseBuilder類中在寫一個方法builder,返回Course:

public Course builder(){
    return new Course(this);
}
複製程式碼

然後再來完善一下Course的構造器:

public Course(CourseBuilder courseBuilder) {
    this.courseName = courseBuilder.courseName;
    this.coursePPT = courseBuilder.coursePPT;
    this.courseVideo = courseBuilder.courseVideo;
    this.courseArticle = courseBuilder.courseArticle;
    this.courseQA = courseBuilder.courseQA;
}
複製程式碼

這樣呢,Course裡面的的所有屬性就通過CourseBuilder構建成功了

最後再來寫一個測試類:

public class Test {
    public static void main(String[] args) {
        /**
         * 這就是鏈式呼叫(也叫鏈式程式設計)的效果,可以一直呼叫
         * 並且可以選擇性呼叫
         * 因為使用Course接收,所以最後要呼叫CourseBuilder的builder方法
         */
        Course course = new Course.CourseBuilder().builderCourseName("JavaEE高階")
                .builderCoursePPT("JavaEE高階PPT").builderCourseVideo("JavaEE高階Video")
                .builderCourseQA("JavaEE高階QA").builder();
        System.out.println(course);
    }
}
複製程式碼

大家可以和之前的Test的程式碼對比,感受一下演進版的好處。

再來看一下v2版本的UML類圖:

這個圖現在非常簡單,Test建立Course具體的建造者CourseBuilder,在通過CourseBuilder建造Course。

原始碼分析

jdk原始碼:

以java.lang.StringBuilder為例,從這個類名就可以看出他是一個Builder,他的append方法是我們經常用的,裡面很多過載:

StringBuffer也是一樣的,只不過StringBuffer裡面加了同步鎖。

Guava原始碼:

除了jdk,很多開源框架也大量使用建造者模式,Google的開源框架Guava為例,找到avro.shaded.com.google.common.collect.ImmutableSet,這個類本身就是不可變的Set,

裡面的copyOf方法返回值也是ImmutableSet

還有add方法:

返回的是ArrayBasedBuilder:

那麼這個Builder肯定存在一個builder方法,Ctrl+F12 搜尋發現最後確實有一個builder方法:

/**
 * Returns a newly-created {@code ImmutableSet} based on the contents of
 * the {@code Builder}.
 */
@Override public ImmutableSet<E> build() {
  ImmutableSet<E> result = construct(size, contents);
  // construct has the side effect of deduping contents, so we update size
  // accordingly.
  size = result.size();
  return result;
}
複製程式碼

這個就很像我們寫的v2版本的程式碼。

可以實際寫一下,還是在v2包的Test程式碼中寫(暫時忽略前面寫的):

Set<String> set = ImmutableSet.<String>builder().add("a").add("b").build();
System.out.println(set);
複製程式碼

Spring原始碼:

再看一個Spring中的org.springframework.beans.factory.support.BeanDefinitionBuilder:

可以看到它裡面的方法返回的都是自己本身。也是一個典型的建造者模式。

Mybatis原始碼:

看一些Mybatis中對於建造者模式的典型應用:

org.apache.ibatis.session.SqlSessionFactoryBuilder

從名字就可以看出這也是一個Builder,

這個Builder返回的都是SqlSessionFactory,裡面還有一個:

這個就是解析Mybatis的xml檔案,這裡面的核心就是builder方法:

public SqlSessionFactory build(Configuration config) {
  return new DefaultSqlSessionFactory(config);
}
複製程式碼

這個builder傳入的是Configuration配置,再把配置傳給DefaultSqlSessionFactory進行構造,看一下哪裡使用了這個方法(方法名選中,Alt+F7,雙擊進入):

發現還是在剛剛解析xml的地方,在返回的時候呼叫了這個方法,這就是在建造者模式中在使用建造者,parser就是XMLConfigBuilder型別,然後呼叫他的parse方法,進入parse方法:

public Configuration parse() {
  if (parsed) {
    throw new BuilderException("Each XMLConfigBuilder can only be used once.");
  }
  parsed = true;
  parseConfiguration(parser.evalNode("/configuration"));
  return configuration;
}
複製程式碼

而parse方法呼叫了parseConfiguration,進入parseConfiguration

這裡程式碼就很明白,主要負責Configuration各個元件的建立及裝配,從上到下就是裝配的流程。那說明XMLConfigBuilder主要負責建立複雜物件的Configuration,SqlSessionFactoryBuilder只不過做了層簡單的封裝,用建造者包裝一層建造者。