分享

设计模式入门教程(二)

 夜猫速读 2022-05-05 发布于湖北

1、适配器模式

适配器在生活中无处不在,比如电脑的转接头、读卡器、电源转接头。他们的共同点就是接口标准不一样,需要通过适配器转换后才能使用。就拿读卡器来说,存储卡的接口只能适配相机或者手机的卡槽。而电脑普遍为 USB 接口。那么如何在电脑上使用存储卡呢?我们可以用读卡器,一头卡槽能够插入存储卡,另一头 USB 可以插在电脑上。通过适配器可以解决接口不兼容的问题。还有个例子就是电脑的变压器,电脑一般接收20V电压,但是我国电压是220V,因此就需要变压器做转换,如下图所示,进来是220V,出来被转为20V。变压器其实就是适配器。

1. 实现适配器模式

我们通过如下例子,来看看如何实现适配器模式。假如我们的电视机屏幕输出为 4K 画质,但播放器只能输出 2K 的画质,此时就需要一个适配器完成 2K 到 4K 的转换。代码如下:

只能输出 2k 信号的 player

public class Player {
    public TwoThousandSignal play() {
        return new TwoThousandSignal();
    }
}

我们定义一个更为现代的播放器的接口,输出 4K 信号:

public interface ModernPlayer {
    FourThousandSignal play();
}
代码块123

这个接口的实现就是一个适配器( adapter ),通过复用 Player 输出的 2K 信号,转化为 4K 信号,让支持 ModernPlayer 的设备来播放 2K 信号源。

public class ModernPlayerAdapter implements ModernPlayer {
    private Player player = new Player();
    @Override
    public FourThousandSignal play() {
        TwoThousandSignal twoThousandSignal = player.play();
        return convertToFourThousandSignal(twoThousandSignal);
    }
    private FourThousandSignal convertToFourThousandSignal(TwoThousandSignal twoThousandSignal) {
        //4k信号通过算法计算,从2k转换而来。省略转换逻辑,
        return new FourThousandSignal();
    }
}

电视机作为调用方,只需要使用 ModernPlayerAdapter 的实例就可以播放 2K 信号,代码如下:

public class Television {
    private ModernPlayer modernPlayer = new ModernPlayerAdapter();
    public void display(){
        modernPlayer.play();
    }
}
代码块1234567

看代码是不是很像代理模式?ModernPlayerAdapter 只是调用了Adaptee的方法,获得 2k 信号后转换为 4K 信号。区别在于 Player 并没有实现 ModernPlayer 接口。而代理模式,ProxyRealSubject 是都需要实现同一个接口的。Adapter 的作用是适配不同接口,两个接口的返回值是不同的,Adapter 中需要实现转换逻辑。

类图:

2. 适配器模式优点

  1. 不需要修改现有的接口和实现,就能复用已有的类;

  2. 灵活度高,可以在接口不变的情况下,兼容多种不同的类。

3. 适配器模式适用场景

  1. 想要使用一个已有的类,但是此类的接口并不符合使用方要;

  2. 多个类做的事情相同或者类似,但是各自接口又不同。调用方希望统一接口。

第一个场景可以认为是亡羊补牢。由于种种原因造成系统接口不同,但功能却类似。此时很可能我们不能直接修改已经存在的接口,我们只能通过适配器模式去适配这个接口。

第二个场景其实也很常见。比如我们开发一个比价网站,需要从不同网站抓取同类商品的价格,然后按照自己系统的数据结构保存。不同网站抓取到的数据肯定是不同的,可能字段名不一样,也可能数据结构都不同。但是最终都要保存为同样的数据结构,此时就需要适配器来做转换。

4. 小结

当我们面对难以改造,又想复用的对象时,可以考虑采用适配器模式。但切记一定不要滥用适配器。我们应该在最初设计程序的时候就考虑代码的可扩展性。而不是最后通过适配器来解决问题。能修改重构的,尽量去修改。实在不能修改的,比如外部系统的接口,我们就只能通过适配器模式来解决问题。

2、模版方法模式

我先问个问题,把大象放冰箱,总共分几步?你一定脱口而出:三步!第一步把冰箱门打开,第二步把大象放进去,第三部把冰箱门关上。这道题是不是太简单了?但当我们考虑细节的时候就没这么简单了。

假如你的冰箱门加了锁,那么第一步开门时就需要开锁。第二步把大象放进去也有细节要考虑。如果你的冰箱是卧式的,那么大象需要躺在里面。如果你的冰箱是立式的,那么大象可以站在里面。像这种主体逻辑一样,细节实现不同的场景,我们可以考虑使用模版方法模式

在软件建模中,把大象放冰箱是一个算法,我们将其定义为一个模版方法。模版方法用一系列抽象的操作定义一个算法。就像我们例子中的三步,打开冰箱、大象放进去、关上冰箱门。具体如何开门和关门,如何放大象进去,则在子类中实现。冰箱不同,采用的方式自然也不同。

模版方法中定义操作的方法和先后步骤。而真正的操作方法实现则在子类中。

1. 实现模版方法

我们看看用代码如何实现把大象放冰箱。为了便于理解,我们尽量少引入类,我们假设冰箱自身有个行为是把大象放进来

冰箱抽象类 Fridge

public abstract class Fridge {
    public void placeElephant(){
        System.out.println("开始装大象");
        openDoor();
        putElephant();
        closeDoor();
        System.out.println("结束");
    }
    public abstract void openDoor();
    public abstract void putElephant();
    public abstract void closeDoor();
}

Fridge 类中定义了一个模版方法 placeELephan, 里面按照顺序调用 openDoor、putELephant、closeDoor。这三个方法留待子类实现。

下面是两个具体的冰箱实现类代码。

立式冰箱类 VerticalFridge

public class VerticalFridge extends Fridge{
    @Override
    public void openDoor() {
        System.out.println("打开立式冰箱门");
    }
    @Override
    public void putElephant() {
        System.out.println("将大象站着放进去");
    }
    @Override
    public void closeDoor() {
        System.out.println("关上立式冰箱门");
    }
}

卧式冰箱 HorizontalFridge

public class HorizontalFridge extends Fridge{
    @Override
    public void openDoor() {
        System.out.println("打开卧式冰箱门");
    }
    @Override
    public void putElephant() {
        System.out.println("将大象躺着放进去");
    }
    @Override
    public void closeDoor() {
        System.out.println("关上卧式冰箱门");
    }
}

两个冰箱子类各自实现三个抽象方法。

客户端代码:

public class Client {
    public static void main(String[] args) {
        Fridge fridge;
        fridge = new HorizontalFridge();
        fridge.placeElephant();
        System.out.println("---------I'm a line-----------");
        fridge = new VerticalFridge();
        fridge.placeElephant();
    }
}

客户端代码中,两个不同的子类分表调用了placeElephant 方法,输出如下:

开始装大象
打开卧式冰箱门
将大象躺着放进去
关上卧式冰箱门
结束
---------I'm a line-----------
开始装大象
打开立式冰箱门
将大象站着放进去
关上立式冰箱门
结束
代码块1234567891011

两个子类开始和结束的步骤一样,中间步骤的顺序也一样。这些逻辑在父类中实现。每个步骤具体的逻辑则在子类中各自实现。

类图:

2. 模版方法优缺点

2.1 优点

分离了算法中变和不变的部分。不变的部分定义在父类的模版方法中。变的部分通过子类实现。不变的算法部分可以被充分复用。当变的部分有新需求时,可以定义新的子类。从而实现了开闭原则。

2.2 缺点

模版意味着死板,我们设定好模版就必须按照模版的一、二、三步来执行。如果我们想调换顺序,或者增加几步就很难做到。除非定义新的模版。或者很小心的改动已有模版,避免影响现有程序逻辑。但这已经违反了开闭原则。

3. 模版方法适用场景

如果我们发现一系列的算法,主干一样,只是在局部的实现上有区别。此时我们可以考虑使用模版方法。把算法主干及不变的部分提炼出来,在父类中实现。抽象出变化部分的方法,交由不同的子类自己去实现。

4.小结

一般来说我们都是在子类中调用父类的方法。而模版方法恰恰相反,是父类的方法中调用子类的实现。正是因为这样,我们才能把不变的行为抽象到父类中,变化的部分留给子类实现。此外还有一类方法叫做钩子方法,它不是抽象的方法,父类有其缺省实现,一般是空方法。但是子类也可以通过重写去覆盖。

3、装饰者模式

每天我们出门前,一定都会选择今天上衣穿什么,裤子穿什么,搭配什么鞋子,大衣穿什么。最后一定是做好选择,打扮好才会出门。这个过程其实就是装饰者模要做的事情 ---- 对一个对象增加额外的功能。

我们再看一个例子。我们都吃过煎饼,除了面饼之外,我们还要加鸡蛋、加葱花、香菜、面酱、辣酱。现在还有新花样,加辣条、加鸡柳。一切都始于一张面饼,摊煎饼的过程就是在不断对这张面饼添加新特性。

我们通过继承也可以为对象增加功能,比如我们有个煎饼的父类,默认已经有面饼、面酱、鸡蛋啊。那么我们可以派生出 全都放的普通煎饼、不辣的普通煎饼、不辣不放香菜的普通煎饼、不辣不放葱的普通煎饼、全都放的辣条煎饼、全都放的鸡柳煎饼…… 这只是很小一部分。通过继承的话,由于情况太多,会造成对象爆炸。

那我们还可以通过组合的方式来扩展类啊,比如煎饼对象中,我们可以设置不同属性,比如是否有葱、是否有香菜、是否有辣条、是否有鸡柳等等。这样看起来也能很好的解决摊煎饼的问题。但如果想要加肠、加油条怎么办?想要加两个鸡蛋怎么办?我们只能修改煎饼对象。这就违反了开闭原则。显然这样也是不够灵活的。

装饰者模式能够很好的解决对象的动态扩展,不管你想穿什么,都可以随便搭配。不过这个煎饼要怎么做,也都能随意的扩展支持,而不需要改已有的代码。接下来我们就来看看如何通过装饰者模式来摊煎饼的。

1. 实现装饰者模式

对于摊煎饼来说,我们都是对于一个基础的煎饼对象做装饰,比如我想要一套两个鸡蛋、有辣椒、葱、辣条的煎饼,那么我只需要先声明一个基本的煎饼对象,然后用加鸡蛋装饰类装饰它,然后再用加辣酱装饰类装饰它,再用加葱的装饰类装饰它,最后再用加辣条的装饰类装饰它。最终就得到了我想要的煎饼。不过请注意,不管你怎么装饰,最终得到的还是煎饼,并不是其他东西。

装饰者模式的核心思想是对已有的对象,一层一层的用装饰类去装饰它,扩展它的特性。这样做可以更为动态的为对象增加功能。我们看看代码如何实现:

先定义煎饼接口:

public interface Pancake {
    void cook();
}
代码块123

接口里只定义了一个制作方法。

煎饼接口的实现类:

public class BasicPancake implements Pancake {
    @Override
    public void cook() {
        System.out.println("加一勺面");
        System.out.println("加一个鸡蛋");
    }
}
代码块1234567

作为一个最基本的煎饼,总得有面,有鸡蛋吧。其他的材料留给装饰类来实现。

接下来我们定义装饰抽象类:

public abstract class PancakeDecorator implements Pancake {
    protected Pancake pancake;
    public void setPancake(Pancake pancake) {
        this.pancake = pancake;
    }
    public void cook() {
        if (pancake != null) {
            pancake.cook();
        }
    }
}

可以看到 PancakeDecorator 同样要实现 Pancke 接口。并且持有 Pancke 类型的引用,自己实现的 cook 方法实际调用了持有的 Pancake 对象的 cook 方法。

加辣酱的装饰类代码如下,其他装饰实现类是类似的。

public class AddSpicyDecorator extends PancakeDecorator{
    @Override
    public void cook(){
        super.cook();
        System.out.println("加辣酱");
    }
}

cook 方法首先调父类的 cook 方法,然后再加入自己的特性。

客户端代码如下,我们看看如何利用装饰类来生成你想要的煎饼。

public class Client {
    public static void main(String[] args) {
        Pancake pancake = new BasicPancake();
        PancakeDecorator addEggPancake = new AddEggDecorator();
        addEggPancake.setPancake(pancake);
        PancakeDecorator addSaucePancake = new AddSauceDecorator();
        addSaucePancake.setPancake(addEggPancake);
        PancakeDecorator addLaTiaoPancake = new AddLaTiaoDecorator();
        addLaTiaoPancake.setPancake(addSaucePancake);
        addLaTiaoPancake.cook();
    }
}

我们声明了三个包装类,对 BasicPancake 层层包装,最后得到一套两个鸡蛋、加辣酱、加辣条的煎饼。运行后输出如下:

加一勺面
加一个鸡蛋
加一个鸡蛋
加面酱
加辣条
代码块12345

如果你研发了新煎饼,要加新的辅料,比如香肠、榨菜之类,那么只需要增加装饰类的实现即可。从而实现了开闭原则。

类图如下:

2. 装饰者模式优缺点

2.1 优点

  1. 动态的为对象添加额外职责:通过组合不同装饰类,非常灵活的为对象增加额外的职责;

  2. 避免子类爆炸:当不同的特性组合,构成不同的子类时,必然造成子类爆炸。但通过装饰者灵活组合,可以避免这个问;

  3. 分离核心功能和装饰功能:核心业务保留在 Component 的子类中。而装饰特性在 Decorator 的实现类中去实现。面对装饰特性的变化,实现了开闭原则,只需要增加装饰实现类;

  4. 很方便的重复添加特性:我想要一套两个鸡蛋,双份辣条的煎饼。是不是只需要多装饰一次就可以了?就是这么简单。

2.2 缺点

  1. 由于不是通过继承实现添加职责,所以被装饰后的对象并不能通过对象本身就能了解其特性。而需要分析所有对其装饰过的对象;

  2. 装饰模式会造成有很多功能类似的小对象。通过组合不同的装饰实现,来达成不同的需求。这样对于不了解系统的人,比较难以学习。过多的装饰类进行装饰,也稍显繁琐。

3. 装饰者模式适用场景

使用装饰者模式,有以下几种情况:

  1. 需要一个装饰的载体。不能将全部特性都放在装饰类中。换句话讲得有个装饰主体,核心特性在主体对象中实现。例如浏览器窗口,不管是加边框还是滚动条,都是基于窗口的;

  2. 有多种特性可以任意搭配,对主体进行扩展。并且你想以动态、透明的方式来实;

  3. 不能以生成子类的方式扩展。可能有两种情况,一是对大量子类带来的类爆炸有所顾虑。二是类定义被隐藏,或者不能用于生成子类。

4. 小结

装饰者模式的优势在于动态、透明的添加特性。要记住装饰者装饰完的对象还是之前的对象类型。通过分离核心特性和装饰特性,客户端代码可以灵活的搭配使用包装对象,从而得到具有想要行为的对象。不过要注意,有些时候装饰的顺序是要保证的。比如先放鸡蛋,再放芝麻,芝麻就不会掉下去了。最好的做法是保证装饰类的独立。

4、抽象工厂模式

抽象工厂模式用来解决产品族的实例化问题。比如说现在有个家居设计软件,通过软件模拟房间,摆放各种虚拟的家具,看效果如何。我们可以放入电视柜、茶几、餐桌、床等等。这一系列的家具就叫做产品族。产品族面临的问题是,当一个产品族切换到另外一个产品族时,如何让代码的修改最小。也就是说如何做到开闭原则。

想把设计好的方案从简约现代切换到欧式风格家具,怎么才能做到修改最小?如果采用简单工厂,那么每种产品都对应一个工厂,工厂负责产出不同风格的产品。设计方案中用到 n 种产品就要修改 n 处代码。这显然不是最佳的方法。此时,我们需要抽象工厂模式来解决这个问题。抽象工厂模式中,每个工厂的实现负责生产自己产品族的产品。示意图如下:

1. 实现抽象工厂

为了便于理解和展示,我们假设只有两种家具----椅子和桌子。

首先定义每种家具的接口,只有一个方法用来获取家具说明。

椅子:

public interface Chair {
    void getChairIntroduction();
}
代码块123

桌子:

public interface Desk {
    void getDeskIntroduction();
}
代码块123

以椅子为例,我们分别实现简约现代和欧式两种风格。简约现代风格椅子:

public class ModernStyleChair implements Chair {
    @Override
    public void getChairIntroduction() {
        System.out.println("这是一个现代简约风格的椅子");
    }
}

欧式风格椅子:

public class EuropeanStyleChair implements Chair {
    @Override
    public void getChairIntroduction() {
        System.out.println("这是一个欧式风格的椅子");
    }
}

桌子也有两种实现,代码这里省略。

产品我们已经编写完成。接下来我们来看看工厂的代码。

首先我们定义一个家具工厂接口,可以生产椅子和桌子:

public interface FurnitureFactory {
    Chair createChair();
    Desk createDesk();
}
代码块1234

由于我们支持两种不同的风格,所以我们编写两个实现类。

简约风格家具工厂:

public class ModernFurnitureFactory implements FurnitureFactory{
    @Override
    public Chair createChair() {
        return new ModernStyleChair();
    }
    @Override
    public Desk createDesk() {
        return new ModernStyleDesk();
    }
}

欧式风格家具工厂:

public class EuropeanFurnitureFactory implements FurnitureFactory{
    @Override
    public Chair createChair() {
        return new EuropeanStyleChair();
    }
    @Override
    public Desk createDesk() {
        return new EuropeanStyleDesk();
    }
}

上面的代码中,每种工厂各自实现如何生产两种不同的家具。

客户端代码如下:

public class Client {
    public static void main(String[] args) {
        FurnitureFactory furnitureFactory = new EuropeanFurnitureFactory();
        Chair chair = furnitureFactory.createChair();
        Desk desk = furnitureFactory.createDesk();
        chair.getChairIntroduction();
        desk.getDeskIntroduction();
    }
}
代码块12345678910

客户端代码中,我们实例化的是欧式家具工厂,那么所生产的椅子和桌子应该是欧式风格。执行后输出如下:

这是一个欧式风格的椅子
这是一个欧式风格的桌子
代码块12

和我们的预期相符。如果想要更换产品族,从现代简约切换到欧式,我们只需要修改一处代码。

FurnitureFactory furnitureFactory = new ModernFurnitureFactory();
代码块1

仅通过更换抽象工厂的实现即可实现。修改后执行结果如下:

这是一个现代简约风格的椅子
这是一个现代简约风格的桌子
代码块12

可以看到已经切换到简约风格的产品族。这个过程中并不需要改任何产品使用的代码。

如果增加别的风格产品族,只需要新建新风格的产品族产品,增加新风格产品族的工厂实现即可。类图:

2. 抽象工厂优缺点

2.1 优点

  1. 分离了产品类和客户端类:客户端只依赖抽象的产品接口。此外,如何生产产品被封装在工厂内部;

  2. 方便切换产品族:客户端代码只需要初始化一次工厂实现。这意味着在切换产品族的时候,只需要修改一行代码,换一个工厂实现即可;

  3. 保证产品的一致性:使用抽象工厂,可以保证你从相同工厂生产的产品都属于同一个产品族。不会出现椅子是现代简约风格,而桌子是欧式风格的情况。

2.2 缺点

添加新的产品时,改动较多。例子从两个维度定义产品,一是不同产品,比如桌子、椅子。另外是不同族,例如现代简约和欧式。使用抽象工厂,优化了产品族,也就是第二个维度变化的难度。但是当添加新的产品时改动就会比较多。比如我们要添加一个新的产品是电视柜。那么需要修改抽象工厂,添加生产电视柜的方法。此外,有几种工厂的实现,我们就需要修改几个类,添加具体的生产实现。

3. 抽象工厂适用场景

  1. 你的系统中,需要使用不同产品族中的某一个产品族来操作。 比如说DB源。如果想切换DB,只需要切换DB源即可,其他代码基本上不需要改动;

  2. 你的系统中,需要保证某些产品的一致性。 比如操作系统的外观,当切换到夜间模式时,所有的组件都会换为夜间模式风格。

4. 小结

抽象工厂可以做到一组产品的使用和生产相分离。通过抽象工厂模式,我们切换一组产品族的,只需要更换抽象工厂实现即可。由于产品生产被分离出去,所以添加新的产品族完全通过扩展来实现的。很好的实现了开闭原则。如果你要生产的产品很多,而且是一个产品族。并且面临不同产品族切换的情况。那么可以考虑通过抽象工厂来实现。

5、观察者模式

观察者模式也称为订阅者模式,实际上我觉得订阅者更容易理解。这种设计模式在生活中很常见。比如订阅期刊杂志、定牛奶等等。我们使用的软件中也很常见。比如说微博,你关注了某位明星,其实你就是他的观察者。每当你关注的明星发了新的动态,你就会接收到通知。观察者模式基于发布订阅的方式。订阅者订阅目标对象,目标对象维护订阅者的集合。一旦目标对象状态变化,需要通知所有订阅者,从而触发订阅者的某个行为。

1. 实现观察者模式

实现观察者模式,在目标对象中需要维护所有他的观察者引用。观察者可以观察多个不同目标对象的,所以需要让观察者知道是哪个目标对象发送的通知。下面我们通过一个简单的例子来看看如何实现观察者模式。

这个例子叫老师点名了。上大学时候,经常有同学旷课在宿舍打游戏,并且嘱咐去上课的同学,老师要是点名了给我打电话。还好宿舍离教学楼近,接到通知的同学赶紧跑去教室也能赶上。有的胆子大点的同学,接到通知后也不去上课,而是找个关系好的同学帮忙喊声到。

去上课的同学是通知者(目标对象),他持有所有需要他通知老师点名的同学(观察者)的引用,才能在老师点名的时候通知到每个人。程序中我们一般用容器存储观察者。当通知的时候循环调用所有观察者暴露出的更新方法。

“老师点名了” 目标对象代码如下:

public class TeacherRollCallSubject {
    private List<Observer> observers = new ArrayList<>();
    public void addObserver(Observer observer){
        observers.add(observer);
    }
    public void removeObserver(Observer observer){
        observers.remove(observer);
    }
    public void notifyObservers (){
        observers.forEach(Observer::update);
    }
}

观察者只需要实现一个方法,供通知者做通知的时候调用。我们先定义观察者的接口。

public interface Observer {
    void update();
}
代码块123

我们定义第一类观察者的实现,他接到通知后,会马上去教室。

public class GotoClassObserver implements Observer {
    @Override
    public void update() {
        System.out.println("老师点名了!");
        System.out.println("我要马上赶到教室去!");
    }
}

第二类观察者,接到通知后,会通知自己的好朋友帮自己答到。

public class AskForHelpObserver implements Observer {
    @Override
    public void update() {
        System.out.println("老师点名了!");
        System.out.println("赶紧给XX发信息,让他替我答到!");
    }
}
代码块1234567

客户端代码中,分别声明两个不同的观察者,然后让这两个观察者都观察老师点名了目标对象。最后出发目标对象的通知方法。客户端代码如:

public class Client {
    public static void main(String[] args) {
        Observer studentOne = new GotoClassObserver();
        Observer studentTwo = new AskForHelpObserver();
        TeacherRollCallSubject subject = new TeacherRollCallSubject();
        subject.addObserver(studentOne);
        subject.addObserver(studentTwo);
        subject.notifyObservers();
    }
}

运行后输出如下:

老师点名了!
我要马上赶到教室去!
老师点名了!
赶紧给XX发信息,让他替我答到!
代码块1234

可以看到每个观察者都接到了通知,并且按照自己实现的响应方式作出不同的逻辑处理。第一个同学会赶到教室。第二个同学则是给好朋友发信息,让其替他答到。

2. 观察者模式优缺点

2.1 优点

1、目标对象状态的变化,不需要观察者真的一直观察。当存在大量观察者时,如果所有的观察者都去轮询状态,那么系统资源的消耗极大。而观察者模式避免了这种情况;

2、观察者模式支持广播,状态变化时,目标对象的所有观察者都会得到通知;

3、符合开闭原则,目标对象依赖的是观察者的接口,可以很方便的对观察者进行扩展,而不需要修改已有观察者。反过来观察者也是依赖的目标接口。

2.2 缺点

1、观察者模式中,观察者接口限定了方法签名。有一定的局限性。

3. 观察者模式适用场景

1、抽象模型可以分为两个部分,一部分行为取决于另外一部分状态的变化。并且你想让这两部分各自独立。每部分都可以独自使用和复用;2、一个对象的变化,需要通知其他对象,并且有多少对象需要通知并不清楚3、你想让通知与被通知双方松耦合。

4. 小结

观察者模式最大的优点就是把目标和观察者解耦,观察者根据目标对象状态的变化作出响应。而目标者可以把自己状态变化广播给所有注册的观察者。实际使用中有推/拉两种模型。

  • 在推模型中,目标对象会把状态改变相关的所有信息推送出去,信息的量有可能会很大。

  • 在拉模型中,目标对象只推送出最核心的信息,比如变化的数据 id。

观察者收到消息后再决定如何处理,比如查询与变化相关的自己感兴趣的数据。推模型,目标对象需要知道所有观察者对数据的需求。而拉模型效率会比较差,观察者收到消息后,还需要自己再去获取改变的内容。关于推拉模型总结如下:

6、抽象工厂模式

抽象工厂模式用来解决产品族的实例化问题。比如说现在有个家居设计软件,通过软件模拟房间,摆放各种虚拟的家具,看效果如何。我们可以放入电视柜、茶几、餐桌、床等等。这一系列的家具就叫做产品族。产品族面临的问题是,当一个产品族切换到另外一个产品族时,如何让代码的修改最小。也就是说如何做到开闭原则。

想把设计好的方案从简约现代切换到欧式风格家具,怎么才能做到修改最小?如果采用简单工厂,那么每种产品都对应一个工厂,工厂负责产出不同风格的产品。设计方案中用到 n 种产品就要修改 n 处代码。这显然不是最佳的方法。此时,我们需要抽象工厂模式来解决这个问题。抽象工厂模式中,每个工厂的实现负责生产自己产品族的产品。示意图如下:

1. 实现抽象工厂

为了便于理解和展示,我们假设只有两种家具----椅子和桌子。

首先定义每种家具的接口,只有一个方法用来获取家具说明。

椅子:

public interface Chair {
    void getChairIntroduction();
}
代码块123

桌子:

public interface Desk {
    void getDeskIntroduction();
}
代码块123

以椅子为例,我们分别实现简约现代和欧式两种风格。简约现代风格椅子:

public class ModernStyleChair implements Chair {
    @Override
    public void getChairIntroduction() {
        System.out.println("这是一个现代简约风格的椅子");
    }
}
代码块123456

欧式风格椅子:

public class EuropeanStyleChair implements Chair {
    @Override
    public void getChairIntroduction() {
        System.out.println("这是一个欧式风格的椅子");
    }
}
代码块123456

桌子也有两种实现,代码这里省略。

产品我们已经编写完成。接下来我们来看看工厂的代码。

首先我们定义一个家具工厂接口,可以生产椅子和桌子:

public interface FurnitureFactory {
    Chair createChair();
    Desk createDesk();
}
代码块1234

由于我们支持两种不同的风格,所以我们编写两个实现类。

简约风格家具工厂:

public class ModernFurnitureFactory implements FurnitureFactory{
    @Override
    public Chair createChair() {
        return new ModernStyleChair();
    }
    @Override
    public Desk createDesk() {
        return new ModernStyleDesk();
    }
}
代码块1234567891011

欧式风格家具工厂:

public class EuropeanFurnitureFactory implements FurnitureFactory{
    @Override
    public Chair createChair() {
        return new EuropeanStyleChair();
    }
    @Override
    public Desk createDesk() {
        return new EuropeanStyleDesk();
    }
}

上面的代码中,每种工厂各自实现如何生产两种不同的家具。

客户端代码如下:

public class Client {
    public static void main(String[] args) {
        FurnitureFactory furnitureFactory = new EuropeanFurnitureFactory();
        Chair chair = furnitureFactory.createChair();
        Desk desk = furnitureFactory.createDesk();
        chair.getChairIntroduction();
        desk.getDeskIntroduction();
    }
}

客户端代码中,我们实例化的是欧式家具工厂,那么所生产的椅子和桌子应该是欧式风格。执行后输出如下:

这是一个欧式风格的椅子
这是一个欧式风格的桌子
代码块12

和我们的预期相符。如果想要更换产品族,从现代简约切换到欧式,我们只需要修改一处代码。

FurnitureFactory furnitureFactory = new ModernFurnitureFactory();
代码块1

仅通过更换抽象工厂的实现即可实现。修改后执行结果如下:

这是一个现代简约风格的椅子
这是一个现代简约风格的桌子
代码块12

可以看到已经切换到简约风格的产品族。这个过程中并不需要改任何产品使用的代码。

如果增加别的风格产品族,只需要新建新风格的产品族产品,增加新风格产品族的工厂实现即可。类图:

2. 抽象工厂优缺点

2.1 优点

  1. 分离了产品类和客户端类:客户端只依赖抽象的产品接口。此外,如何生产产品被封装在工厂内部;

  2. 方便切换产品族:客户端代码只需要初始化一次工厂实现。这意味着在切换产品族的时候,只需要修改一行代码,换一个工厂实现即可;

  3. 保证产品的一致性:使用抽象工厂,可以保证你从相同工厂生产的产品都属于同一个产品族。不会出现椅子是现代简约风格,而桌子是欧式风格的情况。

2.2 缺点

添加新的产品时,改动较多。例子从两个维度定义产品,一是不同产品,比如桌子、椅子。另外是不同族,例如现代简约和欧式。使用抽象工厂,优化了产品族,也就是第二个维度变化的难度。但是当添加新的产品时改动就会比较多。比如我们要添加一个新的产品是电视柜。那么需要修改抽象工厂,添加生产电视柜的方法。此外,有几种工厂的实现,我们就需要修改几个类,添加具体的生产实现。

3. 抽象工厂适用场景

  1. 你的系统中,需要使用不同产品族中的某一个产品族来操作。 比如说DB源。如果想切换DB,只需要切换DB源即可,其他代码基本上不需要改动;

  2. 你的系统中,需要保证某些产品的一致性。 比如操作系统的外观,当切换到夜间模式时,所有的组件都会换为夜间模式风格。

4. 小结

抽象工厂可以做到一组产品的使用和生产相分离。通过抽象工厂模式,我们切换一组产品族的,只需要更换抽象工厂实现即可。由于产品生产被分离出去,所以添加新的产品族完全通过扩展来实现的。很好的实现了开闭原则。如果你要生产的产品很多,而且是一个产品族。并且面临不同产品族切换的情况。那么可以考虑通过抽象工厂来实现。

    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多