暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

省心又好用的Builder设计模式

小青菜的技术博客 2021-04-12
355

一、概述

Builder设计模式是什么,什么场景需要,如何用,起到什么样的作用,在优秀源码中有没有使用案例,他有缺点吗,希望本文可以解答您的疑惑。

二、Builder模式是什么

2.1 定义

Builder 模式(又叫建造者模式或生成器模式),它属于对象创建型模式,是将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

2.2 具体是什么

2.2.1 Builder模式主要涉及哪几个角色

  • 要建造的产品Product

  • 指挥者(Director),负责和客户(Client)对话

  • 指挥者将客户的产品需求划分为比较稳定的建造过程(抽象的Builder)

  • 指挥者指挥具体的建造者(ConcreteBuilder)干活

2.2.2 示例

1.首先创建一个Product类


public class Product {

private String param1;

private String param2;

public String getParam1() {

return param1;

}

public void setParam1(String param1) {

this.param1 = param1;

}

public String getParam2() {

return param2;

}

public void setParam2(String param2) {

this.param2 = param2;

}

}


2.将客户对产品的需求划分比较稳定的建造过程,ProductBuilder就是将Product产品的建造过程抽象出来的Builder类


public interface ProductBuilder {

void setParam1();

void setParam2();

Product getProduct();

}


3.刚才的Builder类是Product的抽象建造过程,接下来的OneProductBuilder和TwoProductBuilder类都是具体的Product的建造过程。


public class OneProuductBuilder implements ProductBuilder{

private Product product;

public OneProuductBuilder(){

this.product = new Product();

}

@Override

public void setParam1() {

product.setParam1("oneProduct_Param1");

}

@Override

public void setParam2() {

product.setParam2("oneProduct_Param2");

}

@Override

public Product getProduct() {

return product;

}

}

public class TwoProuductBuilder implements ProductBuilder{

private Product product;

public TwoProuductBuilder(){

this.product = new Product();

}

@Override

public void setParam1() {

product.setParam1("twoProduct_param1");

}

@Override

public void setParam2() {

product.setParam2("twoProduct_param2");

}

@Override

public Product getProduct() {

return product;

}

}


4.这是指挥者,通过setProductBuilder方法可以告诉他客户需要什么样的产品,然后通过createProduct方法来一步步建造产品,最后通过getProduct方法获取客户想要的产品


public class Director {

private ProductBuilder productBuilder;

public void setProductBuilder(ProductBuilder productBuilder){

this.productBuilder = productBuilder;

}

public void createProduct(){

productBuilder.setParam1();

productBuilder.setParam2();

}

public Product getProduct(){

return productBuilder.getProduct();

}

}


5.这是客户类,客户想要一个什么样的产品,只要告诉指挥者我需要什么样的产品,然后指挥者就开始指挥具体工人(也就是具体建造实现者)开始建造产品,最后客户从指挥者那儿拿到自己想要的产品。


public class Client {

public static void main(String[] args){

Director director = new Director();

director.setProductBuilder(new OneProuductBuilder());

director.createProduct();

Product product = director.getProduct();

}

}


2.2.3 什么场景适合使用Builder模式

当Product类可能存在以下情况:

  • 有很多属性

  • 属性分必选属性和非必选属性

  • 产品之间的组成部分相似,具有很多共同点

当Product存在很多属性和可选属性时,我们如何构造这个类的实例呢,通常有两种常用的方式:

  • 折叠构造函数模式


public class Product {

private String param1;

private String param2;

private String param3;

private String param4;

public Product(String param1){

this(param1,"param2");

}

public Product(String param1,String param2){

this(param1,param2,"param3");

}

public Product(String param1,String param2,String param3){

this(param1,param2,param3,"param4");

}

public Product(String param1,String param2,String param3,String param4){

this.param1 = param1;

this.param2 = param2;

this.param3 = param3;

this.param4 = param4;

}

}


  • JavaBean模式


public class Product {

private String param1;

private String param2;

private String param3;

private String param4;

public String getParam1() {

return param1;

}

public void setParam1(String param1) {

this.param1 = param1;

}

public String getParam2() {

return param2;

}

public void setParam2(String param2) {

this.param2 = param2;

}

...

}


这两种方式都存在弊端,第一种方式在使用和阅读上都不方便,当很多参数类型是相同时很容易传混。第二种方式在构建过程中对象的属性容易发生变化,造成错误。

为了更好的解决多属性设置的问题,我们可以通过链式调用的方式来很优雅的设置属性,例如以下代码:


public class Product {

private String param1;

private String param2;

private String param3;

private String param4;

public Product(String param1,String param2)



public void setParam3(String param3) {

this.param3 = param3;

return this;

}

public void setParam4(String param4) {

this.param4 = param4;

return this;

}

...

}



Product product = new Product("xx1","xx2").setParam3("3").setParam4("4")..


通过这种方式可以很优雅的设置对象的属性。对于必选属性可以放在构造函数里,对于可选属性可以通过set的方式任意设置。

三、Builder模式的使用

3.1 Builder的另一种写法

Product是我们要创建的产品,而ProductBuilder是我们将Product的建造过程抽象得到的类,跟上面例子不同的是,这边我们将ProductBuilder作为一个静态内部类放在Product类中。


public class Product {

private int index;

private String name;

Product(int index, String name){

this.index = index;

this.name = name;

}

public static ProductBuilder builder() {

return new ProductBuilder();

}

private static class ProductBuilder {

private int index;

private String name;

public ProductBuilder setIndex(int index) {

this.index = index;

return this;

}

public ProductBuilder setName(String name){

this.name = name;

return this;

}

public Product build(){

Product product = new Product(index,name);

return product;

}

}

}



Product name = Product.builder().setIndex(1).setName("name").build();


第二节的例子具体的创建过程都是由具体实现的Builder的来实现的,而这边是通过在客户端创建时自定义实现。上面还有一个指挥者Director类,主要用来与客户端沟通,客户端需要什么样的实现,指挥者就根据客户端指定的实现找到具体的实现者并建造具体产品。但因为这里将自定义实现放给了客户端,由客户端自由实现,便就不需要Director类了。这两个例子只是使用场景不同,但本质都是Builder模式。

3.2 Fluent Builder

虽然builder模式可以解决多属性的复杂对象的建造问题,但还是很容易获得不完整的(部分初始化的)对象。为了缓解这个问题,我们一般会在build方法上进行校验检查。但这种检查方式将检查转移到了运行时。

那有没有更好的办法将这种检查转移到编译时呢? 答:Fluent API模式

Fluent API有两个部分:

  • 提供一种方便的方式来链式调用方法,并将调用链中的每个后续调用都限制为一组允许调用的方法集

  • 通过限制建造对象的每个步骤中可以调用的方法集,我们可以强制执行特定的调用序列,并且在所有方法调用之后才能调用build方法。

通过Fluent API模式可以将检查转移到编译时。如下面的例子:


public class Product {

private int index;

private String name;

Product(int index, String name){

this.index = index;

this.name = name;

}

public static ProductBuilder builder() {

return new ProductBuilder();

}

private static class ProductBuilder {

public ProductBuilder1 setIndex(int index) {

return new ProductBuilder1() {

@Override

public ProductBuilder2 setName(final String name) {

return new ProductBuilder2() {

@Override

public Product build() {

return new Product(index, name);

}

};

}

};

}

public interface ProductBuilder1 {

ProductBuilder2 setName(final String name);

}

public interface ProductBuilder2 {

Product build();

}

}

}


Product name = Product.builder().setIndex(1).setName("name").build();

为了实现Fluent API的功能,我们可以通过定义接口方式来定义一组调用方法集,每次调用都返回这样的专用接口。然后通过匿名内部类的方式来实现这些接口。并且build方法定义在ProudctBuilder2接口里,这样用户可以避免用户过早构建对象。

当然,我们可以通过Lambda表达式来替代匿名内部类的写法,这样代码可以看起来更精简点


public class Product {

private int index;

private String name;

Product(int index, String name){

this.index = index;

this.name = name;

}

public static ProductBuilder builder() {

return new ProductBuilder();

}

private static class ProductBuilder {

public ProductBuilder1 setIndex(int index) {

return name -> () -> new Product(index, name);

}

public interface ProductBuilder1 {

ProductBuilder2 setName(final String name);

}

public interface ProductBuilder2 {

Product build();

}

}

}


3.3 Builder模式的优缺点

  • 优点:

    • 将产品本身内部组成细节和创建过程进行解耦,这样客户端就无需关心产品内部细节,只需要通过抽象出来的相同的创建过程来创建不同的产品对象

    • 对于抽象的创建过程(Builder),我们可以进行自由的实现具体的建造者(ConcreteBuilder),符合开闭原则。用户使用不同的具体建造者(ConcreteBuilder)就可以得到不同的产品对象

  • 缺点:

    • 对产品足够复杂的情况下,我们的具体建造者会越来越多,不利于管理

    • 产品之间的构建过程差异比较大的情况下,不太适合使用Builder模式

四、Builder模式在开源框架的使用案例

4.1 UriComponents和UriComponentsBuilder


UriComponents uriComponents = UriComponentsBuilder.fromUriString(

"http://127.0.0.180/queryUserInfo/{userId}").build();

URI uri = uriComponents.expand("66").encode().toUri();



 根据上图分析各个角色的对应情况:

  • UriComponentsBuilder:具体建造者

  • UriBuilder:抽象建造者,定义了创建对象的接口

  • UriComponents:产品角色

通过UriComponentsBuilder类,提供了多种UriComponents的建造方式,将UriComponents类本身与它的建造过程解耦

4.2 SqlSessionFactoryBuilder


ClassPathResource resource = new ClassPathResource("mybatis-config.xml");

InputStream inputStream = resource.getInputStream();

sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);


通过上面代码发现,创建SqlSessionFactory的代码在SqlSessionFactoryBuilder中


public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {

try {

XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);

return build(parser.parse());

} catch (Exception e) {

throw ExceptionFactory.wrapException("Error building SqlSession.", e);

} finally {

ErrorContext.instance().reset();

try {

inputStream.close();

} catch (IOException e) {

// Intentionally ignore. Prefer previous error.

}

}

}



public SqlSessionFactory build(Configuration config) {

return new DefaultSqlSessionFactory(config);

}


 根据上图分析各个角色的对应情况:

  • SqlSessionFactoryBuilder:指挥者角色

  • BaseBuilder:抽象Builder

  • XMLConfigBuilder:具体的Builder

  • SqlSessionFactory:需要被创建的产品

五、参考

https://zhuanlan.zhihu.com/p/58093669 

https://www.jianshu.com/p/afe090b2e19c 

https://www.jdon.com/48272

https://dzone.com/articles/why-builder-often-is-antipattern-and-how-to-replac

https://blog.csdn.net/weixin_44848573/article/details/106130651 

https://www.cnblogs.com/54chensongxia/p/12409493.html

https://blog.csdn.net/guorui_java/article/details/106684541


文章转载自小青菜的技术博客,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论