引言:本文并不是从代码角度上来谈设计模式,而是希望类比手机制造尝试以一条线来从常用设计模式的使用场景来介绍设计模式的(毕竟设计模式是面向对象的)思想和使用方法,同时我在每个设计模式场景的介绍中也会提供类图帮助大家理解,如果需要在代码层次有更多的实践可以参考《Head First设计模式》


本文中我会提到以下设计模式:

  • 策略模式
  • 工厂模式
  • 装饰者模式
  • 模板方法模式
  • 适配器模式
  • 迭代器与组合模式
  • 观察者模式

独身模式和构造者模式可以看我的另一篇文章独身模式+构建者模式打造私有个性化“商城”

First:手机制造中的策略模式

大家都知道每个手机制造厂家都有很多机型,每一款机型的每个部件往往都是不一样的,我们拿摄像头和小米手机来举个栗子:小米5的摄像头是光学防抖的,小米5s的摄像头拥有超感光元器件(因此被誉为暗夜之眼),而小米6的摄像头则是变焦双摄。很明显我标识出来的地方都是这些摄像头的属性,依据这些属性每个摄像头可以完成不同的拍照行为,我们姑且做一个映射:

摄像头 拍照能力(或行为)
光学防抖 在手机抖动的情况下依然可以拍出清晰的照片
超感光元器件 在光线很低的情况下依然可以拍出清晰的照片
变焦双摄 单反级的景深和背景虚化

那么不用设计模式,正常的去定义这些手机用类图如何表示呢? 大概如下:

非策略模式.png 很明显所有的手机机型都需要继承一个基类,而且所有手机都具有打电话这个方法且都是一致的,所以把打电话放在基类里面是可以的。但是拍照这个方法不可以,因为每个机型拍照的能力不一样,如果放在基类里面则都会被继承,这样是不对的。所以常见的方法是写一个接口,每个机型都去继承这个接口并各自实现,这样就可以解决问题。但是这又带来另外一个问题,这个接口实在是被继承太多次了!而且只是为了一个不同的行为,假设还有其他行为如打游戏又需要继承另一个接口,简单算个账,假设总共有2300万部手机,5个不同接口,2300万*5,很大的一个数字…..那么没有办法把它放在父类里面去继承吗?


答案:是可以的,而且很简单,“就把它放在父类里面啊!”(感觉自己在找打)实现如下:

策略模式.png 我们按类图的上下两部分来说:

  • 封装行为

    首先我们依然把camera当成一个接口,这一点和非策略模式是一致的,但是我们有了具体实现类,光学防抖的camera,超感光元器件的camera,变焦双摄的camera,并且我们在不同的camera里面实现了自己的takePhoto方法,这样行为就被封装了。

  • 动态设置行为

    看下半部分,父类里面多了一个属性叫camera,其实是引用类,但是没有初始化,初始化的步骤我们放在了setCamera里面,同样takePhoto的我们也作为一个方法。 我们看看子类怎么用,以小米5为🌰: 1.初始化camera private void setCamear() { camera = new 光学防抖的camera(); } 2.可以拍照了 public void useCameraTakePhoto() { print(camera.takePhoto); } 3.拍照结果: log:抖动时的照片依然清晰,具备防抖效果

策略模式的好处是显而易见的,我们不需要再继承大量的接口并实现里面的方法,我们只需要在我们需要的时候在子类中去初始化行为就可以,我们甚至可以不初始化camera,谁知道以后拍照需要不要相机呢?这样是不是提高了可变性?

Second:手机制造中的工厂模式(抽象工厂)

我想这个模式放在手机制造这个例子里面来讲大概是最好理解的,我们先看定义(取自《HeadFirst 设计模式》)

抽象工厂模式提供一个接口,用于创建相关或依赖对象的家族,而不需要明确指定具体类。

回到手机工厂。顾名思义,手机工厂一定拥有制造各种手机的能力,这种能力体现在拥有不同手机的元器件材料,然后再以一种流水线的方式把各种材料组件起来,这样手机就诞生了。这里我们需要再细化一下工作,手机工厂仅仅提供手机的各种元器件材料,我们把组装手机的工作放在流水线里面,这样不同的流水线就可以组装不同的手机,实践生产中肯定也是这样。我们看一下工厂方法的类图:

抽象工厂模式.png

这里我们把工厂更具体化了一点(可能实际上只需要一家工厂),我们有两个工厂,一个是海淀工厂,一个是朝阳工厂,每个工厂提供不同的元器件。这两个工厂生产手机的能力有什么不同呢?答案可以从流水线上看出来,很明显海淀工厂可以拥有两条流水线,一个是小米5s,一个是小米5sPlus,仅仅只需要把屏幕的大小换一下(实际上肯定还有其他的元器件,这里只用屏幕大小来区别)。然而朝阳工厂只拥有一条流水线即米6。 这个时候我们有一家海淀的线下店缺货了,这个时候需要“造一些”手机,这里我们把用海淀工厂来造手机(当然用朝阳工厂也行,或者两个工厂用list传进去)。

    protected Phone createPhone(String type) {
        PipeLine pipeLine = null;
        PhoneFactory  factory = new HaiDianFactory();
        if (type.equals("xiaomi5s")) {
           pipeLine = new xiaomi5sPipeLine(factory);
        } else if (type.equals("xiaomi5splus")) {
           pipeLine = new xiaomi5sPlusPipeLine(factory);
        }
        pipeLine.prepare();
        return pipeLine.getPhone();
    }

这样做的好处是什么?我们解耦了实际的工厂!这是比简单工厂进步的地方,我们可以根据需要去初始化工厂接口,同时根据所需要的产品类型把实际工厂传入,这样就可以制造出不同的产品。如我们线下店的小米5s缺货啦!我们把海淀工厂当做参数传给小米5s的流水线,这样流水线就可以拿到所需要的元器件去准备生产小米5s,这样不是比先判断是什么手机我们再一个一个初始化元器件然后再准备组装的过程方便的多? 抽象工厂果然英明。

Third:手机制造中的装饰者模式

大家都知道每一种机型都是由各种元器件组成的,不同机型之间有相同的元器件也有不相同的元器件,我们还是以小米5、小米6举栗子:

机型|组成 ———-|——- 小米5|camera+fingerPrinter+大猩猩screen 小米6|camera+camera+四曲面screen+fingerPrinter 很明显可以看出小米6与小米5的不同之处,小米5是单摄像头,小米6是双摄像头,同时小米5和小米6的屏幕也是不一样的。这样会有什么影响呢?我们从两个方面谈一下:

  • 价格

    很明显选择不同的器件组合起来的手机价格一定是不同,比如有两款除了摄像头不同其余器件都相同的机型,双摄一定是比单摄贵的;同样四曲面玻璃一定是比普通玻璃贵的。所以仅有上面几个器件决定的手机价格小米6的一定是比小米5高的,这是由成本和工艺决定的(当然前提是同一时间段)。那么价格是如何算的呢?很简单,组件的价格之和,A+B+C=总价格,是具有累加属性的。

  • 描述

    发烧友都喜欢看手机的参数,比如骁龙835就比骁龙820更吸引人的眼球,所以我们可以这样描述小米6,这是一款拥有骁龙835cpu变焦双摄四曲面屏幕的旗舰手机。当然你也可以用同样的方式去描述米5,这些描述赋予了手机鲜明的特点。同样,这些描述也是可以累加的,我有这样一个A+B+C的手机

好了,废话扯完了,该扯到设计模式了,假设不用设计模式,我们需要得到一台机型的价格和描述我们该如何做?可能如下:

非装饰者模式.png

这是一个看上去简单实际上很傻的设计,我们把cost实现全部放在具体的机型里面,这样的后果是可怕的——假设摄像头是从美国采购回来的,因为汇率问题,价格变化了,我们需要在每个机型的cost方法里面重新定义价格,就算我们改良一下,我们把所有的元器件都放在父类里面定义,这样好吗?也不好,这样所有的机型都必须继承这些器件,假设米5继承了父类的四曲面screen,实际上根本用不着,而且显然父类是需要维护的而且维护的成本不低,这并不是一个父类该做的事情,它应该只保留子类共性的部分并提供抽象的方法。好吧,用装饰者模式改造一下设计吧:

装饰者模式.png

我们分成三部分来讨论,被装饰者装饰者装饰过程

  • 被装饰者

    被装饰者这里指的就是小米5和小米6,它们同时继承了手机这个抽象类并且各种实现了cost方法,但是这样还够,因为它们仅仅是孤零零的裸机,什么都没有,没有摄像头,没有屏幕也没有cpu,我们需要装饰一下它们.

  • 装饰者

    我们需要定义一个抽象的decorator来继承抽象父类phone,这样做是为了继承父类的抽象方法cost和getDescription。实际的装饰者是谁?是那些元器件,比如摄像头四曲面玻璃骁龙cpu等等,我们用这些描述累加起来去具体形容一个机型,同时我们在里面还顺便实现一下cost和getDescription方法(如何实现看装饰过程),要注意实际的装饰者必须关联一个实际的被装饰者phone(定义在属性里面)。

  • 装饰过程

    我们先来装饰一个小米5 Phone xiaomi5phone = new 大猩猩screen(new 骁龙cpu(new camera (new fingerPrinter(new xiaomi5())))) 首先,这么做可以吗?毫无疑问是可以的,因为每个装饰者的构造方法里面都必须传入一个已经初始化过的phone对象,而且每个装饰者本身又可以是被装饰的对象。这样就有趣了,我们可以一直装饰下去。我们可以用用摄像头装饰米5,可以描述成有摄像头的米5,我们可以继续用大猩猩屏幕来装饰有摄像头的米5,这样就可以描述成有大猩猩屏幕且有摄像头的米5……米6我们仍然可以这样装饰 Phone xiaomi6phone = new 四曲面screen(new 骁龙cpu(new camera(new camera(new fingerPrinter(new xiaomi6()))))) 我们甚至可以用camera装饰两次让米6变成双摄的手机 我们再实现一下组件的,cost和getDescripition方法,以camera为栗子: public class Camera extends CondimenDecorator { Phone phone;

        public Camera(Phone phone) {
            this.phone = phone;
        }
    
        public String getDescription() {
            return phone.getDescription() + "has 摄像头";
        }
    
        public double cost() {
            return 20 + phone.cost();
        }
      }
    

很容易看出来无论是描述还是价格都具有累加的属性,这正是我前面所强调的地方,这样我们就可以方便的获取到装饰后的价格和描述,因为这些属性均具有累加的性质,这就是装饰者模式的三个部分。

Fouth:手机制造中的模板方法模式

前面说过,手机在工厂中的加工方法其实就是一条流水线,往往很多手机的流水线是相似的,仅仅只是修改了流水线中的一两个步骤,这次我们拿小米5标准版和小米5高配版来做个对比,这两条流水线如下(图画的有点歪见谅):

流程图模拟流水线.png

很明显,搞出来两条流水线是浪费的,因为小米5标准版和高配版之间的区别仅仅是内存和rom的不同,所以我们可以合并一下流程:

合并流水线.png

其实流水线跟程序中的算法是一致,很多时候算法往往是解决一类问题的,但是每个问题都有自己的条件且条件通常不同,这就需要算法可以动态的根据条件来改变算法的步骤,这其实就是模板方法的精髓。


我们先来看看模板方法模式的定义:

模板方法模式在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。

我们把上面的流水线用模板方法改造一下:

模板方法模式.png

我们在子类中重新定义流水线的两个步骤installRAM()installROM,我们再从代码上看一下父类的流水线这个方法:

final void templatePipeLine() {
    installMainBoard();
    installCPU();
    installRAM();
    installROM();
    hook();
}

这个算法和上面的流水线基本一致,但是还有一点欠缺,上面的流水线只要判断是否是高配就决定了流水线的走势,我们这个还是要在子类里面重新实现installRAM()installROM,但是没有关系,模板方法还留了一手,我们可以通过hook(钩子)的方式改变算法的走向,比如我们把hook()定义为一个boolean isHigh(),重新改变一下模板方法:

final void templatePipeLine() {
    installMainBoard();
    installCPU();
    if (isHigh)  {
      install3GRAM();
      install64GROM();
    } else{
      install2GRAM();
      install32GROM();
    }
}

这样改造一下,子类仅仅需要提供一下是否是高版本的这个条件就可以轻易的得到想要的流水线产品,岂不是很方便?