概述
资料来源:力扣的“深入浅出设计模式”
按照自己的阅读习惯做的笔记。
一些提到但没展开讲的原则
开闭原则
软件中的对象(类,模块,函数等等)应该对于扩展是开放的,但是对于修改是封闭的。
合成 / 聚合复用原则
优先使用合成 / 聚合,而不是类继承。
接口隔离原则
客户端不应依赖它不需要的接口。如果一个接口在实现时,部分方法由于冗余被客户端空实现,则应该将接口拆分,让实现类只需依赖自己需要的接口方法。
构建型模式
工厂模式
在平时编程中,构建对象最常用的方式是 new 一个对象,这种方法属于一种硬编码。每 new 一个对象,调用者要多记住一个类。
除了new创建一个实例外,可能还要进行其他的初始化操作。
工厂模式就是用一个类封装构建对象的构造函数及所需的初始化过程。
简单工厂模式
用一个工厂类来负责所有对象的构建。
例如:
/*简单工厂
* 成员函数FruitFactory::create()根据传入的字符串,决定要生成哪个对象实例。
*/
class FruitFactory
{
public:
Fruit create(String type)
{
switch (type)
{
case "苹果":
return new Apple();
case "梨子":
return new Pear();
default:
...
}
}
};
/*调用者
* 直接生成1个工厂实例,通过该实例调用FruitFactory::create()决定生成哪个对象
*/
class User
{
public:
void eat()
{
FruitFactory fruitFactory = new FruitFactory();
Fruit apple = fruitFactory.create("苹果");
Fruit pear = fruitFactory.create("梨子");
apple.eat();
pear.eat();
}
};
一个更复杂一些的例子:生产一个苹果需要苹果种子、阳光、水分。
生成苹果类实例需要其它类的实例作为参数,使用工厂类将生成苹果类的所有相关操作封装在内。
调用者的代码完全不需要修改。
class FruitFactory
{
public:
Fruit create(String type)
{
switch (type)
{
case "苹果":
AppleSeed appleSeed = new AppleSeed();
Sunlight sunlight = new Sunlight();
Water water = new Water();
return new Apple(appleSeed, sunlight, water);
...
}
}
};
形象的理解
用户想要苹果时,不关心生产一个苹果需要什么前提条件,也不关心如何生产一个苹果。用户只需要知道从工厂中一定能得到一个苹果实例就行了。
即使生产苹果的前提条件、流程需要改变,也是在工厂中修改。用户完全不需要变化。
缺点
- 如果要生产的产品过多,会导致工厂类过于庞大,变成超级类。且任一产品的生成需要修改时,都要改动工厂类,不符合单例原则。
- 要生产新的产品时,要在工厂类成员函数中添加新的分支。开闭原则告诉我们:类应该对修改封闭。所以在添加新产品时,应该添加新的类而不是对已有的类进行修改。
工厂方法模式
为了解决简单工厂模式的2个弊端,就有了工厂方法模式。
将超级类工厂拆分成多个小工厂。
它规定每个产品都有一个专属工厂:苹果有苹果工厂、梨子有梨子工厂……
代码:
// 苹果工厂
class AppleFactory
{
public:
Fruit create()
{
AppleSeed appleSeed = new AppleSeed();
Sunlight sunlight = new Sunlight();
Water water = new Water();
return new Apple(appleSeed, sunlight, water);
}
};
// 梨子工厂
class PearFactory
{
public:
Fruit create()
{
return new Pear();
}
};
// 调用者
class User
{
public:
void eat(){
AppleFactory appleFactory = new AppleFactory();
Fruit apple = appleFactory.create();
PearFactory pearFactory = new PearFactory();
Fruit pear = pearFactory.create();
apple.eat();
pear.eat();
}
};
虽然从上述例子看,工厂方法模式比简单工厂模式复杂了一点,但仍有“封装类的构建过程”
这一优势。且克服了简单工厂的缺点:
- 某个产品的生产过程需要修改时,只需要修改相应的工厂,符合单例原则。
- 产品种类增加时,只需要增加相应的工厂类就行了,不会有工厂类成为超级类。保持了面向对象的可扩展性,符合开闭原则。
抽象工厂模式
概念
将工厂方法模式进一步优化,提取出公共的工厂接口,为所有的工厂类设计一个顶层抽象类IFactory
。
// 抽象工厂
class IFactory
{
public:
virtual Fruit create() = 0;
};
// 苹果工厂
class AppleFactory : public IFactory
{
public:
Fruit create()
{
return new Apple();
}
};
// 梨子工厂
class PearFactory : public IFactory
{
public:
Fruit create()
{
return new Pear();
}
};
这样一来,用户可以将 AppleFactory
和 PearFactory
统一作为 IFactory
对象使用
调用者代码如下:
class User
{
public:
void eat()
{
IFactory appleFactory = new AppleFactory();
Fruit apple = appleFactory.create();
IFactory pearFactory = new PearFactory();
Fruit pear = pearFactory.create();
apple.eat();
pear.eat();
}
};
在创建时仍需指定具体的工厂类
,但在使用时就无须关心该工厂对象具体是哪个工厂类了,统一将其视为顶层抽象类IFactory
。(多态特性)
在使用时只需要了解抽象类IFactory
的接口方法,不需要知道具体在哪个工厂,如何创建的该对象。
优势
替换工厂变得非常容易:
class User
{
public:
void eat()
{
IFactory factory = new AppleFactory();
Fruit fruit = factory.create();
fruit.eat();
}
};
如果要改为吃梨子,只需要修改一行代码:
class User
{
public:
void eat()
{
IFactory factory = new PearFactory(); // !
Fruit fruit = factory.create();
fruit.eat();
}
};
只需要修改生成工厂实例时的具体工厂
。
用处
IFactory
中只有一个抽象方法,看不出抽象工厂模式的威力。
实际上抽象工厂模式主要用于替换一系列方法。例如将程序中的 SQL Server 数据库整个替换为 Access 数据库。
使用抽象方法模式的话,只需在 IFactory
接口中定义好增删改查四个方法,让 SQLFactory
和 AccessFactory
实现此接口,调用时实例化IFactory
对象,直接使用IFactory
对象中的抽象方法即可,调用者无需知道使用的什么数据库,我们就可以非常方便的整个替换程序的数据库,并且让客户端毫不知情。
优缺点
优点:
- 替换工厂非常方便,仅需修改一行代码就行了。
缺点:
- 顶层抽象类增加功能非常麻烦,如果要新增一个抽象方法,则要修改所有的派生具体工厂类。
所以抽象工厂模式适用于增加同类工厂这样的横向扩展需求,不适合新增功能这样的纵向扩展。
单例模式
意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
如何实现:全局变量能实现全局访问点,但不能防止类实例化多个对象。所以只能让类自身负责保存它的唯一实例,并保证仅能创建一个实例、提供一个访问该实例的方法。
单例模式非常常见,某个对象全局只需要一个实例时,就可以使用单例模式。
优点:
- 它能够避免对象重复创建,节约空间并提升效率
- 避免由于操作不同实例导致的逻辑错误
单例模式有两种实现方式:饿汉式和懒汉式。
饿汉式(java)
变量在声明时便初始化。
使用java是因为,C++中带有类内初始值的成员必须因为常量。
public class Singleton
{
private static Singleton instance = new Singleton();
private Singleton()
{ }
public static Singleton getInstance()
{
return instance;
}
}
将构造方法定义为 private,这就保证了其他类无法实例化此类,必须通过 getInstance 方法才能获取到唯一的 instance 实例。
缺点
即使这个单例不需要使用,它也会在类加载之后立即创建出来,占用一块内存,并增加类初始化时间。
就好比一个电工在修理灯泡时,先把所有工具拿出来,不管是不是所有的工具都用得上。就像一个饥不择食的饿汉,所以称之为饿汉式。
懒汉式(java)
先声明一个空变量,需要使用时才初始化。
public class Singleton {
private static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance(){
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
就好比电工在修理灯泡时,开始比较偷懒,什么工具都不拿,当发现需要使用螺丝刀时,才把螺丝刀拿出来。当需要用钳子时,再把钳子拿出来。就像一个不到万不得已不会行动的懒汉,所以称之为懒汉式。
懒汉式单例乍一看没什么问题,但其实它不是线程安全的。如果有多个线程同一时间调用 getInstance 方法,instance 变量可能会被实例化多次。为了保证线程安全,我们需要给判空过程加上锁。
在同步化之前,再加上一层检查,免得每次都需要执行 synchronized 同步化方法,这样会严重影响程序的执行效率。
public class Singleton {
private static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
原文之后都是与Java语言相关的知识了。
Singleton类定义(C++)
书中将这称为“惰性(lazy)”初始化。
// 声明
class Singleton
{
public:
static Singleton* Instance();
protected:
Singleton();
private:
static Singleton* _instance;
};
// 实现
Singleton* Singleton::_instance = 0;
Singleton* Singleton::Instance()
{
if(_instance == 0)
_instance = new Singleton;
return _instance;
}
用户仅通过Singleton::Instance
成员函数访问这个单件。
为什么不将单件定义为一个全局或静态的对象?
- 不能保证该类型的静态对象只有一个声明。
- 可能静态对象的初始化需要一些程序运行后才能得到的值。
创造者模式
建造者模式用于创建过程稳定,但配置多变的对象。在《设计模式》一书中的定义是:将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。
从例子中看,大概意思是:当“类的构造函数的参数较多(大于4个),且有些选项是可选”
的时候,适合用创造者模式。
!:创建了另一个类:Builder
,这个Builder
类专门用于为已有的Computer
类进行初始化,”替代“了Computer
类的构造函数,通过有选择地调用Builder
的成员函数,以实现有选择地初始化Computer
的成员变量。
尝试总结
当前有一个需要实例化的Computer
类。
特性:类有一些参数是必须有效的,而有一些参数是不固定、可选的。
**实现方法:**将这个类的构造函数设为private
且接收一个Builder
类对象,使其只能通过Builder
类来生成实例;
Builder
类作为Computer
类的友元(C++),拥有与Computer
类完全一致的成员变量,Builder
类的成员函数:
1、Builder
类构造函数中接收Computer
类的必要参数;
2、Builder
类成员函数用于设置Computer
类的可选参数,且返回自身,以实现链式调用;
3、最后以build()
首尾(此时已经在Builder
对象中设置好了参数),调用Computer
类的构造函数,传入Builder
类对象作为参数。
Java代码。
public class Computer {
private final String cpu;//必须
private final String ram;//必须
private final int usbCount;//可选
private final String keyboard;//可选
private final String display;//可选
private Computer(Builder builder){
this.cpu=builder.cpu;
this.ram=builder.ram;
this.usbCount=builder.usbCount;
this.keyboard=builder.keyboard;
this.display=builder.display;
}
public static class Builder{
private String cpu;//必须
private String ram;//必须
private int usbCount;//可选
private String keyboard;//可选
private String display;//可选
// Builder构造函数接收2个必须初始化的参数
public Builder(String cup,String ram){
this.cpu=cup;
this.ram=ram;
}
// ------------- 余下的都是可选参数 -------------
public Builder setUsbCount(int usbCount) {
this.usbCount = usbCount;
return this;
}
public Builder setKeyboard(String keyboard) {
this.keyboard = keyboard;
return this;
}
public Builder setDisplay(String display) {
this.display = display;
return this;
}
// 链式调用的收尾。在通过链式调用设置好Builder的参数后,--
// --通过build()将自身作为参数传给Computer类完成实例化。
public Computer build(){
return new Computer(this);
}
}
//省略getter方法
}
// 通过Builder类进行链式调用,设置完所需参数后用build()结尾
Computer computer=new Computer.Builder("因特尔","三星")
.setDisplay("三星24寸")
.setKeyboard("罗技")
.setUsbCount(2)
.build();
原型模式
原型模式:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
从java例子看,原型模式就是用来复制的。
基本上在C++中重载“=”
运算符实现深拷贝就行了。
好像就是深拷贝啊
java代码
public class MilkTea{
public String type;
public boolean ice;
// 基本上就是“=”的重载,但多了一个new,所以即使是一个指针也能直接接收clone()的返回结果
public MilkTea clone(){
MilkTea milkTea = new MilkTea();
milkTea.type = this.type;
milkTea.ice = this.ice;
return milkTea;
}
}
// 使用
private void order(){
MilkTea milkTeaOfJay = new MilkTea();
milkTeaOfJay.type = "原味";
milkTeaOfJay.ice = false;
MilkTea yourMilkTea = milkTeaOfJay.clone();
// 一千位粉丝都调用 milkTeaOfJay 的 clone 方法即可
...
}
结构型模式
适配器模式
适配器模式:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。
总结
例子中是设计了一个类,通过类的成员函数转换数值。
自己感觉用过,就是写一个函数作为中介来适配不同函数的形参和数据。就是没有专门地将其独立成一个类。
说到适配器,我们最熟悉的莫过于电源适配器了,也就是手机的充电头。它就是适配器模式的一个应用。
试想一下,你有一条连接电脑和手机的 USB 数据线,连接电脑的一端从电脑接口处接收 5V 的电压,连接手机的一端向手机输出 5V 的电压,并且他们工作良好。
中国的家用电压都是 220V,所以 USB 数据线不能直接拿来给手机充电,这时候我们有两种方案:
单独制作手机充电器,接收 220V 家用电压,输出 5V 电压。
添加一个适配器,将 220V 家庭电压转化为类似电脑接口的 5V 电压,再连接数据线给手机充电。
如果你使用过早期的手机,就会知道以前的手机厂商采用的就是第一种方案:早期的手机充电器都是单独制作的,充电头和充电线是连在一起的。现在的手机都采用了电源适配器加数据线的方案。这是生活中应用适配器模式的一个进步。
适配器模式适用于有相关性但不兼容的结构,源接口通过一个中间件转换后才可以适用于目标接口,这个转换过程就是适配,这个中间件就称之为适配器。
!:适配器模式就像是一个补丁,只有在遇到接口无法修改时才应该考虑适配器模式。如果接口可以修改,那么将接口改为一致的方式会让程序结构更加良好。
从代码看,就是通过修改数据,使得2个类能协作工作。
class HomeBattery {
int supply() {
// 家用电源提供一个 220V 的输出电压
return 220;
}
}
class USBLine {
void charge(int volt) {
// 如果电压不是 5V,抛出异常
if (volt != 5) throw new IllegalArgumentException("只能接收 5V 电压");
// 如果电压是 5V,正常充电
System.out.println("正常充电");
}
}
class Adapter {
int convert(int homeVolt) {
// 适配过程:使用电阻、电容等器件将其降低为输出 5V
int chargeVolt = homeVolt - 215;
return chargeVolt;
}
}
public class User {
@Test
public void chargeForPhone() {
HomeBattery homeBattery = new HomeBattery();
int homeVolt = homeBattery.supply();
System.out.println("家庭电源提供的电压是 " + homeVolt + "V");
// 注释掉中间这一段,就是没使用适配器的情况。
Adapter adapter = new Adapter();
int chargeVolt = adapter.convert(homeVolt);
System.out.println("使用适配器将家庭电压转换成了 " + chargeVolt + "V");
USBLine usbLine = new USBLine();
usbLine.charge(chargeVolt);
}
}
桥接模式
桥接模式:将抽象部分与它的实现部分分离,使它们都可以独立地变化。它是一种对象结构型模式,又称为柄体模式或接口模式。
总结
当2种(或更多)属性的关系是相等关系时(例如图形的
形状
和颜色
),不必费心考虑将谁作为基类。而是通过“包含”的方式,将一个类(A)的对象作为另一个类(B)的成员变量,并在B类中提供A对象的初始化、赋值函数。
这样一来就能同时拥有多种属性了。
考虑这样一个需求:绘制矩形、圆形、三角形这3种图案。按照面向对象的理念,我们至少需要1个抽象类、3个具体类,对应3种不同的图形。
接下来我们有了新的需求,每种形状都需要有4种不同的颜色:红、蓝、黄、绿。
这时我们很容易想到两种设计方案:
为了复用形状类,将每种形状定义为父类,3种图形各4种颜色,一共派生出12个类。
为了复用颜色类,将每种颜色定义为父类,4种颜色各3种图形,一共派生出12个类。
目前没什么问题,但如果以后要增加新颜色,就要新增3个类;如果还要增加新图形,则要新增5个类。
形状和颜色,都是图形的两个属性。他们两者的关系是平等的,所以不属于继承关系。更好的的实现方式是:将形状和颜色分离,根据需要对形状和颜色进行组合,这就是桥接模式的思想。
java代码:
// 抽象类
public interface IColor {
String getColor();
}
// 所4种具体的颜色类继承自抽象类
public class Red implements IColor {
@Override
public String getColor() {
return "红";
}
}
public class Blue implements IColor {
@Override
public String getColor() {
return "蓝";
}
}
public class Yellow implements IColor {
@Override
public String getColor() {
return "黄";
}
}
public class Green implements IColor {
@Override
public String getColor() {
return "绿";
}
}
在形状类中实现桥接接口
class Rectangle implements IShape {
// 一个抽象类IColor作为私有成员
private IColor color; // !!!!!!!!!!!!!
void setColor(IColor color) {
this.color = color;
}
@Override
public void draw() {
System.out.println("绘制" + color.getColor() + "矩形");
}
}
class Round implements IShape {
private IColor color;
void setColor(IColor color) {
this.color = color;
}
@Override
public void draw() {
System.out.println("绘制" + color.getColor() + "圆形");
}
}
class Triangle implements IShape {
private IColor color;
void setColor(IColor color) {
this.color = color;
}
@Override
public void draw() {
System.out.println("绘制" + color.getColor() + "三角形");
}
}
使用
@Test
public void drawTest() {
Rectangle rectangle = new Rectangle();
rectangle.setColor(new Red());
rectangle.draw();
Round round = new Round();
round.setColor(new Blue());
round.draw();
Triangle triangle = new Triangle();
triangle.setColor(new Yellow());
triangle.draw();
}
组合模式
桥接模式用于将同等级的接口互相组合,组合模式和桥接模式的组合完全不一样。组合模式用于整体与部分的结构,当整体与部分有相似的结构,在操作时可以被一致对待时,就可以使用组合模式。例如:
- 文件夹和子文件夹的关系:文件夹中可以存放文件,也可以新建文件夹,子文件夹也一样。
- 总公司子公司的关系:总公司可以设立部门,也可以设立分公司,子公司也一样。
- 树枝和分树枝的关系:树枝可以长出叶子,也可以长出树枝,分树枝也一样。
总结
从例子看,就是提取多个类的共同属性(变量和函数)组成一个抽象类,以此减少重复代码。
例子中,管理人员(整体)虽然与员工(部分)有共同属性,但仍有一些员工没有的属性。由此分出2种方式:
透明方式:只提取出1个抽象类,员工通过继承,也有了不该拥有的属性,进行无效化设置(成员变量设为无效值、成员函数空实现)。
安全方式:将所有共同属性组合成1个抽象类,然后再为管理人员的额外属性派生出1个抽象类。管理人员和员工派生自不同的抽象类。
透明方式
在 Component 中声明所有管理子对象的方法,包括 add 、remove 等,这样继承自 Component 的子类都具备了 add、remove 方法。对于外界来说枝节点和叶节点是透明的(透明指看不到),它们具备完全一致的接口。
优点:使用时不需要对枝节点和叶节点作区分(~~多态?~~不是,枝节点和叶节点之间不是继承关系)。
缺点:违背了接口隔离原则,用户可能会通过叶节点调用其不该拥有的接口(函数)。
安全方式
在 Component 中不声明 add 和 remove 等管理子对象的方法(通过再设计一个派生类来声明),这样叶节点就无需实现它,只需在枝节点中实现管理子对象的方法即可。
优点:遵循了接口隔离原则。
缺点:由于不够透明,枝节点和叶节点不具有相同的接口,使用时必须要区别对待,带来了使用上的不方便。
装饰模式
https://leetcode-cn.com/leetbook/read/design-patterns/99j7re/
装饰品并不会改变物品本身,只是起到一个锦上添花的作用。装饰模式也一样,它的主要作用就是:
- 增强一个类原有的功能
- 为一个类添加新的功能
装饰模式也不会改变原有的类。
装饰模式:动态地给一个对象增加一些额外的职责,就增加对象功能来说,装饰模式比生成子类实现更为灵活。其别名也可以称为包装器,与适配器模式的别名相同,但它们适用于不同的场合。根据翻译的不同,装饰模式也有人称之为“油漆工模式”。
总结
分3个部分:抽象类、要被修饰的Entity类、装饰Entity的类。
后2者都派生自抽象类。
装饰Entity的类中都有一个Entity类对象作为成员变量,且构造函数接收1个Entity对象。
装饰类能做的事有:(具体解释看例子)
1、增强功能(
通过重载函数增强Entity成员函数的返回值); 2、添加功能(装饰类有自己独特的成员函数,那就是添加的功能)
例子:
用于增强功能的装饰模式(仅修改数据)
颜值接口:
public interface IBeauty {
int getBeautyValue();
}
Me 类,实现颜值接口:
public class Me implements IBeauty {
@Override
public int getBeautyValue() {
return 100;
}
}
戒指装饰类,将 Me 包装起来:
public class RingDecorator implements IBeauty {
private final IBeauty me;
public RingDecorator(IBeauty me) {
this.me = me;
}
@Override
public int getBeautyValue() {
return me.getBeautyValue() + 20;
}
}
耳环装饰类:
public class EarringDecorator implements IBeauty {
private final IBeauty me;
public EarringDecorator(IBeauty me) {
this.me = me;
}
@Override
public int getBeautyValue() {
return me.getBeautyValue() + 50;
}
}
测试:
public class Client {
@Test
public void show() {
IBeauty me = new Me();
System.out.println("我原本的颜值:" + me.getBeautyValue());
// 随意挑选装饰
IBeauty meWithRing = new RingDecorator(me);
System.out.println("戴上了戒指后,我的颜值:" + meWithRing.getBeautyValue());
// 多次装饰
IBeauty meWithManyDecorators = new RingDecorator(new EarringDecorator(me));
System.out.println("戴上耳环、戒指后,我的颜值:" + meWithManyDecorators.getBeautyValue());
增强功能:
每个装饰类都重载了getBeautyValue()
函数,函数的返回值是在自身成员变量me.getBeautyValue()
的返回值的基础上做增强。
这里的装饰器仅用于增强功能,并不会改变 Me 原有的功能,这种装饰模式称之为透明装饰模式,由于没有改变接口,也没有新增方法,所以透明装饰模式可以无限装饰。
如果使用继承来实现,则每种装饰的组合都要派生出1种子类,各种各样的排列组合会造成类爆炸。
使用了装饰模式,仅仅需要增加该装饰1个类而已。
用于添加功能的装饰模式
新建房屋接口:
public interface IHouse {
void live();
}
房屋类:
public class House implements IHouse{
@Override
public void live() {
System.out.println("房屋原有的功能:居住功能");
}
}
新建粘钩装饰器接口,继承自房屋接口:
public interface IStickyHookHouse extends IHouse{
void hangThings();
}
粘钩装饰类:
public class StickyHookDecorator implements IStickyHookHouse {
private final IHouse house;
public StickyHookDecorator(IHouse house) {
this.house = house;
}
@Override
public void live() {
house.live();
}
@Override
public void hangThings() {
System.out.println("有了粘钩后,新增了挂东西功能");
}
}
测试:
public class Client {
@Test
public void show() {
IHouse house = new House();
house.live();
IStickyHookHouse stickyHookHouse = new StickyHookDecorator(house);
stickyHookHouse.live();
stickyHookHouse.hangThings();
}
}
装饰类没有增强House类对象的返回结果,而是增加了新的成员函数,扩展了新的功能,这种模式在装饰模式中称之为半透明装饰模式。
半透明:不透明——用户必须了解装饰类,才能知道增加了什么功能;透明——用户不知道装饰了什么对象,是透明的。
半透明装饰模式中,无法多次装饰。因为增加的功能都是当前装饰类独有的,而各个装饰类之间没有继承关系,所以无法获得上一次装饰增加的功能。
外观模式
外观模式:外部与一个子系统的通信必须通过一个统一的外观对象进行,为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。外观模式又称为门面模式。
总结:就是封装。将多个经常协作的子系统的接口再通过一个外观类进一步封装。用户与外观类对象进行交互。
例子
举个例子,比如我们每天打开电脑时,都需要做三件事:
打开浏览器
打开 IDE
打开微信
每天下班时,关机前需要做三件事:
关闭浏览器
关闭 IDE
关闭微信
需要3个类:
public class Browser {
public static void open() {
System.out.println("打开浏览器");
}
public static void close() {
System.out.println("关闭浏览器");
}
}
public class IDE {
public static void open() {
System.out.println("打开 IDE");
}
public static void close() {
System.out.println("关闭 IDE");
}
}
public class Wechat {
public static void open() {
System.out.println("打开微信");
}
public static void close() {
System.out.println("关闭微信");
}
}
客户端调用:
public class Client {
@Test
public void test() {
System.out.println("上班:");
Browser.open();
IDE.open();
Wechat.open();
System.out.println("下班:");
Browser.close();
IDE.close();
Wechat.close();
}
}
使用外观模式
public class Facade {
public void open() {
Browser.open();
IDE.open();
Wechat.open();
}
public void close() {
Browser.close();
IDE.close();
Wechat.close();
}
}
public class Client {
@Test
public void test() {
Facade facade = new Facade();
System.out.println("上班:");
facade.open();
System.out.println("下班:");
facade.close();
}
}
享元模式
享元模式体现的是程序可复用的特点,为了节约宝贵的内存,程序应该尽可能地复用。
有个细节值得注意:有些对象本身不一样,但通过一点点变化后就可以复用,我们编程时可能稍不注意就会忘记复用这些对象。比如说伟大的超级玛丽,谁能想到草和云更改一下颜色就可以实现复用呢?还有里面的三种乌龟,换一个颜色、加一个装饰就变成了不同的怪。
在超级玛丽中,这样的细节还有很多,正是这些精湛的复用使得这一款红遍全球的游戏仅有 40KB 大小。正是印证了那句名言:神在细节之中。
没代码,就这些内容。
https://www.runoob.com/design-pattern/flyweight-pattern.html
享元模式(Flyweight Pattern)主要用于减少创建对象的数量,以减少内存占用和提高性能。这种类型的设计模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结构的方式。
享元模式尝试重用现有的同类对象,如果未找到匹配的对象,则创建新对象。
从例子上看,就是尽量复用对象,而不是创建新对象。
代理模式
在代理模式(Proxy Pattern)中,一个类代表另一个类的功能。这种类型的设计模式属于结构型模式。
在代理模式中,我们创建具有现有对象的对象,以便向外界提供功能接口。
**主要解决:**在直接访问对象时带来的问题,比如说:要访问的对象在远程的机器上。在面向对象系统中,有些对象由于某些原因(比如对象创建开销很大,或者某些操作需要安全控制,或者需要进程外的访问),直接访问会给使用者或者系统结构带来很多麻烦,我们可以在访问此对象时加上一个对此对象的访问层。
**何时使用:**想在访问一个类时做一些控制。
**如何解决:**增加中间层。
总结:
就是增加了一个中介类,其实现了实体类的所有功能(实体类为其成员变量,且有完全相同的成员函数),间接地操作实体类并进行控制。
例子
接口
public interface Image {
void display();
}
实现接口的实体类。
public class RealImage implements Image {
private String fileName;
public RealImage(String fileName){
this.fileName = fileName;
loadFromDisk(fileName);
}
@Override
public void display() {
System.out.println("Displaying " + fileName);
}
private void loadFromDisk(String fileName){
System.out.println("Loading " + fileName);
}
}
代理类
public class ProxyImage implements Image{
private RealImage realImage;
private String fileName;
public ProxyImage(String fileName){
this.fileName = fileName;
}
@Override
public void display() {
if(realImage == null){
realImage = new RealImage(fileName);
}
realImage.display();
}
}
使用
public class ProxyPatternDemo {
public static void main(String[] args) {
Image image = new ProxyImage("test_10mb.jpg");
// 图像将从磁盘加载
image.display();
System.out.println("");
// 图像不需要从磁盘加载
image.display();
}
}
动态代理
涉及到java的反射机制。代理类用一个成员函数做统一处理,根据实际使用时调用的函数名来判断是否需要加以控制。
行为模式
这一部分的描述和例子都不好理解。(例子都很烂)
责任链模式
责任链主要用于处理 职责相同,程度不同的类。
责任链模式:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
总结
责任链上的对象职责相同(类的功能,或者就是同一个类),但程度不同。
将问题沿着责任链传递,直到有一个对象能处理该问题。或该问题无法处理。
重点是问题沿着责任链传递。具体问题如何处理,什么情况下会将体交给下一个结点,根据实际情况决定,上面那段斜体字只是其中一种可能。
推论(例子1)
例如有3个等级的程序员:低级程序员、中级程序员、高级程序员;
3种难度的bug:简单bug、中等bug、高级bug。
每个程序员处理相应等级的bug。
此时出现了3种等级的bug
解决方案一
让3个程序员都轮询3个bug,若遇到了自己等级的bug就进行处理。
**弊端:**让每个程序员都尝试处理了每一个 bug,这也就相当于大家围着讨论每个 bug 该由谁解决,这无疑是非常低效的做法。
解决方案二
让一个项目经理接收所有的bug,然后将任务分派给合适的程序员。
**弊端:**项目经理一个人承担了分配所有 bug 这个体力活。程序没有变得简洁,只是把复杂的逻辑从客户端转移到了项目经理类中。
而且项目经理类承担了过多的职责,如果以后新增一类程序员,必须改动项目经理类,将其处理 bug 的职责插入分支判断语句中,违反了单一职责原则和开闭原则。
解决方案三:责任链
每个程序员的责任都是“解决这个 bug”,当测试提出一个 bug 时,可以走这样一条责任链:
先交由菜鸟程序员之手,如果是简单的 bug,菜鸟程序员自己处理掉。如果这个 bug 对于菜鸟程序员来说太难了,交给普通程序员
如果是中等难度的 bug,普通程序员处理掉。如果他也解决不了,交给优秀程序员
优秀程序员处理掉困难的 bug
代码
程序员抽象类
public abstract class Programmer {
protected Programmer next;
public void setNext(Programmer next) {
this.next = next;
}
abstract void handle(Bug bug);
}
初级程序员(中高级同理)
public class NewbieProgrammer extends Programmer {
@Override
public void handle(Bug bug) {
if (bug.value > 0 && bug.value <= 20) {
solve(bug);
} else if (next != null) {
next.handle(bug);
}
}
private void solve(Bug bug) {
System.out.println("菜鸟程序员解决了一个难度为 " + bug.value + " 的 bug");
}
}
客户端测试
import org.junit.Test;
public class Client4 {
@Test
public void test() {
NewbieProgrammer newbie = new NewbieProgrammer();
NormalProgrammer normal = new NormalProgrammer();
GoodProgrammer good = new GoodProgrammer();
Bug easy = new Bug(20);
Bug middle = new Bug(50);
Bug hard = new Bug(100);
// 组成责任链
newbie.setNext(normal);
normal.setNext(good);
// 从菜鸟程序员开始,沿着责任链传递
newbie.handle(easy);
newbie.handle(middle);
newbie.handle(hard);
}
}
3种等级的程序员形成了一条链表,初级程序员为表头,通过表头结点接收bug进行处理。
例子2
主要看注释
public abstract class AbstractLogger {
public static int INFO = 1;
public static int DEBUG = 2;
public static int ERROR = 3;
protected int level;
//责任链中的下一个元素
protected AbstractLogger nextLogger;
// 设置next指针
public void setNextLogger(AbstractLogger nextLogger){
this.nextLogger = nextLogger;
}
// 客户端实际调用的函数
public void logMessage(int level, String message)
{
// 当信息等级大于处理类的等级时,处理消息
if(this.level <= level)
write(message);
// 只要next非空,就把信息往后传
if(nextLogger !=null)
nextLogger.logMessage(level, message);
}
abstract protected void write(String message);
}
具体类
public class ConsoleLogger extends AbstractLogger
{
public ConsoleLogger(int level)
this.level = level;
@Override
protected void write(String message)
System.out.println("Standard Console::Logger: " + message);
}
public class ErrorLogger extends AbstractLogger
{
public ErrorLogger(int level)
this.level = level;
@Override
protected void write(String message)
System.out.println("Error Console::Logger: " + message);
}
public class FileLogger extends AbstractLogger
{
public FileLogger(int level)
this.level = level;
@Override
protected void write(String message)
System.out.println("File::Logger: " + message);
}
使用
public class ChainPatternDemo {
// 一个函数用于生成责任链
private static AbstractLogger getChainOfLoggers(){
AbstractLogger errorLogger = new ErrorLogger(AbstractLogger.ERROR);
AbstractLogger fileLogger = new FileLogger(AbstractLogger.DEBUG);
AbstractLogger consoleLogger = new ConsoleLogger(AbstractLogger.INFO);
errorLogger.setNextLogger(fileLogger);
fileLogger.setNextLogger(consoleLogger);
return errorLogger;
}
// 用抽象类指针(如果是在C++中)接收责任链的表头,然后通过该指针处理信息
public static void main(String[] args) {
AbstractLogger loggerChain = getChainOfLoggers();
loggerChain.logMessage(AbstractLogger.INFO, "This is an information.");
loggerChain.logMessage(AbstractLogger.DEBUG,
"This is a debug level information.");
loggerChain.logMessage(AbstractLogger.ERROR,
"This is an error information.");
}
}
命令模式
命令模式(Command Pattern)是一种数据驱动的设计模式,它属于行为型模式。请求以命令的形式包裹在对象中,并传给调用对象。调用对象寻找可以处理该命令的合适的对象,并把该命令传给相应的对象,该对象执行命令。
(有点麻烦,回头再看)
解释器模式
解释器模式(Interpreter Pattern):给定一门语言,定义它的文法的一种表示,并定义一个解释器,该解释器使用该表示来解释语言中的句子。
https://leetcode-cn.com/leetbook/read/design-patterns/9ee1cd/
不好总结,给出的例子是“将一句中文的公式解释给计算机,然后计算机为我们运算出了正确的结果。”
在解释器模式中,我们将不可拆分的最小单元称之为终结表达式,可以被拆分的表达式称之为非终结表达式。
迭代器模式
在获得一个类中的容器变量的同时,保证获取容器的用户无法对其进行修改。
就是说,对遍历容器的指针进行递增的操作是在另外提供的接口中实现的,用户只能直接获取容器内元素的数据,而不是让用户直接操作容器。
C++直接用迭代器就好了
中介模式
当类与类之间的关系呈现网状时,引入一个中介者,可以使类与类之间的关系变成星形。将每个类与多个类的耦合关系简化为每个类与中介者的耦合关系。(将多对多关系改为一对多关系)
中介者模式(Mediator Pattern):定义一个中介对象来封装一系列对象之间的交互,使原有对象之间的耦合松散,且可以独立地改变它们之间的交互。
中介者模式的缺点也很明显:由于它将所有的职责都移到了中介者类中,也就是说中介类需要处理所有类之间的协调工作,这可能会使中介者演变成一个超级类。所以使用中介者模式时需要权衡利弊。
例子很烂,完全没有呼应文字说明。
备忘录模式
备忘录模式:在不破坏封装的条件下,通过备忘录对象存储另外一个对象内部状态的快照,在将来合适的时候把这个对象还原到存储起来的状态。
优点是:
给用户提供了一种可以恢复状态的机制,使用户能够比较方便的回到某个历史的状态
实现了信息的封装,使得用户不需要关心状态的保存细节
缺点是:
消耗资源,如果类的成员变量过多,势必会占用比较大的资源,而且每一次保存都会消耗一定的内存。
总体而言,备忘录模式是利大于弊的,所以许多程序都为用户提供了备份方案。比如 IDE 中,用户可以将自己的设置导出成 zip,当需要恢复设置时,再将导出的 zip 文件导入即可。这个功能内部的原理就是备忘录模式。
例子:
一个玩家状态类,在打boss前进行存档,手动创建变量记录类的所有成员变量;重新读档时,又手动对类的所有成员变量重新赋值。
使用了备忘录模式后:
提供2个成员函数来实现存档、读档功能,封装了那些操作。
public class Client {
@Test
public void test() {
Player player = new Player();
// 存档
int savedLife = player.getLife();
int savedMagic = player.getMagic();
// 打 Boss,打不过,壮烈牺牲
player.fightBoss();
// 读档,恢复到打 Boss 之前的状态
player.setLife(savedLife);
player.setMagic(savedMagic);
}
}
public class Client {
@Test
public void test() {
Player player = new Player();
// 存档
Memento memento = player.saveState();
// 打 Boss,打不过,壮烈牺牲
player.fightBoss();
// 读档
player.restoreState(memento);
}
}
例子中创建了一个备忘录结构,专门用于存储玩家状态类的成员变量。
观察者模式
观察者模式(Observer Pattern):定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
举个例子,比如警察一直观察着张三的一举一动,只要张三有什么违法行为,警察马上行动,抓捕张三。
这个过程中:
- 警察称之为观察者(Observer)
- 张三称之为被观察者(Observable,可观察的)
- 警察观察张三的这个行为称之为订阅(subscribe),或者注册(register)
- 张三违法后,警察抓捕张三的行动称之为响应(update)
例子
被观察者的类中一定有一个容纳观察者对象的容器。
接口
观察者的接口:
public interface Observer {
void update(String event);
}
被观察者的父类:一个装观察者的容器;3个成员函数,增加、减少、遍历调用容器内所有观察者的update
函数。
public class Observable {
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(String event) {
for (Observer observer : observers) {
observer.update(event);
}
}
}
具体类
public class PoliceObserver implements Observer {
@Override
public void update(String event) {
System.out.println("警察收到消息,罪犯在" + event);
}
}
public class CriminalObservable extends Observable {
public void crime(String event) {
System.out.println("罪犯正在" + event);
notifyObservers(event);
}
}
使用
public class Client {
@Test
public void test() {
CriminalObservable zhangSan = new CriminalObservable();
PoliceObserver police1 = new PoliceObserver();
PoliceObserver police2 = new PoliceObserver();
PoliceObserver police3 = new PoliceObserver();
zhangSan.addObserver(police1);
zhangSan.addObserver(police2);
zhangSan.addObserver(police3);
zhangSan.crime("放狗咬人");
}
}
扩展:添加观察者不一定只有这种形式,可以在观察者接口
中有1个指向被观察者对象类型的指针
,观察者类构造函数
接收一个被观察者对象指针
,通过这个指针将自己的实例添加到被观察者的容器
中。
领会精神,代码不能直接用。
接口类:
public class Observer
{
private:
Observable* observalbe;
public:
virtual void update(String event) = 0;
}
具体类:
public class PoliceObserver :public Observer
{
public:
PoliceObserver(Observable* observalbe)
{
observalbe.addObserver(this);
}
public void update(String event)
{
System.out.println("警察收到消息,罪犯在" + event);
}
}
使用:
public class ObserverPatternDemo {
public static void main(String[] args) {
Subject subject = new Subject();
new HexaObserver(subject);
new OctalObserver(subject);
new BinaryObserver(subject);
System.out.println("First state change: 15");
subject.setState(15);
System.out.println("Second state change: 10");
subject.setState(10);
}
}
状态模式
就是多态。
策略模式
策略模式(Strategy Pattern):定义了一系列算法,并将每一个算法封装起来,而且使它们还可以相互替换。策略模式让算法独立于使用它的客户而独立变化。
以排序算法为例。排序算法有许多种,如冒泡排序、选择排序、插入排序,算法不同但目的相同,我们可以将其定义为不同的策略,让用户自由选择采用哪种策略完成排序。
interface ISort {
void sort(int[] arr);
}
具体的算法类略。
环境类,将每种算法都作为一种策略封装起来:
class Sort implements ISort {
private ISort sort;
Sort(ISort sort) {
this.sort = sort;
}
@Override
public void sort(int[] arr) {
sort.sort(arr);
}
// 客户端通过此方法设置不同的策略
public void setSort(ISort sort) {
this.sort = sort;
}
}
使用
public class Client {
@Test
public void test() {
int[] arr = new int[]{6, 1, 2, 3, 5, 4};
// 这里可以选择不同的策略完成排序
Sort sort = new Sort(new BubbleSort());
// Sort sort = new Sort(new InsertSort());
// Sort sort = new Sort(new SelectionSort());
sort.sort(arr);
// 输出 [1, 2, 3, 4, 5, 6]
System.out.println(Arrays.toString(arr));
}
}
与工厂模式结合
使用策略模式时,更好的做法是与工厂模式结合,将不同的策略对象封装到工厂类中,用户只需要传递不同的策略类型,然后从工厂中拿到对应的策略对象即可。
class Sort implements ISort {
private ISort sort;
Sort(SortStrategy strategy) {
setStrategy(strategy);
}
@Override
public void sort(int[] arr) {
sort.sort(arr);
}
// 客户端通过此方法设置不同的策略
public void setStrategy(SortStrategy strategy) {
switch (strategy) {
case BUBBLE_SORT:
sort = new BubbleSort();
break;
case SELECTION_SORT:
sort = new SelectionSort();
break;
case INSERT_SORT:
sort = new InsertSort();
break;
default:
throw new IllegalArgumentException("There's no such strategy yet.");
}
}
}
将创建策略类的职责移到了 Sort 类中。如此一来,客户端只需要和 Sort 类打交道,通过 SortStrategy 选择不同的排序策略即可。
public class Client {
@Test
public void test() {
int[] arr = new int[]{6, 1, 2, 3, 5, 4};
// 可以通过选择不同的策略完成排序
Sort sort = new Sort(SortStrategy.BUBBLE_SORT);
// Sort sort = new Sort(SortStrategy.SELECTION_SORT);
// Sort sort = new Sort(SortStrategy.INSERT_SORT);
sort.sort(arr);
// 输出 [1, 2, 3, 4, 5, 6]
System.out.println(Arrays.toString(arr));
}
}
模板方法模式
模板方法模式(Template Method Pattern):定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
通俗地说,模板方法模式就是一个关于继承的设计模式。
每一个被继承的父类都可以认为是一个模板,它的某些步骤是稳定的,某些步骤被延迟到子类中实现。
例子:
本人 ____ 因 ____ 需请假 ___ 天,望批准!
模板:
abstract class LeaveRequest {
void request() {
System.out.print("本人");
System.out.print(name());
System.out.print("因");
System.out.print(reason());
System.out.print("需请假");
System.out.print(duration());
System.out.print("天,望批准");
}
abstract String name();
abstract String reason();
abstract String duration();
}
实现功能的子类:
class MyLeaveRequest extends LeaveRequest {
@Override
String name() {
return "阿笠";
}
@Override
String reason() {
return "参加力扣周赛";
}
@Override
String duration() {
return "0.5";
}
}
最后
以上就是虚心冥王星为你收集整理的【笔记整理 - 设计模式】构建型模式结构型模式行为模式的全部内容,希望文章能够帮你解决【笔记整理 - 设计模式】构建型模式结构型模式行为模式所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复