设计模式
设计模式的基本原则¶
-
设计模式原则:
- 开闭原则:一个软件实体如类、模块和函数应该对修改封闭,对扩展开放。(即增加新功能应该添加新的类,而不是修改现有的类)
- 单一职责原则:一个类只做一件事,一个类应该只有一个引起它修改的原因。
- 里氏替换原则:子类应该可以完全替换父类。也就是说在使用继承时,只扩展新功能,而不要破坏父类原有的功能。
- 依赖倒置原则:细节应该依赖于抽象,抽象不应依赖于细节。把抽象层放在程序设计的高层,并保持稳定,程序的细节变化由低层的实现层来完成。
- 迪米特法则:又名“最少知道原则”,一个类不应知道自己操作的类的细节,换言之,只和朋友谈话,不和朋友的朋友谈话。
- 接口隔离原则:客户端不应依赖它不需要的接口。如果一个接口在实现时,部分方法由于冗余被客户端空实现,则应该将接口拆分,让实现类只需依赖自己需要的接口方法。
-
设计模式速览
- 创建型模式:
- 工厂方法模式:每一类对象建立工厂,对象由工厂创建
- 抽象工厂:为每一类工厂抽象出接口,使得新增替换工厂更加容易
- 建造者模式:构建过程稳定,配置多样的程序
- 单例模式:全局使用一个对象
- 原型模式:为一个类定义 clone 方法,便于创建相同对象
- 结构型模式:
- 适配器模式:用于有相关性但是不兼容的接口
- 装饰模式:增强、添加功能
- 桥接模式:组合同等级接口
- 组合模式:整体和部分的结构
- 外观模式:封装思想
- 享元模式:面向对象的可复用性
- 代理模式:对某个对象加以控制
- 行为型模式:
- 责任链模式:处理职责相同程度不同的对象,在一条链上传递
- 命令模式:封装方法调用,请求和实现解耦
- 解释器模式:定义自己的语法规则
- 迭代器模式:提供访问方式,隐蔽列表内部细节
- 中介模式:网状结构变成星型
- 备忘录模式:存储对象状态、便于恢复
- 观察者模式:处理一对多依赖关系,被观察者改变时多个观察者都收到信息
- 状态模式:关于多态,每个状态类处理一种状态
- 策略模式:殊途同归,多种方式做同一件事
- 模板方法模式:父类是子类的模板
- 访问者模式:数据结构和对数据结构的操作分离
创建型模式¶
- 创建型模式对类的实例化过程进行了抽象,能够将软件模块中对象的创建和对象的使用分离。为了使软件的结构更加清晰,外界对于这些对象只需要知道它们共同的接口,而不清楚其具体的实现细节,使整个系统的设计更加符合单一职责原则。
简单工厂模式¶
- Factory:工厂角色:工厂角色负责实现创建所有实例的内部逻辑
- Product:抽象产品角色:抽象产品角色是所创建的所有对象的父类,负责描述所有实例所共有的公共接口
- ConcreteProduct:具体产品角色:具体产品角色是创建目标,所有创建的对象都充当这个角色的某个具体类的实例。
- 抽象类作为基类,具体产品继承并重写抽象产品角色,工厂根据用户给的参数创建并返回对象
- 优点:外界不再需要关心如何创造各种具体的产品,只要提供一个产品的名称作为参数传给工厂,就可以直接得到一个想要的产品对象
- 缺点:简单工厂模式中的 if else 判断非常多,完全是Hard Code,如果有一个新产品要加进来,就要同时添加一个新产品类,并且必须修改工厂类,再加入一个 else if 分支才可以,这样就违背了 “开放-关闭原则”中的对修改关闭的准则了。
public class FruitFactory { public Fruit create(String type){ switch (type){ case "苹果": return new Apple(); case "梨子": return new Pear(); default: throw new IllegalArgumentException("暂时没有这种水果"); } } }
工厂方法模式¶
- 本质上就是把简单工厂方法的一个类进行了拆分
- ConcreteFactory:具体工厂,担任这个角色的是实现了抽象工厂接口的具体Java类。具体工厂角色含有与业务密切相关的逻辑,并且受到使用者的调用以创建具体产品对象。
- 优点:工厂可以自主确定创建何种产品对象,而如何创建这个对象的细节则完全封装在具体工厂内部。在系统中加入新产品时,无须修改抽象工厂和抽象产品提供的接口
- 缺点:系统中类的个数将成对增加,在一定程度上增加了系统的复杂度
public class SurgicalMaskFactory{ public Mask create() { return new SurgicalMask(); } } public class N95MaskFactory { public Mask create() { return new N95Mask(); } }
抽象工厂模式¶
-
工厂方法模式针对的是一个产品等级结构,而抽象工厂模式则可以面对多个产品等级结构
-
抽象工厂(Abstract Factory):这是一个定义创建产品家族对象的接口,但不具体实现具体的产品创建。
- 具体工厂(Concrete Factory):实现抽象工厂中定义的创建方法,这些方法生成具体的产品对象。
- 抽象产品(Abstract Product):定义一系列产品对象的接口。
- 具体产品(Concrete Product):实现抽象产品定义的接口,并由具体工厂来生产。
- 例子
- 抽象产品:苹果系列,三星系列
- 具体产品:iphone ipad;note tab;
- 抽象工厂:
-
具体工厂:手机工厂,平板工厂
-
优点:增加新的具体工厂和产品族很方便
- 缺点:难以扩展抽象工厂来生产新种类的产品
public interface IFactory {
Fruit create();
}
public class AppleFactory implements IFactory {
@Override
public Fruit create(){
return new Apple();
}
}
public class PearFactory implements IFactory {
@Override
public Fruit create(){
return new Pear();
}
}
public class User {
private void eat(){
//想要生产别的类只需要替换这一行
IFactory appleFactory = new AppleFactory();
Fruit apple = appleFactory.create();
IFactory pearFactory = new PearFactory();
Fruit pear = pearFactory.create();
apple.eat();
pear.eat();
}
}
动态工厂模式¶
public class ShapeFactory2 implements FactoryMethod {
private Map<String, Constructor> factories =
new HashMap<>();
private static Constructor load(String id) {
System.out.println("loading " + id);
try {
return Class.forName("patterns.shapes." + id)
.getConstructor();
} catch(ClassNotFoundException |
NoSuchMethodException e) {
throw new BadShapeCreation(id);
}
}
@Override public Shape create(String id) {
try {
return (Shape)factories
.computeIfAbsent(id, ShapeFactory2::load)
.newInstance();
} catch(Exception e) {
throw new BadShapeCreation(id);
}
}
public static void main(String[] args) {
FactoryTest.test(new ShapeFactory2());
}
}
```
- 使用反射实现类的动态创建
### 单例模式
- 作为对象的创建模式,单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。这个类称为单例类。
#### 单例模式的实现
- 懒汉模式
```java
public class Singleton2 {
//1.定义私有静态变量,类型为类类型,
// 先不创建,等用到时再创建(正是由于等用到时才创建,故而才称为懒汉式)
private static Singleton2 instance = null;
//2.定义私有构造函数
private Singleton2(){
}
//3.定义公共静态方法,返回私有静态变量
// public static Singleton2 getInstance(){//线程不安全的
public static synchronized Singleton2 getInstance(){// 线程安全,通过synchronize保证线程安全
if(instance == null){
instance = new Singleton2();
}
return instance;
}
}
```
- 使用时再创建,效率低线程不安全
- 饿汉模式
```java
public class Singleton1 {
//1.定义私有静态变量,类型为类类型
//直接创建好对象,不论到底会用到用不到,故称为饿汉式
private static Singleton1 instance = new Singleton1();
//2.定义私有构造函数
private Singleton1(){
}
//3.定义公共静态方法,返回私有静态变量
public static Singleton1 getInstance(){
return instance;
}
}
```
- 直接创建对象,效率高线程安全,浪费内存
- 登记(内部)模式
```java
public class Singleton3 {
//1.定义静态内部类,类里面有静态内部属性,为类类型
private static class SingletonInnerClass{
private static final Singleton3 instance = new Singleton3();
}
//2.定义私有构造函数
private Singleton3(){
}
//3.定义公共静态方法,返回内部类的静态常量
public static Singleton3 getInstance(){
return SingletonInnerClass.instance;
}
}
建造者模式¶
- 有些对象的构建可能非常的复杂,使用建造者模式将构建步骤分解,同一个构建过程可以创建不同类型的对象。建造者模式用于创建过程稳定,但配置多变的对象
- 产品(Product):需要构建的复杂对象,它由多个部分组成。
- 抽象建造者(Abstract Builder):定义了构建产品的抽象接口,包括构建产品的各个部分的方法。
- 具体建造者(Concrete Builder):实现抽象建造者接口,负责实际构建产品的各个部分,并提供方法返回构建后的产品。
-
指挥者(Director):负责按照一定的构建步骤来组织和控制建造过程,与具体建造者交互,最终构建出复杂对象。
-
流程
- 客户端创建Director对象,并使用具体的Builder实例化它。
- Director指导Builder开始创建产品。
- Builder构建产品的部分。
-
客户端从Builder检索产品。
-
优点
- 良好的封装性, 使用建造者模式可以使客户端不必知道产品内部组成的细节;
- 建造者独立,容易扩展;
-
在对象创建过程中会使用到系统中的一些其它对象,这些对象在产品对象的创建过程中不易得到。
-
缺点
- 会产生多余的Builder对象以及Director对象,消耗内存;
-
对象的构建过程暴露。
-
以上指挥者-建造者模式已经过时,现在通常通过链式调用生成不同的配置
public class MilkTea {
private final String type;
private final String size;
private final boolean pearl;
private final boolean ice;
private MilkTea() {}
private MilkTea(Builder builder) {
this.type = builder.type;
this.size = builder.size;
this.pearl = builder.pearl;
this.ice = builder.ice;
}
public String getType() {
return type;
}
public String getSize() {
return size;
}
public boolean isPearl() {
return pearl;
}
public boolean isIce() {
return ice;
}
public static class Builder {
private final String type;
private String size = "中杯";
private boolean pearl = true;
private boolean ice = false;
public Builder(String type) {
this.type = type;
}
public Builder size(String size) {
this.size = size;
return this;
}
public Builder pearl(boolean pearl) {
this.pearl = pearl;
return this;
}
public Builder ice(boolean cold) {
this.ice = cold;
return this;
}
public MilkTea build() {
return new MilkTea(this);
}
}
}
//使用
MilkTea chocolate =new MilkTea.Builder("巧克力味")
.ice(false)
.build();
MilkTea strawberry = new MilkTea.Builder("草莓味")
.size("大杯")
.pearl(false)
.ice(true)
.build();
原型模式¶
- 用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。 如 cloine 方法
public class MilkTea{ public String type; public boolean ice; public MilkTea clone(){ MilkTea milkTea = new MilkTea(); milkTea.type = this.type; milkTea.ice = this.ice; return milkTea; } }
结构型模式¶
适配器模式¶
-
旨在使不兼容的接口能够一起工作,通过将一个类的接口转换成客户端期望的接口形式,从而实现两者之间的协调合作。当客户类调用适配器的方法时,在适配器类的内部将调用适配者类的方法
-
目标(Target)角色:这就是所期待得到的接口。
-
源(Adapee)角色:现在需要适配的接口。
-
适配器(Adaper)角色:适配器类是本模式的核心。适配器把源接口转换成目标接口。显然,这一角色不可以是接口,而必须是具体类。
-
类适配器模式与对象适配器模式
- 实现机制:
- 类适配器模式:通过继承实现。适配器类同时实现了目标接口和继承被适配者类。
- 对象适配器模式:通过组合实现。适配器类持有一个被适配者对象的引用,并实现目标接口。
- 灵活性:
- 类适配器模式:由于Java(和许多其他编程语言)不支持多重继承,所以在这些语言中无法直接使用类适配器。但在支持多重继承的语言(如C++)中,类适配器可能会导致一个复杂的继承结构。
- 对象适配器模式:由于它是基于组合的,所以更加灵活。可以在运行时决定适配哪个对象。
- 覆盖被适配者的行为:
- 类适配器模式:由于使用继承,可以覆盖被适配者的某些行为。
- 对象适配器模式:通常更难或不可能覆盖被适配者的行为,因为适配器持有的是被适配者的一个实例。
- 最终的目标:
- 类适配器模式:将适配器转化为被适配者。
- 对象适配器模式:允许适配器与被适配者一起工作。
- 应用场景:
- 类适配器模式:当你想要适配的接口很少(或者你确定只适配一个具体的类),并且你想要通过继承重写一些被适配者的行为时,可以使用类适配器。
- 对象适配器模式:当你想要适配多个被适配者,或者在运行时决定使用哪个被适配者时,对象适配器是更好的选择。
public class Task implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("I'm called.");
return null;
}
}
public class RunnableAdapter implements Runnable {
private final Callable<?> callable;
public RunnableAdapter(Callable<?> callable) {
this.callable = callable;
}
@Override
public void run() {
try {
callable.call();
} catch (Exception e) {
e.printStackTrace();
}
}
}
public class Client {
@Test
public void call() throws InterruptedException {
Callable<Integer> callable = new Task();
//使用适配进行转换
Thread thread = new Thread(new RunnableAdapter(callable));
thread.start();
// 等待 1s 保证 thread 执行完成
Thread.sleep(1000);
}
}
桥接模式¶
- 用于同等级的接口(属性)相互组合
- 如果一个对象有两种或者多种分类方式(平行属性),并且两种分类方式都容易变化。这时使用继承很容易造成子类越来越多,所以更好的做法是把这种分类方式分离出来,让他们独立变化,使用时将不同的分类进行组合即可。
- 比如已经有三个形状类,现在想要添加 4 种颜色属性,如果让 4 个类去继承每种形状(即 12 个颜色类)过于繁琐。应该将形状和颜色分离,根据需要对形状和颜色进行组合
- 合成/聚合原则:有限使用合成/聚合而不是类继承(解决继承造成的依赖、子类灵活性下降的问题)
public interface IShape {
void draw();
}
public interface IColor {
String getColor();
}
public class Red implements IColor {
@Override
public String getColor() {
return "红";
}
}
class Rectangle implements IShape {
//桥接,形状聚合颜色
private IColor color;
void setColor(IColor color) {
this.color = color;
}
@Override
public void draw() {
System.out.println("绘制" + color.getColor() + "矩形");
}
}
组合模式¶
- 组合模式用于整体与部分的结构,当整体与部分有相似的结构,在操作时可以被一致对待时,就可以使用组合模式。
- 比如有管理者和员工两个类,而管理者的操作覆盖并多于员工,那么就应该用一个共同父类定义共同操作
public abstract class Component { // 职位 private String position; // 工作内容 private String job; public Component(String position, String job) { this.position = position; this.job = job; } // 做自己的本职工作 public void work() { System.out.println("我是" + position + ",我正在" + job); } abstract void addComponent(Component component); abstract void removeComponent(Component component); abstract void check(); } public class Manager extends Component { // 管理的组件 private List<Component> components = new ArrayList<>(); public Manager(String position, String job) { super(position, job); } @Override public void addComponent(Component component) { components.add(component); } @Override void removeComponent(Component component) { components.remove(component); } // 检查下属 @Override public void check() { work(); for (Component component : components) { component.check(); } } } public class Employee extends Component { public Employee(String position, String job) { super(position, job); } @Override void addComponent(Component component) { System.out.println("职员没有管理权限"); } @Override void removeComponent(Component component) { System.out.println("职员没有管理权限"); } @Override void check() { work(); } }
- 透明方式:在 Component 中声明所有管理子对象的方法,包括 add 、remove 等,这样继承自 Component 的子类都具备了 add、remove 方法 (尽管不能使用)。对于外界来说叶节点和枝节点是透明的,它们具备完全一致的接口。
- 安全方式:在 Component 中不声明 add 和 remove 等管理子对象的方法,这样叶节点就无需实现它,只需在枝节点中实现管理子对象的方法即可。虽然符合接口隔离原则,但是不能用 Component 统一表现了
外观模式¶
-
适配器模式是面向一个接口,外观模式是面向一组接口。外观类中只需要暴露简洁的接口,隐藏内部的细节,所以说白了就是封装的思想。
-
外部与一个子系统的通信必须通过一个统一的外观对象进行,为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
-
外观(Facade)角色 :客户端可以调用这个角色的方法。此角色知晓相关的(一个或者多个)子系统的功能和责任。在正常情况下,本角色会将所有从客户端发来的请求委派到相应的子系统去。
- 子系统(SubSystem)角色 :可以同时有一个或者多个子系统。每个子系统都不是一个单独的类,而是一个类的集合(如上面的子系统就是由ModuleA、ModuleB、ModuleC三个类组合而成)。每个子系统都可以被客户端直接调用,或者被门面角色调用。子系统并不知道门面的存在,对于子系统而言,门面仅仅是另外一个客户端而已。
-
通常只需要一个外观类,并且此外观类只有一个实例,换言之它是一个单例类。当然这并不意味着在整个系统里只有一个外观类,而仅仅是说对每一个子系统只有一个外观类。
-
优点
- 观模式松散了客户端与子系统的耦合关系,让子系统内部的模块能更容易扩展和维护。
- 外观模式让子系统更加易用,客户端不再需要了解子系统内部的实现,也不需要跟众多子系统内部的模块进行交互,只需要跟外观类交互就可以了。
- 通过合理使用Facade,可以帮助我们更好地划分访问的层次。有些方法是对系统外的,有些方法是系统内部使用的。把需要暴露给外部的功能集中到外观中,这样既方便客户端使用,也很好地隐藏了内部的细节
class A { A(int x) {} } class B { B(long x) {} } class C { C(double x) {} } // Other classes that aren't exposed by the // facade go here ... public class Facade { static A makeA(int x) { return new A(x); } static B makeB(long x) { return new B(x); } static C makeC(double x) { return new C(x); } public static void main(String[] args) { // The client programmer gets the objects // by calling the static methods: A a = Facade.makeA(1); B b = Facade.makeB(1); C c = Facade.makeC(1.0); } } ``` ### 装饰者模式 - 装饰者模式以对客户端透明的方式扩展对象的功能,是继承关系的一个替代方案。**动态地给一个对象增加一些额外的职责** - **抽象构件**(Component)角色:给出一个抽象接口,以规范准备接收附加责任的对象。 - **具体构件**(ConcreteComponent)角色:定义一个将要**接收附加责任**的类。 - **抽象装饰**(Decorator)角色:**持有**一个构件(Component)对象的实例,并定义一个与抽象构件接口一致的接口。 - **具体装饰**(ConcreteDecorator)角色:负责给构件对象“贴上”附加的责任。 - 增强功能的装饰器: ```java //抽象构件-颜值 public interface IBeauty { int getBeautyValue(); } //具体构件-等待装饰的我 public class Me implements IBeauty { @Override public int getBeautyValue() { return 100; } } //具体装饰 public class RingDecorator implements IBeauty { //持有具体构建 private final IBeauty me; public RingDecorator(IBeauty me) { this.me = me; } //装饰 @Override public int getBeautyValue() { return me.getBeautyValue() + 20; } } new RingDecorator(new Me());
- 用于添加功能的装饰模式:
//抽象构件 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("有了粘钩后,新增了挂东西功能"); } }
-
这种添加了新的功能但是没有修改员工的装饰模式称为半透明模式,半透明模式下无法多次装饰(定义了不同于抽象构件的新接口)
-
优点
- 装饰模式与继承关系的目的都是要扩展对象的功能,但是装饰模式可以提供比继承更多的灵活性。装饰模式允许系统动态决定“贴上”一个需要的“装饰”,或者除掉一个不需要的“装饰”。继承关系则不同,继承关系是静态的,它在系统运行前就决定了。
- 缺点
- 由于使用装饰模式,可以比使用继承关系需要较少数目的类。使用较少的类,当然使设计比较易于进行。但是,在另一方面,使用装饰模式会产生比使用继承关系更多的对象。更多的对象会使得查错变得困难,特别是这些对象看上去都很相像。
享元模式¶
- 享元模式体现的是程序可复用的特点,为了节约宝贵的内存
- 运用共享技术有效地支持大量细粒度对象的复用。系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用。由于享元模式要求能够共享的对象必须是细粒度对象,因此它又称为轻量级模式。
代理模式¶
- 给某一个对象提供一个代理,并由代理对象控制对原对象的引用。
- 分类
- 远程代理:为一个对象在不同的地址空间提供局部代表。例如,远程方法调用中的stub对象。
- 虚拟代理:用于懒初始化,将一个大计算量对象的创建延迟到真正需要的时候进行。(懒初始化)
- 保护代理:控制原始对象的访问,用于给不同的用户提供不同级别的使用权限。
- 智能引用代理:在访问对象时执行额外的操作,例如引用计数和线程安全检查。
public interface IPerson { void eat(); void sleep(); } //人 public class Person implements IPerson{ @Override public void eat() { System.out.println("我在吃饭"); } @Override public void sleep() { System.out.println("我在睡觉"); } } //人的代理 public class PersonProxy implements IPerson { private final Person person; public PersonProxy(Person person) { this.person = person; } @Override public void eat() { person.eat(); } @Override public void sleep() { person.sleep(); } } public class Client { @Test public void test() { Person person = new Person(); PersonProxy proxy = new PersonProxy(person); proxy.eat(); proxy.sleep(); } }
- 动态代理:使用反射实现,一个类传入,然后根据它正在调用的方法名判断是否需要加以控制
行为型模式¶
- 对在不同的对象之间划分责任和算法的抽象化。
- 行为型模式不仅仅关注类和对象的结构,而且重点关注它们之间的相互作用。
命令模式¶
-
命令模式可以将请求发送者和接收者完全解耦,发送者与接收者之间没有直接引用关系,发送请求的对象只需要知道如何发送请求,而不必知道如何完成请求。
-
命令模式是对命令的封装。命令模式把发出命令的责任和执行命令的责任分割开,委派给不同的对象。
-
客户端(Client)角色:创建请求者,接收者以及命令对象,执行具体逻辑。
- 命令(Command)角色:声明了一个给所有具体命令类的抽象接口。
- 具体命令(ConcreteCommand)角色:定义一个接收者和行为之间的弱耦合;实现execute()方法,负责调用接收者的相应操作。execute()方法通常叫做执行方法。
- 请求者(Invoker)角色:负责调用命令对象执行请求,相关的方法叫做行动方法。
-
接收者(Receiver)角色:负责具体实施和执行一个请求。任何一个类都可以成为接收者,实施和执行请求的方法叫做行动方法。
-
优点
- 命令模式使得发起命令的对象,和具体实现命令的对象完全解耦
- 命令模式把请求封装起来,可以动态地对它进行参数化、队列化和日志化等操作,从而使得系统更灵活。
- 命令模式中的命令对象能够很容易地组合成复合命令,也就是宏命令,从而使系统操作更简单,功能更强大。
-
由于发起命令的对象和具体的实现完全解耦,因此扩展新的命令就很容易,只需要实现新的命令对象,然后在装配的时候,把具体的实现对象设置到命令对象中,然后就可以使用这个命令对象,已有的实现完全不用变化。
-
可以用于请求排队:将每个需要执行的命令依次传入队列中,然后工作线程不断地从命令队列中取出队列头的命令,再执行命令即可。
-
缺点
- 使用命令模式可能会导致某些系统有过多的具体命令类。因为针对每一个命令都需要设计一个具体命令类,因此某些系统可能需要大量具体命令类,这将影响命令模式的使用。
//抽象命令
public interface ICommand {
void execute();
void undo();
}
//具体命令
public class DoorOpenCommand implements ICommand {
private Door door;
public void setDoor(Door door) {
this.door = door;
}
@Override
public void execute() {
door.openDoor();
}
@Override
public void undo() {
door.closeDoor();
}
}
public class DoorCloseCommand implements ICommand {
private Door door;
public void setDoor(Door door) {
this.door = door;
}
@Override
public void execute() {
door.closeDoor();
}
@Override
public void undo() {
door.openDoor();
}
}
//用栈存储历史命令实现撤销功能
public class Client {
// 所有的命令
Stack<ICommand> commands = new Stack<>();
@Test
protected void test() {
// 大门开关遥控
switchDoor.setOnCheckedChangeListener((view, isChecked) -> {
handleCommand(isChecked, doorOpenCommand, doorCloseCommand);
});
// 电灯开关遥控
switchLight.setOnCheckedChangeListener((view, isChecked) -> {
handleCommand(isChecked, lightOnCommand, lightOffCommand);
});
// 电视开关遥控
switchTv.setOnCheckedChangeListener((view, isChecked) -> {
handleCommand(isChecked, turnOnTvCommand, turnOffTvCommand);
});
// 音乐开关遥控
switchMusic.setOnCheckedChangeListener((view, isChecked) -> {
handleCommand(isChecked, musicPlayCommand, musicStopCommand);
});
// 撤销按钮
btnUndo.setOnClickListener(view -> {
if (commands.isEmpty()) return;
// 撤销上一个命令
ICommand lastCommand = commands.pop();
lastCommand.undo();
});
}
private void handleCommand(boolean isChecked, ICommand openCommand, ICommand closeCommand) {
if (isChecked) {
commands.push(openCommand);
openCommand.execute();
} else {
commands.push(closeCommand);
closeCommand.execute();
}
}
}
- 使用方法引用/lambda表达式可以很方便创建并操作函数对象
解释器模式¶
- 给定一门语言,定义它的文法的一种表示,并定义一个解释器,该解释器使用该表示来解释语言中的句子。
- 正则表达式就是一个解释器。
迭代器模式¶
-
顺序访问一个聚合对象中的各个元素,而又不暴露其内部的表示。
-
抽象迭代器(Iterator)角色:此抽象角色定义出遍历元素所需的接口。
- 具体迭代器(ConcreteIterator)角色:此角色实现了Iterator接口,并保持迭代过程中的游标位置。
- 聚集(Aggregate)角色:此抽象角色给出创建迭代器(Iterator)对象的接口。
- 具体聚集(ConcreteAggregate)角色:实现了创建迭代器(Iterator)对象的接口,返回一个合适的具体迭代器实例。
-
客户端(Client)角色:持有对聚集及其迭代器对象的引用,调用迭代子对象的迭代接口,也有可能通过迭代子操作聚集元素的增加和删除。
-
优点
- 简化了遍历方式,对于对象集合的遍历,还是比较麻烦的,对于数组或者有序列表,我们尚可以通过游标来取得,但用户需要在对集合了解很清楚的前提下,自行遍历对象,但是对于hash表来说,用户遍历起来就比较麻烦了。而引入了迭代器方法后,用户用起来就简单的多了。
- 可以提供多种遍历方式,比如说对有序列表,我们可以根据需要提供正序遍历,倒序遍历两种迭代器,用户用起来只需要得到我们实现好的迭代器,就可以方便的对集合进行遍历了。
-
封装性良好,用户只需要得到迭代器就可以遍历,而对于遍历算法则不用去关心。
-
缺点
- 对于比较简单的遍历(像数组或者有序列表),使用迭代器方式遍历较为繁琐,大家可能都有感觉,像ArrayList,我们宁可愿意使用for循环和get方法来遍历集合。
中介模式¶
- 中介者模式(Mediator Pattern):定义一个中介对象来封装一系列对象之间的交互,使原有对象之间的耦合松散,且可以独立地改变它们之间的交互。
-
当类与类之间的关系呈现网状时,引入一个中介者,可以使类与类之间的关系变成星形。将每个类与多个类的耦合关系简化为每个类与中介者的耦合关系。
-
优点:客户端的代码变得更加清晰了。大家不需要再互相打交道,所有交易通过中介者完成即可。
- 缺点:将所有的职责都移到了中介者类中,也就是说中介类需要处理所有类之间的协调工作,这可能会使中介者演变成一个超级类。
备忘录模式¶
- 备忘录模式:在不破坏封装的条件下,通过备忘录对象存储另外一个对象内部状态的快照,在将来合适的时候把这个对象还原到存储起来的状态。
- 备忘录模式最常见的实现就是游戏中的存档、读档功能,通过存档、读档,使得我们可以随时恢复到之前的状态。
- 我们不应该采用将单个属性挨个存取的方式来进行读档、存档。更好的做法是将存档、读档交给需要存档的类内部去实现。
class Player { ... // 存档 public Memento saveState() { return new Memento(life, magic); } // 读档 public void restoreState(Memento memento) { this.life = memento.life; this.magic = memento.magic; } }
- 优点:
- 给用户提供了一种可以恢复状态的机制,使用户能够比较方便的回到某个历史的状态
- 实现了信息的封装,使得用户不需要关心状态的保存细节
- 缺点:
- 消耗资源,如果类的成员变量过多,势必会占用比较大的资源,而且每一次保存都会消耗一定的内存。
观察者模式¶
-
观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态上发生变化时,会通知所有观察者对象,使它们能够自动更新自己。
-
抽象主题(Subject)角色:抽象主题角色把所有对观察者对象的引用保存在一个聚集(比如ArrayList对象)里,每个主题都可以有任何数量的观察者。抽象主题提供一个接口,可以增加和删除观察者对象,抽象主题角色又叫做抽象被观察者(Observable)角色。
- 具体主题(ConcreteSubject)角色:将有关状态存入具体观察者对象;在具体主题的内部状态改变时,给所有登记过的观察者发出通知。具体主题角色又叫做具体被观察者(Concrete Observable)角色。
- 抽象观察者(Observer)角色:为所有的具体观察者定义一个接口,在得到主题的通知时更新自己,这个接口叫做更新接口。
-
具体观察者(ConcreteObserver)角色:存储与主题的状态自恰的状态。具体观察者角色实现抽象观察者角色所要求的更新接口,以便使本身的状态与主题的状态相协调。如果需要,具体观察者角色可以保持一个指向具体主题对象的引用。
-
分类
- 推模型:主题对象向观察者推送主题的详细信息,不管观察者是否需要,推送的信息通常是主题对象的全部或部分数据。
-
拉模型:主题对象在通知观察者的时候,只传递少量信息。如果观察者需要更具体的信息,由观察者主动到主题对象中获取,相当于是观察者从主题对象中拉数据。一般这种模型的实现中,会把主题对象自身通过 update()方法传递给观察者,这样在观察者需要获取数据的时候,就可以通过这个引用来获取了。
-
警察称之为观察者(Observer)
- 张三称之为被观察者(Observable,可观察的)
- 警察观察张三的这个行为称之为订阅(subscribe),或者注册(register)
- 张三违法后,警察抓捕张三的行动称之为响应(update)
//抽象观察者 public interface Observer { void update(String event); } //观察者 public class PoliceObserver implements Observer { @Override public void update(String event) { System.out.println("警察收到消息,罪犯在" + event); } } //抽象被观察者 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 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("放狗咬人"); } }
状态模式¶
- 状态模式(State Pattern):当一个对象的内在状态改变时允许改变其行为,这个对象看起来像是改变了其类。 关于多态的设计模式
- 如果一个对象有多种状态,并且每种状态下的行为不同,,一般的做法是在这个对象的各个行为中添加 if-else 或者 switch-case 语句。但更好的做法是为每种状态创建一个状态对象,使用状态对象替换掉这些条件判断语句,使得状态控制更加灵活,扩展性也更好。
public interface IUser { void mockInterview(); } //不同状态的定义 class Normal implements IUser { @Override public void mockInterview() { System.out.println("模拟面试是 Plus 会员专享功能"); } } class Plus implements IUser { @Override public void mockInterview() { System.out.println("开始模拟面试"); } } class User implements IUser, ISwitchState { IUser state = new Normal(); @Override public void mockInterview() { state.mockInterview(); } @Override public void purchasePlus() { state = new Plus(); } @Override public void expire() { state = new Normal(); } }
- 用动态绑定替代了 if-else
策略模式¶
-
将每一个算法封装到具有共同接口的独立的类中,从而使得它们可以相互替换。针对一组算法,将每一个算法封装到具有共同接口的独立的类中,从而使得它们可以相互替换。
-
环境(Context)角色:持有一个策略的引用,即具有复杂多变行为的对象。
- 抽象策略(Strategy)角色:这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。
-
具体策略(ConcreteStrategy)角色:包装了具体相关的算法或行为。
-
优点
- 策略模式提供了管理相关的算法族的办法。策略类的等级结构定义了一个算法或行为族。恰当使用继承可以把公共的代码移到父类里面,从而避免代码重复。
-
使用策略模式可以避免使用多重条件(if-else)语句。多重条件语句不易维护,它把采取哪一种算法或采取哪一种行为的逻辑与算法或行为的逻辑混合在一起,统统列在一个多重条件语句里面,比使用继承的办法还要原始和落后。
-
缺点
- 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。这就意味着客户端必须理解这些算法的区别,以便适时选择恰当的算法类。换言之,策略模式只适用于客户端知道算法或行为的情况。
- 由于策略模式把每个具体的策略实现都单独封装成为类,如果备选的策略很多的话,那么对象的数目就会很可观。
//抽象策略
interface ISort {
void sort(int[] arr);
}
//具体策略
class BubbleSort implements ISort{
@Override
public void sort(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
for (int j = 0; j < arr.length - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
// 如果左边的数大于右边的数,则交换,保证右边的数字最大
arr[j + 1] = arr[j + 1] + arr[j];
arr[j] = arr[j + 1] - arr[j];
arr[j + 1] = arr[j + 1] - arr[j];
}
}
}
}
}
class SelectionSort implements ISort {
@Override
public void sort(int[] arr) {
int minIndex;
for (int i = 0; i < arr.length - 1; i++) {
minIndex = i;
for (int j = i + 1; j < arr.length; j++) {
if (arr[minIndex] > arr[j]) {
// 记录最小值的下标
minIndex = j;
}
}
// 将最小元素交换至首位
int temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
}
//可以装在任意排序算法的sort类
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;
}
}
访问者模式¶
-
访问者模式(Visitor Pattern):表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
-
对象结构中的每个元素都可以接受一个访问者对象。
- 当访问者被传递给一个元素时,该元素会调用访问者中的方法,并将自己作为参数传递,这样访问者就可以访问元素的数据。
- 访问者根据元素的类型执行相应的操作。
//被访问者提供的资源
class Restaurant {
...
public void welcome(IVisitor visitor) {
visitor.chooseLobster(lobster);
visitor.chooseWatermelon(watermelon);
visitor.chooseSteak(steak);
visitor.chooseBanana(banana);
}
}
//抽象访问者
public interface IVisitor {
void chooseLobster(String lobster);
void chooseWatermelon(String watermelon);
void chooseSteak(String steak);
void chooseBanana(String banana);
}
//具体访问者
public class Aurora implements IVisitor {
@Override
public void chooseLobster(String lobster) {
System.out.println("Aurora gets a " + lobster);
}
@Override
public void chooseWatermelon(String watermelon) {
System.out.println("Aurora gets a " + watermelon);
}
@Override
public void chooseSteak(String steak) {
System.out.println("Aurora doesn't like " + steak);
}
@Override
public void chooseBanana(String banana) {
System.out.println("Aurora doesn't like " + banana);
}
}
public class Client {
@Test
public void test() {
Restaurant restaurant = new Restaurant();
IVisitor Aurora = new Aurora();
restaurant.welcome(Aurora);
}
}
/*
Aurora gets a lobster
Aurora gets a watermelon
Aurora doesn't like steak
Aurora doesn't like banana
*/
双重分配¶
- 方法的接收者和方法的参数统称为方法的宗量。根据分派基于多少个宗量,可以将分派分为单分派和多分派。单分派是指根据一个宗量就可以知道应该调用哪个方法,多分派是指需要根据多个宗量才能确定调用目标。
//抽象食物 public abstract class Food { public abstract String name(); } //具体事食物 public class Lobster extends Food { @Override public String name() { return "lobster"; } } public class Lobster extends Food { @Override public String name() { return "lobster"; } } //抽象访问者 public interface IVisitor { void chooseFood(Lobster lobster); void chooseFood(Watermelon watermelon); void chooseFood(Steak steak); void chooseFood(Banana banana); } class Restaurant { // 准备当天的食物 private List<Food> prepareFoods() { List<Food> foods = new ArrayList<>(); // 简单模拟,每种食物添加 10 份 for (int i = 0; i < 10; i++) { foods.add(new Lobster()); foods.add(new Watermelon()); } return foods; } // 欢迎顾客来访 public void welcome(IVisitor visitor) { // 获取当天的食物 List<Food> foods = prepareFoods(); // 将食物依次提供给顾客选择 for (Food food : foods) { // 由于单分派机制,此处无法编译通过 //因为岁饭food绑定到了各种子类型,但是作为函数参数时会在编译时检查,认为类型是Food,因而找不到对应参数类型的方法 visitor.chooseFood(food); } } }
- 可以使用反射来确定类型解决
- 也可以利用动态绑定颠倒问题实现
public abstract class Food { public abstract String name(); // Food 中添加 accept 方法,接收访问者 public abstract void accept(IVisitor visitor); } public class Lobster extends Food { @Override public String name() { return "lobster"; } @Override public void accept(IVisitor visitor) { visitor.chooseFood(this); } } class Restaurant { // 准备当天的食物 private List<Food> prepareFoods() { List<Food> foods = new ArrayList<>(); // 简单模拟,每种食物添加 10 份 for (int i = 0; i < 10; i++) { foods.add(new Lobster()); foods.add(new Watermelon()); foods.add(new Steak()); foods.add(new Banana()); } return foods; } // 欢迎顾客来访 public void welcome(IVisitor visitor) { // 获取当天的食物 List<Food> foods = prepareFoods(); // 将食物依次提供给顾客选择 for (Food food : foods) { // 由于重写方法是动态分派的,所以这里会调用具体子类的 accept 方法, food.accept(visitor); } } }
模板方法模式¶
-
定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
-
抽象模板角色
- 定义了一个或多个抽象操作,以便让子类实现。这些抽象操作叫做基本操作,它们是一个顶级逻辑的组成步骤。
- 定义并实现了一个模板方法。这个模板方法一般是一个具体方法,它给出了一个顶级逻辑的骨架,而逻辑的组成步骤在相应的抽象操作中,推迟到子类实现。顶级逻辑也有可能调用一些具体方法。
- 具体模板角色
- 实现父类所定义的一个或多个抽象方法,它们是一个顶级逻辑的组成步骤。
-
每一个抽象模板角色都可以有任意多个具体模板角色与之对应,而每一个具体模板角色都可以给出这些抽象方法(也就是顶级逻辑的组成步骤)的不同实现,从而使得顶级逻辑的实现各不相同。
-
模板方法
- 一个模板方法是定义在抽象类中的,把基本操作方法组合在一起形成一个总算法或一个总行为的方法。
- 基本方法
- 抽象方法:一个抽象方法由抽象类声明,由具体子类实现。在Java语言里抽象方法以abstract关键字标示。
- 具体方法:一个具体方法由抽象类声明并实现,而子类并不实现或置换。
- 钩子方法:一个钩子方法由抽象类声明并实现,而子类会加以扩展。通常抽象类给出的实现是一个空实现,作为方法的默认实现。
- 优点
- 在父类中形式化地定义一个算法,而由它的子类来实现细节的处理,在子类实现详细的处理算法时并不会改变算法中步骤的执行次序。
- 模板方法模式是一种代码复用技术,它在类库设计中尤为重要,它提取了类库中的公共行为,将公共行为放在父类中,而通过其子类来实现不同的行为,它鼓励我们恰当使用继承来实现代码复用。
- 可实现一种反向控制结构,通过子类覆盖父类的钩子方法来决定某一特定步骤是否需要执行。
- 在模板方法模式中可以通过子类来覆盖父类的基本方法,不同的子类可以提供基本方法的不同实现,更换和增加新的子类很方便,符合单一职责原则和开闭原则。
- 缺点
- 需要为每一个基本方法的不同实现提供一个子类,如果父类中可变的基本方法太多,将会导致类的个数增加,系统更加庞大,设计也更加抽象,此时,可结合桥接模式来进行设计。
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"; } }
职责链模式¶
-
产生一个调用,一系列策略逐个尝试处理该调用,当出现成功或全不成功时结束
-
优点:
- 降低了对象之间的耦合度,客户只需要将请求发送到责任链上即可,无须关心请求的处理细节和请求的传递过程
- 扩展性强,满足开闭原则
- 灵活性强。可以动态地改变链内的成员或者改变链的次序来适应流程的变化。
- 简化了对象之间的连接。
- 责任分担。
- 缺点:
- 不能保证每个请求一定被处理,该请求可能一直传到链的末端都得不到处理。
- 责任链建立的合理性要靠客户端来保证,增加了客户端的复杂性,可能会由于责任链拼接次序错误而导致系统出错,比如可能出现循环调用。
- 如果责任链过长,请求的处理可能涉及多个处理对象,系统性能将受到一定影响。
//抽象程序员
public abstract class Programmer {
protected Programmer next;
public void setNext(Programmer next) {
this.next = next;
}
abstract void handle(Bug bug);
}
//三个不同级别的程序员,自己先尝试解决bug,解决不了就将bug丢给下一级next
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");
}
}
public class NormalProgrammer extends Programmer {
@Override
public void handle(Bug bug) {
if (bug.value > 20 && bug.value <= 50) {
solve(bug);
} else if (next != null) {
next.handle(bug);
}
}
private void solve(Bug bug) {
System.out.println("普通程序员解决了一个难度为 " + bug.value + " 的 bug");
}
}
public class GoodProgrammer extends Programmer {
@Override
public void handle(Bug bug) {
if (bug.value > 50 && bug.value <= 100) {
solve(bug);
} else if (next != null) {
next.handle(bug);
}
}
private void solve(Bug bug) {
System.out.println("优秀程序员解决了一个难度为 " + bug.value + " 的 bug");
}
}
// 组成责任链
newbie.setNext(normal);
normal.setNext(good);
// 从菜鸟程序员开始,沿着责任链传递
newbie.handle(easy);
newbie.handle(middle);
newbie.handle(hard);
用户图形程序¶
MVC (Model-View-Controller)¶
- Model(模型):负责数据的存储、检索和业务逻辑。
- View(视图):负责显示数据(用户界面),接受用户输入。
- Controller(控制器):从视图接收用户的输入,并决定调用哪个模型组件来执行相应的业务操作,然后更新视图。
- 各模块之间单向通信
- 直接通过 controller 接受指令的版本:
MVP (Model-View-Presenter)¶
- Model(模型):同 MVC 中的 Model。
- View(视图):负责显示数据,并把用户的输入发送给 Presenter。
- Presenter(表现者):处理业务逻辑,从模型中获取数据,然后更新视图。它与视图有一对一的关系,通常是视图的直接引用(单向)。
MVVM (Model-View-ViewModel)¶
- Model(模型):同 MVC 和 MVP 中的 Model。
- View(视图):负责显示数据,但尽量保持逻辑简单或不包含逻辑。
- ViewModel(视图模型):是视图和模型之间的桥梁。它包含视图需要的命令和属性。当模型数据改变时,视图模型会自动更新,反之亦然(双向沟通)。