秒懂设计模式笔记
为了让软件架构良好、稳固,设计模式针对各种场景提供了适合的代码模块的复用及扩展解决方案。
面向对象的三大特性:继承、封装、多态。
封装
现实中,计算机主机机箱对于主板、CPU及内存等配件的封装等。
饮料是被装在杯子里面的,杯子的最上面封上盖子,只留有一个孔用于插吸管,这其实就是封装。封装隐藏了杯子内部的饮料,也许还会有冰块,而对于杯子外部来说只留有一个“接口”用于访问。
如果把一些不相干的对象硬生生封装在一起,就会使代码变得莫名其妙,难于维护与管理,所谓“物极必反,过犹不及”,所以封装一定要适度。
继承
继承可以使父类的属性和方法延续到子类中,这样子类就不需要重复定义,并且子类可以通过重写来修改继承而来的方法实现,或者通过追加达到属性与功能扩展的目的。
多态
多态其实是利用了继承(或接口实现)这个特性体现出来的另一番景象。
如人类所能接受的食物对象可以是番茄、苹果、牛肉等有机食物的多形态表现。
1、单例
单例即单一的实例,某个系统中只存在一个实例,同时提供集中、统一的访问接口,以使系统行为保持协调一致。
饿汉模式
public class Sun {private static final Sun sun = new Sun(); // 自有永有的单例private Sun(){ // 构造方法私有化}public static Sun getInstance() { // 阳光普照,方法公开化return sun;}
}
private”关键字确保太阳实例的私有性、不可见性和不可访问性;
“static”关键字确保太阳的静态性
“final”关键字则确保这个太阳是常量、恒量
new”关键字初始化太阳类的静态实例,并赋予静态常量sun。
这就是“饿汉模式”(eager initialization),即在初始阶段就主动进行实例化,并时刻保持一种渴求的状态,无论此单例是否有人使用。
对外部来说只要调用Sun. getInstance()就可以得到太阳对象了,并且不管谁得到,或是得到几次,得到的都是同一个太阳实例,这样就确保了整个太阳系中恒星太阳的唯一合法性,他人无法伪造。
饿汉模式”,让太阳一开始就准备就绪,随时供应免费日光。然而,如果始终没人获取日光,那岂不是白造了太阳,一块内存区域被白白地浪费了?
此时就可以考虑“懒汉模式”。
懒汉模式
public class Sun {private static Sun sun; // 这里不进行实例化private Sun(){ // 构造方法私有化}public static synchronized Sun getInstance() { // 加入同步锁if(sun == null) {// 如果无日才造日sun = new Sun();}return sun;}
}
同步锁:避免多线程陷阱(多线程执行 getInstance 导致的 sun 多次 new Sun() 的情况)。
public class Sun {private volatile Sun sun; // 这里不进行实例化private Sun(){ // 构造方法私有化}public static Sun getInstance() { // 华山入口if(sun == null) { // 观日台入口synchronized(Sun.class) { // 观日者进行排队if(sun == null) {sun = new Sun(); // 只有排头兵造了太阳,旭日东升}}}return sun; // 阳光普照,其余人不必再造日}
}
相比“懒汉模式”,其实在大多数情况下我们通常会更多地使用“饿汉模式”,原因在于这个单例迟早是要被实例化占用内存的,延迟懒加载的意义并不大,加锁解锁反而是一种资源浪费,同步更是会降低CPU的利用率,使用不当的话反而会带来不必要的风险。
2、原型
原型模式,参照这个原型进行批量生产。原型模式达到以原型实例创建副本实例的目的即可,并不需要知道其原始类,也就是说,原型模式可以用对象创建对象,而不是用类创建对象,以此达到效率的提升。
原型模式的关系就相当于 原型与副本,打印与复印。第一份打印出来的原文稿,即为“原型文件”,复印过程,即为“原型拷贝”。
类的实例化与克隆之间的区别,二者都是在造对象,但方法绝对是不同的。
对于那些有非常复杂的初始化过程的对象或者是需要耗费大量资源的情况,原型模式是更好的选择。
public class EnemyPlaneFactory {// 此处用单例饿汉模式造一个敌机原型private static EnemyPlane protoType = new EnemyPlane(200);// 获取敌机克隆实例public static EnemyPlane getInstance(inx x) {EnemyPlane clone = protoType.clone(); // 复制原型机clone.setX(x); //重新设置克隆机的x坐标return clone;}
}
注意 深拷贝 与 浅拷贝。
从类到对象叫作“创建”,而由本体对象至副本对象则叫作“克隆”,当需要创建多个类似的复杂对象时,我们就可以考虑用原型模式。
3、工厂方法
程序设计中的工厂类往往是对对象构造、实例化、初始化过程的封装,而工厂方法(Factory Method)则可以升华为一种设计模式,它对工厂制造方法进行接口规范化,以允许子类工厂决定具体制造哪类产品的实例,最终降低系统耦合,使系统的可维护性、可扩展性等得到提升。
1、简单工厂
public class SimpleFactory {private int screenWidth;private Random random;public SimpleFactory(int screenWidth) {this.screenWidth = screenWidth;this.random = new Random();}public Enemy create(String type) {int x = random.nextInt(screenWidth); // 生成敌人横坐标随机数Enemy enemy = null;switch(type) {case "Airplane":enemy = new Airplane(x,0); // 实例化飞机case "Tank":enemy = new Tank(x,0); // 实例化坦克break;}return enemy;}
}
简单工厂一定要保持简单,否则就不要用简单工厂。
随着游戏项目需求的演变,简单工厂的可扩展性也会变得很差,例如对于那段对产品种类的判断逻辑,如果有新的敌人类加入,我们就需要再修改简单工厂。随着生产方式不断多元化,工厂类就得被不断地反复修改,严重缺乏灵活性与可扩展性,尤其是对于一些庞大复杂的系统,大量的产品判断逻辑代码会被堆积在制造方法中,看起来好像功能强大、无所不能,其实维护起来举步维艰,简单工厂就会变得一点也不简单了。
2、工厂方法模式
将简单工厂的制造方法进行拆分,构建起抽象化、多态化的生产模式。
public interface Factory {Enemy create(int screenWidth);
}
// 飞机工厂类
public class AirplaneFactory implements Factory {@Overridepublic Enemy create(int screenWidth) {Random random = new Random();return new Ariplane(random.nextInt(screenWidth),0);}
}
// 坦克工厂类
public class TankFactory implements Factory {@Overridepublic Enemy create(int screenWidth) {Random random = new Random();return new Tank(random.nextInt(screenWidth),0);}
}
public class Client {public static void main(String[] args) {int screenWidth = 100;Factory factory = new TankFactory();factory.create(screenWidth).show();factory = new AirPlaneFactory();factory.create(screenWidth).show();//......}
}
之后若要加入新的敌人类,只需添加相应的工厂类,无须再对现有代码做任何更改。不同于简单工厂,工厂方法模式可以被看作由简单工厂演化而来的高级版,后者才是真正的设计模式。
在工厂方法模式中,不仅产品需要分类,工厂同样需要分类。(一个工厂不是万能的,可以有手机工厂(手机工厂不必生产汽车),汽车工厂(汽车工厂不必生产手机))
4、抽象工厂
抽象工厂模式(Abstract Factory)是对工厂的抽象化,而不只是制造方法。
工厂不会只局限于生产一类产品,但是系统如果按工厂方法那样为每种产品都增加一个新工厂又会造成工厂泛滥。所以,为了调和这种矛盾,抽象工厂模式提供了另一种思路,将各种产品分门别类,基于此来规划各种工厂的制造接口,最终确立产品制造的顶级规范,使其与具体产品彻底脱钩。
抽象工厂是建立在制造复杂产品体系需求基础之上的一种设计模式,在某种意义上,我们可以将抽象工厂模式理解为工厂方法模式的高度集群化升级版。
在工厂方法模式中每个实际的工厂只定义了一个工厂方法。随着产品种类丰富,就需要进行产业规划与整合,对现有工厂进行重构。例如,我们可以基于产品品牌与系列进行生产线规划,按品牌划分A工厂与B工厂。具体以汽车工厂举例,A品牌汽车有轿车、越野车、跑车3个系列的产品,同样地,B品牌汽车也包括以上3个系列的产品,如此便形成了两个产品族,分别由A工厂和B工厂负责生产,每个工厂都有3条生产线,分别生产这3个系列的汽车:
基于这2个品牌汽车工厂的系列生产线,如果今后产生新的C品牌汽车、D品牌汽车等,都可以沿用此种规划好的生产模式,这便是抽象工厂模式的基础数据模型。
与工厂方法模式不同,抽象工厂模式能够应对更加复杂的产品族系,它更类似于一种对“工业制造标准”的制定与推行,各工厂实现都遵循此标准来进行生产活动,以工厂类划分产品族,以制造方法划分产品系列,达到无限扩展产品的目的。
兵种抽象类
public abstract class Unit {protected int attack; // 攻击力protected int defence; // 防御力protected int health; // 生命力protected int x; // 横坐标protected int y; // 纵坐标public Unit(int attack, int defence, int health, int x, int y) {this.attack = attack;this.defence = defence;this.health = health;this.x = x;this.y = y;}public abstract void show();public abstract void attack();
}
1级兵种类
public abstract class LowClassUnit extends Unit {public LowClassUnit(int x, int y) {super(5,2,35,x,y)}
}
2级兵种类
public abstract class MidClassUnit extends Unit {public MidClassUnit(int x, int y) {super(10,8,80,x,y)}
}
3级兵种类
public abstract class HighClassUnit extends Unit {public HighClassUnit(int x, int y) {super(25,30,300,x,y)}
}
海军陆战队员类
public class Marine extends LowClassUnit {public Marine(int x, int y) {super(x,y)}@Overridepublic void show() {System.out.println("士兵出现在坐标":[" + x + "," + y + "]");}@Overridepublic void attack() {System.out.println("士兵用机关枪射击,攻击力:" + attack);}
}
变形坦克类
public class Tank extends MidClassUnit {public Tank(int x, int y) {super(x,y)}@Overridepublic void show() {System.out.println("坦克出现在坐标":[" + x + "," + y + "]");}@Overridepublic void attack() {System.out.println("坦克用炮轰击,攻击力:" + attack);}
}
…
毒液类
public class Poison extends MidClassUnit {public Poison(int x, int y) {super(x,y)}@Overridepublic void show() {System.out.println("毒液兵出现在坐标":[" + x + "," + y + "]");}@Overridepublic void attack() {System.out.println("毒液兵用毒液喷射,攻击力:" + attack);}
}
…
至此,所有兵种类已定义完毕,代码不是难点,重点集中在对兵种的划分上,横向划分族,纵向划分等级(系列),利用类的抽象与继承描绘出所有的游戏角色以及它们之间的关系,同时避免了不少重复代码。
既然产品类的数据模型构建完成,相应的产品生产线也应该建立起来,接下来我们就可以定义这些产品的制造工厂了。
抽象兵工厂接口
public interface AbstractFactory {LowClassUnit createLowClass(); // 初级兵制造标准MidClassUnit createMidClass(); // 中级兵种制造标准HighClassUnit createHighClass(); // 高级兵种制造标准
}
人类兵工厂
public class HumanFactory implements AbstractFactory {private int x; // 工厂横坐标private int y; // 工厂纵坐标public HumanFactory(int x, int y) {this.x = x;this.y = y;}@Overridepublic LowClassUnit CreateLowClass() {LowClassUnit unit = new Marine(x,y);System.out.println("制造海军陆战队员成功");return unit;}@Overridepublic MidClassUnit creaetMidClass() {MidClassUnit unit new Tank(x,y);System.out.println("制造变形坦克成功");return unit;}// ......
}
外星母巢
public class AlienFactory implements AbstractFactory {private int x;private int y;public AlienFactory(int x,int y) {this.x = x;this.y = y;}@Overridepublic LowClassUnit createLowClass() {LowClassUnit unit = new Roach(x,y);System.out.println("制造蟑螂兵成功");return unit;}// ......
}
主方法调用
public class Client {public static void main(String[] args) {// 第一位玩家选择了人类AbstractFactory factory = new HumanFactory(10,10);Unit marine = factory.createLowClass();marine.show();Unit tank = factory.createMidClass();tank.show();//......// 第二位玩家选择了外星怪兽factory = new AlienFactory(200,200);Unit poison = facotry.createMidClass();poison.show();//......// 开始大战marine.attack();poison.attack();//......}
}
以上就是抽象工厂,分而治之的模式,总结一下就是:
一个游戏,有两种类型的角色可以选择:人类和怪兽。
而角色又分为1级、2级、3级(低级、中级、高级)。
抽象类 Unit 定义 角色的通用属性、方法;
抽象类 LowClassUnit、MidClassUnit、HighClassUnit 继承 Unit,以区分角色级别
实现类 Marine、Tank 等 定义具体角色(人类、怪兽)并继承级别类(LowClassUnit、HighClasUnit 等)
以上的抽象类定义了完整的产品类的数据模型。
接下来就是产品的生产线了(产品的具体制造、生成):
抽象接口 AbstractFactory 定义了初级、中级、高级角色的生产能力
实现类 HumanFactory 继承抽象接口,具有创建人类角色的低级、中级、高级的实现
AlienFactory 类似 HumanFactory ,是怪兽角色的低级、中级、高级实现。
通过具体调用,如 alienFactory.createMidClass() 就是创建了中级的 poison 角色。
一个工厂(alienFactory),不同生产线(createMidClass、createHighClass 等),一个生产线创建一个产品/角色(poison、mammoth 等)
简单说,抽象工厂 = 产品类 + 工厂类。
5、建造者
建造者模式(Builder,又称为生成器模式)所构建的对象一定是庞大而复杂的,并且一定是按照既定的制造工序将组件组装起来的,例如计算机、汽车、建筑物等。
它可以将多个简单的组件对象按顺序一步步组装起来,最终构建成一个复杂的成品对象。与工厂系列模式不同的是,建造者模式的主要目的在于把烦琐的构建过程从不同对象中抽离出来,使其脱离并独立于产品类与工厂类,最终实现用同一套标准的制造工序能够产出不同的产品。
建筑物类:建筑需要地基、墙体和屋顶
public class Building {// 用List来模拟建筑物组件的组装private List<String> buildingComponents = new ArrayList<>();public void setBasement(String basement) {// 地基this.buildingComponents.add(basement);}public void setWall(String wall) {// 墙体this.buildingComponents.add(wall);}public void setRoof(String roof) {// 屋顶this.buildingComponents.add(roof);}@Overridepublic String toString() {String buildingStr = ufor (int i = buildingComponents.size() - l;i= 0;i--) {buildingStr += buildingComponents.get(i);}return buildingStr;}
}
施工方接口:对应地基、墙体、屋顶的建造标准
别墅施工方类:不同类型的建筑对应不同的施工方,但任意类型的建筑都有地基、墙体、屋顶的建造
多层公寓施工方类
工程总监类:施工方保证了建筑物三大组件的施工质量,只负责干活,但工程总监可以用来控制施工流程
真正建造,开始实施
复杂对象的构建显然需要专业的建造团队,建造标准的确立让产品趋向多样化,其建造工艺可以交给多位建造者去各显其长,而建造工序则交由工程总监去全局把控,把“变”与“不变”分开,使“工艺多样化”“工序标准化”,最终实现通过相同的构建过程生产出不同产品
单例、原型、工厂方法、抽象工厂、建造者为 创建型设计模式
6、门面
门面模式(Facade)它能将多个不同的子系统接口封装起来,并对外提供统一的高层接口,使复杂的子系统变得更易使用。顾名思义,“门”可以理解为建筑物的入口,而“面”则通常指物体的外层表面,比如人脸。
打个比方,以前的相机拍照时需要调各种参数;而现在有了智能模式,只要按下拍照即可(而智能模式也不过是对于各种参数的自动调整罢了)。
如下所示:
Facade 类(门面类) 封装了 VegVendor(蔬菜类:蔬菜配送)、Chef(厨师类:做菜)、Waiter(服务员类:订单、洗碗)。
客户进来只需要 facade.order() 即可,即只需要下单点餐即可,无需考虑从哪里找到各种蔬菜、谁来做、吃完后洗碗的操作。
7、组合
组合模式(Composite)是针对由多个节点对象(部分)组成的树形结构的对象(整体)而发展出的一种结构型设计模式,它能够使客户端在操作整体对象或者其下的每个节点对象时做出统一的响应,保证树形结构对象使用方法的一致性,使客户端不必关注对象的整体或部分,最终达到对象复杂的层次结构与客户端解耦的目的。
比如:读取、遍历一个文件夹下的所有文件(文件夹下可能有子文件夹、子文件,子文件夹下有可能有孙子文件夹、孙子文件…),并将它们的名字存到 txt 文件中。那么就要遍历所有文件、文件夹,做【读取名字】这样同样的操作(方法),然后写入 txt 即可。
8、装饰器
装饰指在某物件上装点额外饰品的行为。
装饰器模式(Decorator)能够在运行时动态地为原始对象增加一些额外的功能,使其变得更加强大。从某种程度上讲,装饰器非常类似于“继承”,它们都是为了增强原始对象的功能,区别在于方式的不同,后者是在编译时(compile-time)静态地通过对原始类的继承完成,而前者则是在程序运行时(run-time)通过对原始对象动态地“包装”完成,是对类实例(对象)“装饰”的结果。
如:一个素颜女孩 通过 化妆品 的装饰,变成了美妆博主:
Showable:表示要展示自己妆容或素颜的女生的通用行为:展示
Girl:未经【装饰】的素颜女生,也具有 展示 的能力
Decorator:化妆品装饰器:注入的 Showable(人)将会被【化妆/装饰】
执行:素颜女孩经过【装饰】具有了妆容,成为了美妆博主
同理还有:
file 文件通过 FileInputStream 构造了文件输入流
文件输入流 FileInputStream 经过 BufferedInputStream 【装饰】具有了内存缓冲功能
内存缓冲流 BufferedInputStream 经过 ZipInputStream【装饰】具有了 Zip 格式文件的功能
客户端可以根据自己的需求自由搭配各种装饰器,每加一层装饰就会有新的特性体现出来,巧妙的设计让功能模块层层叠加,装饰之上套装饰,最终使原始对象的特性动态地得到增强。
9、适配器
适配器模式(Adapter)通常也被称为转换器,当一个对象或类的接口不能匹配用户所期待的接口时,适配器就充当中间转换的角色,以达到兼容用户接口的目的,同时适配器也实现了客户端与接口的解耦,提高了组件的可复用性。
例如:两相插头 要插入 三相插头,就需要适配器(两相转三相)。
10、享元
“享元”则是共享元件的意思。享元模式的英文flyweight是轻量级的意思,这就意味着享元模式能使程序变得更加轻量化。当系统存在大量的对象,并且这些对象又具有相同的内部状态时,我们就可以用享元模式共享相同的元件对象,以避免对象泛滥造成资源浪费。
例如一个游戏地图由四类地块构成:
绘制接口:
河流类:
道路类:
草地类、房屋类 与上类似
图件工厂类:维护图件元对象
调用:
抛弃了利用“new”关键字随意制造对象的方法,改用这个图件工厂类来构建并共享图件元,外部需要什么图件直接向图件工厂索要即可。
从而降低耗时,提升速度。
11、代理
代理模式(Proxy),顾名思义,有代表打理的意思。某些情况下,当客户端不能或不适合直接访问目标业务对象时,业务对象可以通过代理把自己的业务托管起来,使客户端间接地通过代理进行业务访问。
例如:消费者去手机专卖店买手机,而不必跟手机制造商打交道(打交道的过程由手机专卖店去执行)。
再如:顾客去 4S 店买汽车,而不是找汽车制造商购买。4S店就是顾客与制造商之间的【代理】
再如:门票代理、明星经纪人(对接明星和广告商)
互联网访问接口:
路由代理:
用户调用 routerProxy.httpAccess() 就通过 路由代理 的方式 访问互联网了。
12、桥接
桥接模式(Bridge)能将抽象与实现分离,使二者可以各自单独变化而不受对方约束,使用时再将它们组合起来,就像架设桥梁一样连接它们的功能,如此降低了抽象与实现这两个可变维度的耦合度,以保证系统的可扩展性。
为了实现可以绘制出颜色各异、形状不同的色块,可以利用桥接模式,分成不同颜色的画笔(用于绘制以及涂色)、不同形状的尺子(用于画笔沿着尺子画出不同形状)。通过将【画笔】和【尺子】——【桥接】起来,就可以实现绘制颜色各异、形状不同的色块了。同时【画笔】和【尺子】又可以独立变化(如增加蓝色、紫色、绿色等不同的画笔,如改变矩形尺子为梯形尺子等)
尺子接口:
正方形尺子:
三角形尺子:
画笔类: 【桥接】尺子
黑色画笔:
白色画笔:
执行:
门面、组合、装饰器、适配器、享元、代理、桥接为 结构型设计模式
13、模板方法
模板是对多种事物的结构、形式、行为的模式化总结,而模板方法模式(Template Method)则是对一系列类行为(方法)的模式化。我们将总结出来的行为规律固化在基类中,对具体的行为实现则进行抽象化并交给子类去完成,如此便实现了子类对基类模板的套用。
模板方法模式非常类似于定制表格:
如一张表格中需要填姓名、身份证号、联系电话、联系地址等模板表格,然后这样的模板表格可以复印、分发给其他任何人来进行填写。
再如:对于项目管理,会有需求分析、软件设计、代码开发等等标准流程,那么:
项目管理类:
而恰好, API 项目类也是这个流程, 就可以使用【项目管理类这个模板了】
API 项目类:
人力资源管理系统也可以使用这个流程,这样就有了 PM 模板的使用:
模板方法模式巧妙地结合了抽象类虚部方法与实部方法,分别定义了可变部分与不变部分,其中前者留给子类去实现,保证了系统的可扩展性;而后者则包含一系列对前者的逻辑调用,为子类提供了一种固有的应用指导规范,从而达到虚中带实、虚实结合的状态。
14、迭代器
迭代器模式(Iterator)提供了一种机制来按顺序访问集合中的各元素,而不需要知道集合内部的构造。换句话讲,迭代器满足了对集合迭代的需求,并向外部提供了一种统一的迭代方式,而不必暴露集合的内部数据结构。
简单来讲,可以把集合理解为“一堆”或者“一群”类似的元素集结起来的整体。为了承载不同的数据形式,集合类提供了多种多样的数据结构,如我们常用的 ArrayList、HashSet、HashMap 等
例如:作为一种拥有特殊数据结构的集合,弹夹可以容纳多颗子弹。向弹夹内装填子弹与压栈操作非常类似,而射击则类似于出栈操作。首先要弹出最后一次装填的子弹,子弹发射后再弹出下一颗子弹,直到弹出装填的第一颗子弹为止,最后弹夹被清空,遍历结束。这与栈集合“先进后出,后进先出”的数据结构如出一辙。
迭代器接口
行车记录仪类
调用:
15、责任链
责任链是由很多责任节点串联起来的一条任务链条,其中每一个责任节点都是一个业务处理环节。责任链模式(Chain of Responsibility)允许业务请求者将责任链视为一个整体并对其发起请求,而不必关心链条内部具体的业务逻辑与流程走向,也就是说,请求者不必关心具体是哪个节点起了作用,总之业务最终能得到相应的处理。
责任链其实类似于【踢皮球】,用户发起请求,可以由多个责任者处理请求,只要有一个责任者处理了请求即可。(比如报销申请,提交申请后,财务专员看到金额大于1000,他不会通过该请求,而是将该请求转发给财务经理,财务经理看到金额大于1000元,他也不会处理请求,而将该请求继续转发给财务总监…,直到该请求找到对应的责任者来将请求处理通过,或者被最后的或其中的一个责任者否决。)
审批人
财务专员类
财务经理类
财务总监类
执行:
16、策略
策略模式(Strategy)强调的是行为的灵活切换,比如一个类的多个方法有着类似的行为接口,可以将它们抽离出来作为一系列策略类,在运行时灵活对接,变更其算法策略,以适应不同的场景。
一个设计优秀的系统,绝不能来回更改底层代码,而是要站在高层抽象的角度构筑一套相对固化的模式,并能使新加入的代码以实现类的方式接入系统,让系统功能得到无限的算法扩展,以适应用户需求的多样性。
通过对计算机USB接口的标准化,使计算机系统拥有了无限扩展外设的能力,需要什么功能只需要购买相关的USB设备。
USB接口
USB键盘
USB鼠标
USB摄像头
计算机主机
执行:
策略模式是对算法的分立。
17、状态
状态模式(State)构架出一套完备的事物内部状态转换机制,并将内部状态包裹起来且对外部不可见,使其行为能随其状态的改变而改变,同时简化了事物的复杂的状态变化逻辑。
如红绿灯:
状态接口
红灯状态
黄灯状态
绿灯状态
交通灯类
执行:
状态模式与策略模式非常类似,其不同之处在于,策略模式是将策略算法抽离出来并由外部注入,从而引发不同的系统行为,其可扩展性更好;而状态模式则将状态及其行为响应机制抽离出来,这能让系统状态与行为响应有更好的逻辑控制能力,并且实现系统状态主动式的自我转换。状态模式与策略模式的侧重点不同,所以适用于不同的场景。总之,如果系统中堆积着大量的状态判断语句,那么就可以考虑应用状态模式,它能让系统原本复杂的状态响应及维护逻辑变得异常简单。状态的解耦与分立让代码看起来更加清晰、明了,可读性大大增强,同时系统的运行效率与健壮性也能得到全面提升。
18、备忘录
备忘录用来记录曾经发生过的事情,使回溯历史变得切实可行。备忘录模式(Memento)则可以在不破坏元对象封装性的前提下捕获其在某些时刻的内部状态,并像历史快照一样将它们保留在元对象之外,以备恢复之用。
比如浏览器的前进与后退、文档的修改与撤销、数据库的备份与恢复等。
历史快照类
文档类
19、中介
中介是在事物之间传播信息的中间媒介。中介模式(Mediator)为对象构架出一个互动平台,通过减少对象间的依赖程度以达到解耦的目的。
如婚介所、房产中介、通信基站等。
用户通过聊天室这个中介进行聊天。
聊天室抽象类
用户类
20、命令
命令是一个对象向另一个或多个对象发送的指令信息。命令的发送方负责下达指令,接收方则根据命令触发相应的行为。作为一种数据(指令信息)驱动的行为型设计模式,命令模式(Command)能够将指令信息封装成一个对象,并将此对象作为参数发送给接收方去执行,以使命令的请求方与执行方解耦,双方只通过传递各种命令过象来完成任务。
诸如响应键盘命令,按下键盘的某个键,执行对应命令:
电视机类
电视机开机命令类
电视机调频命令类
键盘控制器类
执行
21、访问者
访问者模式(Visitor)主要解决的是数据与算法的耦合问题,尤其是在数据结构比较稳定,而算法多变的情况下。为了不“污染”数据本身,访问者模式会将多种算法独立归类,并在访问数据时根据数据类型自动切换到对应的算法,实现数据的自动响应机制,并且确保算法的自由扩展。
访问者模式也许是最复杂的一种设计模式,这让很多人望而却步。为了更轻松、深刻地理解其核心思想,我们从最简单的超市购物实例开始,超市货架上摆放着琳琅满目的商品,有水果、糖果及各种酒水饮料等,这些商品有些按斤卖,有些按袋卖,而有些则按瓶卖,并且优惠力度也各不相同,所以它们应该对应不同的商品计价方法。
商品抽象类
糖果类
酒类
水果类
访问者接口
商品数据类定义好后,顾客便可以挑选商品并加入购物车了,最后一定少不了去收银台结账的步骤,这时收银员会对商品上的条码进行扫描以确定单品价格,
这就像“访问”了顾客的商品信息,并将其显示在屏幕上,最终将商品价格累加完成计价,所以收银员角色非常类似于商品的“访问者”。
假设超市对每件商品都进行一定的打折优惠,对于生产日期越早的商品打折力度越大,而过期商品则不能出售,但这种计价策略不适用于酒类商品。针对不同商品的优惠计价策略是不一样的,作为访问者的收银员应该针对不同的商品应用不同的计价方法。
折扣计价访问者类
执行:
但如果这样执行就会报错:
因为当前的 product 是泛型的 Product,而不是 Product 中具体的 Candy、Wine、Fruit。
此时可以如下改造:
接待者
糖果类的改造(其他具体产品的改造雷同)
此时再执行
访问者模式成功地将数据资源(需实现接待者接口)与数据算法(需实现访问者接口)分离开来。重载方法的使用让多样化的算法自成体系,多态化的访问者接口保证了系统算法的可扩展性,而数据则保持相对固定,最终形成一个算法类对应一套数据。此外,利用双派发确保了访问者对泛型数据元素的识别与算法匹配,使数据集合的迭代与数据元素的自动分拣成为可能。
访问者模式的核心在于对重载方法与双派发方式的利用,这是实现数据算法自动响应机制的关键所在。
22、观察者
观察行为通常是一种为了对目标状态变化做出及时响应而采取的监控及调查活动。观察者模式(Observer)可以针对被观察对象与观察者对象之间一对多的依赖关系建立起一种行为自动触发机制,当被观察对象状态发生变化时主动对外发起广播,以通知所有观察者做出响应。
订阅-发布模式:
商店类
买家类
手机买家类
海淘买家类
执行:
23、解释器
解释有拆解、释义的意思,一般可以理解为针对某段文字,按照其语言的特定语法进行解析,再以另一种表达形式表达出来,以达到人们能够理解的目的。类似地,解释器模式(Interpreter)会针对某种语言并基于其语法特征创建一系列的表达式类(包括终极表达式与非终极表达式),利用树结构模式将表达式对象组装起来,最终将其翻译成计算机能够识别并执行的语义树。例如结构型数据库对查询语言SQL的解析,浏览器对HTML语言的解析,以及操作系统Shell对命令的解析。不同的语言有着不同的语法和翻译方式,这都依靠解释器完成。
解释器是一种行为型模式,它允许你定义一个语言并解释该语言中的表达式。
说到解释器模式,映入脑海中的便是编程语言中的语法树,以及规则解析相关的内容。
在平时编码中,其实我们或多或少的已经接触到这个解释器设计模式了。比如:使用正则表达式提取相关内容,或者判断是否符合某种格式;
数学表达式解析器
// 抽象解释器接口
interface Expression {int interpret(Context context);
}// 终结符表达式类
class NumberExpression implements Expression {private int number;public NumberExpression(int number) {this.number = number;}public int interpret(Context context) {return number;}
}// 非终结符表达式类
class AddExpression implements Expression {private Expression leftExpression;private Expression rightExpression;public AddExpression(Expression leftExpression, Expression rightExpression) {this.leftExpression = leftExpression;this.rightExpression = rightExpression;}public int interpret(Context context) {return leftExpression.interpret(context) + rightExpression.interpret(context);}
}// 上下文类
class Context {// 可以在上下文中保存一些全局信息
}// 客户端代码
public class Client {public static void main(String[] args) {// 创建上下文Context context = new Context();// 创建表达式对象Expression expression = new AddExpression(new NumberExpression(2),new AddExpression(new NumberExpression(3),new NumberExpression(4)));// 解释和执行表达式int result = expression.interpret(context);System.out.println("解释和执行结果:" + result); // 输出:解释和执行结果:9}
}
设计原则
设计模式是以语言特性(面向对象三大特性)为“硬件基础”,再加上软件设计原则的“灵魂”而总结出的一系列软件模式。
一般地,这些“灵魂”原则可被归纳为5种,分别是单一职责原则、开闭原则、里氏替换原则、接口隔离原则和依赖倒置原则,它们通常被合起来简称为“S.O.L.I.D”原则。
最后我们再附加上迪米特法则,简称“LoD”。
单一职责
面向对象以“类”来划分模块边界,再以“方法”来分隔其功能。
可以将某业务功能划归到一个类中,也可以拆分为几个类分别实现,但是不管对其负责的业务范围大小做怎样的权衡与调整,这个类的角色职责应该是单一的,或者其方法所完成的功能也应该是单一的。总之,不是自己分内之事绝不该负责,这就是单一职责原则(Single Responsibility Principle)。
比如:
鞋子是用来穿的,其主要意义就是为人的脚部提供保护、保暖的功能;电话的功能是用来通话的,保证人们可以远程通信。鞋子与电话完全是两类东西,它们应该各司其职。然而有人为了省事可能会把这两个类合并为一个类,变成一只能打电话的鞋子,这就不对了。
单一职责原则由罗伯特·C.马丁(Robert C.Martin)提出,其中规定对任何类的修改只能有一个原因。例如之前的例子灯泡类,它的职责就是照明,那么对其进行的修改只能有与“照明功能”相关这样一个原因,否则不予考虑,这样才能确保类职责的单一性原则。同时,类与类之间虽有着明确的职责划分,但又一起合作完成任务,它们保持着一种“对立且统一”的辩证关系。
以最典型的“责任链模式”为例,其环环相扣的每个节点都“各扫门前雪”,这种清晰的职责范围划分就是单一职责原则的最佳实践。符合单一职责原则的设计能使类具备“高内聚性”,让单个模块变得“简单”“易懂”。
开闭原则
开闭原则(Open/Closed Principle),其中“开”指的是对扩展开放,而“闭”则指的是对修改关闭。简单来讲就是不要修改已有的代码,而要去编写新的代码。这对于已经上线并运行稳定的软件项目尤为重要。
举个例子,我们设计了一个集成度很高的计算机主板,各种部件如CPU、内存、硬盘一应俱全,该有的都已集成了,大而全的设计看似不需要再进行扩展了。
然而当用户需要安装一个摄像头的时候,我们不得不拆开机箱对内部电路进行二次修改,并加装摄像头。在满足用户的各种需求后,主板会被修改得面目全非,各种导线焊点杂乱无章。
“大而全”的模块堆叠让主板变得臃肿不堪,这就违反了开闭原则。
所以主板的设计应该是预留好接口,使得用户可以自由扩展外设,而主板则被封装于机箱中,不再需要做任何更改,这便是对扩展的开放,以及对修改的关闭。
当系统升级时,如果为了增强系统功能而需要进行大量的代码修改,则说明这个系统的设计是失败的,是违反开闭原则的。反之,对系统的扩展应该只需添加新的软件模块,系统模式一旦确立就不再修改现有代码,这才是符合开闭原则的优雅设计。
其实开闭原则在各种设计模式中都有体现,对抽象的大量运用奠定了系统可复用性、可扩展性的基础,也增加了系统的稳定性。
符合开闭原则的设计,一定要通过抽象去实现,高层抽象的泛化保证了底层实现的多态化扩展,而不需要对现有系统做反复修改。
里氏替换
里氏替换原则(Liskov Substitution Principle)是由芭芭拉·利斯科夫(Barbara Liskov)提出的软件设计规范,里氏一词便来源于其姓氏Liskov,“替换”指的是父类与子类的可替换性。
此原则指的是在任何父类出现的地方子类也一定可以出现,也就是说一个优秀的软件设计中有引用父类的地方,一定也可以替换为其子类。其实面向对象设计语言的特性“继承与多态”正是为此而生。
策略模式就是很好的例子。例如我们要使用计算机进行文档录入,计算机会依赖抽象USB接口去读取数据,至于具体接入什么录入设备,计算机不必关心,可以是手动键盘录入,也可以是扫描仪录入图像,只要是兼容USB接口的设备就可以对接。这便实现了多种USB设备的里氏替换,让系统功能模块可以灵活替换,功能无限扩展,这种可替换、可延伸的软件系统才是有灵魂的设计。
接口隔离
接口隔离原则(Interface Segregation Principle)指的是对高层接口的独立、分化,客户端对类的依赖基于最小接口,而不依赖不需要的接口。简单来说,就是切勿将接口定义成全能型的,否则实现类就必须神通广大,这样便丧失了子类实现的灵活性,降低了系统的向下兼容性。反之,定义接口的时候应该尽量拆分成较小的粒度,往往一个接口只对应一个职能。
其实接口隔离原则与单一职责原则如出一辙,只不过前者是对高层行为能力的一种单一职责规范,这非常好理解,分开的容易合起来,但合起来的就不容易分开了。接口隔离原则能很好地避免了过度且臃肿的接口设计,轻量化的接口不会造成对实现类的污染,使系统模块的组装变得更加灵活。
依赖倒置
面向对象中的依赖是类与类之间的一种关系,如H(高层)类要调用L(底层)类的方法,我们就说H类依赖L类。依赖倒置原则(Dependency Inversion Principle)指高层模块不依赖底层模块,也就是说高层模块只依赖上层抽象,而不直接依赖具体的底层实现,从而达到降低耦合的目的。
总经理决定年底要上线一套全新的办公自动化软件。那么总经理作为发起方该如何实施这个计划呢?直接发动基层程序员并调用他们的研发方法吗?
显然不是。
总经理会调用IT部门经理的work方法并传入目标即可,至于这个work方法的具体实现者也许是架构师甲,也可能是程序员乙,总经理也许根本不认识他们,这就达到了公司高层与底层员工实现解耦的目的。这就是将“高层依赖底层”倒置为“底层依赖高层”的好处。
迪米特法则
迪米特法则(law of Demeter)也被称为最少知识原则,它提出一个模块对其他模块应该知之甚少,或者说模块之间应该彼此保持陌生,甚至意识不到对方的存在,以此最小化、简单化模块间的通信,并达到松耦合的目的。
例如,
一台游戏机,主机内部集成了非常复杂的电路及电子元件,这些对外部来说完全是不可见的,就像一个黑盒子。虽然我们看不到黑盒子的内部构造与工作原理,但它向外部开放了控制接口,让我们可以接上手柄对其进行访问,这便构成了一个完美的封装
除了封装起来的黑盒子主机,手柄是另一个封装好的模块,它们之间的通信只是通过一根线来传递信号,至于主机内部的各种复杂逻辑,手柄一无所知。
**“门面模式”**就是极好的范例。例如我们去某单位办理一项业务,来到业务大厅一脸茫然,各种填表、盖章等复杂的办理流程让人一头雾水,有可能来回折腾几个小时。假若有一个提供快速通道服务的“门面”办理窗口,那么我们只需简单地把材料递交过去就可以了,“办理人“与“门面”保持最简单的通信,对于门面里面发生的事情,办理人则知之甚少,更没有必要去亲力亲为。
总结
在面向对象软件系统中,优秀的设计模式一定不能违反设计原则,恰当的设计模式能使软件系统的结构变得更加合理,让软件模块间的耦合度大大降低,从而提升系统的灵活性与扩展性,使我们可以在保证最小改动或者不做改动的前提下,通过增加模块的方式对系统功能进行增强。
相较于简单的代码堆叠,设计模式能让系统以一种更为优雅的方式解决现实问题,并有能力应对不断扩展的需求。
随着业务需求的变动,系统设计并不是一成不变的。在设计原则的指导下,我们可以对设计模式进行适度地改造、组合,这样才能应对各种复杂的业务场景。然而,设计模式绝不可以被滥用,以免陷入“为了设计而设计”的误区,导致过度设计。
世界上并不存在无所不能的设计,而且任何事物都有其两面性,任何一种设计模式都有其优缺点,所以对设计模式的运用一定要适可而止,否则会使系统臃肿不堪。满足目前需求,并在未来可预估业务范围内的设计才是最合理的设计。当然,在系统不能满足需求时我们还可以做出适当的重构,这样的设计才是切合实际的。