精雕细琢——全方位解析工厂模式
工厂模式是面向对象设计模式中非常重要,非常流行的模式,是应该首先被理解透彻的模式。
我们讲对象的相关职责包括:
- 对象本身的职责(数据和行为)
- 创建对象的职责
- 使用对象的职责
而对象的创建在Java中有四种方式:
- new
- 反射
- clone()
- 工厂类创建
工厂模式是创建型设计模式
程序员的敏感地带:
- 大量的重复性代码,大量的if...else...语句
- 一个类过于复杂,违反了“单一职责原则”
- 如果有扩展会引发修改,违反了“开闭原则”
- 使用对象时不要用new来创建对象,耦合度高
我们先描述一个场景,某产品展销会上,用户甲需要了解多种产品的详细情况,他需要自己去根据每个产品名字在地图上查找其陈列位置,然后跑过去挨家看。而此时,待在家里的用户乙也想了解一下这个展销会里面的产品,他给举办此次展销会的厂商丙打了个电话,厂商接待人员说,“你想了解哪个产品,把名字告诉我,我直接就告诉那个产品的情况。”
- 用户甲自己new完成了产品实例的查找及创建,然后调取了产品的内部详细信息。总共只有一个类,丝毫没有设计可言,所有代码堆砌在这个类中,维护性,灵活性,扩展性,复用性全为0。
- 用户乙通过工厂类丙创建的方式获得了产品的实例,然后调取了产品的内部详细信息。
上面提到了对象的三种职责,根据“单一职责原则”。
两个类A和B之间的关系应该仅仅是A创建B或者A使用B,而不能是两种都有。
工厂模式就是要增加一层厂商丙来统一管理对象的创建职责,而不是让对象实例化的代码掺杂在多个类中到处都是。
厂商丙的多种实现方式之一:简单工厂模式
定义一个工厂类,增加一个静态方法(所以也叫静态工厂模式),传入不同的参数,内部根据这个参数进行判断,返回不同类的实例,这些被创建实例的类通常都有共同的父类。我们看一下在简单工厂模式中,用户是如何获取一个实例化对象的。
class User {
public static void main(String args[]) {
Book book1 = Factory.createBook("novel");
book1.read();
}
}
而工厂类的内容是:
class Factory {
public static Book createBook(String bookType) {
if("novel".equals(bookType){
return new NovelBook();
}else if("poem".equals(bookType){
return new PoemBook();
}else if("history".equals(bookType){
return new HistoryBook();
}else{
return null;
}
}
}
缺陷:
- 工厂类内部复杂,有大量if...else...判断
- 扩展要修改工厂类中的if...else...语句,违反“开闭原则”
厂商丙的多种实现方式之二:工厂方法模式
定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类。
我们看一下在工厂方法模式中,用户是如何获取一个实例化对象的。
class User {
public static void main(String args[]) {
Factory factory = new NovelBookFactory();
Book book1 = factory.createBook();
book1.read();
}
}
稍作解读,就是在简单工厂模式里的工厂类的基础上又抽象出一层,将其改为工厂接口Factory,声明一个工厂方法factoryMethod(),而具体产品要有对应的工厂类去实现Factory接口,并重写工厂方法返回对应的具体产品实例。将实例化时机又转移给了用户,用户来决定用哪个具体产品实例。
interface Factory {
Book creatBook();
}
class NovelBookFactory implement Factory{
Book creatBook(){
return new NovelBook();
}
}
这与前面提到的场景中,用户甲也是自己判断然后创建实例是截然不同的,因为工程方法只是让用户决定实例化哪个具体产品,而不会让他去创建,创建的工作还是交给工厂,所以在扩展的时候,当前工厂类,产品类都是不用改动的,只需要再增加一对具体工厂类和具体产品即可。
缺陷:
- 每次扩展需要增加一对具体工厂类和具体产品类,久而久之,会造成程序中类的数量爆炸。
问题:我们为什么不这样写?
class User {
public static void main(String args[]) {
Book book1 = new NovelBook();
book1.read();
}
}
看上去与上面的工厂方法模式的使用没有任何区别,扩展的时候,也只是需要新增一个子类即可,符合“开闭原则”,但是,这样写是不是就违反了“单一职责原则”呢?我们希望能将具体对象的创建职责通过工厂去管理起来,使程序的可读性更高,也降低类之间的耦合度,不在使用对象时直接new其实例。
厂商丙的多种实现方式之三:抽象工厂模式
提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
接着上面的代码改造一下。
class User {
public static void main(String args[]) {
Factory factory = new LuXunBookFactory();
Prose prose1 = factory.creatProseBook();
Essay essay1 = factory.creatEssayBook();
Novel novel1 = factory.creatNovelBook();
prose1.read();
essay1.read();
novel1.read();
}
}
class LuXunBookFactory implement Factory{
Prose creatProseBook(){//散文
return new ProseBook();
}
Essay creatEssayBook(){//杂文
return new EssayBook();
}
Novel creatNovelBook(){
return new NovelBook();
}
}
相较于工厂方法模式,抽象工厂模式将具体工厂类中的具体产品类进行了组合的关联。如上面代码所示,用户要得到的是鲁迅全集的实例,鲁迅全集工厂类就要包括散文,小说和杂文,而这些都是书的子类。如果是工厂方法模式的话,恐怕就要每种书记形式都要建一个自己的工厂类,那实在是庞大至极。程序变得很冗长。所以抽象工厂模式在具体子类中具备一定的关联关系的时候,非常好用。
缺陷:
- 抽象工厂模式由于将系统又分出一层,利用工厂去管理归并后的“鲁迅全集”,而不是单独管理每个具体类,当扩展时,再出个也是包括散文、小说和杂文的“老舍全集”还好说,只要新增一个老舍全集的类,和一个老舍全集工厂类就好。但是如果“老舍全集”多了诗歌,评论等,那就灾难了,要去修改工厂基类Factory的方法声明,这就违反了“开闭原则”。抽象工厂模式在你确定使用它的那一刻,就要十分确定这个工厂类内部子类的结构是稳定的,不会改变的,否则就不要使用抽象工厂,去用工厂方法模式或许更适合。
我们看到了上面用户在使用工厂模式创建实例时,避免了直接new具体对象实例的方式,但是能否把new LuXunBookFactory()也避免了呢?进一步解耦?
答案是肯定的。
使用反射加配置文件代替new LuXunBookFactory()
我们在创建一个实例的时候,能否直接用字符串本身当做参数来创建对象呢?使用反射就可以达到这个目的。
Factory factory = (Factory) Class.forName("LuXunBookFactory").newInstance();
这样就代替了
Factory factory = new LuXunBookFactory();
那么如果要完全符合“开闭原则”,即用户使用的部分也不去修改,那么就可以采用配置文件的方式。
将字符串"LuXunBookFactory"保存在xml配置文件中。
<?xml version="1.0"?>
<config>
<className>LuXunBookFactory</className>
</config>
每次利用Java工具XMLUtil类去读取该配置文件,用变量代替原代码中字符串的位置。
工厂方法的隐藏
我们将上面的工厂方法模式用户操作部分粘贴过来看一下:
class User {
public static void main(String args[]) {
Factory factory = new NovelBookFactory();
Book book1 = factory.createBook();
book1.read();
}
}
interface Factory {
Book creatBook();
}
class NovelBookFactory implement Factory{
Book creatBook(){
return new NovelBook();
}
}
我们发现Book中的read方法应该是直接在基类中声明,所有Book子类去重写具体book方法。那么在用户使用时,这个book方法的调用方法就可以抽象到Factory中去。
当前Factory是接口,接口是无法写方法体的,因此要改为abstract class。
abstract class Factory{
abstract Book creatBook();
public void read(){
Book book = this.creatBook();
book.read();
}
}
那么此时,用户在操作时就可以改为
class User {
public static void main(String args[]) {
Factory factory = new NovelBookFactory();
factory.read();
}
}
具体产品工厂类NovelBookFactory中并不需要去管藏方法的事,藏方法一定是在abstract class中放一个具体方法,用于获取当前抽象方法创建的具体子类实例后,直接调用其操作方法。
这样一来,在外部操作的时候,直接用Factory对象就可以调用具体子类里面的操作方法。
三种模式的适用场景
- 简单工厂模式之所以没有被官方收入,是因为它只是用来学习入门工厂模式的,在实际工作中,很少被使用。
- 工厂方法模式更适合那种子类之间并没有直接关联关系的结构,没办法,只能每种产品建立一个独立的工厂去创建。
- 抽象工厂模式就适合那种子类之间有明显的关联关系的结构,将他们拆分归并,仅给一批关联在一起的子类建立一个独立的工厂,它可以批量生产出这批子类的所有实例。