在面试中,候选人经常会被问到,你在项目里用到过哪些设计模式?对此,你可以按本文给出的步骤,系统地通过工厂模式展示自己在设计思想方面的能力。
工厂模式(Factory Method)是用来向使用者屏蔽创建对象的细节。之前我们在讲SAX解析XML文件时,已经用到过工厂模式,当时我们是通过如下代码用SAXParserFacotry这个工厂对象来创建用于解析的parse对象,代码如下所示。
1 SAXParserFactory factory = SAXParserFactory.newInstance();
2 SAXParser parser = factory.newSAXParser();
作为使用者,我们只要能得到parser对象进行后继的解析动作,至于parser对象是如何创建的,我们不需要,也不应管。如果不用工厂模式,那么我们还得亲自关注如何创建parser对象,比如得考虑创建时传入的参数,以及是否改用“池”的方式来创建从而提升效率。
这样亲力亲为的后果是,会让使用和创建parser对象的代码耦合度很高,这样一旦创建parser的方法发生改变,比如日后需要传入不同的参数,那么使用parser的代码也需要对应修改。
大家别以为增加修改量没什么大不了,如果我们在某个模块里修改了代码,哪怕这个修改点再小,也得经过完整的测试才能把这段代码放入生产环境,这是需要工作量的。如果我们把“使用”和“创建”对象放在一个模块里,那么“使用”部分的代码也得测试(虽然没改),但我们通过了工厂模式分离了两者,那么只需要测“创建”模块,就可以减少工作量了。
下面我们先来看下工厂模式的实现代码,比如我们要编写(创建)Java和数据库方面的两本书,先在第1行构建一个Book的基类,在第4和第7行创建两个子类,而且我们可以把一些通用性的方法(比如“查资料”)放入Book类里。
1 class Book {
2 public book(){ }
3 }
4 public class JavaBook extends Book {
5 public JavaBook(){System.out.println("Write Java Book");}
6 }
7 public class DBBook extends Book{
8 public DBBook(){System.out.println("Write DB Book"); }
9 }
随后我们通过如第10行的接口来定义创建动作,根据需求,我们可以在第11和17行实现这个接口,在其中分别实现“编写Java书”和“编写数据库书”的代码。
10 interface BookFactory { Book createBook(); }
11 public class JavaFactory implements BookFactory{
12 public JavaBook createBook(){
13 //省略其它编写Java书的代码
14 return new JavaBook();
15 }
16 }
17 public class DBFactory implements BookFactory{
18 public DBBook createBook() {
19 //省略其它编写数据库书的代码
20 return new DBBook();
21 }
22 }
在上述代码里,我们提供了“创建”的方法,下面我们给出了“调用”的代码,从第2和第4行的代码中我们能看到,这里外部对象可以通过两种不同的createBook方法分别得到Java和数据库书。
1 BookFactory javaFactory = new JavaFactory ();
2 JavaBook javaBook = javaFactory.createBook();
3 BookFactory dbFactory = new DBFactory ();
4 DBBook dbBook = dbFactory.createBook();
大家在通过上文,举例讲清楚工厂模式后,可以立即说出这个结论。具体举例如下。
在上述的案例中,如果遇到新需求,需要再创建C语言的书,首先可以在Book父类下再创建一个CBook子类,随后可以在BookFactory接口下再创建一个新的工厂来创建,代码如下。
1 public class CBook extends Book { //构建一个新的类
2 public CBook(){System.out.println("Write C Book");}
3 }
4 public class CFactory implements BookFactory{
5 public CBook createBook() {
6 //省略其它写C语言书的代码
7 return new CBook();
8 }
9 }
对于这个修改需求,我们并没有修改原有的创建Java和数据库书籍相关的代码,而是通过添加新的模块来实现,这种做法很好地符合了“开闭原则”。
开闭原则(Open Closed Principle,也叫OCP)和设计模式无关,它是一种设计架构的原则,其核心思想是,系统(或模块或方法)应当对扩展开放,对修改关闭,比如对于上述案例,遇到扩展了,我们没有修改现有代码,从而可以避免测试不相干的模块。
我们就用简单工厂为例,来看下没采用开闭原则的后果,比如我们还是要创建Java和数据库方面的书,那么是在一个方法里根据参数的不同来返回不同种的类型。
1 public class BookFactory {
2 public Book create(String type) {
3 switch (type) {
4 case "Java": return new JavaBook();
5 case "DB":return new DBBook();
6 //要扩展的话,只能加在这里
7 case "C":return new CBook();
8 default: return null;
9 }
10 }
11 }
如果要加新类型的书,只能是新加一个case,一旦有修改,那么我们得改动第2行的create方法,这样一来,create方法(乃至BookFactory类)对修改就不关闭了。如果大家对此不理解,可以回顾下工厂模式的案例,当时遇到这个需求,我们是通过添加CFactory类来实现的,原来的BookFactory和DBFactory并没有改动(它们对修改关闭了)。
对比一下两者的差别,由于简单工厂模式没遵循开闭原则,那么一旦添加C语言的书籍,那么就影响到其它不相干的Java和DB书籍了(这两部分的case代码也得随之测试),这也是为什么简单工厂模式适用场景比较少的原因。
抽象工厂是对一般工厂模式的扩展,比如我们在写java和数据库方面的书籍时,需要添加录制讲解视频的方法,也就是说,在Java书和数据库书这两个产品里,我们不仅要包含文稿,还得包含视频。
具体到生产Java书和数据库书的这两个工厂里,我们要生产多类产品,不仅得包括文稿,还得包括代码,此时就可以使用抽象模式,示例代码如下。
1 class Video { //视频的基类
2 public Video(){ }
3 }
4 public class JavaVideo extends Video { 省略定义动作 }
5 public class DBBook extends Video { 省略定义动作 }
在第1行里,我们创建了视频的基类,在第4和第5行里,创建了针对Java和数据库书视频的两个类。
6 abstract class CreateBook{ //抽象工厂
7 public abstract Book createBook();//编写文稿
8 public abstract Book createVideo();//录制视频
9 }
10 //具体创建java书的工厂
11 class CreateJavaBook extends CreateBook{
12 public JavaBook createBook() {省略编写文稿的具体动作}
13 public JavaVideo createVideo() {省略录制视频的具体动作}
14 }
15 //具体创建数据库书的工厂
16 class CreateDBBook extends CreateBook{
17 public DBBook createBook() {省略编写文稿的具体动作}
18 public DBVideo createVideo() {省略录制视频的具体动作}
19 }
在第6行里,我们定义了一个抽象工厂,在其中定义了创建视频和书籍的两个方法,在第11和16行,我们通过继承这个抽象工厂,实现了生产两个具体Java和数据库书籍的工厂。
和一般工厂相比,抽象工厂的顶层类一般是抽象类(也就是抽象工厂名称的来源),但和一般工厂模式相比,没有优劣之分,只看哪种模式更能适应需求。比如要在同一类产品(比如书)里生产多个子产品(比如文稿和视频),那么就可以通过抽象工厂模式,而如果需要生产的产品里只有主部件(比如文稿),而不需要附属产品(比如视频),那么就可以用一般工厂模式。
建造者模式和工厂模式都是关注于“创建对象”,在面试时,我们一般会问它们的差别。通过工厂模式,我们一般都是创建一个(或一类)产品,而不关心产品的组成部分,建造者模式也是用来创建一个产品,但它不仅创建产品,更专注这个产品的组件和组成过程。
通过下面的代码,我们来看下建造者模式的用法,大家可以对比下建造者和工厂模式的差别。
1 //定义一个待生产的产品,比如带视频讲解的书
2 public class BookwithVideo {
3 //其中包括了稿件和视频两个组件
4 Book PaperBook;
5 Video Video;
6 }
7 //定义一个抽象的建造者
8 public abstract class Builder {
9 public abstract Book createPaperBook();//编写稿件
10 public abstract Video createVideo();//录制视频
11 }
12 //定义一个具体的建造者,用来创建Java书
13 public class JavaBookProduct extends Builder {
14 private BookwithVideo bookWithVideo = new BookwithVideo();
15 //通过这个方法返回组装后的书(稿件加视频)
16 public BookWithVideo getBook(){return bookWithVideo;}
17 //编写稿件
18 public void setPaperBook() {
19 //创造Java文稿,并赋予javaBook对象
20 bookWithVideo.book = javaBook;
21 }
22 //录制视频
23 public void setVideo() {
24 录制Java书的视频,并赋予javaVideo对象
25 bookWithVideo.video = javaVideo;
26 }
27 }
28 //定义一个具体的数据库书籍的建造者
29 public class DBBookProduct extends Builder {
30 private BookwithVideo bookWithVideo = new BookwithVideo();
31 //通过这个方法返回组装后的书(稿件加视频)
32 public BookWithVideo getBook(){return bookWithVideo;}
33 //纸质书
34 public void setPaperBook() {
35 写数据库书的文稿,并赋予dbBook对象
36 bookWithVideo.book = dbBook;
37 }
38 //录制视频
39 public void setVideo() {
40 录制数据库书的视频,并赋予dbVideo对象
41 bookWithVideo.video = dbVideo;
42 }
43 }
在第8行里,我们定义了一个抽象的创造者类Builder,在第13和29这两行里,我们通过继承Builder这个创造者类创建了两个实体创造者,分别用来创造Java和数据库的书籍。
在每一个创造者里,我们通过了setPaperBook方法创造文稿,通过setVideo创建视频,并把创造好的文稿和视频分别赋予bookWithVideo对象里的两个文稿和视频组件。
看到这里,似乎和工厂模式差不多,由于建造者模式会偏重于组件的创建过程,所以会通过如下的总控类来组装对象,而工厂模式偏重于“创建产品“的这个结果,而不关注产品中组装各组件的过程,所以一般不会有总控类。
44 //总控类
45 public class Director {
46 void productBook(Builder builder){
47 builder.setPaperBook();
48 builder.setVideo();
49 }
50 }
在总控类里的第46行里,我们定义了用来创建书的productBook方法,请注意这个方法是抽象的builder类,通过下面的代码,我们能看到如何通过上述定义的总控类和建造者类来动态地创建不同种类的对象。
1 Director director = new Director();
2 Builder javaBookBuild = new JavaBookProduct();
3 Builder dbBookBuilder = new DBBookProduct();
4 director.productBook(javaBookBuild);
5 director.productBook(dbBookBuilder);
在第1行里,我们定义了一个总控类,在第2和第3行里,我们定义了具体的创建Java和数据库书籍的建造者对象,在第4和第5行里,分别传入了javaBookBuilder和dbBookBuilder这两个建造者对象,这样在总控类的productBook方法里,会根据传入参数类型的不同,分别建造java和数据库书籍。
我们经常通过建造者模式来创建项目里的业务对象,所以候选人在他们的项目里一般都会用到这种模式,在面试中也经常听到候选人用这种模式来举例,这里列一种比较好的回答。
第一,这位候选人用电商平台的订单来举例,首先他创建了一个订单的基类,在其中包括了商品列表、总价钱、总积分和发货地址这四个组件。
第二,通过继承这个订单基类,创建了两类订单,分别是“一般用户的订单”和“VIP客户的订单”,它们的算总价和算总积分的业务逻辑是不同的。
第三,定义了一个抽象的建造者对象,在其中定义了诸如“统计商品”和“算总价”等的方法。
第四,通过继承抽象的建造者,定义了两个具体的建造者,分别用来建造“一般订单”和“VIP订单”,在每个具体的建造者对象里,创建商品列表、总价钱、总积分和发货地址,并把它们组装成一个订单。
第五,也是关键点,需要创建一个总控类(这也是建造者模式的核心,也是和工厂模式的差别点),在其中提供一个productOrder(Builder builder)方法,它的参数是抽象的建造者。
至此构造了建造者模式的全部代码,在需要创建订单时,则可以通过productOrder(VIP订单的建造者对象)的调用方式,通过传入的具体的建造者对象(不是抽象的建造者对象)来完成建造。
上述的叙述是给大家做个参考,其实根据实际的项目需求叙述建造者模式并不困难,一般来说,很多面试官会多问句,建造者模式和工厂模式有什么差别?这在前文里也说过了,大家可以通过项目需求详细说明。