本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!
将自己的设计模式笔记贡献出来啦,这个话题真不敢大气出,哈哈毕竟很多珠玉在前了。没有谦虚,这其实就是一篇笔记,是给自己速记用的,我平时在开发一个功能模块前,也都会花点时间将所有模式过一遍,看看能不能真撞出了个啥设计灵感。
不过模式是“活的”,别让设计模式给你带来困扰,了解它的“意图”就好,别中毒:如果你只有一把铁锤,那么任何东西看上去都像是钉子。
代理模式
代理是最基本的设计模式之一。它能够插入一个用来替代“实际”对象的“代理”对象,来提供额外的或不同的操作。这些操作通常涉及到与“实际”对象的通信,因此“代理”对象通常充当着中间人的角色。
代理对象为“实际”对象提供一个替身或占位符以控制对这个“实际”对象的访问。被代理的对象可以是远程的对象,创建开销大的对象或需要安全控制的对象,来看下类图结构:
再来看下类图对应代码,这是IObject接口,真实对象RealObj和代理对象ObjProxy都实现此接口:
/**
* 为实际对象Tested和代理对象TestedProxy提供对外接口
*/
public interface IObject {
void request();
}
复制代码
RealObj是实际处理request() 逻辑的对象,但是出于设计的考量,需要对RealObj内部的方法调用进行控制访问
public class RealObject implements IObject {
@Override
public void request() {
// 模拟一些操作
}
}
复制代码
ObjProxy是RealObj的代理类,其同样实现了IObject接口,所以具有相同的对外方法。客户端与RealObj的所有交互,都必须通过ObjProxy。
public class ObjProxy implements IObject {
IObject realT;
public ObjProxy(IObject t) {
realT = t;
}
@Override
public void request() {
if (isAllow()) realT.request();
}
private boolean isAllow() {
return true;
}
}
复制代码
装饰者模式
关于装饰者模式的定义,我就直接引用Head First了:装饰者模式动态的将责任附加到对象上。若要扩展功能,装饰者提供了比继承更有弹性的替代方案。
其实装饰者模式的重点在于给对象动态的附加职责,通过对象组合的方式,运行时装饰对象,在不改变任何底层代码的情况下,给现有对象赋予新的职责。
举个例子
现在要为卖煎饼的大妈设计一套系统,让大妈能更好的算账收钱。大妈主要经营煎饼(7元),并可以往其中添加配料烤肠(1元),鸡蛋(1元),如果生意红火,可能会考虑扩大经营其他小吃或添加配料种类。现需要设计出一套系统,以便快速计算出每位顾客所购小吃的价格。
方案
这是一个食品类接口。常说的,要符合针对接口编程,不针对实现编程的OO原则。
public interface ISnack {
String getDescription(); // 食物的描述
double cost(); // 食物的价格
}
复制代码
这是具体的我们的煎饼类。
public class PancakeSnack implements ISnack {
@Override public String getDescription() {
return "煎饼";
}
@Override public double cost() {
return Price.PancakeSnack.price;
}
}
复制代码
这是一个小料类接口 IDecoratorSnack ,其继承自 ISnack 接口:
public interface IDecoratorSnack extends ISnack {
// 可根据需要扩展属性,如配料大份,小份等
}
复制代码
食品类(被装饰者)既可以单独使用,也可以被配料类(装饰者)包着使用,因为装饰者和被装饰者对象具有相同的超类型,所以在任何需要原始对象(被包装的)的场合,都可以用装饰过的对象替代它。
所以,我想要一份加鸡蛋的煎饼的价格是多少?
public class Egg implements IDecoratorSnack {
// 持有被修饰的食品对象
ISnack iSnack;
public Egg(ISnack iSnack) {
this.iSnack = iSnack;
}
@Override public String getDescription() {
return iSnack.getDescription() + "+鸡蛋";
}
@Override public double cost() {
/* 被修饰对象的价格 + 鸡蛋价格,想想这里我为啥不说煎饼+鸡蛋? */
return iSnack.cost() + Price.Egg.price;
}
}
复制代码
加烤肠的小吃同理,来看代码:
public class Sausage implements IDecoratorSnack {
ISnack iSnack;
public Sausage(ISnack iSnack) {
this.iSnack = iSnack;
}
@Override public String getDescription() {
return iSnack.getDescription() + "+香肠";
}
@Override public double cost() {
/* 被修饰对象的价格 + 烤肠价格 */
return iSnack.cost() + Price.Sausage.price;
}
}
复制代码
我们来收钱吧。我们能够发现,我们能够使用被装饰的对象就像使用原始对象一样,这归功于装饰者与被装饰者具备同样的接口。同样一个原始对象是能够被包装多层的,而在使用者的眼里,它只是一个被赋予了新功能的原始对象而已,这也是我为啥不说 Egg 是煎饼+鸡蛋的原因:
public class DecoratorPatternTest {
public static void main(String[] ags) {
// 价格:煎饼 + 鸡蛋 + 烤肠
ISnack snackOneISnack = new PancakeSnack();
snackOneISnack = new Egg(snackOneISnack);
snackOneISnack = new Sausage(snackOneISnack);
System.out.println(snackOneISnack.cost() + “元”);
}
}
复制代码
是不是很巧妙,每个装饰者都持有一个被装饰者的对象,这个被装饰者对象不一定是原始对象,也可能是被包装了多层的对象。通过这种组合,加入了新的行为。
需要注意的是,通过利用装饰者模式,会造成设计中产生大量的小类,如果过度使用,会使程序变得很复杂。另外可能还会出现类型问题,如果把代码写成依赖于具体的被装饰者类型,不针对抽象接口进行编程,那么就会出现问题。
番外
代理模式和装饰者模式不管是在类图,还是在代码实现上,几乎是一样的,但我们为何还要进行划分呢?其实学设计模式,不能拘泥于格式,不能死记形式,重要的是要理解模式背后的意图,意图只有一个,但实现的形式却可能多种多样。这也就是为何那么多变体依然属于xx设计模式的原因。
代理模式的意图是替代真正的对象以实现访问控制,而装饰者模式的意图是为对象加入额外的行为。
外观模式
外观模式提供了一个统一的接口,用来访问子系统中的一群接口。外观定义了一个高级接口,让子系统更容易使用。听起来很抽象,其实外观模式的本质就是使用组合的方式,来管理一个子系统内的一群对象,它的意图即提供一个接口,好让一个子系统更易于使用。
举个例子
假如厂商的生态工厂提供了一系列智能设备,并对外公开了如下接口,方便用户远程控制:
这是上图的智能灯类:
public class Light {
public void on() {
System.out.println("开灯");
}
public void off() {
System.out.println("关灯");
}
}
复制代码
这是智能空调类:
public class AirConditioner {
public void on() {
System.out.println("打开空调");
}
public void adjustTemperature() {
System.out.println("调节温度");
}
public void off() {
System.out.println("关闭空调");
}
}
复制代码
这是智能热水器类:
public class Heater {
public void on() {
System.out.println("打开热水器蓄水加热");
}
public void adjustTemperature() {
System.out.println("调节温度");
}
public void off() {
System.out.println("关闭热水器");
}
}
复制代码
正常情况下,用户回到家中,可能都需要进行如下操作,见下:
public class ClientTest {
private static Light mLight; // 智能灯
private static AirConditioner mAc; // 智能空调
private static Heater mHeater; // 智能热水器
public static void main(String[] ags) {
/**
* 对用户来说,需要进行一系列操作,是不是特别繁琐。
* <ul>
* <li>打开灯光
* <li>打开空调
* <li>调节室内温度
* <li>打开热水器准备热水
* <li>调节水温
* </ul>
*/
mLight.on();
mAc.on();
mAc.adjustTemperature();
mHeater.on();
mHeater.adjustTemperature();
}
public ClientTest(Light light,
AirConditioner ac,
Heater heater) {
this.mLight = light;
this.mAc = ac;
this.mHeater = heater;
}
}
复制代码
这对于用户来说就显得非常繁琐不友好了。外观模式正是在这种情况下应运而生的。外观对用户提供简化的接口,将用户类与具体的智能设备类解耦。
方案
我们来创建一个外观类:
public class HardwareFacade {
private Light mLight;
private AirConditioner mAc;
private Heater mHeater;
/**
* 为智能硬件这个子系统提供一个简单接口。
* <p>用户到家只需要调用这一个请求,
* 就可以享受有灯、有水、凉飕飕。
*/
public void openAppliance() {
mLight.on();
mAc.on();
mAc.adjustTemperature();
mHeater.on();
mHeater.adjustTemperature();
}
public HardwareFacade(Light light,
AirConditioner ac,
Heater heater) {
this.mLight = light;
this.mAc = ac;
this.mHeater = heater;
}
}
复制代码
用户只需与我们的外观类 HardwareFacade 进行交互即可,这就大大简化了用户的操作。外观类通过组合的方式实现了对智能硬件这个子系统的管理,让智能硬件与用户实现了解耦。当然这并不影响用户单独对于具体智能硬件类的使用,即外观提供简单接口的同时,依然将系统完整的功能暴露出来,以供用户需要时使用。
再来看下用户类的调用代码:
public class ClientTest {
public static void main(String[] ags) {
HardwareFacade facade = new HardwareFacade(light, ac, heater);
facade.openAppliance();
}
}
复制代码
番外
外观与代理模式的相似之处在于它们都缓存了一个复杂实体并自行对其进行初始化。 代理与其服务对象遵循同一接口, 使得自己和服务对象可以互换, 在这一点上它与外观不同。
适配器模式
适配器模式将一个类的接口,转换成客户期望的另一个接口。适配器让原本接口不兼容的类可以合作无间。 适配器模式的意图是进行接口的转换,这点很重要,这是区分装饰者模式的“扩展行为”和代理模式的“访问控制”的依据。
举个例子
现有硬件厂商提供一种智能音箱,其对外提供了“打开”和“关闭”两个接口:
这是智能设备接口类:
public interface Ihardware {
void on();
void off();
}
复制代码
这是具体的智能音箱类:
public class SpeakerBox implements Ihardware {
@Override public void on() {
System.out.println("打开音箱");
}
@Override public void off() {
System.out.println("关闭音箱");
}
}
复制代码
现厂商对所属硬件进行了技术升级,发现“关闭”接口并不常用,所以替换成了“休眠”接口,现智能设备接口类2.0见下:
public interface Ihardware2 {
void on();
void sleep();
}
复制代码
这是新版智能音箱类2.0具体类:
public class SpeakerBox2 implements Ihardware2 {
@Override public void on() {
System.out.println("体验版-打开音箱");
}
@Override public void sleep() {
System.out.println("体验版-休眠音箱");
}
}
复制代码
公司A,一直是该厂商智能音箱的忠实用户,今天在将智能音箱从1.0升级到2.0后,发现已有的业务无法正常运行了。在痛斥硬件厂商私自更改公开接口外,公司A的工程师只能先进行紧急修复。
方案
为了兼容老版本和尽可能少的代码改动,工程师老王提出:我们要将不兼容的接口变成兼容的接口。可怎么将2.0的接口转换成客户期待的1.0接口呢?适配器对象见下:
public SpeakerBox2Adapter implements Ihardware {
private SpeakerBox2 mSb2;
public SpeakerBox2Adapter(SpeakerBox2 sb2) {
this.mSb2 = sb2;
}
@Override public void on() {
mSb2.on();
}
@Override public void off() {
/* 当然这里可以进行一系列的转换逻辑 */
mSb2.sleep();
}
}
复制代码
通过适配器对象,我们就能对SpeakerBox2 对象像SpeakerBox一样使用了。测试代码见下:
public class ClientTest {
public static void main(String[] ags) {
Ihardware hw1 = new SpeakerBox();
hw1.on();
hw1.off();
Ihardware hw2 = new SpeakerBox2Adapter(new SpeakerBox2());
hw2.on();
hw2.off();
}
}
复制代码
番外
外观模式为现有对象定义了一个新接口, 适配器模式则会试图运用已有的接口。 适配器通常只封装一个对象, 外观通常会作用于整个对象子系统上。适配器模式的意图是进行接口的转换,这也是区分装饰者模式的“扩展行为”和代理模式的“访问控制”的依据。
再多说一嘴,适配器可以对已有对象的接口进行修改,装饰模式则能在不改变对象接口的前提下强化对象功能。此外,装饰还支持递归组合,适配器则无法实现。
适配器能为被封装对象提供不同的接口,代理模式能为对象提供相同的接口,装饰则能为对象提供加强的接口。
命令模式
命令模式将“请求”封装成对象,以便使用不同的请求。目的是将发起请求的对象和具体执行处理请求的对象完全解耦,并能灵活扩展具有不同操作的接收者对象。
举个例子
有厂家发布了一款遥控器,声称可以控制旗下的所有硬件设备。你入手了一台,对着家里的智能台灯就按下“智能键”按钮,哎灯亮了,又按了下,灯灭了。
这是智能灯类,当前只有两个方法,即开灯和关灯操作:
public class Light {
public void on() {
System.out.println("开灯");
}
public void off() {
System.out.println("关灯");
}
}
复制代码
这是我们的遥控器类了,我们的第一想法可能是直接创建一个智能硬件接口 Ihardware ,让我们的 Control 直接持有 Ihardware:
public class Control {
Ihardware mHardware;
/** 点击智能键按钮 */
public void btnPressed() {
if ("开逻辑") { mHardware.on() }
else if(“关逻辑”) { mHardware.off() }
}
}
复制代码
可当你又对着净化器按下此按钮,净化器居然开始“自动换气”了,你发现随着硬件的稀奇古怪的接口增多, Ihardware 接口变得不适用了。
public class AirCon {
/* 换气 */
public void changeOfAir();
}
复制代码
方案
针对上面那种问题,我们可以创建一个命令对象接口:
public interface Command {
public void execute();
}
复制代码
这是具体的智能灯命令对象,是 Command 的实现类,我们将“开灯这个操作”封装成了一个请求类:
public class LightOnCommand implements Command {
private Light mLight;
public LightOnCommand(Light light) {
mLight = light;
}
@Override public void execute() {
mLight.on();
}
}
复制代码
同样我们将“接收者关灯”这个请求,也封装成对象。
public class LightOffCommand implements Command {
private Light mLight;
public LightOnCommand(Light light) {
mLight = light;
}
@Override public void execute() {
mLight.off();
}
}
复制代码
这是我们的遥控器类,通过下面的方式就可以动态的执行命令了,代码见下:
public class Control {
private Command mComBtn;
public void setCommand(Command comBtn) {
mComBtn = comBtn;
}
public void btnPressed() {
mComBtn.execute();
}
}
复制代码
这样发出请求的对象(Control 类)与接收者(Light)是完全解耦的,两者是通过命令对象(Command实现类)来沟通的,命令对象封装了接收者的一组动作,当然可多组,这里即“打开智能灯”,“关闭智能灯”的操作。我们来进行下测试:
public class Test {
public static void main(String[] ags) {
Control control = new Control();
Light light = new Light();
// 智能键开灯操作
control.setCommand(new LightOnCommand(light));
control.btnPressed();
// 智能键关灯操作
control.setCommand(new LightOffCommand(light));
control.btnPressed();
}
}
复制代码
这个时候再想想“自动换气”功能,是不是变得简单了?我们只需要将“自动换气”这个请求封装成对象 AirConCommand,而完全不必更改 Control 对象。
番外
我们可以使用命令来将任何操作转换为对象,操作的参数将成为对象的成员变量。当然这也会和装饰者模式一样,会造成大量的小类,所以具体的选型需要全方位衡量判断。
策略模式
策略模式定义了算法蔟,并分别封装起来,让它们之间可以互相替换,此模式让算法的变化独立于使用算法的对象。
举个例子
叮叮当,微信群~
小王:周六公园团建,安排见下,望周知:
- 09:00-09:15 集合
- 09:15-11:00 登山
- 11:00-13:00 山顶午餐
小王:时间较紧,大家请自行合理安排上山路线
-
上山路程最短,较陡,xx->xx
-
适合拍照,景色美,xx->xx
-
山路平坦,xx->xx
方案
这是上山路线的策略接口:
public interface IStrategyRoutes {
// 上山路线信息
void mcRoutesInfo();
}
复制代码
这是为员工提供的路线A,路程最短。登山路线的不同,这也是这个需求(集合-登山-午餐)每个员工变化的部分,我们之所以把每种具体的上山路线都单独的封装起来,而不是将方法一个个的添加到各自员工类内部,是因为考虑到代码的复用,符合封装变化的OO原则:
public class ShortestWalkRoutes implements IStrategyRoutes {
@Override public void mcRoutesInfo() {
System.out.println("登山:上山路程最短");
}
}
复制代码
这是为员工提供的路线B,适合拍照,景色美:
public class CameraRoutes implements IStrategyRoutes {
@Override public void mcRoutesInfo() {
System.out.println("登山:适合拍照,景色美");
}
}
复制代码
这是为员工提供的路线C,山路好走:
public class EasyRoadRoutes implements IStrategyRoutes {
@Override public void mcRoutesInfo() {
System.out.println("登山:山路好走");
}
}
复制代码
同样我们可以将每个员工的共性抽离出来。在StaffBase类中加入接口类型变量(而不是具体的上山路线),每个员工都会动态的设置这个变量意在运行时引用正确的行为类型(例如:步行最少、适合拍照等),所以我们应该针对接口编程,而非针对实现编程。
这是一个员工类的基类:
public abstract class StaffBase {
IStrategyRoutes routes; // 策略类
public void setRoutes(IStrategyRoutes routes) {
this.routes = routes;
}
public void showStaffRoutes() {
routes.trafficRoutesInfo();
}
}
复制代码
登山时,员工可以选择适合自己的上山路线,如员工李四。这里采用静态工厂方法,让员工的代码与路线对象创建代码解耦。添加其他员工,我们只需要添加新类并继承 StaffBase 基类即可,不需要更改其他代码,符合对扩展开放,对修改关闭原则。
public class StaffOne extends StaffBase {
public StaffOne(){
// 员工李四:通过工厂方法选择了路线1
routes = RoutesFactory.getRoutes(1);
}
}
复制代码
最后来个测试类吧,打印员工李四 StaffOne 的路线方案:
public class StrategyPatternTest {
public static void main(String[] args) {
message("集合");
StaffBase staff1 = new StaffOne();
staff1.showStaffRoutes();
message("午餐");
}
}
复制代码
再回头来看下策略模式的定义:我们定义了算法蔟,并分别封装起来,让它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。
在本例中,上山具有不同的路线方案,都能达到到达山顶的目的,我们可称其为策略,而具体的路线策略变更对于团建活动并无影响,员工可根据需要自由选择就好。
模版方法模式
模版方法模式在一个方法中定义了一个算法的骨架,而将一些步骤延迟到子类中。模版方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
这和上面的策略模式很相似,但是策略模式是封装可替换的行为,然后使用组合的方式来决定采用哪一个行为,其意图是提供一组可替换的算法,让算法的变化独立于使用算法的客户,即用户可选择不同的算法。
而我们的模版方法模式,是提供一个算法的大纲,而由子类来决定如何实现算法中的步骤,即用户可自行决定如何实现这套算法中的步骤,但却不能更换“不同的”算法。
举个例子
我们以线上购物为例,在购物时,一般有下面几个步骤:
我现在创建一个购物类,并将购物流程封装在一个方法内,这个方法就是模版方法模式里的“方法”二字了,细品下,为啥叫模版方法模式,而不是模版模式。
这里将该方法使用 final 关键字修饰,以防止子类篡改算法模版。 能够注意到,该类是抽象类,其中包含 “下单” 和 “配送” 两个抽象方法,其意图就是由具体子类来决定实现的细节。不仅如此,“发货” 和 “收货” 两个方法,也体现了代码复用的思想。
public abstract class OnlineShopping {
final void shoppingProcess {
order(); // 下单
delivery() // 发货
distribution(); // 配送
receipt(); // 收货
}
abstract void order();
void delivery() {
message("卖家发货");
}
abstract void distribution();
void receipt() {
message("买家收货");
}
}
复制代码
接下来创建一个具体的某东商城购物类。JDShopping 实现了OnlineShopping 类的抽象方法,能够看出,我们并没有破坏掉算法模版,也没有改变算法的顺序,我们仅仅是给了某些步骤一些“符合自身”的具体的操作,即为某个步骤定义了不同的实现细节。
public class JDShopping extends OnlineShopping {
@Override void order() {
System.out.println("某东自营店铺下单");
}
@Override void distribution() {
System.out.println("使用某东快递配送");
}
}
复制代码
同理,来创建一个具体的某宝商城购物类:
public class TBShopping extends OnlineShopping {
@Override void order() {
System.out.println("某宝小店下单");
}
@Override void distribution() {
System.out.println("使用某风快递配送");
}
}
复制代码
我们来执行一段测试程序:
public class ClientTest {
public static void main(String[] ags) {
// 某东商城购物流程
JDShopping jd = new JDShopping();
jd.shoppingProcess();
// 某宝商城购物流程
TBShopping tb = new TBShopping();
tb.shoppingProcess();
}
}
复制代码
番外
模板方法模式基于继承机制:它允许你通过扩展子类中的部分内容来改变部分算法。策略基于组合机制:你可以通过对相应行为提供不同的策略来改变对象的部分行为。模板方法在类层次上运作, 因此它是“静态”的。策略在对象层次上运作,因此允许在运行时切换行为。
状态模式
状态模式允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。状态模式的意图是封装状态,将状态封装成独立的类,并将动作委托给当前的状态对象,让当前状态在状态对象间游走改变,从而使我们能够通过组合的方式引用不同的状态对象来改变类的行为。
举个例子
以地铁站自动售票机为例(与真实流程会有出入,此处重点在于封装状态的思想),用户可执行如下操作:投币、退款、购买、出票4个动作,而执行这些动作会因用户处于不同状态而具有不同的行为。如:
- 在未投币状态,用户可选择投币并进入已投币状态,而不能退款、购票、打印车票;
- 在已投币状态,用户可继续投币,或退款回到未投币状态,或购票进入购票状态,但不能直接打印车票。
- 在购票状态,也可理解成提交订单状态,购票成功则自动出票,并回到未投币状态。
如果我们不使用状态模式,而是直接硬写,你会发现每个动作都会伴随着大量的 if 判断,假设我们又扩展一个“非运营时间状态”呢?甚至多个呢?是不是要疯了。
public class TicketMachine {
static final NoSlotStateType = 0; // 未投币状态
static final SlotStateType = 1; // 已投币状态
static final BuyingTicketStateType = 2; // 购票状态
/**
* 以投币动作为例:
* <p>如果我们再增加其他状态呢?
* 是不是每个方法都需要重新添加判断。
*/
public void toSlot(int money) {
// 当前是未投币状态
switch (curState) {
case NoSlotStateType:
curState = SlotState;
break;
case SlotStateType:
message("已再次投币!");
break;
case BuyingTicketStateType:
message("此状态不支持投币!");
break;
}
}
...
}
复制代码
方案
状态模式是一种可以当作不需要多条件判断的替代方案。我们将每个状态单独抽离成类,而在每个状态对象中,各自响应着用户的动作。下面采用状态模式实现的自动售票机类:
public class TicketMachine {
/**
* 这是状态类的具体实现。
* 详情见下{@code NoSlotState}。
*/
NoSlotState noSlotState; // 未投币状态
SlotState slotState; // 已投币状态
BuyingTicketState btState; // 购票中状态
/* 可根据状态动态指定到具体执行类 */
TicketState curState; // 当前状态抽象类
/**
* 在构造中初始化当前状态为未投币状态。
*/
public TicketMachine() {
this.curState = noSlotState;
noSlotState = new NoSlotState(this);
slotState = new SlotState(this);
btState = new BuyingTicketState(this);
}
/**
* 将动作委托给当前的状态对象处理。
*/
public void toSlot(int money) {
curState.toSlot();
}
public void refund() {
curState.refund();
}
public void tickets() {
curState.tickets();
// 当购买请求成功后,会自动出票
printTicket();
}
public void printTicket() {
curState.printTicket();
}
/**
* 当前状态在状态对象间游走改变。
*/
public void setState(TicketState state) {
this.curState = state;
}
/**
* 提供了get方法,防止状态对象间彼此耦合。
*/
public TicketState getNoSlotState() {
return noSlotState;
}
public TicketState getSlotState() {
return slotState;
}
public TicketState getBuyingState() {
return btState;
}
}
复制代码
这是我们的状态接口类。之所以使用抽象类,是因为我们可以将每个状态内复用的处理逻辑,放在超类中。
public abstract class TicketState {
/**
* 这是投币动作。
*
* <p>我们将用户的操作抽离出来。
* 其实状态模式就是多条件判断的替代方案。
* 在每个状态下,用户都有可能会执行下面
* 的动作,在不合规时要提醒用户。
*/
abstract void toSlot(int money);
/**
* 这是退款动作。
*/
abstract void refund();
/**
* 这是购票动作。
*/
abstract void tickets();
/**
* 这是打印车票动作。
*/
abstract void printTicket();
...
}
复制代码
这是我们的未投币首页类。可以看到在此类中,处理着在“未投币状态”下用户所有可能会触发的操作,并随时更新自动售票机的状态。
public NoSlotState implements TicketState {
TicketMachine tm;
public NoSlotState(TicketMachine tm) {
this.tm = tm;
}
/**
* 在未投币状态,执行投币操作后,变成已投币状态。
*/
@Override public void toSlot(int money) {
tm.setState(tm.getSlotState());
}
/**
* 在未投币状态,不能退款等操作。
*/
@Override public void refund() {
System.out.println("您还未投币!");
}
@Override public void tickets() {
System.out.println("需投币后操作!");
}
@Override public void printTicket() {
System.out.println("需投币后操作!");
}
}
复制代码
这是我们的已投币状态类:
public SlotState implements TicketState {
TicketMachine tm;
public SlotState(TicketMachine tm) {
this.tm = tm;
}
/**
* 在购买前支持多次投币。
*/
@Override public void toSlot(int money) {
tm.setState(tm.getSlotState());
}
/**
* 购票前可以申请退款。
*/
@Override public void refund() {
tm.setState(tm.getNoSlotState());
}
/**
* 如果金额足够,则发起购票请求。
*/
@Override public void tickets() {
if (isEnough)
tm.setState(tm.getBuyingState());
else
System.out.println("金额不够!");
}
@Override public void printTicket() {
System.out.println("够票后可操作!");
}
}
复制代码
这是我们的购票状态类:
public BuyingTicketState implements TicketState {
TicketMachine tm;
public BuyingTicketState(TicketMachine tm) {
this.tm = tm;
}
/**
* 出票中,不支持投币、退款、重复提交等操作。
*/
@Override public void toSlot(int money) {
System.out.println("出票中,不支持投币!");
}
@Override public void refund() {
System.out.println("出票中,不支持退款!");
}
@Override public void tickets() {
System.out.println("不支持重复提交打印请求");
}
/**
* 出票成功后,切换状态为未投票状态。
*/
@Override public void printTicket() {
System.out.println("已出票!");
tm.setState(tm.getNoSlotState());
}
}
复制代码
通过上面代码我们能发现,我们不仅避免了在售票机类TicketMachine 中大量的 if 判断,还将“主要的变化”封装并抽离了出来,让当前已有的状态对修改关闭。
当然状态模式也会造成大量的小类,这是为了弹性而付出的代价,但这绝对是值得的。其实真正重要的是我们暴露给客户的类数目,而不是我们自己的类数目,我们完全可以将这些额外的状态类对外隐藏起来。
番外
状态可被视为策略的扩展。两者都基于组合机制:它们都通过将部分工作委派给 “帮手” 对象来改变其在不同情景下的行为。策略使得这些对象相互之间完全独立,它们不知道其他对象的存在。但状态模式没有限制具体状态之间的依赖,且允许它们自行改变在不同情景下的状态。
观察者模式
观察者模式定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。我们通常把有状态的对象称为主题,收到主题通知的依赖者对象称为观察者。主题和观察者定义了一对多的关系,观察者依赖于主题,只要主题状态一有变化,观察者就会被通知。
举个例子
以彩票官网为例,彩民可以自由的向其注册或取消注册,当中奖号码更新后,即官网此状态改变后,每个注册过的彩民都会收到官网传来的通知。这里的官网就相当于我们所说的主题,彩民相当于我们的观察者。
我们可先创建一个主题接口类:
/**
* 这是主题类。
* <p>用户只要向其注册,主题状态改变后,
* 就可以收到官网发送来的彩票信息。
*/
public interface ISubject {
/** 给彩民用户提供的注册和移除方法 */
void registerObserver(IObserver o);
void removeObserver(IObserver o);
/** 给用户发送“彩票信息变化通知” */
void notifyLottery();
}
复制代码
为什么要使用接口,而不是直接使用具体的主题类,因为不想主题与观察者过分耦合,要努力使对象之间的互相依赖降到最低,这样才能够应付变化,建立有弹性的OO系统。
这是一个彩民接口即观察者接口,这个接口只有一个 updateLottery 方法,当主题的状态改变时它就会被调用:
/**
* 观察者接口类。
*
* <p>所有的观察者都必须实现该接口,关于观察者的一切
* 主题只知道观察者实现了当前接口即 IObserver
* 主题不需要知道观察者的具体类是谁、做了些什么或其他任何细节
* 这就使主题和观察者之间的依赖程度非常低。
*/
public interface IObserver {
/** 当知道彩票信息更新后的处理方法 */
void updateLottery(Lottery lottery);
}
复制代码
这是一个具体的主题类,一个具体的主题总是实现主题接口,除了注册和取消注册方法之外,具体主题还实现了 notifyLottery() 方法,此方法用于在状态改变时更新所有当前观察者,即彩票信息改变时,将彩票的当前信息通知给彩民。
public class LotteryData implements ISubject{
private ArrayList<IObserver> list = new ArrayList<>();
/* 彩票信息类,可根据需要在 notify 前刷新 */
private Lottery lottery;
@Override public void registerObserver(IObserver o) {
list.add(o);
}
@Override public void removeObserver(IObserver o) {
final int index = ;
if((index = list.indexOf(o)) != -1) {
list.remove(index);
}
}
@Override public void notifyLottery() {
for(IObserver o : list) {
o.updateLottery(lottery);
}
}
/**
* 模拟智能彩票机开始摇号。
*/
public void beginWork() {
new Timer().schedule(() -> notifyLottery(), 0, 5000);
}
}
复制代码
这是彩票的实体类,包括彩票的所属日期和当前中奖号码:
public class Lottery {
/** 彩票的日期 */
private Date date;
/** 彩票的获奖数字 */
private int winningCount;
}
复制代码
这是具体的观察者彩民1号,观察者必须实现 IObserver 接口和注册具体主题,以便接收更新:
public class LotteryBuyerOne implements IObserver{
public LotteryBuyerOne(ISubject s) {
s.registerObserver(this);
}
@Override public void updateLottery(Lottery lottery) {
System.out.println("中奖号码:"+lottery.winningCount);
}
}
复制代码
根据需要我们可以随意添加观察者,因为观察者和主题之间是松耦合的,所以我们改变观察者或者主题其中一方,并不会影响另一方。我们来测试一下这个设计吧。
public class ObserverPatternTest {
public static void main(String[] args) {
final LotteryData subject = new LotteryData();
final LotteryBuyerOne loOne = new LotteryBuyerOne(subject);
subject.beginWork();
}
}
复制代码
内置观察者模式
除了我们自己实现一整套观察者模式,java 还提供了内置的观察者模式。java.util 包(package)内包含最基本的 Observer 接口和 Observable 类,这和我们的 Observer 接口和 Subject 接口很相似。同样的场景我们用内置观察者模式看下:
这是一个具体的主题类,因为 Observable 是个具体类而不是接口,所以在扩展性上不是很灵活,限制了Observable 的复用潜力。
public class LotteryData extends Observable {
private Lottery lottery;
public void beginWork() {
new Timer().schedule(() -> updata(), 0, 5000);
}
public Date getDate() {
return lottery.date;
}
public int getWinningCount() {
return lottery.winningCount;
}
private void updata() {
setChanged(); // 改变状态
notifyObservers(this); // 通知观察者
}
}
复制代码
Observable 为我们提供了 notifyObservers() 方法和 notifyObservers(Object arg) 方法,所以如果你想推(push)数据给观察者,直接可以把数据对象传递给一个参数的更新方法,而如果你想让观察者拉(pull)数据,只需要调用无参数更新方法,同时提供公开的 get 方法即可。这是具体的观察者彩民1号:
public class LotteryBuyerOne implements Observer{
public LotteryBuyerOne(Observable observable) {
observable.addObserver(this);
}
@Override public void update(Observable o, Object arg) {
// 当彩票状态改变的时候,彩民需要获得通知更新
if(o != null && o instanceof LotteryData){
LotteryData lotteryData = (LotteryData)o;
System.out.println("中奖号码为:" + lotteryData.winningCount);
}
}
}
复制代码
来测试一下这个设计吧。需要注意的是,内置的观察者模式,通知的次序不同于我们注册的次序,所以当我们对于通知顺序有要求的时候,不能使用内置的观察者模式。
public class BuiltInObserverPatternTest {
public static void main(String[] args) {
final LotteryData lotteryData = new LotteryData();
final LotteryBuyerOne lBuyerOne = new LotteryBuyerOne(lotteryData);
lotteryData.beginWork();
}
}
复制代码
单例模式
单例模式确保一个类只有一个实例,并提供一个全局访问点。 单例模式,按加载时机可以分为:饿汉方式和懒汉方式。我们具体看下:
懒汉加载
这是最简单的懒汉方式,但是却不能保证线程安全。所以如果项目中涉及到多线程,应避免使用。懒汉式是延迟加载的,优点在于资源利用率高,但第一次调用时的初始化工作会导致性能延迟,以后每次获取实例时也都要先判断实例是否被初始化,造成些许效率损失。
public class Singleton {
private static Singleton singleton;
/**
* 私有构造,只通过getInstance 对外提供实例
*/
private Singleton() { }
public static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
复制代码
如果我们为上面代码加上 synchronized 修饰符呢?通过添加 synchronized 修饰符,确实能解决线程不安全问题,但这又会引入另外一个问题,那就是执行效率问题。每个线程每次执行 getInstance() 方法获取类的实例时,都会进行同步,而事实上实例创建完成后,同步就变为不必要的开销了。
public class Singleton {
private static Singleton singleton;
private Singleton() { }
public static synchronized Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
复制代码
通过上面我们发现,要想保持同步,还要兼顾效率问题,那么一个自然而然的思路就是:缩小同步区域的范围了。这样同步块的方式就出来了。
public class Singleton {
private static Singleton singleton;
private Singleton() { }
/**
* 这样存在线程问题。
*
* <p>可能存在多个线程同时通过第一次检查,
* 导致创建了不同的对象。
*/
public static Singleton getInstance() {
// 第一次检查,避免了不必要的同步
if (singleton == null) {
synchronized (Singleton.class) {
singleton = new Singleton();
}
}
return singleton;
}
}
复制代码
针对上面的情况,我们增加第二次检查,来保证多线程问题。代码见下:
public class Singleton {
private static Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if(Singleton == null){ // 第一次检查,避免不必要同步
synchronized (Singleton.class) {
if(Singleton == null){ // 第二次检查,线程安全
singleton = new Singleton();
}
}
}
return singleton;
}
}
复制代码
到这里就高枕无忧了吗?并不是,双重检查加锁(DCL)会由于 Java 编译器允许处理器乱序执行,所以会有 DCL 失效的问题。好在在JDK1.5 版本后,Java提供了 volatile 关键字,来保证执行的顺序,从而使单例起效。至于在JDK1.5 版本以下,我们应避免使用该方式。双重检查加锁 单例代码见下:
public class Singleton {
private volatile static Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if(Singleton == null){
synchronized (Singleton.class) {
if(Singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
复制代码
volatile 关键字,是因为 volatile 确保了应用中的可视性,如果你将一个域声明为 volatile 的,那么只要对这个域产生了写操作,那么所有的读操作就都可以看到这个修改。即便使用了本地缓存,情况也是如此,volatile 域会立即被写入到主存中,而读取操作就发生在主存中(摘自java编程思想)。其实就是为了防止编译器优化指令执行,防止指令重排序问题,让每次操作都是到主存中对同一份数据进行读写,防止发生问题。
饿汉加载
饿汉式天生是线程安全的。在只考虑一个类加载器的情况下,饿汉方式在系统运行起来装载类的时候就进行初始化实例的操作,由 JVM 虚拟机来保证一个类的初始化方法在多线程环境中被正确加锁和同步。饿汉式在类创建的同时就实例化了静态对象,其资源已经初始化完成,所以第一次调用时更快,优势在于速度和反应时间,但是不管此单例会不会被使用,在程序运行期间会一直占据着一定的内存。
public class Singleton {
public static final Singleton instance = new Singleton();
private Singleton() { }
}
复制代码
静态代码块的饿汉式加载同样能保证线程安全。
public class Singleton {
private static final Singleton instance = null;
static {
instance = new Singleton();
}
private Singleton () { }
public static Singleton getInstance() {
return instance;
}
}
复制代码
这是我们常用的书写形式,与上面两种一样。
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() { }
public static Singleton getInstance() {
return instance;
}
}
复制代码
静态内部类式加载
当未使用 InterSingleton 静态内部类时,InterSingleton 类并不会被初始化,只有在显式调用 getInstance() 方法时,才会装载 InterSingleton 类,从而实例化对象,所以能达到延迟实例化的目的。同时在静态初始化器中创建单件,也保证了线程的安全。此方式也避免了在JDK 版本低于 1.5 时双重检查加锁方式的缺陷。
public class Singleton {
private Singleton(){}
public static Singleton getInstance() {
return InterSingleton.singleton;
}
private static class InterSingleton {
private static Singleton singleton = new Singleton();
}
}
复制代码
枚举方式
枚举不仅在创建实例的时候默认是线程安全的,而且在反序列化、反射、克隆时都可以自动防止重新创建新的对象。枚举类也是在第一次访问时才被实例化,属于懒加载方式。由于枚举是 JDK 1.5 才加入的特性,所以双重加锁检查的方式一样,对于版本有一定要求。
/**
* 枚举同Java中的普通Class一样,
* 也可以添加自己的属性和方法。
*/
public enum Singleton {
INSTANCE;
}
复制代码
Map登记式单例
随着项目越来越复杂,我们可能会需要同时管理多个不同业务的单例。这时我们就可以通过Map容器来统一管理这些单例,使用时,通过统一的接口来获取相应单例:
public class SingletonManager {
private static Map<String, Object> objMap = new HashMap<String,Object>();
private SingletonManager() { }
private static void registerService(String key, Object instance) {
if (!objMap.containsKey(key)) {
objMap.put(key, instance);
}
}
public static Object getService(String key) {
return objMap.get(key);
}
}
复制代码
好了,受文章篇幅影响,后面的设计模式没办法在本节写了,下次有机会我再分享。如果你喜欢我的文章,点个赞吧,大家的肯定也是我(Battler)坚持分享下去的动力。
- 参考《Head First 设计模式》
- 参考《深入设计模式》