如何理解设计模式
设计模式无论是书上还是其他网上的博客都有详细的说明如何使用,但真正的想实际运用到项目开发时却总会不知道如何使用,这主要的原因还是在于 你可能只把设计模式当成一套公式套用,但没有实际的理解设计模式是在哪个场景适合使用、设计模式的优点和缺点没有理解透。
在我看来,设计模式它只是对应业务场景的实现方式或解决方案。
在前期我们会习惯将设计模式的固定写法按 “公式” 一样套用,但实际的业务场景是复杂的,很多时候固定写法并不能完全满足业务需求,这时候就要根据业务需要将固定写法主观修改,也就是设计模式的变种。
对于设计模式如何使用,这里有一个概念:无论是一种设计模式还是多种设计模式的组合使用,我们只拿设计模式的一半!我们在源码分析时看到的各种设计模式,很多时候就是对固有设计模式写法的变种,祢补设计模式原有的缺点,以此达到符合业务场景的目的。
我们学习设计模式的目的是,能够基于业务与当前代码结构,针对业务场景考虑哪种或哪些设计模式组合后在该业务下的适用性。
怎么锻炼使用设计模式
那么在日常的业务开发中,到底该怎么懂得去使用设计模式呢?下面从对内或对外两个方面说明,也是我们需要在日常开发中要锻炼的一种编码意识。
对内:
-
每写一个类,问自己创建对象要搞事情吗?考虑下后期维护问题和对象生产问题,如果要考虑,选择哪种设计模式,单例?工厂?是否对外?要对外可能要用构建者模式提供配置等
-
碰到一个事情有多套方案的时候,问一下自己有没有必要搞策略
-
行为型模式是要去记业务实现方案,下次有相关业务,可以有答案抄!抄答案不是照抄官方,是结合自己业务去抄!
对外:
-
提供 sdk,抽象概念去排查需求,去固定需求,固定给外部提供什么,就是外观模式应用
-
评估功能,是否需要有功能版本迭代,根据不同业务诉求去考虑设计模式使用场景
除了在日常业务开发锻炼,还可以通过看源码的方式加深对设计模式的使用,你能够理解源码的实现思路,为什么这么做,好处是什么,弊端在哪里,那么这就是经验。当经验足够后,面对任何场景、方案都能给出一套落地方案,这就是架构。
当我们长时间的按以上的编码意识和思维可以练习,就能很快的从业务需求场景中挑选出合适的设计模式,甚至是对设计模式的变种达成业务需求的目的。
为什么设计模式有时候看着很像?
设计模式看着很像就是因为用的都是面向对象特性实现的。就像我们从零开始学 Java 语言,都是从了解 JDK 语法、面向对象特性、设计原则、设计模式逐步渐进,让我们的编码方式和习惯越来越好。
-
JDK 语法:固定的东西。告诉我们有什么东西能写
-
面向对象特性(封装继承多态):官方教我们怎么写好代码
-
六大设计原则:教你怎么写会更好
-
设计模式:教你针对不同业务场景有这种比较好的解决方案
设计原则主要告诉你三件事:
-
请合理划分业务重点
-
请尊重不修改原则写代码
-
请在划分业务的时候一个类一个独立业务,一个类不做两件事情
设计模式:用的面向对象特性组合成能实现不同业务场景的实现
设计原则
SOLID 原则及其他设计原则
SOLID:SOLID 原则并非单纯的 1 个原则,而是由 5 个设计原则组成,它们分别是:单一职责原则、开闭原则、里式替换原则、接口隔离原则和依赖倒置原则,SOLID 由 5 个设计原则的头一个字母组成。
在了解设计原则时我们要时刻记得,设计原则是有关联的,并不是独立存在的!
1、单一职责原则
一个类或者模块只负责完成一个职责(或者功能)。
注:该定义并不是绝对的,类或模块的单一职责要根据具体的需求区分,可能在某些场景是单一职责,在另外的场景反而会降低代码可读性难以管理。
2、开闭原则
添加一个新的功能应该是,在已有代码基础上扩展代码(新增模块、类、方法等),而非修改已有代码(修改模块、类、方法等)。
注:该定义并不是绝对的,要根据具体的需求区分,合适的修改代码也是合理的,比如简单工厂模式的应用。
遵循开闭原则对历史版本查验有帮助。
3、里氏替换原则
可以替换父类出现的任何位置,并且原来代码的逻辑行为不变且正确性也没有被破坏。
里氏替换原则是开闭原则实现的补充,用继承的子类去替换。
4、接口隔离原则
客户端不应该被强迫依赖它不需要的接口。
对 “接口” 的理解和解释,主要有三个:
-
一组API接口集合:不要将不需要的接口强迫提供给其他调用者,而是应该将这部分接口单独隔离提供接口出来
-
单个API接口或函数:函数的设计要功能单一,将不同功能的逻辑拆分成更细粒度的多个函数,让调用者只依赖它需要的那个函数。
-
OOP 中的接口概念:接口的设计要尽量单一,不要让接口的实现类和调用者,依赖不需要的接口函数。
5、依赖倒置原则
高层模块和低层模块通过抽象来相互依赖,调用者属于高层模块,被调用者属于低层模块,即隔了一层抽象避免直接有依赖关系。
简单理解就是,用构造函数或提供 setter 依赖注入一个抽象接口,注入的是抽象(接口或抽象父类)而不是具体的 class。
除了 SOLID 原则,还有其他原则也是我们在开发中可以留意遵循的:
6、DRY(Don’t Repeat Yourself)原则
典型的三种重复情况:实现逻辑重复、功能语义重复、代码执行重复。
-
实现逻辑重复,但功能语义不重复(即功能不同)的代码,并不违反 DRY 原则
-
实现逻辑不重复,但功能语义重复的代码,也算是违反 DRY 原则
-
代码执行重复也算是违反 DRY 原则
7、迪米特原则
不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口。
业务功能划分的原则,明确以谁为核心降低业务类之间的耦合。
-
KISS(Keep It Short and Simple) 原则:保持代码可读性和可维护性的重要手段。
-
YAGNI(You Ain’t Gonna Need It) 原则:不要去设计当前用不到的功能;不要去编写当前用不到的代码。这条原则的核心思想就是:不要过度设计。
多态和里氏替换原则的区别
多态 是面向对象编程的一大特性,也是面向对象编程语言的一种语法,它是一种代码实现的思路。
里氏替换是一种设计原则,是用来指导继承关系中子类该如何设计的。子类的设计要保证在替换父类的时候,不改变原有程序的逻辑以及不破坏原有程序的正确性。
接口隔离原则和单一职责原则的区别
当接口隔离原则中,将 “接口” 理解为单个 API 接口或函数时,此时的接口隔离原则和单一职责的关系是子集关系,即单一职责原则包含接口隔离原则。
接口隔离原则跟单一职责原则有点类似,不过稍微有点区别:单一职责原则针对的是模块、类、接口的设计。接口隔离原则 相对于单一职责原则,一方面它更侧重于接口的设计,另一方面它的思考角度不同。它提供了一种判断接口是否职责单一的标准:通过调用者如何使用接口来间接地判定。如果调用者只使用部分接口或接口的部分功能,那接口的设计就不够职责单一。
KISS 原则和 YAGNI 原则的区别
YAGNI 原则跟 KISS 原则并非一回事。KISS 原则讲的是 “如何做” 的问题(尽量保持简单),而 YAGNI 原则说的是 “要不要做” 的问题(当前不需要的就不要做)。
设计模式分类概览
设计模式主要分类三类:创建型模式、行为型模式和结构型模式。
-
创建型模式目的:解决对象的创建问题,封装复杂的创建过程,将创建和使用代码解耦(主体是为了对象的创建,为对象创建的性能、灵活性提供方案的代码写法)
-
结构型模式目的:解决 “类或对象的组合或组装” 问题,将不同功能代码解耦(通过对结构的设计达到性能、可扩展型、业务目的的代码写法)
-
行为型模式目的:解决 “类或对象之间的交互” 问题,将不同的行为代码解耦(为了达到具体的业务目的的代码写法)
1、常用的设计模式
-
创建型:单例模式、工厂模式(简单工厂、工厂方法)、建造者模式
-
结构型:代理模式、装饰模式、适配器模式、外观模式
-
行为型:观察者模式、模板方法模式、策略模式、责任链模式
2、不常用的设计模式
-
创建型:原型模式
-
结构型:桥接模式、组合模式、享元模式
-
行为型:状态模式、访问者模式、备忘录模式、命令模式、解释器模式、中介模式
创建型模式
单例模式
一个类只允许创建一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。
单例模式有多种写法,如懒汉模式、饿汉模式、DCL、枚举、静态内部类、容器单例。
(1)使用场景
适用于类 只允许创建一个对象(一般是进程内只有一个对象),被创建的类一般不需要太多的代码改动,并且不依赖外部系统时,可以使用单例模式。
(2)源码使用场景
context.getSystemService() 就是使用的容器单例。
优点:
-
内存中只有一个实例,减少内存开支和性能开销
-
避免对资源的多重占用。例如写文件动作,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作
-
全局访问点,优化和共享资源访问
缺点:
- 一般没有接口,扩展困难,除了修改代码基本没有第二种途径可以实现
工厂模式
工厂模式主要是将对象的创建和使用隔离。
简单工厂
(1)使用场景
提供 Factory 工厂类结合多态创建对象。适用于代码不复杂、类型不多改动较少的对象创建场景。
(2)源码使用场景
BitmapFactory.decodeXxx(),通过不同的工厂方法如 decodeResource、decodeFile 等传递不同的参数创建 Bitmap 对象。
(3)公式写法
public interface IPhone {}
public class IPhone4s implements IPhone {}
public class IPhone5s implements IPhone {}
// 简单工厂
public class Factory {
public static IPhone create(String type) {
if (type == "5s") {
return new IPhone5s();
} else if (type == "4s") {
return new IPhone4s();
}
return null;
}
}
优点:
-
客户端不再负责创建对象,把创建对象的职责交给了工厂
-
抽象与实现分离,客户端不知道具体实现
缺点:
- 违反开闭原则,加一个子类型或改一个类型就要修改 Factory
工厂方法
(1)使用场景
对象创建条件比较复杂,要组合其他类对象和判断条件,做各种初始化操作的时候。
(2)源码使用场景
ArrayList,Iterator 是 Product,ArrayListIterator 是 ConcreteProduct;List 是 Factory 声明创建 Iterator 对象的接口,ArrayList 是 ConcreteFactory 用于创建具体的 Iterator 对象。
(3)公式写法
// 产品抽象类,可以是接口也可以是抽象类
public abstract class Product {
public abstract void method();
}
// 具体产品
public class ConcreteProduct1 extends Product {}
public class ConcreteProduct2 extends Product {}
// 工厂抽象类
public abstract class Factory {
public abstract Product create();
}
// 具体工厂
public class ConcreteFactory extends Factory {
@Override
public Product create() {
return new ConcreteProduct1();
}
}
Factory factory = new ConcreteFactory();
Product product = factory.create();
product.method();
优点:
- 更符合开闭原则,有新产品只需要添加 Factory
缺点:
- 是优点同样也是缺点,加一个产品就要创建新的 Factory,类型很多时 Factory 类过多
简单工厂和工厂方法的选择
如果对象的创建类型不多,创建对象的条件不复杂,改动也不多的场景(一般表现为只需要简单的 if-else 判断和 new 对象),推荐使用简单工厂。
当对象的创建逻辑比较复杂,不只是简单的 new 一下就可以,而是要组合其他类对象做各种初始化操作的时候,推荐使用工厂方法模式,将复杂的创建逻辑拆分到多个工厂类中,让每个工厂类不至于过于复杂。
抽象工厂
(1)使用场景
如果创建对象需要的条件不是单一的,而是可以有多种分类方式(注意是分类方式,而不是多个判断条件),如果用工厂方法将会创建多个 Factory,反而会变得难以维护。抽象工厂模式适合用于创建多种不同类型对象且创建逻辑比较复杂的场景。
(2)公式写法
// 抽象产品类
public interface AbstractProduct1 {
void method();
}
public interface AbstractProduct2 {
void method();
}
// 具体产品
public class ConcreteProduct1 implements AbstractProduct1 {}
public class ConcreteProduct2 implements AbstractProduct2 {}
// 抽象工厂,可以创建多个不同类型的对象
public abstract class AbstractFactory {
public abstract AbstractProduct1 createProduct1();
public abstract AbstractProduct2 createProduct2();
}
// 具体工厂
public class ConcreteFactory extends AbstractFactory {
@Override
public AbstractProduct1 createProduct1() {
return new ConcreteProduct1();
}
@Override
public AbstractProduct2 createProduct2() {
return new ConcreteProduct2();
}
}
AbstractFactory factory = new ConcreteFactory();
AbstractProduct1 product1 = factory.createProduct1();
AbstractProduct2 product2 = factory.createProduct2();
优点:
- 抽象工厂可以创建多个不同类型的产品,同一产品族不同类型对象可以放在一个工厂创建,相比工厂方法能减少工厂类的创建
缺点:
-
单条产品线的情况等同于工厂方法
-
新增产品涉及多个角色修改,抽象工厂类和多个具体工厂实现类都要改动
工厂模式小结
-
简单工厂:代码块本身并不复杂,就几行代码而已,完全没必要将它拆分成单独的函数或者类(一般表现为只需要简单的 if-else 判断和 new 对象)的场景,使用简单工厂模式即可
-
工厂方法:当对象的创建逻辑比较复杂,不只是简单 new 一下就可以,而是要组合其他类对象和判断条件,做各种初始化操作的时候,推荐使用工厂方法模式,将复杂的创建逻辑拆分到多个工厂类中,让每个工厂类都不至于过于复杂
-
抽象方法:当对象的创建需要是多个不同类型且创建逻辑比较复杂的情况,推荐使用抽象工厂模式
构建者模式
(1)使用场景
创建 一种类型 的复杂对象(怎么算是复杂对象?简单理解就是需要多种条件的校验判断才能创建的对象),通过设置不同的可选参数,“定制化” 地创建不同的对象。
(2)源码使用场景
AlertDialog,AlertDialog.Builder 同时扮演了 Builder、ConcreteBuilder、Director 角色,简化了构建者模式的设计。
(3)公式写法
// 产品抽象类
public abstract class Computer {
protected int cpuCore = 1;
protected int ramSize = 0;
protected String os = "Dos";
protected Computer() {}
public abstract void setCPU(int core);
public abstract void setRAM(int gb);
public abstract void setOs(String os);
}
// 具体产品
public class AppleComputer extends Computer {
protected AppleComputer() {}
@Override
public void setCPU(int core) {
cpuCore = core;
}
@Override
public void setRAM(int gb) {
ramSize = gb;
}
@Override
public void setOs(String os) {
this.os = os;
}
}
// 构建者抽象类
public abstract class Builder {
public abstract void buildCPU(int core);
public abstract void buildRAM(int gb);
public abstract void buildOs(String os);
public abstract Computer create();
}
// 实际构建类
public class ApplePCBuilder extends Builder {
private Computer appleComputer = new AppleComputer();
@Override
public void buildCPU(int core) {
appleComputer.setCPU(core);
}
@Override
public void buildRAM(int gb) {
appleComputer.setRAM(gb);
}
@Override
public void buildOs(String os) {
appleComputer.setOs(os);
}
@Override
public Computer create() {
return appleComputer;
}
}
// 指挥者,可省略
public class Director {
private Builder builder = null;
public Director(Builder builder) {
this.builder = builder;
}
public void construct(int cpu, int ram, String os) {
builder.buildCPU(cpu);
builder.buildRAM(ram);
builder.buildOs(os);
}
}
Builder builder = new ApplePCBuilder();
Director director = new Director(builder);
director.construct(4, 2, "Mac Os X 10.9.1");
(4)特点
-
隔离属性赋值(对象创建后不允许修改属性值)
-
隔离对象创建(客户端不能直接 new 对象)
优点:
-
良好的封装性,使用构建者模式客户端不必知道产品内部组成的细节
-
容易扩展
-
对象构建完创建后,客户端不能修改属性
缺点:
-
产生多余的 Builder 对象及 Director 对象,消耗内存(AlertDialog 变种写法没有该缺点)
-
对象的构建过程暴露
原型模式
(1)使用场景
适用于复杂对象创建成本较大、同一类型不同对象之间差别不大(大部分字段都相同)的对象创建场景。
Java 提供了原型模式的写法,通过 实现 Clonable 接口重写 clone() 方法即可实现原型模式。
需要注意深拷贝和浅拷贝:
-
浅拷贝:只会拷贝基本类型和对象的引用地址
-
深拷贝:会将基本类型包括对象都复制
(2)源码使用场景
Intent 的 clone()。
(3)公式写法
// ConcretePrototype,Prototype 为 Cloneable 接口
public class WordDocument implements Cloneable {
private String text;
private ArrayList<String> images = new ArrayList<>();
public WordDocument() {
// clone() 对象不会执行构造函数
}
// getter and setter
@Override
protected WordDocument clone() {
try {
WordDocument doc = (WordDocument) super.clone();
doc.text = this.text;
// 浅拷贝,会影响原始对象
doc.images = this.images;
// 深拷贝,只会影响 clone 对象
// doc.images = (ArrayList<String>) this.images.clone();
return doc;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
WordDocument originDoc = new WordDocument();
originDoc.setText("text");
originDoc.addImage("image");
WordDocument doc2 = originDoc.clone();
doc2.setText("modify text");
优点:
- 原型模式是在内存二进制流的拷贝,比直接 new 对象性能好很多,特别是要在一个循环内产生大量对象时,原型模式可以更好体现其优点
缺点:
- 既是优点也是缺点,直接在内存中拷贝,构造函数不会执行,在实际开发中应该注意这个潜在问题。优点是减少了约束,缺点也是减少了约束,实际使用时需谨慎考虑
创建型模式总结
创建型模式的核心:让你们知道怎么去 new 对象。让你考虑内存性能和扩展性,以及后续代码维护的工作量。
再简单梳理下创建型模式的使用场景:
-
单例模式:适用于类只允许创建一个对象(一般是进程内只有一个对象),被创建的类一般不需要太多的代码改动,并且不依赖外部系统时
-
简单工厂:适用于代码不复杂、类型不多改动较少的对象创建场景
-
工厂方法:对象创建条件比较复杂,要组合其他类对象和判断条件,做各种初始化操作的时候
-
抽象工厂(使用较少):抽象工厂模式适合用于创建多种不同类型对象且创建逻辑比较复杂,有可能会需要更换一整条业务线场景
-
构建者模式:适用于一个对象内部是由多个条件进行组合的业务场景
-
原型模式(使用较少):适用于复杂对象创建成本较大、同一类型不同对象之间差别不大(大部分字段都相同)的对象创建场景
结构型模式
代理模式
在 不改变原始类(或叫被代理类)代码的情况下,通过引入代理类来 给原始类附加功能。
(1)使用场景
需要功能附加增强的业务场景(即不影响实际业务执行的功能附加)。例如监控、统计、日志、AOP 等场景。
代理模式可以分为静态代理和动态代理。
(2)静态代理公式写法
public interface IUserController {
UserVo login(String telephone, String password);
UserVo register(String telephone, String password);
}
public class UserController implements IUserController {
// ...省略其他属性和方法...
@Override
public UserVo login(String telephone, String password) {
// ...省略login逻辑...
// ...返回UserVo数据...
}
@Override
public UserVo register(String telephone, String password) {
// ...省略register逻辑...
// ...返回UserVo数据...
}
}
public class UserControllerProxy implements IUserController {
private MetricsCollector metricsCollector;
private UserController userController;
public UserControllerProxy(UserController userController) {
this.userController = userController;
this.metricsCollector = new MetricsCollector();
}
@Override
public UserVo login(String telephone, String password) {
long startTimestamp = System.currentTimeMillis();
// 委托
UserVo userVo = userController.login(telephone, password);
long endTimeStamp = System.currentTimeMillis();
long responseTime = endTimeStamp - startTimestamp;
RequestInfo requestInfo = new RequestInfo("login", responseTime, startTimestamp);
metricsCollector.recordRequest(requestInfo);
return userVo;
}
@Override
public UserVo register(String telephone, String password) {
long startTimestamp = System.currentTimeMillis();
UserVo userVo = userController.register(telephone, password);
long endTimeStamp = System.currentTimeMillis();
long responseTime = endTimeStamp - startTimestamp;
RequestInfo requestInfo = new RequestInfo("register", responseTime, startTimestamp);
metricsCollector.recordRequest(requestInfo);
return userVo;
}
}
// UserControllerProxy使用举例
// 因为原始类和代理类实现相同的接口,是基于接口而非实现编程
// 将UserController类对象替换为UserControllerProxy类对象,不需要改动太多代码
IUserController userController = new UserControllerProxy(new UserController());
那么 如果代理类和原始类需要实现相同的接口,原始类不是我们开发或并没有定义接口,那该怎么办呢?此时可以让代理类继承原始类实现:
// 代理类继承原始类,也能实现静态代理
public class UserControllerProxy extends UserController {
private MetricsCollector metricsCollector;
public UserControllerProxy() {
this.metricsCollector = new MetricsCollector();
}
public UserVo login(String telephone, String password) {
long startTimestamp = System.currentTimeMillis();
UserVo userVo = super.login(telephone, password);
long endTimestamp = System.currentTimeMillis();
long responseTime = endTimeStamp - startTimestamp;
RequestInfo requestInfo = new RequestInfo("login", responseTime, startTimestamp);
metricsCollector.recordRequest(requestInfo);
return userVo;
}
public UserVo register(String telephone, String password) {
long startTimestamp = System.currentTimeMillis();
UserVo userVo = super.register(telephone, password);
long endTimeStamp = System.currentTimeMillis();
long responseTime = endTimeStamp - startTimestamp;
RequestInfo requestInfo = new RequestInfo("register", responseTime, startTimestamp);
metricsCollector.recordRequest(requestInfo);
return userVo;
}
}
UserController userController = new UserControllerProxy();
静态代理的优点:
-
附加功能不直接在原始类编写,解耦代码
-
基于接口而非实现编程,原始类对象替换为代理对象,不需要改动太多代码,附加功能也不影响实际代码执行
静态代理的缺点:
- 需要将原始类的所有方法重新实现一遍,每个方法都附加相似的代码逻辑,有多个代理类就要重复多次模板代码
针对静态代理的缺点,动态代理能够在运行时动态的创建原始类的代理类,然后在系统中用代理类替换掉原始类。
(3)动态代理公式写法
public class MetricsCollectorProxy {
private MetricsCollector metricsCollector;
public MetricsCollectorProxy() {
this.metricsCollector = new MetricsCollector();
}
public Object createProxy(Object proxiedObject) {
// 使用 JDK 提供的动态代理方案
Class<?>[] interfaces = proxiedObject.getClass().getInterfaces();
DynamicProxyHandler handler = new DynamicProxyHandler(proxiedObject);
return Proxy.newProxyInstance(proxiedObject.getClass().getClassLoader(), interfaces, handler);
}
private class DynamicProxyHandler implements InvocationHandler {
private Object proxiedObject;
public DynamicProxyHandler(Object proxiedObject) {
this.proxiedObject = proxiedObject;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
long startTimestamp = System.currentTimeMillis();
Object result = method.invoke(proxiedObject, args);
long endTimestamp = System.currentTimeMillis();
Log.v(TAG, "time = " + (endTimestamp - startTimestamp));
return result;
}
}
}
MetricsCollectorProxy proxy = new MetricsCollectorProxy();
IUserController userController = (IUserController) proxy.createProxy(new UserController());
动态代理的优点:
-
附加功能不直接在原始类编写,解耦代码
-
解决了静态代理针对每个类都要创建一个代理类,每个代理类中的代码都存在模板重复代码的问题
(4)简单梳理下静态代理和动态代理的写法:
-
静态代理:代理类和原始类实现相同接口;代理类继承原始类的方法实现
-
动态代理:运行时动态的创建原始类的代理类,然后在系统中用代理类替换掉原始类。解决静态代理多个代理类重复多次模板代码问题
装饰模式
(1)使用场景
适用于在同一套体系下的功能叠加增强,业务功能职责不断扩充(更具体讲,当不能采用继承的方式对系统进行扩展或者采用继承不利于系统扩展和维护时,可以使用装饰模式)。
(2)源码使用场景
JDK 的 IO 流,如 FileInputStream、BufferedInputStream 等。
(3)公式写法
public interface Programer {
void skills();
}
public class AndroidProgramer implements Programer {
@Override
public void skills() {
System.out.println("会写 Android 代码");
}
}
// 抽象装饰类
public abstract class ProgramerDecorator implements Programer {
protected Programer programer;
public ProgramerDecorator(Programer programer) {
this.programer = programer;
}
public void skills() {
programer.skills();
}
}
// 具体装饰类
public class PatternDecorator extends ProgramerDecorator {
public PatternDecorator(Programer programer) {
super(programer);
}
@Override
public void skills() {
super.skills();
System.out.println("会设计模式");
}
}
Programer programer = new AndroidProgramer();
programer = new PatternDecorator(programer); // Android 程序员会设计模式了
programer.skills();
优点:
-
对于扩展一个对象的功能,装饰模式比继承更加灵活,可以随意组合做功能增强
-
具体构件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构件类和具体装饰类,原有类库代码无需改变,符合开闭原则
缺点:
-
多次装饰的对象调试时可能需要逐级排查,较为繁琐
-
可以随意组合,可能会出现一些不合理的逻辑
代理模式和装饰模式的区别
代理模式和装饰器模式都是基于原始类的功能增强,它们之间的区别是,代理模式是使用在附加不影响实际业务的功能场景,装饰模式是会影响业务的对原始类功能的不断扩充叠加。
享元模式
享元模式的意图是复用对象,节省内存,前提是享元对象是 不可变对象。
“不可变对象” 指的是,一旦通过构造函数初始化完成之后,它的状态(对象的成员变量或者属性)就不会再被修改了。所以,不可变对象不能暴露任何 set() 等修改内部状态的方法。之所以要求享元是不可变对象,那是因为它会被多处代码共享使用,避免一处代码对享元进行了修改,影响到其他使用它的代码。
(1)使用场景
之前产生的对象不进行销毁而选择保留复用,节省内存。
(2)源码使用场景
Message、线程池、数据库连接池。
优点:
- 减少内存中对象的数量,节约系统资源
缺点:
- 需要及时维护复用对象的状态,操作不当容易引发问题
外观模式
门面模式为子系统提供一组统一的接口,定义一组高层接口让子系统更易用。
“子系统” 可以有多种理解,可以是一个完整的系统,也可以是更细粒度的类或者模块。
(1)使用场景
用于提供 sdk 高层次接口,方便调用隐藏实现细节(简单理解就是要搞 sdk 给别人用,提供些简单的接口公开,接口内部细节隐藏)。
(2)源码使用场景
(3)公式写法
public class PowerSystem {
public void powerOn() {}
public void powerOff() {}
}
public class VoiceSystem {
public void turnUp() {}
public void turnDown() {}
}
public class ChannelSystem {
public void next() {}
public void prev() {}
}
// 外观类
public class TvController {
private PowerSystem powerSystem = new PowerSystem();
private VoiceSystem voiceSystem = new VoiceSystem();
private ChannelSystem channelSystem = new ChannelSystem();
public void powerOn() {
powerSystem.powerOn();
}
public void powerOff() {
powerSystem.powerOff();
}
public void turnUp() {
voiceSystem.turnUp();
}
public void turnDown() {
voiceSystem.turnDown();
}
public void nextChannel() {
channelSystem.next();
}
public void prevChannel() {
channelSystem.prev();
}
}
(4)注意事项
外观模式简单理解就是用于接口的整合。接口粒度设计得太大,太小都不好。太大会导致接口不可复用,太小会导致接口不易用。在实际的开发中,接口的可复用性和易用性需要 “微妙” 的权衡。针对这个问题,我的一个基本的处理原则是,尽量保持接口的可复用性,但针对特殊情况,允许提供冗余的外观接口,来提供更易用的接口。
优点:
-
使用方便,客户端不需要知道子系统实现过程
-
降低客户端与子系统的耦合
-
更好的划分访问层次
缺点:
-
减少了可变形和灵活性
-
在不引入抽象外观类的情况下,增加新的子系统可能需要修改外观类或客户端的源代码,违反开闭原则
组合模式
将一组对象(文件和目录)组织成 树形结构,以表示一种 “部分 - 整体” 的层次结构(目录与子目录的嵌套结构)。组合让调用者可以统一单个对象(文件)和组合对象(目录)的处理逻辑(递归遍历)。
(1)使用场景
适用于树状结构、能上下级包含无限级联的场景。
(2)源码使用场景
File、View 的触摸反馈。
(3)公式写法
public abstract class Company {
protected String name;
public Company(String name) {
this.name = name;
}
protected abstract void add(Company company);
protected abstract void remove(Company company);
/**
* 展示公司或部门
*/
protected abstract void display();
/**
* 公司或部门职责
*/
protected abstract void response();
}
public class ConcreteCompany extends Company {
private List<Company> childCompany = new ArrayList<>();
public ConcreteCompany(String name) {
super(name);
}
@Override
protected void add(Company company) {
childCompany.add(company);
}
@Override
protected void remove(Company company) {
childCompany.remove(company);
}
@Override
protected void display() {
for (Company company : childCompany) {
company.display();
}
}
@Override
protected void response() {
for (Company company : childCompany) {
company.response();
}
}
}
public class FinanceDepartment extends Company {
public FinanceDepartment(String name) {
super(name);
}
@Override
protected void add(Company company) {}
@Override
protected void remove(Company company) {}
@Override
protected void display() {
System.out.println("-" + name);
}
@Override
protected void response() {
System.out.println(name + "财务管理培训");
}
}
public class HRDepartment extends Company {
public HRDepartment(String name) {
super(name);
}
@Override
protected void add(Company company) {}
@Override
protected void remove(Company company) {}
@Override
protected void display() {
System.out.println("-" + name);
}
@Override
protected void response() {
System.out.println(name + "人力管理培训");
}
}
Company rootCompany = new ConcreteCompany("北京公司总部");
rootCompany.add(new HRDepartment("北京公司总部人力资源部"));
rootCompany.add(new FinanceDepartment("北京公司总部财务部"));
Company shenzhenCompany = new ConcreteCompany("深圳分公司");
shenzhenCompany.add(new HRDepartment("深圳分公司人力资源部"));
shenzhenCompany.add(new FinanceDepartment("深圳分公司财务部"));
rootCompany.add(shenzhenCompany);
Company guangzhouCompany = new ConcreteCompany("广州分公司");
guangzhouCompany.add(new HRDepartment("广州分公司人力资源部"));
guangzhouCompany.add(new FinanceDepartment("广州分公司财务部"));
rootCompany.add(guangzhouCompany);
rootCompany.display();
rootCompany.response();
优点:
- 在满足树形结构的业务场景能很好的简化代码实现
缺点:
- 使用场景有限
适配器模式
(1)使用场景
处理两套数据结构不同的方案有设计缺陷不兼容的场景,由第三方 Adapter 提供标准做转换整合(简单理解:我要一个目标数据,Adapter 帮我将某类数据转换成这个数据,具体怎么转换原始数据是怎样的细节不关心)。
(2)源码使用场景
ListView、RecyclerView 最终要获取的是 View,View 的生产由什么东西转换提供 Adapter 转换。
适配器有两种实现方式:类适配器和对象适配器。
(3)类适配器公式写法
// 类适配器: 基于继承
public interface ITarget {
void f1();
void f2();
void fc();
}
public class Adaptee {
public void fa() { //... }
public void fb() { //... }
public void fc() { //... }
}
public class Adaptor extends Adaptee implements ITarget {
public void f1() {
super.fa();
}
public void f2() {
// ...重新实现f2()...
}
// 这里fc()不需要实现,直接继承自Adaptee,这是跟对象适配器最大的不同点
}
(4)对象适配器公式写法
// 对象适配器:基于组合
public interface ITarget {
void f1();
void f2();
void fc();
}
public class Adaptee {
public void fa() { //... }
public void fb() { //... }
public void fc() { //... }
}
public class Adaptor implements ITarget {
private Adaptee adaptee; // 作为属性持有
public Adaptor(Adaptee adaptee) {
this.adaptee = adaptee;
}
public void f1() {
adaptee.fa(); // 委托给Adaptee
}
public void f2() {
// ...重新实现f2()...
}
public void fc() {
adaptee.fc();
}
}
那么该选择哪一种呢?判断标准主要有两个,一个是 Adaptee 接口的个数,另一个是 Adaptee 和 ITarget 的契合程度。
-
如果 Adaptee 接口并不多,那两种实现方式都可以
-
如果 Adaptee 接口很多,而且 Adaptee 和 ITarget 接口定义大部分都相同,推荐使用类适配器。因为 Adaptor 复用父类 Adaptee 的接口,比起对象适配器的实现方式,Adaptor 的代码量要少一些
-
如果 Adaptee 接口很多,而且 Adaptee 和 ITarget 接口定义大部分都不相同,那推荐使用对象适配器,因为组合结构相对于继承更加灵活
优点:
-
更好的复用性:系统需要使用现有的类,而此类的接口不符合系统需要,通过适配器模式就可以让这些功能得到更好的复用
-
更好的扩展性:在实现适配器功能的时候,可以调用自己开发的功能,从而自然的扩展系统功能
缺点:
- 过多的使用适配器会让系统非常凌乱,不易整体进行把握,适配器模式更多的应该是作为缺陷设计的补偿方案,已经难以维护更应该选择的是重构
桥接模式
(1)使用场景
在一个业务体系下割离出两套需要扩展的业务(顾名思义用桥梁连接,说明是两套不同的东西)。
(2)源码使用场景
(3)公式写法
// 两套不同的方案
public class DP1 {
public void draw_1_Rectangle() {
System.out.println("DP1的程序画矩型");
}
public void draw_1_Circle() {
System.out.println("DP1的程序画圆形");
}
}
public class DP2 {
public void drawRectangle() {
System.out.println("DP2的程序画矩型");
}
public void drawCircle() {
System.out.println("DP2的程序画圆形");
}
}
// Implementor,将两套不同的方案抽离一层结构
public interface Drawing {
void drawRectangle();
void drawCircle();
}
public class V1Drawing implements Drawing {
private DP1 dp1;
public V1Drawing() {
dp1 = new DP1();
}
@Override
public void drawRectangle() {
dp1.draw_1_Rectangle();
}
@Override
public void drawCirlce() {
dp1.draw_1_Circle();
}
}
public class V2Drawing implements Drawing {
private DP2 dp2;
public V2Drawing() {
dp2 = new DP2();
}
@Override
public void drawRectangle() {
dp2.drawRectangle();
}
@Override
public void drawCircle() {
dp2.drawCircle();
}
}
// Abstraction,要绘制的类型
public abstract class Shape {
protected Drawing drawing;
public Shape(Drawing drawing) {
this.drawing = drawing;
}
protected void drawRectangle() {
drawing.drawRectangle();
}
protected void drawCircle() {
drawing.drawCircle();
}
public abstract void draw();
}
public class Rectangle extends Shape {
public Rectangle(Drawing drawing) {
super(drawing);
}
@Override
public void draw() {
drawRectangle();
}
}
public class Circle extends Shape {
public Circle(Drawing drawing) {
super(drawing);
}
@Override
public void draw() {
drawCircle();
}
}
结构性模式总结
结构型模式的核心:通过对类的结构关系的调整来完成特定的业务。考虑做功能增强的场景。
再简单梳理下结构型模式的使用场景:
-
代理模式:需要功能附加增强的业务场景(即不影响实际业务执行的功能附加)
-
装饰模式:适用于在同一套体系下的功能叠加增强,业务功能职责不断扩充
-
享元模式:产生的对象不进行销毁而选择保留复用,节省内存
-
外观模式:用于提供 sdk 高层次接口,方便调用隐藏实现细节
-
组合模式:适用于树状结构、能上下级包含无限级联的场景
-
适配器模式:处理两套数据结构不同的方案有设计缺陷不兼容的场景,由第三方 Adapter 提供标准做转换整合
-
桥接模式:在一个业务体系下割离出两套需要扩展的业务
行为型模式
策略模式
定义一族算法类,将每个算法分别封装起来,让它们可以互相替换。策略模式可以使算法的变化独立于使用它们的客户端(这里的客户端代指使用算法的代码)。
(1)使用场景
为了达到某种业务,提供多种算法方案给用户选择,算法方案是独立的。
策略模式适用于根据类型的动态,决定使用哪种策略的场景。最常见的应用场景是,利用它来避免冗长的 if-else 或 switch 分支判断。
(2)源码使用场景:
Interpolator。
(3)公式写法
// 策略的抽象
public interface Strategy {
double calc(double paramA, double paramB);
}
// 具体的策略实现
public class AddStrategy implements Strategy {
@Override
public double calc(double paramA, double paramB) {
return paramA + paramB;
}
}
public class SubStrategy implements Strategy {
@Override
public double calc(double paramA, double paramB) {
return paramA - paramB;
}
}
// Context 上下文操作策略
public class Calc {
private Strategy strategy;
public void setStrategy(Strategy strategy) {
this.strategy = strategy;
}
public double calc(double paramA, double paramB) {
if (strategy == null) {
throw new IllegalArgumentException();
}
return strategy.calc(paramA, paramB);
}
}
Calc calc = new Calc();
Strategy addStrategy = new AddStrategy();
calc.setStrategy(addStrategy);
calc.calc(5, 21);
Strategy subStrategy = new SubStrategy();
calc.setStrategy(subStrategy);
calc.calc(5, 21);
(4)策略模式的用法
一个完成的策略模式又策略的定义、创建、使用三部分组成。
-
策略类的定义:定义一个策略接口和一组实现这个接口的策略类。因为所有的策略类都实现相同的接口
-
策略类的创建:一般会通过类型(type)来判断创建哪个策略来使用,将策略的创建由工厂类完成,封装策略创建的细节
-
策略类的使用:最常见的是运行时动态确定使用哪种策略。”运行时动态“ 的意思是在代码运行前不知道具体会使用哪种策略,会通过读取配置的方式获取创建对应的策略(例如 APT 的 SPI 机制)。而 直接在代码写好的方式并不能发挥策略模式的优势,在这种场景下,策略模式实际上退化成了 “面向对象的多态性” 或 “基于接口而非实现编程原则”
(5)注意事项
策略模式的作用并不是避免 if-else 分支判断逻辑,而是 if-else 判断逻辑有一定的复杂度,所以才需要用策略模式拆分成简单的代码。if-else 如果代码已经足够简单后期不需要改动,完全可以直接使用,没必要因此引入策略模式导致过度设计。
优点:
-
遵循开闭原则,有新的方案只需要新增策略算法替换
-
提供有多个 if-else 分支维护的代码的另一种实现方案(消除 if-else),提高代码可维护性和复用性
缺点:
- 如果方案较多,会有很多的策略算法类
责任链模式
(1)使用场景
针对流程传递相关的业务,抽象业务对象,将业务对象串联成链式结构(并不是指定要用链表,只要能实现链式结构就行,比如列表容器),到某个节点能完成条件就返回的业务场景。比如钉钉 OA 流程审批,拦截器等。
(2)源码使用场景
OkHttp 拦截器。
(3)公式写法
public class Request {
private String requestType;
private String requestContent;
private int requestNumber;
// getter and setter
}
public abstract class Manager {
protected Manager superior;
public Manager(Manager superior) {
this.superior = superior;
}
protected abstract void handleRequst(Request request);
}
public class CommonManager extends Manager {
public CommandManager(Manager superior) {
super(superior);
}
@Override
protected void handleRequest(Request request) {
if (request.getRequestType().equals("请假") && request.getRequestNumber() <= 2) {
System.out.println("经理直接批准两天内的请假");
} else {
superior.handleRequest(request);
}
}
}
public class MajordomoManager extends Manager {
public MajordomoManager(Manager superior) {
super(superior);
}
@Override
protected void handleRequest(Request request) {
if (request.getRequestType().equals("请假") && request.getRequestNumber() <= 5) {
System.out.println("总监直接批准5天内的请假");
} else {
superior.handleRequest(request);
}
}
}
public class GeneralManager extends Manager {
public GeneralManager(Manager superior) {
super(superior);
}
@Override
protected void handleRequest(Request request) {
if (request.getRequestType().equals("请假")) {
System.out.println("总经理批准任意天数的请假");
} else if (request.getRequestType().equals("加薪") && request.getRequestNumber() <= 500){
System.out.println("500元的加薪同意");
} else if (request.getRequestType().equals("加薪") && request.getRequestNumber() > 500) {
System.out.println("提薪再说吧");
}
}
}
Manager generalManager = new GeneralManager(null);
Manager majordomoManager = new MajordomoManager(generalManager);
Manager commonManager = new CommonManager(majordomoManager);
Request request1 = new Request();
request1.setRequestType("请假");
request1.setRequestContent("我要请假");
request1.setRequestNumber(1);
commonManager.handleRequest(request1);
Request request2 = new Request();
request2.setRequestType("请假");
request2.setRequestContent("我要请假");
request2.setRequestNumber(4);
commonManager.handleRequest(request2);
Request request3 = new Request();
request3.setRequestType("加薪");
request3.setRequestContent("我要加薪");
request3.setRequestNumber(500);
commonManager.handleRequest(request3);
Request request4 = new Request();
request4.setRequestType("加薪");
request4.setRequestContent("我要加薪,不然走人");
request4.setRequestNumber(1000);
commonManager.handleRequest(request4);
优点:
- 让框架的使用者在不需要修改框架源码的情况下,添加新的过滤拦截功能,符合开闭原则
状态模式
状态模式是状态机的一种实现方式,状态机有 3 个组成部分:状态(State)、事件(Event)、动作(Action)。其中,事件也称为转移条件(Transition Condition)。事件触发状态的转移及动作的执行。
不过,动作不是必须的,也可能只转移状态,不执行任何动作。
(1)使用场景
对象的行为取决于它的状态,并且必须在运行时根据状态改变它的行为;一个操作中含有庞大的分支结构(需要很多 if-else 处理状态),并且这些分支决定于对象的状态时。比如已登陆时一种处理,未登陆是一种处理。
(2)源码使用场景
WIFI 状态的应用。
(3)公式写法
public abstract class State {
public abstract void handle(Context context);
}
// 状态之间有关联,这是与策略模式的不同,策略模式的算法是独立的
public class ConcreteStateA extends State {
@Override
public void handle(Context context) {
context.setState(new ConcreteStateB()); // 关联了状态 B
}
}
public class ConcreteStateB extends State {
@Override
public void handle(Context context) {
context.setState(new ConcreteStateA()); // 关联了状态 A
}
}
public class Context {
private State state = new ConcreteStateA(); // 默认状态
public Context() {}
public void setState(State state) {
this.state = state;
}
public State getState() {
return state;
}
public void handle() {
state.handle(this);
}
}
Context context = new Context();
context.handle();
优点:
-
将与特定状态相关的行为局部化到一个状态中,并且将不同的状态的行为分割开来,满足单一职责原则
-
将状态转换显式化,减少对象间的相互依赖
-
状态类职责明确,有利于程序扩展。通过定义新的子类很容易新增新的状态和转换
缺点:
-
增加类与对象的个数
-
结构与实现较为复杂,使用不当会导致程序结构和代码混乱
-
开闭原则支持不太好,对于可以切换状态的状态模式,增加新的状态类需要修改那些负责状态转换的源码,否则无法切换到新增状态,而且修改某个状态类的行为也需要修改对应类的源码
策略模式、状态模式、责任链模式的区别
首先看策略模式和状态模式的区别,可以看到策略模式和状态模式的结构图几乎一样,但两者的应用场景是不一样的。策略模式的算法彼此之间是独立的,用户可自行更换策略算法;而状态模式的各个状态之间存在相互关系,彼此之间在一定条件下存在自动切换状态的效果,并且用户无法指定状态,只能设置初始状态。
策略模式和责任链模式的区别是,策略模式是单个方案的实现不关注结果,责任链是链式串起来符合条件才返回。
状态模式和责任链模式的区别是,状态模式和责任链模式都能消除 if-else 分支过多问题,但在某些情况下,状态模式中的状态可以理解为责任,那么在这种情况下,两种模式都可以使用。
从定义来看,状态模式强调的是一个对象内在状态的改变,而责任链模式强调的是外部节点对象间的改变。两者最大的区别是,状态模式的各个状态对象知道自己要进入的下一个状态对象,而责任链模式并不清楚其下一个节点处理对象,因为链式组装是由上层负责。
解释器模式
解释器模式为某个语言定义它的语法(或者叫文法)表示,并定义一个解释器用来处理这个语法。
(1)使用场景
将表达式(翻译规则)抽象出来,专门做字符串翻译的场景。一般用的比较少。只在一些特定的领域会被用到,比如编译器、规则引擎、正则表达式。
(2)公式写法
public interface Expression {
void interpret(PlayContext context);
}
public class Note implements Expression {
@Override
public void interpret(PlayContext context) {
String note = "";
switch (context.getCurrentText()) {
case 'C':
note = "1";
break;
case 'D':
note = "2";
break;
case 'E':
note = "3";
break;
case 'F':
note = "4";
break;
case 'G':
note = "5";
break;
case 'A':
note = "6";
break;
case 'B':
note = "7";
break;
}
System.out.print(note + " ");
}
}
public class Scale implements Expression {
@Override
public void interpret(PlayContext context) {
String scale = "";
switch (String.valueOf(context.getCurrentText())) {
case "0.5":
case "1":
scale = "低音";
break;
case "2":
scale = "中音";
break;
case "3":
scale = "高音";
break;
}
System.out.print(scale + " ");
}
}
public class PlayContext {
private String playText;
private char currentText;
public String getPlayText() {
return playText;
}
public void setPlayText(String playText) {
this.playText = playText;
}
public char getCurrentText() {
return currentText;
}
public void setCurrentText(char currentText) {
this.currentText = currentText;
}
}
PlayContext context = new PlayContext();
context.setPlayText("O 2 E 0.5 G 0.5 A 3 E 0.5 G 0.5 D 3 E 0.5 G 0.5 A" +
" 0.5 O 3 C 1 O 2 A 0.5 G 1 C 0.5 E 0.5 D 3");
String playText = context.getPlayText().trim();
int length = playText.length();
for (int i = 0; i < length; i++) {
char c = playText.charAt(i);
context.setCurrentText(c);
// 根据字符获取表达式对象
Expression expression = ExpressionFactory.createExpression(String.valueOf(c));
if (expression != null) {
expression.interpret(context);
}
}
优点:
- 每种表达式负责一种类型的解析,也能够很容易的改变和扩展表达式,因为该模式使用类来表示表达式规则,可以使用继承来改变或扩展表达式
缺点:
- 每一种表达式定义一个类,包含许多规则时可能难以管理和维护,容易引起类膨胀
命令模式
命令模式将请求(命令)封装为一个对象,这样可以使用不同的请求参数化其他对象(将不同请求依赖注入到其他对象),并且能够支持请求(命令)的排队执行、记录日志、撤销等(附加控制)功能。
(1)使用场景
一般伴随着执行指令和撤销指令,用在需要记录每一步操作又可以回撤的业务场景,命令式做到功能增强。
(2)公式写法
public interface Command {
void execute();
void undo();
void redo();
}
public class ConcreteCommand1 implements Command {
private Receiver receiver;
public ConcreteCommand1(Receiver receiver) {
this.receiver = receiver;
}
@Override
public void execute() {
// 加入命令排队等待,未执行的命令支持 redo 操作
receiver.opActionUpdateAge(1001);
}
@Override
public void undo() {
// 撤销回滚操作
receiver.rollBackAge();
}
@Override
public void redo() {
// 命令执行前修改命令的执行
}
}
public class ConcreteCommand2 implements Command {
private Receiver receiver;
public ConcreteCommand2(Receiver receiver) {
this.receiver = receiver;
}
@Override
public void execute() {
// 加入命令排队等待,未执行的命令支持 redo 操作
receiver.opActionUpdateName("xxx");
}
@Override
public void undo() {
// 撤销回滚操作
receiver.rollBackName();
}
@Override
public void redo() {
// 命令执行前修改命令执行
}
}
public class Receiver {
private People people;
private People peopleCache = new People(); // 用于回滚
public Receiver() {
this.people = new People(-1, "defaultName");
}
public void opActionUpdateAge(int age) {
people.update(age);
}
public void opActionUpdateName(String name) {
people.update(name);
}
public void rollBackAge() {
people.setAge(peopleCache.getAge());
}
public void rollBackName() {
people.setName(peopleCache.getName());
}
}
public class Invoke {
private Command command1;
private Command command2;
public void setCommand1(Command command1) {
this.command1 = command1;
}
public void setCommand2(Command command2) {
this.command2 = command2;
}
public void invoke(int args) {
if (args == 0) {
command1.execute();
command2.execute();
} else if (args == 1) {
command1.undo();
command2.undo();
}
}
}
Receiver receiver = new Receiver();
Command command1 = new ConcreteCommand1(receiver);
Command command2 = new ConcreteComman2(receiver);
Invoke invoke = new Invoke();
invoke.setCommand1(command1);
invoke.setCommand2(command2);
invoke.invoke(0);
invoke.invoke(1);
优点:
-
降低对象之间的耦合度
-
新的命令可以很容易加入到系统中,可以比较容易设计一个组合命令
缺点:
- 一个命令一个操作,可能会有过多的命令类,影响使用
观察者模式
在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知。一般情况下,被依赖的对象叫作被观察者(Observable),依赖的对象叫作观察者(Observer)。
(1)使用场景
一个对象的改变需要同时改变其他对象,且它不知道具体有多少个对象有待改变的订阅发布场景。
(2)源码使用场景
JDK 的 Observable 和 Observer、EventBus、RxJava、LiveData 等。
(3)公式写法
public interface Observable {
void register(Observer observer);
void unregister(Observer observer);
void notifyChanged(Object obj);
}
public interface Observer {
void update(Object obj);
}
public class StockObserver implements Observer {
@Override
public void update(Object obj) {
String msg = (String) obj;
System.out.println(msg + "停止观看股票继续工作!!!");
}
}
public class NBAObserver implements Observer {
@Override
public void update(Object obj) {
String msg = (String) obj;
System.out.println(msg + "停止观看NBA继续工作!!!");
}
}
public class ReceptionObservable implements Observable {
private List<Observer> observerList = new ArrayList<>();
@Override
public void register(Observer observer) {
observerList.add(observer);
}
@Override
public void unregister(Observer observer) {
observerList.remove(observer);
}
@Override
public void notifyChanged(Object obj) {
for (Observer observer : observerList) {
observer.update(obj);
}
}
}
Observable receptionObservable = new ReceptionObservable();
Observer nbaObserver = new NBAObserver();
Observer stockObserver = new StockObserver();
receptionObservable.register(nbaObserver);
receptionObservable.register(stockObserver);
receptionObservable.notifyChanged("自定义观察者通知老板回来了!!!");
bossObservable.notifyChanged("自定义观者者通知以后注意!!!");
备忘录模式
在不违背封装原则的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便之后恢复对象为先前的状态。
这个模式的定义表达了两部分内容:一部分是,存储副本以便后期恢复;另一部分是,要在不违背封装原则的前提下,进行对象的备份和恢复。
(1)使用场景
需要记录对象的内部状态(备忘录类存储对象状态),支持保存/恢复数据状态的业务场景。
(2)源码使用场景
onSaveInstanceState,Activity 相当于 Caretaker,Activity、Fragment、View、ViewGroup 相当于 Originator,Bundle 相当于 Memento。
(3)公式写法
public class Memento {
private String state;
public Memento(String state) {
this.state = state;
}
public String getState() {
return state;
}
}
public class Originator {
private String state;
public void setState(String state) {
this.state = state;
}
public String getState() {
return state;
}
public Memento saveStateToMemento() {
return new Memento(state);
}
public void getStateFromMemento(Memento memento) {
state = Memento.getState();
}
}
public class CareTaker {
private List<Memento> mementoList = new ArrayList<>();
public void add(Memento state) {
mementoList.add(state);
}
public Memento get(int index) {
return mementoList.get(index);
}
}
Originator originator = new Originator();
CareTaker careTaker = new CareTaker();
originator.setState("State #1");
originator.setState("State #2");
careTaker.add(originator.saveStateToMemento());
originator.setState("State #3");
careTaker.add(originator.saveStateToMemento());
originator.setState("State #4");
System.out.println("Current State:" + originator.getState());
originator.getStateFromMemento(careTaker.get(0));
System.out.println("First saved State:" + originator.getState());
originator.getStateFromMemento(careTaker.get(1));
System.out.println("Second saved State:" + originator.getState());
优点:
-
给用户提供了一种可以恢复状态的机制,可以使用户能够比较方便回到某个历史状态
-
实现信息封装,用户不需要关心状态保存细节
缺点:
- 消耗资源,类成员变量过多势必占用比较大资源,每一次保存都会消耗一定内存。一般还会搭配享元模式,因为可能会大量创建对象引发GC
模板方法模式
模板方法模式在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。
“算法” 可以理解为广义上的 “业务逻辑”,并不特指数据结构和算法中的 “算法”。“算法骨架” 的方法其实就是 “模板方法”。
(1)使用场景
模板模式主要是用来 解决复用和扩展 两个问题
简单说就是继承,把公共的抽象提供方法出来让子类各自实现,统一流程。
(2)使用方式
模板方法定义为 final,可以避免被子类重写。需要子类重写的方法定义为 abstract,可以强迫子类去实现。
优点:
-
封装不变部分,扩展可变部分
-
提取公共部分代码,便于维护
缺点:
- 降低代码可读性
模板模式和回调的区别
从代码实现上来看,回调和模板模式完全不同。回调基于组合关系来实现,把一个对象传递给另一个对象,是一种对象之间的关系;模板模式基于继承关系来实现,子类重写父类的抽象方法,是一种类之间的关系。
迭代器模式
(1)使用场景
迭代器模式其实就是因为数据结构的不同才用,不同的人做了差不多的事情,运用的结构不一致,结构的提取算法会不同,为了统一提取算法用迭代器模式。即 将数据提供方案统一,屏蔽掉数据结构不同所带来的额外工作量。
(2)源码使用场景
List 和 Map 统一可以使用 Iterator 处理遍历。
优点:
- 分离了集合对象的遍历行为,抽象出一个迭代器来负责,这样不暴露集合的内部结构,又可让外部代码透明的访问集合内部的数据
中介者模式
中介模式定义了一个单独的(中介)对象,来封装一组对象之间的交互。将这组对象之间的交互委派给与中介对象交互,来避免对象之间的直接交互。
(1)使用场景
中介者模式解决的问题是耦合太高,对象之间依赖耦合严重,提供一个中介者统一管理这些业务有交互的对象,由中介者统一处理,整理关系(简单理解就是,中介对象持有各个对象之间的引用,对象之间不直接关联,对象之间的交互交由中介对象处理)。
(2)公式写法
public abstract class Colleague {
protected int number;
// getter and setter
public abstract void setNumber(int number, Colleague coll);
}
public class ColleagueA extends Colleague {
@Override
public void setNumber(int number, Colleague coll) {
this.number = number;
coll.setNumber(number * 100);
}
}
public class ColleagueB extends Colleague {
@Override
public void setNumber(int number, Colleague coll) {
this.number = number;
coll.setNUmber(number / 100);
}
}
Colleague collA = new ColleagueA();
Colleague collB = new ColleagueB();
// ColleagueA 和 ColleagueB 发生关联
collA.setNumber(1288, collB);
collB.setNumber(87635, collA);
上面的代码 ColleagueA 和 ColleagueB 发生了关联,如果现在我们想让两者独立,就可以使用中介者模式。看下怎么修改:
public abstract class Colleague {
protected int number;
// getter and setter
// 修改为中介者 Mediator
public abstract void setNumber(int number, Mediator mediator);
}
public class ColleagueA extends Colleague {
@Override
public void setNumber(int number, Mediator mediator) {
this.number = number;
mediator.AeffectB();
}
}
public class ColleagueB extends Colleague {
@Override
public void setNumber(int number, Mediator mediator) {
this.number = number;
mediator.BeffectA();
}
}
// 中介者
public abstract class Mediator {
protected Collegue collA;
protected Collegue collB;
public Mediator(Colleague collA, Colleague collB) {
this.collA = collA;
this.collB = collB;
}
public abstract void AeffectB();
public abstract void BeffectA();
}
public class MediatorImpl extends Mediator {
public MediatorImpl(Colleague collA, Colleague collB) {
super(collA, collB);
}
public void AeffectB() {
int number = collA.getNumber();
B.setNumber(number * 100);
}
public void BeffectA() {
int number = collB.getNumber();
A.setNumber(number / 100);
}
}
Colleague collA = new ColleagueA();
Colleague collB = new ColleagueB();
Mediator mediator = new MediatorImpl(collA, collB);
collA.setNumber(1000, mediator);
collB.setNumber(1000, mediator);
优点:
- 适当的使用中介者模式可以避免对象之间过度耦合,使得对象之间可以相对独立的使用
缺点:
- 中介对象 Mediator 集中化,把交互复杂性变为中介者的复杂性,使得每个 Mediator 都变得复杂
访问者模式
允许一个或者多个操作应用到一组对象上,解耦操作和对象本身。保持类职责单一、满足开闭原则以及应对代码的复杂性。
(1)使用场景
适用于数据结构相对稳定,有易于变化算法的系统,即适用于对已有功能的重构。
比如说,一个项目的基本功能已经确定下来,Element 类的数据已经基本确定下来不会变了,会变的只有这些 Element 类内的相关操作,这时候,就可以使用访问者模式对原有代码进行重构一遍,这样一来,就可以在不修改各个 Element 类的情况下,对原有功能进行修改。
(2)公式写法
// Element
public abstract class Staff {
public String name;
public int kpi;
public Staff(String name) {
this.name = name;
kpi = new Random().nextInt(10);
}
public abstract void accept(Visitor visitor);
}
public interface Visitor {
// 访问者类对每个 Element 都有对应的方法处理
// 所以新增 Element 就要加一个方法处理,Visitor 实现类也要同步修改
// 这是访问者模式的缺点,也是访问者模式适用场景提及的要 “数据结构相对稳定”
void visit(Engineer engineer);
void visit(Manager leader);
}
public class CEOVisitor implements Visitor {
@Override
public void visit(Engineer engineer) {
System.out.println("工程师:" + engineer.name + ", kpi:" + engineer.kpi);
}
@Override
public void visit(Manager leader) {
System.out.println("经理:" + leader.name + ", kpi:" + leader.kpi + ", 新产品数量:" + leader.getProducts());
}
}
public class CTOVisitor implements Visitor {
@Override
public void visit(Engineer engineer) {
System.out.println("工程师:" + engineer.name + ", 代码函数:" + engineer.getCodeLines());
}
@Override
public void visit(Manager leader) {
System.out.println("经理:" + leader.name + ", 产品数量:" + leader.getProducts());
}
}
// ObjectStructur,允许访问者访问
public class BusinessReport {
private final List<Staff> staffs = new ArrayList<>();
public BusinessReport() {
staffs.add(new Manager("王经理"));
staffs.add(new Manager("工程师1"));
staffs.add(new Manager("工程师2"));
staffs.add(new Manager("工程师3"));
staffs.add(new Manager("工程师4"));
}
public void showReport(Visitor visitor) {
for (Staff staff : staffs) {
staff.accept(visitor);
}
}
}
BusinessReport report = new BusinessReport();
CEOVisitor ceoVisitor = new CEOVisitor();
CTOVisitor ctoVisitor = new CTOVisitor();
report.showReport(ceoVisitor);
report.showReport(ctoVisitor);
优点:
- 凡是适用访问者模式的场景中,Element 类需要封装在访问者中的操作必定是与 Element 类本身关系不大且是易变的操作,使用访问者模式一方面符合单一职责原则,另一方面,因为被封装的操作通常来说都是易变的,所以当发生变化时,就可以在不改变 Element 类本身的前提下,实现对变化部分的扩展
缺点:
- 增加新的 Element 类比较困难。在 Visitor 类中,每一个 Element 类都有它对应的处理方法,也就是说,每增加一个 Element 类都要修改访问者类(也包括访问者的子类或者实现类),修改起来相当麻烦。也就是说,在 Element 类数目不确定的情况下,应该慎用访问者模式
行为型模式总结
行为型模式的核心:通过特别的写法,达成某个业务场景下的具体业务诉求。看行为型模式的时候。主要看这些代码所完成的业务。
-
策略模式:提供多种算法方案给用户选择,算法方案是独立的
-
责任链模式:针对流程传递相关的业务,抽象业务对象,将业务对象串联成链式结构。例如 OA 流程、拦截器等
-
状态模式:对象的行为取决于它的状态,并且必须在运行时根据状态改变它的行为;一个操作中含有庞大的分支结构(需要很多 if-else 处理状态),并且这些分支决定于对象的状态时
-
解释器模式:主要针对字符串的翻译。例如协议解析 json,自定义协议,正则表达式等
-
命令模式:一般伴随着执行指令和撤销指令,用在需要记录每一步操作又可以回撤的业务场景,命令式做到功能增强
-
观察者模式:一个对象的改变需要同时改变其他对象,且它不知道具体有多少个对象有待改变的订阅发布场景
-
备忘录模式:需要记录对象的内部状态(备忘录类存储对象状态),支持保存/恢复数据状态的业务场景
-
模板方法模式:将一些步骤延后到子类,并保证流程正常。简单说就是继承,把公共的抽象提供方法出来让子类各自实现,统一流程
-
迭代器模式:将数据提供方案统一,屏蔽掉数据结构不同所带来的额外工作量
-
中介者模式:中介者模式解决的问题是耦合太高,对象之间依赖耦合严重,提供一个中介者统一管理这些业务有交互的对象,由中介者统一处理,整理关系
-
访问者模式:适用于数据结构相对稳定,有易于变化算法的系统,即适用于对已有功能的重构
总结
-
创建型模式:让你们知道怎么去 new 对象。让你考虑内存性能和扩展性,以及后续代码维护的工作量
-
结构型模式:通过对类的结构关系的调整来完成特定的业务。考虑做功能增强的场景
-
行为型模式:让你见识到业务开发过程当中的常规业务。行为模式的每个模式都有一个核心目的,要关注业务的实现,而不是模式下的固定写法,可以根据业务只用一半