又臭又长的set方法
经常进行Java项目开发使用各类starter的你一定见过这种代码:
public class SwaggerConfig { @Bean public Docket api() { return new Docket(DocumentationType.SWAGGER_2) .select() .apis(RequestHandlerSelectors.any()) .paths(PathSelectors.any()) .build(); } }
相比较你的实体类的set方法,就感觉自己的set直观上就非常的low,十分不优雅,当然这里并不是指实体类,而是类似于一些接口调用时的参数dto对象的构造,在业务代码中构建参数实体十分的麻烦,例如:
@Data public class DevParam { private Long id; private String filter; private String type; private int pageNo; private int pageSize; } public class HttpUtil { private final static URL = "http://127.0.0.1:8080/deviceserver/search" public static JSONObject searchDeviceData(DevParam param) { HttpClient client = HttpClient.newHttpClient(); HttpRequest request = new HttpRequest(); request.uri(URL); request.body(JSONUtil.parseObj(param)) //……其他http调用,组装返回数据逻辑、省略 return XXX } } public class Test{ public static void main(String[] args) { //需要调用接口获取数据 DevParam param = new DevParam(); param.setFilter("关键字"); param.setPageNo(1); param.setPageSize(10); JSONObject retData = HttpUtil.searchDeviceData(param); JSONArray array = retData.getJSONArray("data"); for(int i=0; i < array.size();i++){ //……其他逻辑XXX } } }
而如果写成这样,逼格就会高很多
public class Test{ public static void main(String[] args) { //需要调用接口获取数据 JSONObject retData = HttpUtil.searchDeviceData(new DevParam().filter("XXXX") .pageNo(1) .pageSize(10)); JSONArray array = retData.getJSONArray("data"); for(int i=0; i < array.size();i++){ //……其他逻辑XXX } } }
实体类的改进:简易版的Builder模式
而上述的DevParam的实现大家应该一眼就能看出来,即将原本void的set方法修改一下就可以满足装X的条件:
public class DevParam { private Long id; private String filter; private String type; private int pageNo; private int pageSize; public DevParam() { } public DevParam setId(Long id) { this.id = id; return this; } public DevParam setFilter(String filter) { this.filter = filter; return this; } public DevParam setType(String type) { this.type = type; return this; } public DevParam setPageNo(int pageNo) { this.pageNo = pageNo; return this; } public DevParam setPageSize(int pageSize) { this.pageSize = pageSize; return this; } }
实体类的改进:基于内部类的Builder实现
如果想看起来跟一开始的swgger的配置类一样高级,则可以使用内部静态类的方式实现该方法,即类内创建一个内部类,通过内部类创建外部类的实体对象。
public class DevParam { private Long id; private String filter; private String type; private int pageNo; private int pageSize; private DevParam() { } //省略此处的get和set方法 public static class Builder { private Long id; private String filter; private String type; private int pageNo; private int pageSize; public Builder setId(Long id) { this.id = id; return this; } public Builder setFilter(String filter) { this.filter = filter; return this; } public Builder setType(String type) { this.type = type; return this; } public Builder setPageNo(int pageNo) { this.pageNo = pageNo; return this; } public Builder setPageSize(int pageSize) { this.pageSize = pageSize; return this; } public DevParam build() { DevParam devParam = new DevParam(); devParam.id = this.id; devParam.filter = this.filter; devParam.type = this.type; devParam.pageNo = this.pageNo; devParam.pageSize = this.pageSize; return devParam; } } } public class Test { public static void main(String[] args) { DevParam devParam = new DevParam.Builder() .setId(1L) .setFilter("filterValue") .setType("typeValue") .setPageNo(1) .setPageSize(10) .build(); System.out.println(devParam); } }
该种实现即是我们常用一些配置类、链接类创建时带build小尾巴的方式。
真正意义的建造者模式
其实虽然上述的DevParam类在构建时携带build,但是其实不是真正意义上的建造者模式(Builder),只能说是裁剪版的。建造者模式属于创建型模式,也就生成器模式,其核心思想是:
将复杂对象的构建和表示分离,使同样的构建过程可以创建不同的表示
建造者模式的结构
Builder:生成器接口,定义创建一个产品(Product)对象所需的各个部件的操作。
Concrete Builder:具体的生成器实现,实现各个部件的创建,并负责组装产品对象的各个部件,同时还提供一个让用户获取组装完成后的产品对象的方法。
Director:指导者,也被称为导向者,主要用来使用Builder(Builder)接口,以一个统一的过程来构建所需要的Product(Product)对象。
Product:产品,表示被生成器构建的复杂对象,包含多个部件。
样例代码:
class Product { private Long id; private String filter; private String type; private int pageNo; private int pageSize; public Product(Long id, String filter, String type, int pageNo, int pageSize) { this.id = id; this.filter = filter; this.type = type; this.pageNo = pageNo; this.pageSize = pageSize; } public Long getId() { return id; } public String getFilter() { return filter; } public String getType() { return type; } public int getPageNo() { return pageNo; } public int getPageSize() { return pageSize; } } // 构建者接口 interface Builder { void setId(Long id); void setFilter(String filter); void setType(String type); void setPageNo(int pageNo); void setPageSize(int pageSize); Product getResultProduct(); } // 具体构建者 class ConcreteBuilder implements Builder { private Long id; private String filter; private String type; private int pageNo; private int pageSize; @Override public void setId(Long id) { this.id = id; } @Override public void setFilter(String filter) { this.filter = filter; } @Override public void setType(String type) { this.type = type; } @Override public void setPageNo(int pageNo) { this.pageNo = pageNo; } @Override public void setPageSize(int pageSize) { this.pageSize = pageSize; } @Override public Product getResultProduct() { return new Product(id, filter, type, pageNo, pageSize); } } // 指挥者 class Director { private Builder builder; public Director(Builder builder) { this.builder = builder; } public void construct() { builder.setId(1L); builder.setFilter("filterValue"); builder.setType("typeValue"); builder.setPageNo(1); builder.setPageSize(10); } public Product getResultProduct() { return builder.getResultProduct(); } } public class Main { public static void main(String[] args) { Director director = new Director(new ConcreteBuilder()); director.construct(); Product product = director.getResultProduct(); System.out.println(product); } }
一个建造者经典案例
可能仅看上述建造者的demo代码,发现还不如简易版或者内部类版本的创建者,甚至都不如单纯的实体类set省事,但是先别急,我们带入一个实际的场景来看,以一个导出数据的案例来看,在做导出功能时,通常需要支持导出不同格式的数据文件,以此来做一个文件导出框架,通常对于导出框架,需要对导出内容和格式进行一个整体的约束:
- 导出的文件,不管什么格式,都分成3个部分,分别是文件头、文件体和文件尾。
- 在文件头部分,需要描述如下信息:分公司或门市点编号、导出数据的日期,对于文本格式,中间用逗号分隔。
- 在文件体部分,需要描述如下信息:表名称,然后分条描述数据。对于文本格式,表名称单独占一行,数据描述一行算一条数据,字段间用逗号分隔。
- 在文件尾部分,需要描述如下信息:输出人。
下面对以上需求基于建造者进行设计类图:
具体代码实现:
// 抽象构建者接口 interface Builder { void buildHeader(); void buildBody(); void buildFooter(); StringBuffer getResult(); } // 具体构建者:XML格式 class XmlBuilder implements Builder { private StringBuffer buffer; public XmlBuilder() { this.buffer = new StringBuffer(); } @Override public void buildHeader() { buffer.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"); buffer.append("<document>\n"); } @Override public void buildBody() { buffer.append(" <body>\n"); buffer.append(" <content>Example content</content>\n"); buffer.append(" </body>\n"); } @Override public void buildFooter() { buffer.append("</document>\n"); } @Override public StringBuffer getResult() { return buffer; } } // 具体构建者:TXT格式 class TxtBuilder implements Builder { private StringBuffer buffer; public TxtBuilder() { this.buffer = new StringBuffer(); } @Override public void buildHeader() { buffer.append("Document Content:\n"); } @Override public void buildBody() { buffer.append("Example content\n"); } @Override public void buildFooter() { buffer.append("End of Document\n"); } @Override public StringBuffer getResult() { return buffer; } } // 指挥者 class Director { private Builder builder; public Director(Builder builder) { this.builder = builder; } public void construct() { builder.buildHeader(); builder.buildBody(); builder.buildFooter(); } public StringBuffer getResult() { return builder.getResult(); } } // 客户端使用示例 public class BuilderPatternDemo { public static void main(String[] args) { Director director; // 创建XML格式的构建者 director = new Director(new XmlBuilder()); director.construct(); System.out.println("XML Format:\n" + director.getResult()); // 创建TXT格式的构建者 director = new Director(new TxtBuilder()); director.construct(); System.out.println("TXT Format:\n" + director.getResult()); } }
首先通过builder定义了构建文档所需的方法buildHeader
, buildBody
, buildFooter
和 getResult,
以两个类分别根据构建的文件特征实现了Builder
接口;最后通过指导者使用Builder
来构建文档,并在构建完成后获取最终的文档内容。
建造者模式的理解
建造者模式的主要功能是构建复杂的产品,而且是细化的、分步骤的构建产品,也就是建造者模式重在一步一步解决构造复杂对象的问题。如果仅仅这么认识建造者模式的功能是不够的。更为重要的是,这个构建的过程是统一的、固定不变的,变化的部分放到生成器部分了,只要配置不同的生成器,那么同样的构建过程,就能构建出不同的产品来。
再直白点说,建造者模式的重心在于分离构建算法和具体的构造实现,从而使得构建算法可以重用。具体的构造实现可以很方便地扩展和切换,从而可以灵活地组合来构造出不同的产品对象。
建造者模式的构成
要特别注意,建造者模式分成两个很重要的部分。
- 一个部分是 Builder 接口,这里是定义了如何构建各个部件,也就是知道每个部件功能如何实现,以及如何装配这些部件到产品中去;
- 另外一个部分是 Director,Director 是知道如何组合来构建产品,也就是说Director负责整体的构建算法,而且通常是分步骤地来执行。
不管如何变化,Builder 模式都存在这么两个部分,一个部分是部件构造和产品装配,另一个部分是整体构建的算法。认识这点是很重要的,因为在生成器模式中,强调的是固定整体构建的算法,而灵活扩展和切换部件的具体构造和产品装配的方式,所以要严格区分这两个部分。
在指导者(Director)实现整体构建算法时,遇到需要创建和组合的具体部件的时候,就会把这些功能通过委托,交给Builder去完成。
建造者模式的本质
建造者模式的本质是分离整体构建算法和部件构造。构建一个复杂的对象,本来就有构建的过程,以及构建过程中具体的实现。建造者模式就是用来分离这两个部分,从而使得程序结构更松散、扩展更容易、复用性更好,同时也会使得代码更清晰,意图更明确。
虽然在建造者模式的整体构建算法中,会一步一步引导Builder来构建对象,但这并不是说生成器主要就是用来实现分步骤构建对象的。建造者模式的重心还是在于分离整体构建算法和部件构造,而分步骤构建对象不过是整体构建算法的一个简单表现,或者说是一个附带产物。
何时使用建造者模式
- 如果创建对象的算法,应该独立于该对象的组成部分以及它们的装配方式时。
- 如果同一个构建过程有着不同的表示时(例如导出案例)。