设计模式——工厂模式

工厂模式类型

1、简单工厂模式
2、工厂方法模式
3、抽象工厂模式

面向接口编程

面向接口编程.png

面向接口编程
1、每个模块负责自己的职责(单一职责),各个模块之间通过接口隔离
2、每个模块都应该"承诺"自己对外暴露的接口是不变的。当模块内部发生变化时,其他模块是不需要知道的。这便是依赖于抽象而不依赖于实现(依赖倒置原则)
3、上层模块只知道下层模块暴露出的接口即可,至于实现细节不需要也不应该知道。

举个简单的例子来简单的说明上面三个条件。
三国时期,曹操和刘备各自有自己的军队。他们分别管理蜀国和魏国(职责单一)。各国之间只能通过外交官(接口隔离)进行传话。各国的外交官这个人对外是不变的,当有的小国换了君主,其他国家是不用知道的,有什么事只用找这个外交官商量即可(依赖抽象不依赖实现)。

概念

产品:类
抽象产品:抽象类、接口
产品簇:多个内在联系的产品(比如 小米手机 小米手环)
产品等级:
作者:可以理解为开发者 如:开发mybatis的为作者 文中也以服务器端来表示
用户:可以理解为使用者 如:使用mybatis的程序员为用户 文中也以客户端来表示

简单工厂模式

看如下一段代码

//抽象产品
interface Food{
    void eat();
}
// 具体产品
class Hamburger implements Food{
    @Override
    public void eat() {
        System.out.println("吃汉堡包!");
    }
}

/**==============================================================
 */
public class AppTest {
    public static void main(String[] args) {
        Food f = new Hamburger();
        f.eat();
    }
}

虚线以上为作者开发,虚线以下为用户调用。
如果有一天作者突然做出了修改将Hamburger变成了Hamburger2,此时用户就无法使用了。

interface Food{
    void eat();
}

class Hamburger2 implements Food{
    @Override
    public void eat() {
        System.out.println("吃汉堡包!");
    }
}

/**==============================================================
 */
public class AppTest {
    public static void main(String[] args) {
        //Cannot resolve symbol 'Hamburger'
        Food f = new Hamburger();
        f.eat();
    }
}

那么用户这边也需要跟着修改,因为耦合度太高。面向接口编程本身就是解耦的,用户只需要知道接口,至于你是hamburger1还是hamburger2不是用户所关心的。作者把细节暴露给用户就是违反了迪米特法则。
这种设计相当脆弱!为什么呢?因为,只要作者修改了具体产品的类名,那么客户端代码,也要随着一起改变。这样服务器端代码和客户端代码就是耦合的!我们希望的效果是,无论服务器端代码如何修改,客户端代码都应该不知道,不用修改客户端代码。

针对以上问题修改

使用简单工厂模式

class FoodFactory{
    public static Food getFood(int n){
        Food food =null;
        switch (n){
            case 1:
                food = new Hamburger();
                break;
        }
        return food;
    }
}

/**==============================================================
 */
public class AppTest {
    public static void main(String[] args) {
        Food f = FoodFactory.getFood(1);
        f.eat();
    }
}

分析
这样做的好处是如果作者将Hamburger改成Hamburger2或者Hamburger3,用户只用知道getFood(1)中的1对应的是Hamburger,而不用去管它是Hamburger2还是Hamburger3。面向接口编程说的不是java类中的interface。而是上层向下层暴露的接口,如上面代码中的静态方法getFood。这样就隔离了变化,也是符合依赖倒置原则,调用者不用依赖细节,而应该依赖抽象。

面对变化

此时用户觉得汉堡太腻了,想要再添加一个食物——面条呢?用户先得创建一个面条类

class Noddle implements Food{
    @Override
    public void eat() {
        System.out.println("吃面条");
    }
}

然后在简单工厂中添加一个case条件分支即可

class FoodFactory{
    public static Food getFood(int n){
        Food food =null;
        switch (n){
            case 1:
                food = new Hamburger();
                break;
                //添加面条的条件分支
            case 2:
                food = new Noddle();
                break;
        }
        return food;
    }
}

很显然,简单工厂是不利于拓展的,每次新加一个新的食物都需要去修改简单工厂中的switch语句,违背了“对修改关闭,对拓展开放”的原则。

类图展示

最终类图展示如下:


类图展示.png

用户不属于类图的范畴,这里加上只是为了方便理解,对比类图和面向接口编程的图就很容易理解了。

总结

【简单工厂】
优点
1、把具体产品的类型,从客户端代码中解耦出来 。
2、服务器端,如果修改了具体产品的类型,客户端也无影响
这便符合了“面向接口编程”的思想
缺点
1、客户端不得不死记硬背那些常量与具体产品的映射关系 比如:1对应Hamburger
2、如果客户端产品特别多,则简单工厂就会变得臃肿。比如有100个具体产品,则需要在简单的switch中写100多个case!
3、最重要的是,变化来了:客户端需要拓展具体产品的时候,势必需要修改简单工厂中的代码,这样便违背了“开闭原则”

工厂方法模式

为了使简单工厂更方便的拓展,在上面简单工厂的例子中升级成为工厂方法模式。首先,我们需要从代码层面去明确简单工厂为什么不方便做拓展,答案是因为它只有一个工厂,所有的食物都必须在这一个工厂内进行实例化,那就无法避免的要使用条件分支确定食物类型。
将简单工厂方式改为工厂方法方式
1、去掉简单工厂中的FoodFactory
2、创建一个工厂接口

interface FoodFactory {
    Food getFood();
}

3、创建生产汉堡包的工厂并实现接口

class HamburgerFactory implements FoodFactory{
    @Override
    public Food getFood() {
        return new Hamburger();
    }
}

4、调用

public class AppTest {
    public static void main(String[] args) {
        FoodFactory ff =new HamburgerFactory();
        Food f=ff.getFood();
        f.eat();
    }
}

吃汉堡包!
工厂方法创建工厂,工厂内部进行实例化。这样每新加一个食物的时候,就需要添加一个与之相关的工厂。
比如:添加大米Rice

class Rice implements Food{
    @Override
    public void eat() {
        System.out.println("吃大米");
    }
}
class RiceFactory implements FoodFactory{
    @Override
    public Food getFood() {
        return new Rice();
    }
}
public class AppTest {
    public static void main(String[] args) {
        FoodFactory ff =new RiceFactory();
        Food f=ff.getFood();
        f.eat();
    }
}

吃大米

这样就把简单工厂里的修改转变为拓展了。除此之外,在客户端调用时,也不用再去记hamburger对应什么,Noddle对应什么,Rice对应什么。直接见名知意的调用即可。

类图展示

工厂方法.png

总结

【工厂方法】
优点
1、仍然具有简单工厂的优点:服务器端修改了具体产品以后,客户端无影响
2、当客户端需要拓展一个新的产品时,不需要修改作者原来的代码,只是拓展一个新的工厂而已
问题
我们一开始创建简单工厂就是了让客户端感受不到服务器端的变化(即不论Hamburger1或Hamburger2客户端都不要去了解),现在如果服务器把RiceFactory改为了RiceFactory1或RiceFactory2,客户端仍然需要做相应的修改,这不是白忙活了吗?
这里我们可以看面向接口编程中的第2条:

每个模块都应该"承诺"自己对外暴露的接口是不变的。当模块内部发生变化时,其他模块是不需要知道的。这便是依赖于抽象而不依赖于实现(依赖倒置原则)

每个模块应该"承诺"对外暴露的接口是不变的。当然,如果确实需要改变的,那就只能同步进行修改。

缺点
1、每一个实体类都需要一个工厂类与之对应,这里的产品是Food,下一次是Drink呢,过多的产品等级将会导致类爆炸。

工厂方法模式.png

抽象工厂模式

当有过多产品等级时,使用抽象工厂模式会比工厂方法模式更适用。
此处普及一下产品等级和产品簇的概念


image.png

同样颜色的为产品簇,具有一定联系,如图中,红色的电器均属于华为。产品等级为同一类产品,电冰箱为一个产品等级,电视机为另外一个产品等级。如图中有5个产品等级。

以工厂方法类图为例
先创建基本的产品

//抽象产品
interface Drink{
    void drink();
}
interface Food{
    void eat();
}
// 产品
class Rice implements Food {
    @Override
    public void eat() {
        System.out.println("吃大米");
    }
}
class Hamburger implements Food {
    @Override
    public void eat() {
        System.out.println("吃汉堡包!");
    }
}
class Cola implements Drink {
    @Override
    public void drink() {
        System.out.println("喝可乐");
    }
}
class IcePeak implements Drink {
    @Override
    public void drink() {
        System.out.println("喝冰峰");
    }
}

①工厂方法模式

// 工厂相关
interface FoodFactory {
    Food getFood();
}
interface DrinkFactory{
    Drink getDrink();
}
class HamburgerFactory implements FoodFactory{
    @Override
    public Food getFood() {
        return new Hamburger();
    }
}

class RiceFactory implements FoodFactory{
    @Override
    public Food getFood() {
        return new Rice();
    }
}

class ColaFactory implements DrinkFactory{

    @Override
    public Drink getDrink() {
        return new Cola();
    }
}

class IcePeakFactory implements DrinkFactory {

    @Override
    public Drink getDrink() {
        return new IcePeak();
    }
}

②抽象工厂模式

abstract class Factory{
    abstract Food getFood();
    abstract Drink getDrink();
}

class KFCfFactory extends Factory{
    @Override
    Food getFood() {
        return new Hamburger();
    }
    @Override
    Drink getDrink() {
        return new Cola();
    }
}

class QiansionFactory extends Factory{

    @Override
    Food getFood() {
        return new Rice();
    }

    @Override
    Drink getDrink() {
        return new IcePeak();
    }
}

从对比中就能看出在产品等级多的情况下,抽象工厂的类更少,或者说它本身就很契合于多产品等级的。在工厂方法模式中,每一个产品都有自己的工厂——大米工厂、汉堡工厂、可乐工厂、冰峰工厂。但是这些工厂都有一些特点,大米和汉堡工厂都是食物,可乐和冰峰工厂都是饮品。食物和饮品这就是两个产品等级了,那么生产食物和饮品工厂既不能叫做食物工厂也不能叫做饮品工厂,所以可以用抽象工厂来指代,这个抽象工厂技能生产食物也能生产饮品。

类图展示

抽象工厂.png

可以看出,如果一个工厂里只生产一个产品等级(如:getFood()),那就是工厂方法,如果一个工厂里生产多个产品等级(如:getFood(),getDrink())那就是抽象工厂。
拓展性(增加产品簇)
新增一类食品(HotDryNoodles,热干面)和饮品(eggnog,蛋酒)

class HotDryNoodles implements Food{
    @Override
    public void eat() {
        System.out.println("吃热干面");
    }
}
class Eggnog implements Drink{
    @Override
    public void drink() {
        System.out.println("喝蛋酒");
    }
}
class WHFactory extends Factory {
    @Override
    Food getFood() {
        return new HotDryNoodles();
    }
    @Override
    Drink getDrink() {
        return new Eggnog();
    }
}
class diet{
    public static void taste(Factory ff){
        Food f = ff.getFood();
        Drink d = ff.getDrink();
        System.out.println("品尝:");
        f.eat();
        d.drink();
    }
}
public class AppTest {
    public static void main(String[] args) {
        Factory ff = new WHFactory();
        diet.taste(ff);
    }
}

品尝:
吃热干面
喝蛋酒

总结

【抽象工厂】
优点
1、仍然有简单工厂和工厂方法的特点
2、更重要的是,抽象工厂把工厂类的数量减少了!无论多少个产品等级,工厂就一套
问题
1.为什么KFCFactory中,就必须是Humburger搭配Cola呢?为什么不能是Rice搭配Cola呢?
抽象工厂中,可以生产多个产品,但这多个产品之间,必须有内在联系。即必须为产品簇
缺点
1、当产品等级发生变化时(增加产品等级、删除产品等级),都要引起所有工厂代码的修改,这就违反了“开闭原则”。

三种工厂模式场景比较

当产品不需要扩充时,使用简单工厂
当产品等级只有一个的时候,使用工厂方法
当产品等级比较固定时,使用抽象工厂。

工厂模式应用

Calendar
Calendar是java的日历类,是简单工厂的代表。

Calendar calendar = Calendar.getInstance();

关注getInstance()方法中的createCalendar方法


image.png

这里截取createCalendar关键部分


image.png

可以说,这很简单工厂了。

LoggerFactory
log4j是我们平常经常使用的日志工具,其中的日志工厂就是用工厂方法模式来实现的。此处产品等级只有一个,就是Logger。

        Logger logger = LoggerFactory.getLogger("logger");

可以关注这个getLogger的方法中的getILoggerFactory

image.png

根据条件的不同得到不同的工厂。
image.png

BeanFactory
spring的核心Bean工厂自然是不能的缺席工厂模式了。
image.png

从图中可以看出beanFactory里存在多个返回值,可以确定它的产品等级是多个的。
image.png

而每一个接口的实现最少都有5个实现类,这5个实现类可以看成5个工厂,每个工厂都会实现里面的方法形成一系列的产品簇,是典型的抽象工厂模式。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,444评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,421评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,363评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,460评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,502评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,511评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,280评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,736评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,014评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,190评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,848评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,531评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,159评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,411评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,067评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,078评论 2 352

推荐阅读更多精彩内容