工厂模式

概念

工厂模式是非常常用的一种设计模式,属于创建型设计模式一种。它主要解决代码中条件的判断吧和对象创建以及对象使用高度耦合问题。一旦模块功能频繁的往代码里扩展,高度耦合会增加代码的复杂度,系统会变导致的结构不清晰,难以维护,也不利于扩展。

工厂模式分为三类:

  • 简单工厂模式
  • 工厂方法模式
  • 抽象工厂模式

其中简单工厂模式在GoF的《设计模式》一书中归为工厂模式中的一种特例,与工厂模式是一类的。但看到更多的分类是把简单工厂模式分为工厂模式中的一种。

简单工厂模式

简单工厂模式又称静态工厂方法模式,因为其中创建对象的方法是静态的。

简单工厂模式应用非常广泛,我们拿电视机举例:

电视机的平牌有很多,比如华为,小米,索尼等,他们都可以播放电视台的节目。

/**
 * 电视机基类
 */
public abstract class Tv {
    /**
     * 播放电视台节目
     * @param tvStationId 电视台id
     */
    public abstract void play(int tvStationId);
}

/**
 * 华为电视机
 */
public class HuaweiTv extends Tv {
    @Override
    public void play(int tvStationId) {
        System.out.println("华为电视机播放电视台" + tvStationId + "频道的节目");
    }
}

/**
 * 小米电视机
 */
public class XiaomiTv extends Tv {
    @Override
    public void play(int tvStationId) {
        System.out.println("小米电视机播放电视台" + tvStationId + "频道的节目");
    }
}

/**
 * sony电视机
 */
public class SonyTv extends Tv {
    @Override
    public void play(int tvStationId) {
        System.out.println("索尼电视机播放电视台" + tvStationId + "频道的节目");
    }
}

因为一开始项目中的对象只有一个,但是随着需求的增加,最后结构可能会变成这样:

public void playTvStation(int tvStationId, String brand) {
    Tv tv = null;
    switch (brand) {
      case "Huawei":
        tv = new HuaweiTv();
        break;
      case "Xiaomi":
        tv = new XiaomiTv();
        break;
      case "Sony":
        tv = new SonyTv();
        break;
      default:
        throw new RuntimeException("不支持的品牌");
    }
    tv.play(tvStationId);
}

如果碰到像举例一样比较简单,并且不会再频繁扩展新的功能了,那么我觉得向上面的代码一样,用if...else或者switch...case就可以了。

但是要是频繁增加新功能,我们可以再创建一个工厂类,把条件的判断和对象的创建放到这个工厂类里面。

public class TvFactory {

    public static Tv getTv(String brand) {
        Tv result = null;
        switch (brand) {
            case "Huawei":
                result = new HuaweiTv();
                break;
            case "Xiaomi":
                result = new XiaomiTv();
                break;
            case "Sony":
                result = new SonyTv();
                break;
            default:
                throw new RuntimeException("不支持的品牌");
        }
        return result;
    }
}

有了工厂类后,我们的主程序就只有如下两句代码,并且就算扩展新的产品,也不会修改原有代码:

public void playTvStation(int tvStationId, String brand) {
    Tv tv = TvFactory.getTv(brand);
    tv.play(tvStationId);
}

如果不想用switch...case或if...else,可以采用第二种形式,由Map进行管理子类,不过这样获取到的对象是单例

public class TvFactory {

    private static final Map<String, Tv> TV_MAP = new HashMap<>();

    static {
        TV_MAP.put("Huawei", new HuaweiTv());
        TV_MAP.put("Xiaomi", new XiaomiTv());
        TV_MAP.put("Sony", new SonyTv());
    }

    public static Tv getTv(String brand) {
        if (ObjectUtils.isEmpty(brand)) {
            return null;
        }
        return TV_MAP.get(brand);
    }
}

优点

  1. 调用者不需要知道是哪个类,具体的实现,只要知道接口就行,而接口一般都是通用的。
  2. 把条件判断和对象创建以及对象使用解耦,增加了代码的可读性,也易于扩展。

缺点

当我们扩展新产品时,虽不用修改playTvStation客户端主程序接口的代码,但是我们需要修改工厂类代码,一定程度违反了开闭原则。

工厂方法模式

工厂方法模式在一定程度上解决了简单工厂不符合开闭原则问题。

工厂方法模式利用多态的思想,把创建对象的任务放到了子类工厂。

public interface ITvFactory {
    Tv getTv();
}

public class HuaweiTvFactory implements ITvFactory {
    @Override
    public Tv getTv() {
        return new HuaweiTv();
    }
}

public class XiaomiTvFactory implements ITvFactory {
    @Override
    public Tv getTv() {
        return new XiaomiTv();
    }
}

public class SonyTvFactory implements ITvFactory {
    @Override
    public Tv getTv() {
        return new SonyTv();
    }
}

我们再为多个工厂子类创建一个简单工厂类,来管理工厂类的创建。

public class TvFactoryMap {
    private static final Map<String, ITvFactory> TV_FACTORIES = new HashMap<>();

    static {
        TV_FACTORIES.put("Huawei", new HuaweiTvFactory());
        TV_FACTORIES.put("Xiaomi", new XiaomiTvFactory());
        TV_FACTORIES.put("Sony", new SonyTvFactory());
    }

    public static ITvFactory getTvFactory(String brand) {
        if (ObjectUtils.isEmpty(brand)) {
            return null;
        }
        return TV_FACTORIES.get(brand);
    }
}

优点

  1. 调用者不需要知道是哪个类,具体的实现,只要知道接口就行,而接口一般都是通用的。
  2. 扩展不再需要修改工厂类,一定程度解决了简单工厂模式不符合开闭原则的问题。

缺点

  1. 虽然扩展不需要修改工厂类,但是需要修改管理工厂子类的简单工厂类。看到很多文章说是符合开闭原则,我并不觉得这是完全符合的。
  2. 当想增加新的产品时,需要多增加一个工厂类,但频繁扩展新功能时,类会成倍增加,系统会变得复杂,维护起来会变得麻烦。

简单工厂模式与工厂方法模式适用场合

简单工厂模式是把条件判断和创建对象写在一个方法里的,所以:

  • 如果是举例一样简单的业务,创建对象只需要new,不需要考虑装配组合其他对象,考虑使用简单工厂模式(因为工厂类方法不会很长,并且只有new,比较清晰)。

  • 如果是复杂的创建对象,需要装配和组合其他对象,那么考虑使用工厂方法模式(用简单工厂模式方法会很长,并且因为有组合其他对象的代码,结构会比较复杂)。

抽象工厂模式

抽象工厂模式一定程度上解决了工厂方法模式增加新产品时类成倍增加的问题,但是抽象工厂模式应用比较特殊,不像前两者这么宽泛。

抽象工厂模式应用于系统中有多个产品族,但是每次只使用一个情况。

这里有个新的概念:产品族。

产品组指位于不同产品等级结构中,功能相关的产品组成的家族。

比如以屏幕分类,3个品牌的电视分别都有LCD和OLED屏幕的产品,那么不同品牌的LCD屏幕电视机分为一个产品族,不同品牌的OLED屏幕电视机也分为一个产品族。

按照这个想法划分就会有下面的6个类。

// LCD屏幕电视机:
interface ILcdTv;
class HuaweiLcdTv;
class XiaomiLcdTv;
class SonyLcdTv;

// OLED屏幕电视机:
interface IOledTv;
class HuaweiOledTv;
class XiaomiOledTv;
class SonyOledTv;

如果按照工厂方法的思路,我们给每个类增加一个工厂类。如果想增加一个品牌的电视机,比如LG电视机,那么要继续增加两个工厂类,因为LG也大概率生产LCD和OLED屏幕的电视机。这也是上面工厂方法模式的缺点,增加过多的类就会增加系统复杂度,维护的时候也会需要耗更多时间。

因此我们按照抽象工厂模式的思路,让一个工厂类创建多个对象,这样几个工厂类可以合一,类的数量就会减少。·

public interface ITvFactory {
  ILcdTv createLcdTv();
  IOledTv createOledTv();
}

public class HuaweiTvFactory {
  @Override
  public ILcdTv createLcdTv() {
    return new HuaweiLcdTv();
  }
  
  @Override
  public IOledTv createOledTv() {
    return new HuaweiOledTv();
  }
}

// ...省略XiaomiTvFactory和SonyTvFactory

当我们想增加一个新品牌的时候只要增加一个工厂就行了,维护起来也会更加的方便。

优点

  1. 同样调用者不需要知道具体类的实现,只知道接口就行
  2. 一定程度解决上解决了工厂方法模式新增产品导致类过度增加的问题。

缺点

  1. 因为有新的产品族概念,类的结构要比工厂方法模式复杂得多。
  2. 虽然解决了工厂方法模式新增产品导致类过度增加的问题。但是抽象工厂模式如果新增一个产品族,比如LED屏幕电视,这样的话新写一个接口ILedTv ,然后新写三个他的子类HuaweiLedTv,XiaomiLedTv,SonyLedTv,并且需要在接口ITvFactory中扩展一个新的方法createLedTv() ,工厂子类也需要增加相应的实现,这实在不能说是方便。

用简单工厂改进抽象工厂

上面的抽象工厂的缺点指出增加产品族时并不方便,所有的工厂子类都要增加相应实现,这点我们可以依靠简单工厂来解决。

public class TvFactory {

    public static ILcdTv createLcdTv(String brand) {
        ILcdTv result = null;
        switch (brand) {
            case "Huawei":
                result = new HuaweiLcdTv();
                break;
            case "Xiaomi":
                result = new XiaomiLcdTv();
                break;
            case "Sony":
                result = new SonyLcdTv();
                break;
            default:
                throw new RuntimeException("不支持的品牌");
        }
        return result;
    }
  
  public static IOledTv createOledTv(String brand) {
        IOledTv result = null;
        switch (brand) {
            case "Huawei":
                result = new HuaweiOledTv();
                break;
            case "Xiaomi":
                result = new XiaomiOledTv();
                break;
            case "Sony":
                result = new SonyOledTv();
                break;
            default:
                throw new RuntimeException("不支持的品牌");
        }
        return result;
    }
}

这样当我们增加新的产品族时只需要工厂扩展一个新的方法就行。

但是既然回到了简单工厂模式,那么简单工厂模式关于开闭原则问题也来了。

所以三个工厂模式并不都是完美的,各有各的优缺点,我们也不必特别在意开闭原则,选择合适的就行。

关于开闭原则

在工厂方法模式中我认为工厂方法模式一定程度解决了简单工厂不符合开闭原则的问题,但没有完全解决,实际上抽象工厂我也觉得不完全符合开闭原则,因为只要有new来创建对象的存在,那么扩展势必会修改创建对象的代码。如果想完全解决,我觉得要依靠反射来取代new创建,这样扩展的时候只要修改配置文件,不用动原来代码。

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

推荐阅读更多精彩内容