主要参考资料
设计模式要干的事情就是解耦。创建型模式是将创建和使用代码解耦,结构型模式是将不同功能代码解耦,行为型模式是将不同的行为代码解耦。
SOLID设计原则
- S:单一职责。
- O:开闭原则。“对扩展开放、修改关闭”。
- L:里氏代换。子类可替代父类。跟多态不同的是,子类不会改变父类的行为。
- I:接口隔离。不依赖不需要的接口。例如将函数或类拆为更细粒度的接口。单一职责侧重于接口设计,接口隔离则从调用者角度考虑
- D:依赖反转。控制权由程序员交给系统。
创建型
主要解决“对象的创建”问题。焦点是如何创建对象。用于取代new
来创建对象。
- 单例模式用来创建全局唯一的对象。
- 工厂模式用来创建不同但是相关类型的对象。(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象。
- 建造者模式是用来创建复杂对象,可以通过设置不同的可选参数,“定制化”地创建不同的对象。
- 原型模式针对创建成本比较大的对象,利用对已有对象进行复制的方式进行创建,以达到节省创建时间的目的。
单例模式
一个类只允许创建一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例模式。
使用用途
- 处理资源访问冲突:如日志多线程向同一个文件写数据,加类级别锁可以解决冲突,但是每个线程都要新建一个对象,太浪费了。
- 表示全局唯一类:比如,配置信息类,唯一递增 ID 号码生成器。
单例模式的缺点是对扩展性支持不好,因为是写死的,将来要扩展的话很难扩展,测试时也很难mock。单例模式可以用IOC、工厂等方式替代。
实现方案
- 1.私有化构造方法,并提供获取对象的方法
getInstance()
。 - 2.考虑是否支持延迟加载:饿汉式。直接
new()
好。 - 3.考虑对象创建时的线程安全问题:懒汉式。直接在
getInstance()
上加类级锁。 - 4.考虑 getInstance() 性能是否高(是否加锁):双检锁。减小锁粒度,两次判null。其他:内部类,枚举。
示例代码1:饿汉式
最简单的,对象在初始化时就new好。不支持延迟加载。
public class IdGenerator {
private AtomicLong id = new AtomicLong(0);
private static final IdGenerator instance = new IdGenerator();
private IdGenerator() {}
public static IdGenerator getInstance() {
return instance;
}
public long getId() {
return id.incrementAndGet();
}
}
示例代码2:懒汉式
支持延迟加载,加了判空操作,并加类级别锁。
缺点是加了类级别锁,性能很差。如果这个对象是频繁使用的,那么相当于串行操作了。
public class IdGenerator {
private AtomicLong id = new AtomicLong(0);
private static final IdGenerator instance;
private IdGenerator() {}
public static synchronized IdGenerator getInstance() {
if (instance == null) {
instance = new IdGenerator();
}
return instance;
}
public long getId() {
return id.incrementAndGet();
}
}
示例代码3:双检锁
用双检锁提高了性能。双检的意思是判断为空判断了两次,以此不加锁的判断,一次加锁的判断。
判断空时不加锁,判断为空后新建对象时才加类级别的锁。
注意加了volatile
关键字。因为老版本的java,new
一个对象不是原子操作。新版本已解决该问题,不用加volatile
。
public class IdGenerator {
private AtomicLong id = new AtomicLong(0);
private static volatile final IdGenerator instance;
private IdGenerator() {}
public static IdGenerator getInstance() {
if (instance == null) {
synchronized (IdGenerator.class) {
if (instance == null) {
instance = new IdGenerator();
}
}
}
return instance;
}
public long getId() {
return id.incrementAndGet();
}
}
示例代码4:静态内部类
达到了双检锁延迟加载、高性能的效果,但是比双检锁更简单。
把对象放到了静态内部类里。这样就实现了懒加载。insance 的唯一性、创建过程的线程安全性,都由 JVM 来保证。
public class IdGenerator {
private AtomicLong id = new AtomicLong(0);
private IdGenerator() {}
private static class SingletonHolder{
private static final IdGenerator instance = new IdGenerator();
}
public static IdGenerator getInstance() {
return SingletonHolder.instance;
}
public long getId() {
return id.incrementAndGet();
}
}
示例代码5:枚举
最简单优雅的方式
public enum IdGenerator {
INSTANCE;
private AtomicLong id = new AtomicLong(0);
public long getId() {
return id.incrementAndGet();
}
}
工厂模式
分为简单工厂、工厂、抽象工厂。
使用用途
创建一组相似对象。
实现方案
- 简单工厂:也叫静态工厂。很简单的把创建逻辑封装到工厂方法即可。创建方法是静态的。原始类可以用接口抽象。
- 工厂方法:对工厂再进行一次抽象,使用时先用工厂的工厂对象取得工厂,再用工厂取得原始类对象。工厂方法最主要的还是进一步解耦创建逻辑,避免所有逻辑都丢到简单工厂,让简单工厂过于庞大。
- 抽象工厂:用得比较少。一个抽象工厂类,多个生成方法,每个生成方法对应一种生成对象的类别(接口)。其实就是,原本是每个类型对应一个工厂,每个工厂有一个create()方法。现在是把多个类型的工厂合并成一个工厂,这个工厂也就对应了多个ctreate()方法。具体实现:一个抽象工厂,有多个create()方法,每个create()方法对应一种对象。
下面是把配置文件加载到内存的代码。配置文件有多种格式,需要不同格式的转换器。
代码示例1:简单工厂
public class RuleConfigSource {
public RuleConfig load(String ruleConfigFilePath) {
String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);
IRuleConfigParser parser = RuleConfigParserFactory.createParser(ruleConfigFileExtension);
if (parser == null) {
throw new InvalidRuleConfigException(
"Rule config file format is not supported: " + ruleConfigFilePath);
}
String configText = "";
//从ruleConfigFilePath文件中读取配置文本到configText中
RuleConfig ruleConfig = parser.parse(configText);
return ruleConfig;
}
private String getFileExtension(String filePath) {
//...解析文件名获取扩展名,比如rule.json,返回json
return "json";
}
}
public class RuleConfigParserFactory {
// 静态的对象生成方法
public static IRuleConfigParser createParser(String configFormat) {
IRuleConfigParser parser = null;
if ("json".equalsIgnoreCase(configFormat)) {
parser = new JsonRuleConfigParser();
} else if ("xml".equalsIgnoreCase(configFormat)) {
parser = new XmlRuleConfigParser();
} else if ("yaml".equalsIgnoreCase(configFormat)) {
parser = new YamlRuleConfigParser();
} else if ("properties".equalsIgnoreCase(configFormat)) {
parser = new PropertiesRuleConfigParser();
}
return parser;
}
}
// 也可以实现把原始类的对象缓存起来
public class RuleConfigParserFactory {
private static final Map<String, RuleConfigParser> cachedParsers = new HashMap<>();
static {
cachedParsers.put("json", new JsonRuleConfigParser());
cachedParsers.put("xml", new XmlRuleConfigParser());
cachedParsers.put("yaml", new YamlRuleConfigParser());
cachedParsers.put("properties", new PropertiesRuleConfigParser());
}
public static IRuleConfigParser createParser(String configFormat) {
if (configFormat == null || configFormat.isEmpty()) {
return null;//返回null还是IllegalArgumentException全凭你自己说了算
}
IRuleConfigParser parser = cachedParsers.get(configFormat.toLowerCase());
return parser;
}
}
以上代码,如果要增加新类型的Parser,那么需要改代码。
那么我们可以利用多态,进一步抽象工厂。这样带有新的Parser时,我们扩展新的工厂类就行,不用改代码。然后由于工厂也进行了抽象,所以再新建一个工厂的工厂,用来获取具体的工厂。也就是下面的工厂方法。
代码示例2:工厂方法
// 抽象工厂类
public interface IRuleConfigParserFactory {
IRuleConfigParser createParser();
}
public class JsonRuleConfigParserFactory implements IRuleConfigParserFactory {
@Override
public IRuleConfigParser createParser() {
return new JsonRuleConfigParser();
}
}
public class XmlRuleConfigParserFactory implements IRuleConfigParserFactory {
@Override
public IRuleConfigParser createParser() {
return new XmlRuleConfigParser();
}
}
public class YamlRuleConfigParserFactory implements IRuleConfigParserFactory {
@Override
public IRuleConfigParser createParser() {
return new YamlRuleConfigParser();
}
}
public class PropertiesRuleConfigParserFactory implements IRuleConfigParserFactory {
@Override
public IRuleConfigParser createParser() {
return new PropertiesRuleConfigParser();
}
}
// 增加工厂的工厂
//因为工厂类只包含方法,不包含成员变量,完全可以复用,
//不需要每次都创建新的工厂类对象,所以,简单工厂模式的第二种实现思路更加合适。
public class RuleConfigParserFactoryMap { //工厂的工厂
private static final Map<String, IRuleConfigParserFactory> cachedFactories = new HashMap<>();
static {
cachedFactories.put("json", new JsonRuleConfigParserFactory());
cachedFactories.put("xml", new XmlRuleConfigParserFactory());
cachedFactories.put("yaml", new YamlRuleConfigParserFactory());
cachedFactories.put("properties", new PropertiesRuleConfigParserFactory());
}
public static IRuleConfigParserFactory getParserFactory(String type) {
if (type == null || type.isEmpty()) {
return null;
}
IRuleConfigParserFactory parserFactory = cachedFactories.get(type.toLowerCase());
return parserFactory;
}
}
// 最终使用:
public class RuleConfigSource {
public RuleConfig load(String ruleConfigFilePath) {
String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);
IRuleConfigParserFactory parserFactory = RuleConfigParserFactoryMap.getParserFactory(ruleConfigFileExtension); // 注意这里,获取工厂
if (parserFactory == null) {
throw new InvalidRuleConfigException("Rule config file format is not supported: " + ruleConfigFilePath);
}
IRuleConfigParser parser = parserFactory.createParser(); // 注意这里,获取对象
String configText = "";
//从ruleConfigFilePath文件中读取配置文本到configText中
RuleConfig ruleConfig = parser.parse(configText);
return ruleConfig;
}
private String getFileExtension(String filePath) {
//...解析文件名获取扩展名,比如rule.json,返回json
return "json";
}
}
当我们需要添加新的规则配置解析器的时候,我们只需要创建新的 parser 类和 parser factory 类,并且在 RuleConfigParserFactoryMap 类中,将新的 parser factory 对象添加到 cachedFactories 中即可。代码的改动非常少,基本上符合开闭原则。
代码示例3:抽象工厂
用的比较少。可以让一个工厂负责创建多个不同类型的对象(IRuleConfigParser、ISystemConfigParser 等),而不是只创建一种 parser 对象。
public interface IConfigParserFactory {
IRuleConfigParser createRuleParser();
ISystemConfigParser createSystemParser();
//此处可以扩展新的parser类型,比如IBizConfigParser
}
public class JsonConfigParserFactory implements IConfigParserFactory {
@Override
public IRuleConfigParser createRuleParser() {
return new JsonRuleConfigParser();
}
@Override
public ISystemConfigParser createSystemParser() {
return new JsonSystemConfigParser();
}
}
public class XmlConfigParserFactory implements IConfigParserFactory {
@Override
public IRuleConfigParser createRuleParser() {
return new XmlRuleConfigParser();
}
@Override
public ISystemConfigParser createSystemParser() {
return new XmlSystemConfigParser();
}
}
// 省略YamlConfigParserFactory和PropertiesConfigParserFactory代码
建造者模式
也叫生成器模式。
使用用途
用于对象的建造及初始化过程比较复杂时。例如对属性字段的必填/非必填、默认值要求很多,需要初始化的属性字段也很多,而且可能各个字段的值还有相互制约关系等等。此时,构造函数的参数列表会很长,而且各种默认值的设置逻辑也很多,各个属性间还相互有联系,整个构建过程比较复杂。
实际上,使用建造者模式创建对象,还能避免对象存在无效状态。我再举个例子解释一下。比如我们定义了一个长方形类,如果不使用建造者模式,采用先创建后 set 的方式,那就会导致在第一个 set 之后,对象处于无效状态。为了避免这种无效状态的存在,我们就需要使用构造函数一次性初始化好所有的成员变量。如果构造函数参数过多,我们就需要考虑使用建造者模式,先设置建造者的变量,然后再一次性地创建对象,让对象一直处于有效状态。
典型的lombok插件的@Build就是建造者模式。
实现方案
专门新写一个建造者类,将原始类的初始化逻辑放到**build()
方法**,调用build()方法以取代原始类的用new调用构造方法。
调用build()
前,先调用**set()
方法**设置各种属性初始化参数,然后最后调用build()
方法一次完成所有的初始化。
建造者类可以独立写,也可以作为原始类的内部类。
使用方式:
new Builder().setXX(xxx).setXX(xxx).setXX(xxx).setXX(xxx).build();
示例代码
以下是对资源池的建造:
public class ResourcePoolConfig {
private String name;
private int maxTotal;
private int maxIdle;
private int minIdle;
private ResourcePoolConfig(Builder builder) {
this.name = builder.name;
this.maxTotal = builder.maxTotal;
this.maxIdle = builder.maxIdle;
this.minIdle = builder.minIdle;
}
//...省略getter方法...
//我们将Builder类设计成了ResourcePoolConfig的内部类。
//我们也可以将Builder类设计成独立的非内部类ResourcePoolConfigBuilder。
public static class Builder {
private static final int DEFAULT_MAX_TOTAL = 8;
private static final int DEFAULT_MAX_IDLE = 8;
private static final int DEFAULT_MIN_IDLE = 0;
private String name;
private int maxTotal = DEFAULT_MAX_TOTAL;
private int maxIdle = DEFAULT_MAX_IDLE;
private int minIdle = DEFAULT_MIN_IDLE;
public ResourcePoolConfig build() {
// 校验逻辑放到这里来做,包括必填项校验、依赖关系校验、约束条件校验等
if (StringUtils.isBlank(name)) {
throw new IllegalArgumentException("...");
}
if (maxIdle > maxTotal) {
throw new IllegalArgumentException("...");
}
if (minIdle > maxTotal || minIdle > maxIdle) {
throw new IllegalArgumentException("...");
}
return new ResourcePoolConfig(this);
}
public Builder setName(String name) {
if (StringUtils.isBlank(name)) {
throw new IllegalArgumentException("...");
}
this.name = name;
return this;
}
public Builder setMaxTotal(int maxTotal) {
if (maxTotal <= 0) {
throw new IllegalArgumentException("...");
}
this.maxTotal = maxTotal;
return this;
}
public Builder setMaxIdle(int maxIdle) {
if (maxIdle < 0) {
throw new IllegalArgumentException("...");
}
this.maxIdle = maxIdle;
return this;
}
public Builder setMinIdle(int minIdle) {
if (minIdle < 0) {
throw new IllegalArgumentException("...");
}
this.minIdle = minIdle;
return this;
}
}
}
// 这段代码会抛出IllegalArgumentException,因为minIdle>maxIdle
ResourcePoolConfig config = new ResourcePoolConfig.Builder()
.setName("dbconnectionpool")
.setMaxTotal(16)
.setMaxIdle(10)
.setMinIdle(12)
.build();
原型模式
使用用途
用于快速创建成本比较大的对象。分为深拷贝和浅拷贝两种方式。
如果对象的创建成本比较大,而同一个类的不同对象之间差别不大(大部分字段都相同),在这种情况下,我们可以利用对已有对象(原型)进行复制(或者叫拷贝)的方式来创建新对象,以达到节省创建时间的目的。
创建成本大说的是,对象中的数据需要经过复杂的计算才能得到(比如排序、计算哈希值),或者需要从 RPC、网络、数据库、文件系统等非常慢速的 IO中读取等。
实现方案
具体实现:实现cloneable接口,然后重写clone()方法即可(不重写会报错)。用super.clone()调用父类clone()方法。根据浅拷贝或深拷贝需求,按需重写clone()逻辑。
Object的clone()
方法是一个native方法,不实现Cloneable
接口而直接调用它的话会抛出一样。另外Cloneable
接口只是一个声明接口,内部没有实现任何方法。
在 Java 中,Object 类的 clone() 是浅拷贝。即copy()对象时,对象的非基本类型字段是用的浅拷贝。
深拷贝需要手动重写clone()方法,并递归拷贝对象至基本数据类型。*或者先将对象序列化,然后再反序列化成新的对象(流或Json轮子)*。深拷贝apache.common包也有现成的轮子。
示例代码1:更新缓存
以下是更新缓存的示例,有浅拷贝、深拷贝(递归/反序列化及序列化)几种方式。
浅拷贝:
public class Demo {
private HashMap<String, SearchWord> currentKeywords=new HashMap<>();
private long lastUpdateTime = -1;
public void refresh() {
// 原型模式就这么简单,拷贝已有对象的数据,更新少量差值
HashMap<String, SearchWord> newKeywords = (HashMap<String, SearchWord>) currentKeywords.clone();
// 从数据库中取出更新时间>lastUpdateTime的数据,放入到newKeywords中
List<SearchWord> toBeUpdatedSearchWords = getSearchWords(lastUpdateTime);
long maxNewUpdatedTime = lastUpdateTime;
for (SearchWord searchWord : toBeUpdatedSearchWords) {
if (searchWord.getLastUpdateTime() > maxNewUpdatedTime) {
maxNewUpdatedTime = searchWord.getLastUpdateTime();
}
if (newKeywords.containsKey(searchWord.getKeyword())) {
SearchWord oldSearchWord = newKeywords.get(searchWord.getKeyword());
oldSearchWord.setCount(searchWord.getCount());
oldSearchWord.setLastUpdateTime(searchWord.getLastUpdateTime());
} else {
newKeywords.put(searchWord.getKeyword(), searchWord);
}
}
lastUpdateTime = maxNewUpdatedTime;
currentKeywords = newKeywords;
}
private List<SearchWord> getSearchWords(long lastUpdateTime) {
// TODO: 从数据库中取出更新时间>lastUpdateTime的数据
return null;
}
}
深拷贝:
递归拷贝对象方式:
public class Demo {
private HashMap<String, SearchWord> currentKeywords=new HashMap<>();
private long lastUpdateTime = -1;
public void refresh() {
// Deep copy
HashMap<String, SearchWord> newKeywords = new HashMap<>();
for (HashMap.Entry<String, SearchWord> e : currentKeywords.entrySet()) {
SearchWord searchWord = e.getValue();
SearchWord newSearchWord = new SearchWord(
searchWord.getKeyword(), searchWord.getCount(), searchWord.getLastUpdateTime());
newKeywords.put(e.getKey(), newSearchWord);
}
// 从数据库中取出更新时间>lastUpdateTime的数据,放入到newKeywords中
List<SearchWord> toBeUpdatedSearchWords = getSearchWords(lastUpdateTime);
long maxNewUpdatedTime = lastUpdateTime;
for (SearchWord searchWord : toBeUpdatedSearchWords) {
if (searchWord.getLastUpdateTime() > maxNewUpdatedTime) {
maxNewUpdatedTime = searchWord.getLastUpdateTime();
}
if (newKeywords.containsKey(searchWord.getKeyword())) {
SearchWord oldSearchWord = newKeywords.get(searchWord.getKeyword());
oldSearchWord.setCount(searchWord.getCount());
oldSearchWord.setLastUpdateTime(searchWord.getLastUpdateTime());
} else {
newKeywords.put(searchWord.getKeyword(), searchWord);
}
}
lastUpdateTime = maxNewUpdatedTime;
currentKeywords = newKeywords;
}
private List<SearchWord> getSearchWords(long lastUpdateTime) {
// TODO: 从数据库中取出更新时间>lastUpdateTime的数据
return null;
}
}
序列化及反序列化方式:
public Object deepCopy(Object object) {
ByteArrayOutputStream bo = new ByteArrayOutputStream();
ObjectOutputStream oo = new ObjectOutputStream(bo);
oo.writeObject(object);
ByteArrayInputStream bi = new ByteArrayInputStream(bo.toByteArray());
ObjectInputStream oi = new ObjectInputStream(bi);
return oi.readObject();
}
示例代码2:简单的克隆
实现Cloneable接口,重写clone()方法,用super.clone()调用父类克隆方法:
public abstract class Shape implements Cloneable {
private String id;
protected String type;
abstract void draw();
public String getType(){
return type;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public Object clone() {
Object clone = null;
try {
clone = super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return clone;
}
}
结构型
主要解决“类或对象的组合或组装”问题。焦点是如何用接口、抽象类进行多态、继承、组合,以实现良好的扩展性。
代理模式
用于在不改变原始类的情况下,给原始类附加功能。具体使用时,用代理类替代原始类即可。
代码实现
代理/装饰器:
- 代理和被代理对象实现统一的接口,代理对象组合被代理对象。
- 代理可动态代理。
- 装饰可嵌套装饰。
静态代理
使用用途
在不改变原始类的情况下,给原始类附加功能。
实现方案1:接口方式
- 原始类和代理类实现同一个接口
- 代理类持有原始类对象
- 代理类在具体实现的方法里面,加入自己的代理逻辑,原始逻辑可引用原始类的对应的方法。
- 具体使用时用代理类代替原始类即可
使用举例1:接口方式
Controller层,在登录和注册方法中,加上接口调用计时功能。
//===== 1.共同接口
public interface IUserController {
UserVo login(String telephone, String password);
UserVo register(String telephone, String password);
}
//===== 2.原始类实现共同接口
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数据...
}
}
//===== 3.代理类实现共同接口,并在相应方法里引用原始类方法,以及重写逻辑。
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());
实现方案2:继承方式
- 有时候原始类是已经写好的jar包,我们无法使用接口。这个时候我们就用继承来取代接口。
- 代理类继承原始类,并重写原始类的方法即可。在重写的方法里加入自己的代理逻辑,原始逻辑可用
super
引用原始类的方法。 - 具体使用时用代理类代替原始类即可
使用举例2:继承方式
与接口方式大同小异,思想是相通的。
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;
}
}
//UserControllerProxy使用举例
UserController userController = new UserControllerProxy();
动态代理
动态代理内容可参照AOP相关笔记。以下内容基本上是从AOP笔记直接搬过来的。
使用用途
用来简化静态代理。静态代理有以下缺点:1.需要为每个原始类都创建一个代理类。假设有一万个原始类需要代理,就要创建一万个代理类。2.代理类需要把原始类中的所有的方法都重新实现一遍,假设原始类中有一万个方法需要代理,就要重新实现一万个方法。
不事先为原始类添加代理类。而是运行的时候,动态添加代理类,然后用代理类替换原始类。
实现方案1:jdk动态代理
实现时底层就是使用Java的jdk动态代理包,更底层则是Java的反射。
具体实现为:1.实现InvocationHandler
接口的invoke(Object proxy, Method method, Object[] args)
方法来封装增强(method.invoke(target, args)
调用原始方法)。2.用Proxy:newProxyInstance(ClassLoader cl, Class[] Interface.Class, InvocationHandler InvocationHandlerImpl)
生成新的代理对象。
使用示例1:jdk动态代理
假设我们要代理两个类,他们的方法0点到6点的时候禁止访问。我们只需要统一实现一个invocationHandler就够了。
首先我们用InvocationHandler
接口来实现我们的增强逻辑:
// 增强逻辑:0点到6点期间不让访问
@Slf4j
public class RequestCtrlInvocationHandler implements InvocationHandler {
private Object target;
public RequestCtrlInvocationHandler(Object target) {this.target = target;}
// 增强逻辑写到invoke()方法里
// 参数:代理对象,所有的方法,方法的入参
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 只增强request()方法
if (!method.getName.equals("request")) {
return null;
}
// 进行增强
TimeOfDay startTime = new TimeOfDay(0, 0, 0);
TimeOfDay endTime = new TimeOfDay(5, 59, 59);
TimeOfDay currentTime = new TimeOfDay();
if (currentTime.isAfter(startTime) && currentTime.isBefore(endTime)) {
log.warn("Service is not available now!");
return null;
}
return method.invoke(target, args);
}
}
然后我们在用Proxy
类,根据RequestCtrlInvocationHandler
的增强逻辑,为ISubject
和IRequestable
两种类型生成相应的代理对象实例即可。
public class Demo {
public void demo() {
// 用InvocationHandler增强生成第一个动态代理对象
ISubject subject = (ISubject)Proxy.newProxyInstance(
ProxyRunner.class.getClassLoader(), // 类加载器
new Class[] {ISubject.class}, // 被代理对象实现的接口
new RequestCtrlInvocationHandler(new SubjectImpl)); // InvocationHandler增强
subject.request();
// 用InvocationHandler增强生成第二个动态代理对象
IRequestable requestable = (IRequestable)Proxy.newProxyInstance(
ProxyRunner.class.getClassLoader(),
new Class[] {IRequestable.class},
new RequestCtrlInvocationHandler(new RequestableImpl()));
requestable.request();
}
}
上述的InvocationHandler
就相当于我们的Advice,我们还可以在InvocationHandler
的基础上细化结构,后续会看到。然后上述动态代理实现的方式是有缺点的,就是被代理的类一定要实现某个接口。
默认情况下,如果SpringAOP发现目标对象实现了相应的Interface
,那么就采用动态代理机制生成代理对象实例。否则,Spring AOP会尝试用CGLIB(Code Generation Library)的开源动态字节码生成类库,为目标生成动态代理的对象。
这也是下一节的内容。
实现方案2:动态字节码
具体实现为:1.实现Callback
或MethodInterceptor
接口的intercept()
方法来封装增强(methodProxy.invokeSuper()
调用原始方法)。2.用Enhancer:create()
来生成新的代理对象。
使用示例2:动态字节码
假设下面是被代理的类:
public class Requestable{
public void request {
System.out.println("rq in Requestable without implement any interface");
}
}
和动态代理类似,首先第一步,封装增强逻辑到Callback
或MethodInterceptor
接口。封装到子类里需要实现一个net.sf.cglib.proxy.Callback
接口。不过我们一般直接使用net.sf.cglib.proxy.MethodInterceptor
接口就行了,它是CallBack
接口的扩展。
@Slf4j
public class RequestCtrlCallBack implements MethodInterceptor {
public Object intercept(Object object, Method method, Object[] args, MethodProxy proxy) throws Throwable {
// 只增强request()方法
if (!method.getName.equals("request")) {
return null;
}
// 进行增强
TimeOfDay startTime = new TimeOfDay(0, 0, 0);
TimeOfDay endTime = new TimeOfDay(5, 59, 59);
TimeOfDay currentTime = new TimeOfDay();
if (currentTime.isAfter(startTime) && currentTime.isBefore(endTime)) {
log.warn("Service is not available now!");
return null;
}
return proxy.invokeSuper(object, args);
}
}
然后通过CGLIB的Enhancer
为目标对象动态生成一个子类,并把RequestCtrlCallBack
封装的逻辑织入子类即可。
public class Demo {
public void demo() {
Enhancer enhancer = new Enhancer();
enhancer.setSuperClass(Requestable.class);
enhancer.setCallback(new RequestCtrlCallback());
Requestable proxy = (Requestable)enhancer.create();
proxy.request();
}
}
使用CGLIB唯一的限制就是无法对final方法进行覆写。
扩展:常用代理方式总结
代理的实现方式有很多,例如:
- 源代码阶段:Java代码生成。专用代码生成工具。早期的EJB容器用的多。现在已经退休不用了。
- 编译阶段:AOL扩展。也就是上面说的静态AOP方式。用专门的AOL语言描述切面信息,然后用专用的编译器织入后直接生成字节码。
- 加载阶段:自定义类加载器,通过读取外部文件规定的织入规则和必要信息,在加载class文件的时候,就可以把横切逻辑织入现有逻辑中,然后把改动后的class交给JVM运行。来个偷梁换柱。
- 执行阶段:动态字节码,jdk动态代理
桥接模式
桥接模式(Bridge Design Pattern)有两种不同的理解方式:
- 一个类存在两个(或多个)独立变化的维度,我们通过组合的方式,让这两个(或多个)维度可以独立进行扩展。
- 将抽象和实现解耦,让它们可以独立变化。这个是GOF的标准理解方式,也是对前者的升华。
代码实现
桥接模式:抽象类组合接口,两个抽象维度都可以独立变化。
下面就两种理解方式进行讲解,其实他们最后的实现示例代码是一样的:
理解方式1:让多个维度根据抽象类及接口独立进行扩展
使用用途
一个类需要在多个维度进行扩展,此时如果采用继承的话会使子类数量过多。变化的维度都可以封装到接口里。
设计目标
将抽象部分与实现部分解耦。所谓的抽象部分,就是抽象类及其子类。实现部分,就是接口及其实现。那么此时,所谓的桥也就是抽象类。
实现方案
抽象类持有接口,然后抽象类自己的各个子类方法里可以使用接口的各个方法。这里抽象类的方法是一个抽象,接口的方法又是两一个抽象,这两个方法可以通过子类独立变化,抽象类和接口的各个方法又可以交叉使用,这样就完成了多个变化维度的解耦,以及抽象部分和实现部分的解耦。
使用举例1
我们吃烧烤,有种类、风味两个扩展维度。种类可以选鸡翅、鸡腿等,风味可以选微辣、中辣、特辣等。
实现部分变化:
风味类Spicy
public interface Spicy{
String taste();
}
微辣SlightSpicy
public class SlightSpicy implements Spicy{
public String taste(){
return "微辣";
}}
特辣ExtraSpicy
public class ExtraSpicy implements Spicy{
public String taste(){
return "特辣";
}
}
抽象部分变化:
烧烤类Barbecue
public abstract class Barbecue{
protected Spicy mSpicy; //实现部分实例
public Barbecue(Spicy spicy){ 桥接模式的关键,组合实现和抽象
this.mSpicy= spicy;
}
public abstract void eat();
}
鸡翅类ChickenWing
public class ChickenWing extends Barbecue{
public ChickenWing (Spicy spicy){
super(spicy);
}
public void eat(){
System.out.println( "鸡翅:"+super.mSpicy.taste());
}
}
鸡腿类ChickenLeg
public class ChickenLeg extends Barbecue{
public ChickenLeg (Spicy spicy){
super(spicy);
}
public void eat(){
System.out.println( "鸡腿:"+super.mSpicy.taste());
}
}
测试:
public class Test{
public static void main(String[] args){
Barbecue barbecue = new ChickenWing(new SlightSpicy());
barbecue .eat();
Barbecue barbecue2 = new ChickenLeg(new ExtraSpicy());
barbecue2.eat();
}
}
输出:
鸡翅:微辣
鸡腿:特辣
使用举例2
这个例子和例1的形式是一模一样的:我们服务器发送一条消息,有发送方式、消息级别两个扩展维度。
// ===== 实现:发送方式
public interface MsgSender {
void send(String message);
}
public class TelephoneMsgSender implements MsgSender {
private List<String> telephones;
public TelephoneMsgSender(List<String> telephones) {
this.telephones = telephones;
}
@Override
public void send(String message) {
//...
}
}
public class EmailMsgSender implements MsgSender {
// 与TelephoneMsgSender代码结构类似,所以省略...
}
public class WechatMsgSender implements MsgSender {
// 与TelephoneMsgSender代码结构类似,所以省略...
}
// ===== 抽象:消息级别
public abstract class Notification {
protected MsgSender msgSender;
public Notification(MsgSender msgSender) {
this.msgSender = msgSender;
}
public abstract void notify(String message);
}
public class SevereNotification extends Notification {
public SevereNotification(MsgSender msgSender) {
super(msgSender);
}
@Override
public void notify(String message) {
msgSender.send(message);
}
}
public class UrgencyNotification extends Notification {
// 与SevereNotification代码结构类似,所以省略...
}
public class NormalNotification extends Notification {
// 与SevereNotification代码结构类似,所以省略...
}
public class TrivialNotification extends Notification {
// 与SevereNotification代码结构类似,所以省略...
}
理解方式2:将抽象和实现解耦,让它们可以独立变化
在这种理解方式的定义中,定义中的“抽象”,指的并非“抽象类”或“接口”,而是被抽象出来的一套“类库”,它只包含骨架代码,真正的业务逻辑需要委派给定义中的“实现”来完成。而定义中的“实现”,也并非“接口的实现类”,而是的一套独立的“类库”。“抽象”和“实现”独立开发,通过对象之间的组合关系,组装在一起。
使用举例
Java中JDBC的实现:
JDBC 本身就相当于“抽象”。注意,这里所说的“抽象”,指的并非“抽象类”或“接口”,而是跟具体的数据库无关的、被抽象出来的一套“类库”。具体的 Driver(比如,com.mysql.jdbc.Driver)就相当于“实现”。注意,这里所说的“实现”,也并非指“接口的实现类”,而是跟具体数据库相关的一套“类库”。JDBC 和 Driver 独立开发,通过对象之间的组合关系,组装在一起。JDBC 的所有逻辑操作,最终都委托给 Driver 来执行。
装饰器模式
使用用途
用组合代替继承,解决继承关系过于复杂的问题,它主要的作用是给原始类添加增强功能。
代码实现
和代理模式类似。参加代理模式。
实现方案
- 装饰器类和原始类继承同样的父类,这样我们可以对原始类“嵌套”多个装饰器类。
- 装饰器类是对功能的增强,这也是装饰器模式应用场景的一个重要特点。
代码的结构和代理模式一样,也可以用抽象类取代接口。与代理模式的区别是:代理模式中,代理类附加的是跟原始类无关的功能,而在装饰器模式中,装饰器类附加的是跟原始类相关的增强功能。
使用示例
以下是核心代码。此外装饰器典型的运用就是Java的BIO,这里略过。
// 代理模式的代码结构(下面的接口也可以替换成抽象类)
public interface IA {
void f();
}
public class A impelements IA {
public void f() { //... }
}
public class AProxy impements IA {
private IA a;
public AProxy(IA a) {
this.a = a;
}
public void f() {
// 新添加的代理逻辑
a.f();
// 新添加的代理逻辑
}
}
// 装饰器模式的代码结构(下面的接口也可以替换成抽象类)
public interface IA {
void f();
}
public class A impelements IA {
public void f() { //... }
}
public class ADecorator impements IA {
private IA a;
public ADecorator(IA a) {
this.a = a;
}
public void f() {
// 功能增强代码
a.f();
// 功能增强代码
}
}
适配器模式
使用用途
将不兼容的接口转换为可兼容的接口,让原本由于接口不兼容而不能一起工作的类可以一起工作。例如 USB 转接头。
代码实现
适配器模式:适配器实现目标接口,并组合老对象。如果目标接口和看对象相同的方法逻辑较多,也可以继承老对象。
实现方案
有两种实现方案,类适配器和对象适配器。区别就是一个是通过继承引用老接口,一个是通过组合引用老接口,仅此而已。前者可以复用很多老接口的方法,而后者的话后续扩展可以更灵活。
假如ITarget
表示要转化成的新接口定义、Adaptee
是一组不兼容 ITarget 接口定义的老接口、Adaptor
是连接两者的适配器,那么实现方式很明显了。用适配器连接新老接口,新接口通过实现连接,老接口通过继承或组合连接,Adaptee
–继承/组合–> Adaptor
–实现–> ITarget
,就那么简单。。以下为代码解析:
// 类适配器: 基于继承
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,这是跟对象适配器最大的不同点
}
// 对象适配器:基于组合
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();
}
}
使用示例:
- 封装有缺陷的接口设计:假设我们依赖的外部系统在接口设计方面有缺陷(比如包含大量静态方法),引入之后会影响到我们自身代码的可测试性。为了隔离设计上的缺陷,我们希望对外部系统提供的接口进行二次封装,抽象出更好的接口设计,这个时候就可以使用适配器模式了
- 统一多个类的接口设计:某个功能的实现依赖多个外部系统(或者说类)。通过适配器模式,将它们的接口适配为统一的接口定义,然后我们就可以使用多态的特性来复用代码逻辑。
- 替换依赖的外部系统:当我们把项目中依赖的一个外部系统替换为另一个外部系统的时候,利用适配器模式,可以减少对代码的改动。
- 兼容老版本升级:在做版本升级的时候,对于一些要废弃的接口,我们不直接将其删除,而是暂时保留,并且标注为 deprecated,并将内部实现逻辑委托为新的接口实现。
- 适配不同格式的数据:比如,把从不同征信系统拉取的不同格式的征信数据,统一为相同的格式,以方便存储和使用。
具体的使用场景例如:
- 自己wind-cloud项目里面使用的adaptor微服务,用于匹配数据格式和外部接口。
- Slf4j日志框架统一log4j和logback等多个日志框架。
Wrapper 模式:代理、装饰器、桥接、适配器对比
代理、桥接、装饰器、适配器,这 4 种模式是比较常用的结构型设计模式。它们的代码结构非常相似。笼统来说,它们都可以称为 Wrapper 模式,也就是通过 Wrapper 类二次封装原始类。
尽管代码结构相似,但这 4 种设计模式的用意完全不同,它们之间的区别:
- 代理模式:代理模式在不改变原始类接口的条件下,为原始类定义一个代理类,主要目的是控制访问,而非加强功能,这是它跟装饰器模式最大的不同。
- 装饰器模式:装饰者模式在不改变原始类接口的情况下,对原始类功能进行增强,并且支持多个装饰器的嵌套使用。
- 桥接模式:桥接模式的目的是将接口部分和实现部分分离,从而让它们可以较为容易、也相对独立地加以改变。
- 适配器模式:适配器模式是一种事后的补救策略。适配器提供跟原始类不同的接口,而代理模式、装饰器模式提供的都是跟原始类相同的接口。
代码结构上的个人理解:
代理模式和装饰器模式代码结构一致,都是实现共同的接口,然后在方法里调用原始类的同一个方法。涉及原始类、接口、接口实现类(也就是代理类或装饰类)。使用时具体用的是接口实现类(也就是代理类或装饰类),它包裹了老逻辑和代理逻辑。
桥接模式,抽象类和接口没有实现同一个接口。可以在不同的方法里调用原始类的不同方法。抽象类和接口两个维度可以独立扩展。然后抽象类就是桥。涉及抽象类、抽象类实现类、接口、接口实现类。使用时具体用的是抽象类的实现类,它包裹了所有维度的功能扩展。
适配器模式。很简单,涉及老接口、适配器、新接口。然后用组合或继承连接即可,就那么简单。使用时具体用的是适配器包裹了新老功能的集成。
结构型设计模式,涉及了类之间的结构关系。而类之间的结构关系,无非也就是抽象、接口、继承、组合。那么各种结构型设计模式的组合也就是这四种关系,掌握好设计模式的思想,然后用这四个关系组合即可。
门面模式
使用用途
用来做接口整合,封装不同大小粒度的接口,提供统一的接口给外部使用。这样就不用外部直接调用我们内部的接口,且可以合理控制接口的粒度大小,而且提供了封装,还更好扩展。
门面模式也叫外观模式。如系统有a、b、c接口,用起来非常不方便。用门面模式直接提供接口x,整合a、b、c。
实现方案
用统一的对外接口封装自己内部子系统的各种接口功能即可。各种封装,然后提供统一访问入口。
使用示例
门面模式很简单,代码就不写了,就是各种封装,然后提供统一访问入口。以下是几个常用场景:
- 让接口更好用:比如Linux的命令就可以看成门面。它屏蔽了内部细节。
- 让性能更强:前端访问后端,只访问一次肯定比多次要好。
- 分布式事务:屏蔽了分布式事务内部处理的复杂细节,将一个事务的多个接口直接同意封装成一个。
适配器和门面的区别:适配器做接口转换,门面做接口整合。
组合模式
使用用途
用来处理树形结构数据。将其表示成一种“部分 - 整体”的层次结构,然后就可以用一样的逻辑处理单个对象和组合对象。
使用场景较少。这里的“组合”和继承中的“组合”不是一个东西。
实现方案
其实就是自己持有自己类型的List字段组成树形结构。
代码示例
比如遍历文件夹和文件(略)。
遍历一个公司的所有人,并计算他们的工资:
public abstract class HumanResource {
protected long id;
protected double salary;
public HumanResource(long id) {
this.id = id;
}
public long getId() {
return id;
}
public abstract double calculateSalary();
}
public class Employee extends HumanResource {
public Employee(long id, double salary) {
super(id);
this.salary = salary;
}
@Override
public double calculateSalary() {
return salary;
}
}
public class Department extends HumanResource {
private List<HumanResource> subNodes = new ArrayList<>();
public Department(long id) {
super(id);
}
@Override
public double calculateSalary() {
double totalSalary = 0;
for (HumanResource hr : subNodes) {
totalSalary += hr.calculateSalary();
}
this.salary = totalSalary;
return totalSalary;
}
public void addSubNode(HumanResource hr) {
subNodes.add(hr);
}
}
// 构建组织架构的代码
public class Demo {
private static final long ORGANIZATION_ROOT_ID = 1001;
private DepartmentRepo departmentRepo; // 依赖注入
private EmployeeRepo employeeRepo; // 依赖注入
public void buildOrganization() {
Department rootDepartment = new Department(ORGANIZATION_ROOT_ID);
buildOrganization(rootDepartment);
}
private void buildOrganization(Department department) {
List<Long> subDepartmentIds = departmentRepo.getSubDepartmentIds(department.getId());
for (Long subDepartmentId : subDepartmentIds) {
Department subDepartment = new Department(subDepartmentId);
department.addSubNode(subDepartment);
buildOrganization(subDepartment);
}
List<Long> employeeIds = employeeRepo.getDepartmentEmployeeIds(department.getId());
for (Long employeeId : employeeIds) {
double salary = employeeRepo.getEmployeeSalary(employeeId);
department.addSubNode(new Employee(employeeId, salary));
}
}
}
享元模式
使用用途
用来复用对象,节省内存,前提是享元对象是不可变对象。所谓享元,就是被共享的单元。
后续会讲它和单例、缓存、对象池的区别和联系。
具体来讲,当一个系统中存在大量重复对象的时候,如果这些重复的对象是不可变对象,我们就可以利用享元模式将对象设计成享元,在内存中只保留一份实例,供多处代码引用。
不仅仅相同对象可以设计成享元,对于相似对象,我们也可以将这些对象中相同的部分(字段)提取出来,设计成享元,让这些大量相似对象引用这些享元。
实现方案
其实就是工厂类容器字段缓存的不可变对象。使用工厂类,加一个static final Map来缓存享元,然后在static{}静态块中初始化享元的map,把享元放进去即可。当然也可以动态的加入使用享元时从工厂拿。
代码示例1:象棋大厅
要构造n个棋盘,象棋的颜色和文字都是一样的,唯有坐标不同,所以颜色文字这部分可以用享元。
// 享元类
public class ChessPieceUnit {
private int id;
private String text;
private Color color;
public ChessPieceUnit(int id, String text, Color color) {
this.id = id;
this.text = text;
this.color = color;
}
public static enum Color {
RED, BLACK
}
// ...省略其他属性和getter方法...
}
public class ChessPieceUnitFactory {
private static final Map<Integer, ChessPieceUnit> pieces = new HashMap<>();
static {
pieces.put(1, new ChessPieceUnit(1, "車", ChessPieceUnit.Color.BLACK));
pieces.put(2, new ChessPieceUnit(2,"馬", ChessPieceUnit.Color.BLACK));
//...省略摆放其他棋子的代码...
}
public static ChessPieceUnit getChessPiece(int chessPieceId) {
return pieces.get(chessPieceId);
}
}
public class ChessPiece {
private ChessPieceUnit chessPieceUnit;
private int positionX;
private int positionY;
public ChessPiece(ChessPieceUnit unit, int positionX, int positionY) {
this.chessPieceUnit = unit;
this.positionX = positionX;
this.positionY = positionY;
}
// 省略getter、setter方法
}
public class ChessBoard {
private Map<Integer, ChessPiece> chessPieces = new HashMap<>();
public ChessBoard() {
init();
}
private void init() {
chessPieces.put(1, new ChessPiece(
ChessPieceUnitFactory.getChessPiece(1), 0,0));
chessPieces.put(1, new ChessPiece(
ChessPieceUnitFactory.getChessPiece(2), 1,0));
//...省略摆放其他棋子的代码...
}
public void move(int chessPieceId, int toPositionX, int toPositionY) {
//...省略...
}
}
代码示例2:文本编辑器
文字的格式是有限且固定的,可以用享元。享元动态加入。
public class CharacterStyle {
private Font font;
private int size;
private int colorRGB;
public CharacterStyle(Font font, int size, int colorRGB) {
this.font = font;
this.size = size;
this.colorRGB = colorRGB;
}
@Override
public boolean equals(Object o) {
CharacterStyle otherStyle = (CharacterStyle) o;
return font.equals(otherStyle.font)
&& size == otherStyle.size
&& colorRGB == otherStyle.colorRGB;
}
}
public class CharacterStyleFactory {
private static final List<CharacterStyle> styles = new ArrayList<>();
public static CharacterStyle getStyle(Font font, int size, int colorRGB) {
CharacterStyle newStyle = new CharacterStyle(font, size, colorRGB);
for (CharacterStyle style : styles) {
if (style.equals(newStyle)) {
return style;
}
}
styles.add(newStyle);
return newStyle;
}
}
public class Character {
private char c;
private CharacterStyle style;
public Character(char c, CharacterStyle style) {
this.c = c;
this.style = style;
}
}
public class Editor {
private List<Character> chars = new ArrayList<>();
public void appendCharacter(char c, Font font, int size, int colorRGB) {
Character character = new Character(c, CharacterStyleFactory.getStyle(font, size, colorRGB));
chars.add(character);
}
}
享元模式 vs 单例、缓存、对象池
- 享元模式VS单例:实际上,享元模式有点类似于之前讲到的单例的变体:多例。单例模式中,一个类只能创建一个对象。享元模式中,一个类可以创建多个对象。当然他们的思想不一样,前者是为了共享,后者是为了限制个数。
- 享元模式VS缓存:缓存是为了提供访问效率,享元是为了复用。思想不一样。且缓存可变,享元不可变。
- 享元模式VS对象池:都是复用,对象池是为了“重复使用”,目的是节省时间,此时对象只会被一个使用者独占。而享元的复用是“共享复用‘’,目的是节省空间,所有使用者都可以共享。
享元模式不要过度使用,因为享元不会被JVM回收。
扩展Integer和String的享元
相关概念:
// 1.自动装箱
Integer i = 59;底层执行了:Integer i = Integer.valueOf(59);
// 2.自动拆箱
int j = i; 底层执行了:int j = i.intValue();
// 3.“==” 比较,其实就是地址比较。
// 实际上是在判断两个局部变量存储的地址是否相同,换句话说,是在判断两个局部变量是否指向相同的对象
Long、 Integer、Short、Byte等类型中,-128 到 127 是享元。所以:
Integer i1 = 56;
Integer i2 = 56;
Integer i3 = 129;
Integer i4 = 129;
System.out.println(i1 == i2); // true
System.out.println(i3 == i4); // false
String中,某个字符串常量第一次被用到的时候会被存储到常量池中,即享元。JVM 开辟一块存储区专门存储字符串常量,这块存储区叫作字符串常量池。所以:
String s1 = "打工人";
String s2 = "打工人";
String s3 = new String("打工人");
System.out.println(s1 == s2); // true
System.out.println(s1 == s3); // false
行为型
主要解决的就是“类或对象之间的交互”问题。焦点是对象之间如何进行通信。
观察者模式
使用用途
解耦观察者和被观察者。一个对象状态改变时,通知其他对象。
实现方案
也叫“发布订阅”模式、“生产者消费者”等。定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知。
发布者持有订阅者的list。发布者有新消息时,直接调用所有订阅者的update()方法即可。订阅者需要提前注册到发布者。
观察者模式只是一种思想,根据应用场景的不同,观察者模式会对应不同的实现方式:有同步阻塞、异步非阻塞的实现方式,也有进程内、进程间的实现方式。
代码示例1:同步阻塞方式
最经典的实现方式:
public interface Subject {
void registerObserver(Observer observer);
void removeObserver(Observer observer);
void notifyObservers(Message message);
}
public interface Observer {
void update(Message message);
}
public class ConcreteSubject implements Subject {
private List<Observer> observers = new ArrayList<Observer>();
@Override
public void registerObserver(Observer observer) {
observers.add(observer);
}
@Override
public void removeObserver(Observer observer) {
observers.remove(observer);
}
@Override
public void notifyObservers(Message message) {
for (Observer observer : observers) {
observer.update(message);
}
}
}
public class ConcreteObserverOne implements Observer {
@Override
public void update(Message message) {
//TODO: 获取消息通知,执行自己的逻辑...
System.out.println("ConcreteObserverOne is notified.");
}
}
public class ConcreteObserverTwo implements Observer {
@Override
public void update(Message message) {
//TODO: 获取消息通知,执行自己的逻辑...
System.out.println("ConcreteObserverTwo is notified.");
}
}
public class Demo {
public static void main(String[] args) {
ConcreteSubject subject = new ConcreteSubject();
subject.registerObserver(new ConcreteObserverOne());
subject.registerObserver(new ConcreteObserverTwo());
subject.notifyObservers(new Message());
}
以上主要是反应思想,实际应用时一般不会照搬。
其他使用举例:Google Guava EventBus 框架、MQ等。
代码示例2:异步非阻塞方式
EventBus,后续补充。
模板模式
模板、策略、责任链都有相同的作用:复用和扩展。在实际的项目开发中比较常用,特别是框架开发中,我们可以利用它们来提供框架的扩展点,能够让框架的使用者在不修改框架源码的情况下,基于扩展点定制化框架的功能。
使用用途
在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。
实现方式
用抽象类定义好算法逻辑骨架,可变的部分使用抽象方法封装,让子类去实现。当然也不一定用抽象方法,可以定义空方法、或抛出异常的方法,等待子类实现,具体代码比较灵活,核心思想是提供骨架。
代码示例
Java Servlet、JUnit TestCase、Java AbstractList都有用到。
以下是典型的代码:
public abstract class AbstractClass {
public final void templateMethod() {
//...
method1();
//...
method2();
//...
}
protected abstract void method1();
protected abstract void method2();
}
public class ConcreteClass1 extends AbstractClass {
@Override
protected void method1() {
//...
}
@Override
protected void method2() {
//...
}
}
public class ConcreteClass2 extends AbstractClass {
@Override
protected void method1() {
//...
}
@Override
protected void method2() {
//...
}
}
AbstractClass demo = ConcreteClass1();
demo.templateMethod();
扩展:模板方法和回调的区别:模板方法和回调应用场景是一致的,都是定义好算法骨架,并对外开放扩展点,符合开闭原则;两者的却别是代码的实现上不同,模板方法是通过继承来实现,是自己调用自己;回调是类之间的组合。
策略模式
使用用途
解耦算法和使用算法的代码,可以根据情况动态使用不同的算法。准确的来说,是解耦策略的定义、创建、使用这三部分。
实现方案
策略定义:包含一个策略接口和一组实现这个接口的策略类。
策略创建:通过类型(type)来判断创建哪个策略来使用。用工厂封装判断逻辑。逻辑可以是静态或动态,静态直接用享元缓存好,动态的话直接new。然后策略还可以通过反射根据注解自动注入。
策略使用:直接调用工厂获取策略,然后执行方法即可。
使用示例
// ========== 策略的定义
public interface DiscountStrategy {
double calDiscount(Order order);
}
// 省略NormalDiscountStrategy、GrouponDiscountStrategy、PromotionDiscountStrategy类代码...
// ========== 策略的创建
// 策略对象如果是无状态的,直接缓存
public class DiscountStrategyFactory {
private static final Map<OrderType, DiscountStrategy> strategies = new HashMap<>();
static {
strategies.put(OrderType.NORMAL, new NormalDiscountStrategy());
strategies.put(OrderType.GROUPON, new GrouponDiscountStrategy());
strategies.put(OrderType.PROMOTION, new PromotionDiscountStrategy());
}
public static DiscountStrategy getDiscountStrategy(OrderType type) {
return strategies.get(type);
}
}
// 如果业务场景需要每次都创建不同的策略对象,我们就要直接new
public class DiscountStrategyFactory {
public static DiscountStrategy getDiscountStrategy(OrderType type) {
if (type == null) {
throw new IllegalArgumentException("Type should not be null.");
}
if (type.equals(OrderType.NORMAL)) {
return new NormalDiscountStrategy();
} else if (type.equals(OrderType.GROUPON)) {
return new GrouponDiscountStrategy();
} else if (type.equals(OrderType.PROMOTION)) {
return new PromotionDiscountStrategy();
}
return null;
}
}
// ========== 策略的使用
public class OrderService {
public double discount(Order order) {
OrderType type = order.getType();
DiscountStrategy discountStrategy = DiscountStrategyFactory.getDiscountStrategy(type);
return discountStrategy.calDiscount(order);
}
}
责任链模式
使用用途
实现方案
多个处理器(也就是刚刚定义中说的“接收对象”)依次处理同一个请求。一个请求先经过 A 处理器处理,然后再把请求传递给 B 处理器,B 处理器处理完后再传递给 C 处理器,以此类推,形成一个链条。链条上的每个处理器各自承担各自的处理职责。
在 GoF 的定义中,如果处理器链上的某个节点能够处理这个请求,那就不会继续往下传递请求。实际上,职责链模式还有一种变体,那就是请求会被所有的处理器都处理一遍,不存在中途终止的情况。
职责链模式有两种常用的实现。一种是使用链表来存储处理器,另一种是使用数组来存储处理器,后面一种实现方式更加简单:
链表方式实现:每个节点保存它的后节点。责任链保存首节点和尾节点。使用时责任链直接调用首节点的处理方法进行处理即可,处理方法中处理完之后自动调用下一节点处理方法。增加节点则从尾节点处增加。
数组方式实现:责任链中直接用数组保存。使用时责任链直接从头到尾遍历这个数据进行处理即可。
Servlet Filter、Spring Interceptor、日志框架等。都会用到责任链。
代码示例1:链表方式
这里使用了模板模式对处理方法进行了一点小优化。
public abstract class Handler {
protected Handler successor = null;
public void setSuccessor(Handler successor) {
this.successor = successor;
}
public final void handle() {
boolean handled = doHandle();
if (successor != null && !handled) {
successor.handle();
}
}
protected abstract boolean doHandle();
}
public class HandlerA extends Handler {
@Override
protected boolean doHandle() {
boolean handled = false;
//...
return handled;
}
}
public class HandlerB extends Handler {
@Override
protected boolean doHandle() {
boolean handled = false;
//...
return handled;
}
public class HandlerChain {
private Handler head = null;
private Handler tail = null;
public void addHandler(Handler handler) {
handler.setSuccessor(null);
if (head == null) {
head = handler;
tail = handler;
return;
}
tail.setSuccessor(handler);
tail = handler;
}
public void handle() {
if (head != null) {
head.handle();
}
}
}
// 使用举例
public class Application {
public static void main(String[] args) {
HandlerChain chain = new HandlerChain();
chain.addHandler(new HandlerA());
chain.addHandler(new HandlerB());
chain.handle();
}
}
代码示例2:数组方式
public interface IHandler {
boolean handle();
}
public class HandlerA implements IHandler {
@Override
public boolean handle() {
boolean handled = false;
//...
return handled;
}
}
public class HandlerB implements IHandler {
@Override
public boolean handle() {
boolean handled = false;
//...
return handled;
}
}
public class HandlerChain {
private List<IHandler> handlers = new ArrayList<>();
public void addHandler(IHandler handler) {
this.handlers.add(handler);
}
public void handle() {
for (IHandler handler : handlers) {
boolean handled = handler.handle();
if (handled) {
break;
}
}
}
}
// 使用举例
public class Application {
public static void main(String[] args) {
HandlerChain chain = new HandlerChain();
chain.addHandler(new HandlerA());
chain.addHandler(new HandlerB());
chain.handle();
}
}
状态模式
使用用途
状态模式一般用来实现状态机,而状态机常用在游戏、工作流引擎等系统开发中。
状态机的实现方式有多种,除了状态模式,比较常用的还有分支逻辑法和查表法。
在实际开发中,状态模式并不是很常用,但是在能够用到的场景里,它可以发挥很大的作用。从这一点上来看,它有点像我们之前讲到的组合模式。
实现方案
状态机有 3 个组成部分:状态(State)、事件(Event)、动作(Action)。动作不是必须执行的。
状态模式:定义一个状态接口,保存状态字段、以及所有事件(每个事件一个处理方法)。用该状态接口封装状态转移的逻辑。状态维度根据接口的各种实现类扩展,事件维度根据接口的方法数量进行扩展。状态转移的逻辑及动作写到方法里就OK。定义一个状态机。状态机持有当前的状态,然后遇到每个事件直接调用当前状态的事件处理方法处理即可。状态类里也持有状态机的引用,以获取当前的状态及业务字段等。
状态机的三种实现方式总结:
- 分支逻辑法。利用 if-else 或者 switch-case 分支逻辑,参照状态转移图,将每一个状态转移原模原样地直译成代码。对于简单的状态机来说,这种实现方式最简单、最直接,是首选。
- 查表法。对于状态很多、状态转移比较复杂的状态机来说,查表法比较合适。通过二维数组来表示状态转移图,能极大地提高代码的可读性和可维护性。
- 状态模式。对于状态并不多、状态转移也比较简单,但事件触发执行的动作包含的业务逻辑可能比较复杂的状态机来说,我们首选这种实现方式。
代码示例1:状态模式
以下是超级玛丽的示例:
public interface IMario { //所有状态类的接口
State getName();
//以下是定义的事件
void obtainMushRoom();
void obtainCape();
void obtainFireFlower();
void meetMonster();
}
public class SmallMario implements IMario {
private MarioStateMachine stateMachine;
public SmallMario(MarioStateMachine stateMachine) {
this.stateMachine = stateMachine;
}
@Override
public State getName() {
return State.SMALL;
}
@Override
public void obtainMushRoom() {
stateMachine.setCurrentState(new SuperMario(stateMachine));
stateMachine.setScore(stateMachine.getScore() + 100);
}
@Override
public void obtainCape() {
stateMachine.setCurrentState(new CapeMario(stateMachine));
stateMachine.setScore(stateMachine.getScore() + 200);
}
@Override
public void obtainFireFlower() {
stateMachine.setCurrentState(new FireMario(stateMachine));
stateMachine.setScore(stateMachine.getScore() + 300);
}
@Override
public void meetMonster() {
// do nothing...
}
}
public class SuperMario implements IMario {
private MarioStateMachine stateMachine;
public SuperMario(MarioStateMachine stateMachine) {
this.stateMachine = stateMachine;
}
@Override
public State getName() {
return State.SUPER;
}
@Override
public void obtainMushRoom() {
// do nothing...
}
@Override
public void obtainCape() {
stateMachine.setCurrentState(new CapeMario(stateMachine));
stateMachine.setScore(stateMachine.getScore() + 200);
}
@Override
public void obtainFireFlower() {
stateMachine.setCurrentState(new FireMario(stateMachine));
stateMachine.setScore(stateMachine.getScore() + 300);
}
@Override
public void meetMonster() {
stateMachine.setCurrentState(new SmallMario(stateMachine));
stateMachine.setScore(stateMachine.getScore() - 100);
}
}
// 省略CapeMario、FireMario类...
public class MarioStateMachine {
private int score;
private IMario currentState; // 不再使用枚举来表示状态
public MarioStateMachine() {
this.score = 0;
this.currentState = new SmallMario(this);
}
public void obtainMushRoom() {
this.currentState.obtainMushRoom();
}
public void obtainCape() {
this.currentState.obtainCape();
}
public void obtainFireFlower() {
this.currentState.obtainFireFlower();
}
public void meetMonster() {
this.currentState.meetMonster();
}
public int getScore() {
return this.score;
}
public State getCurrentState() {
return this.currentState.getName();
}
public void setScore(int score) {
this.score = score;
}
public void setCurrentState(IMario currentState) {
this.currentState = currentState;
}
}
可以用单例模式把上述代码重构下,因为状态时不变的:
public interface IMario {
State getName();
void obtainMushRoom(MarioStateMachine stateMachine);
void obtainCape(MarioStateMachine stateMachine);
void obtainFireFlower(MarioStateMachine stateMachine);
void meetMonster(MarioStateMachine stateMachine);
}
public class SmallMario implements IMario {
private static final SmallMario instance = new SmallMario();
private SmallMario() {}
public static SmallMario getInstance() {
return instance;
}
@Override
public State getName() {
return State.SMALL;
}
@Override
public void obtainMushRoom(MarioStateMachine stateMachine) {
stateMachine.setCurrentState(SuperMario.getInstance());
stateMachine.setScore(stateMachine.getScore() + 100);
}
@Override
public void obtainCape(MarioStateMachine stateMachine) {
stateMachine.setCurrentState(CapeMario.getInstance());
stateMachine.setScore(stateMachine.getScore() + 200);
}
@Override
public void obtainFireFlower(MarioStateMachine stateMachine) {
stateMachine.setCurrentState(FireMario.getInstance());
stateMachine.setScore(stateMachine.getScore() + 300);
}
@Override
public void meetMonster(MarioStateMachine stateMachine) {
// do nothing...
}
}
// 省略SuperMario、CapeMario、FireMario类...
public class MarioStateMachine {
private int score;
private IMario currentState;
public MarioStateMachine() {
this.score = 0;
this.currentState = SmallMario.getInstance();
}
public void obtainMushRoom() {
this.currentState.obtainMushRoom(this);
}
public void obtainCape() {
this.currentState.obtainCape(this);
}
public void obtainFireFlower() {
this.currentState.obtainFireFlower(this);
}
public void meetMonster() {
this.currentState.meetMonster(this);
}
public int getScore() {
return this.score;
}
public State getCurrentState() {
return this.currentState.getName();
}
public void setScore(int score) {
this.score = score;
}
public void setCurrentState(IMario currentState) {
this.currentState = currentState;
}
}
代码示例2:分支逻辑法
最简单粗暴的if-else:
public class MarioStateMachine {
private int score;
private State currentState;
public MarioStateMachine() {
this.score = 0;
this.currentState = State.SMALL;
}
public void obtainMushRoom() {
if (currentState.equals(State.SMALL)) {
this.currentState = State.SUPER;
this.score += 100;
}
}
public void obtainCape() {
if (currentState.equals(State.SMALL) || currentState.equals(State.SUPER) ) {
this.currentState = State.CAPE;
this.score += 200;
}
}
public void obtainFireFlower() {
if (currentState.equals(State.SMALL) || currentState.equals(State.SUPER) ) {
this.currentState = State.FIRE;
this.score += 300;
}
}
public void meetMonster() {
if (currentState.equals(State.SUPER)) {
this.currentState = State.SMALL;
this.score -= 100;
return;
}
if (currentState.equals(State.CAPE)) {
this.currentState = State.SMALL;
this.score -= 200;
return;
}
if (currentState.equals(State.FIRE)) {
this.currentState = State.SMALL;
this.score -= 300;
return;
}
}
public int getScore() {
return this.score;
}
public State getCurrentState() {
return this.currentState;
}
}
代码示例3:查表法
直接把状态转移存到表里:
public enum Event {
GOT_MUSHROOM(0),
GOT_CAPE(1),
GOT_FIRE(2),
MET_MONSTER(3);
private int value;
private Event(int value) {
this.value = value;
}
public int getValue() {
return this.value;
}
}
public class MarioStateMachine {
private int score;
private State currentState;
private static final State[][] transitionTable = {
{SUPER, CAPE, FIRE, SMALL},
{SUPER, CAPE, FIRE, SMALL},
{CAPE, CAPE, CAPE, SMALL},
{FIRE, FIRE, FIRE, SMALL}
};
private static final int[][] actionTable = {
{+100, +200, +300, +0},
{+0, +200, +300, -100},
{+0, +0, +0, -200},
{+0, +0, +0, -300}
};
public MarioStateMachine() {
this.score = 0;
this.currentState = State.SMALL;
}
public void obtainMushRoom() {
executeEvent(Event.GOT_MUSHROOM);
}
public void obtainCape() {
executeEvent(Event.GOT_CAPE);
}
public void obtainFireFlower() {
executeEvent(Event.GOT_FIRE);
}
public void meetMonster() {
executeEvent(Event.MET_MONSTER);
}
private void executeEvent(Event event) {
int stateValue = currentState.getValue();
int eventValue = event.getValue();
this.currentState = transitionTable[stateValue][eventValue];
this.score = actionTable[stateValue][eventValue];
}
public int getScore() {
return this.score;
}
public State getCurrentState() {
return this.currentState;
}
}
迭代器模式
也叫作游标模式(Cursor Design Pattern)。
使用用途
用来遍历集合对象。很多编程语言都将迭代器作为一个基础的类库,直接提供出来了,平时很少自己去实现一个迭代器。
“集合对象”也可以叫“容器”“聚合对象”,实际上就是包含一组对象的对象,比如数组、链表、树、图、跳表。
迭代器模式将集合对象的遍历操作从集合类中拆分出来,放到迭代器类中,让两者的职责更加单一。
实际上foreach就是迭代器的语法糖。迭代器可以针对复杂容器遍历,各种深度广度优先遍历等,具有很好的扩展性。
实现方案
容器部分:根据容器结构实现。
迭代器部分:定义好游标,并持有容器对象即可。然后定义hasNext(),next(),currentItem()即可,具体就是对游标的操作。
如何避免遍历时容器增删元素带来的异常:
- 遍历时禁止增删元素:比较难实现,因为不知道什么时候遍历结束。
- 增删后遍历器报错:Java的方案,在容器里面加一个修改次数记录字段或版本号,之后每次迭代器调用方法时检查下这个值即可。
代码示例
// 接口定义方式一
// 游标移动和返回当前元素分开,更灵活一些。
public interface Iterator<E> {
boolean hasNext();
void next();
E currentItem();
}
// 接口定义方式二
// 游标移动和返回当前元素合并为一步。
public interface Iterator<E> {
boolean hasNext();
E next();
}
// ArrayList迭代器示例
public class ArrayIterator<E> implements Iterator<E> {
private int cursor;
private ArrayList<E> arrayList;
public ArrayIterator(ArrayList<E> arrayList) {
this.cursor = 0;
this.arrayList = arrayList;
}
@Override
public boolean hasNext() {
return cursor != arrayList.size(); //注意这里,cursor在指向最后一个元素的时候,hasNext()仍旧返回true。
}
@Override
public void next() {
cursor++;
}
@Override
public E currentItem() {
if (cursor >= arrayList.size()) {
throw new NoSuchElementException();
}
return arrayList.get(cursor);
}
}
public class Demo {
public static void main(String[] args) {
ArrayList<String> names = new ArrayList<>();
names.add("xzg");
names.add("wang");
names.add("zheng");
Iterator<String> iterator = new ArrayIterator(names);
while (iterator.hasNext()) {
System.out.println(iterator.currentItem());
iterator.next();
}
}
}
// 还可以把迭代器的实现细节封装一下,并放到容器里
public interface List<E> {
Iterator iterator();
//...省略其他接口函数...
}
public class ArrayList<E> implements List<E> {
//...
public Iterator iterator() {
return new ArrayIterator(this);
}
//...省略其他代码
}
public class Demo {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("xzg");
names.add("wang");
names.add("zheng");
Iterator<String> iterator = names.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.currentItem());
iterator.next();
}
}
}
访问者模式
使用用途
允许一个或者多个操作应用到一组对象上,解耦操作和对象本身。对象就是抽象类,操作就是访问者接口。
弥补单分派语言无法根据形参类型进行方法重载的不足。
访问者模式比较复杂,使用它会让代码可读性、可维护性变差,实际开发中很少用。在没必要的情况下,不推荐使用。
实现方案
定义一个访问者来作为入参访问自己。这里的访问者是一个接口,自己即抽象类的实现类。
抽象类封装了要处理的各种对象,访问者封装了对应的各种操作。
核心代码是抽象类访问方法的子类实现让访问者访问自己,以及访问方法的入参是一个访问者接口。这样就提供了两个扩展维度,抽象方法的实现是一个维度,访问者接口的实现是另外一个维度。不同类型的访问者会对不同类型的自己做出不同的逻辑处理。核心代码:抽象类及其各种实现类,抽象类及其实现类的accept方法绑定自己的访问者。访问者及其实现类,对不同的被访问者做出各种不同的反应。
访问者有点像一个外挂。通过抽象类的形参建立联系。
示例代码:用各种逻辑处理各种格式文本
public abstract class ResourceFile {
protected String filePath;
public ResourceFile(String filePath) {
this.filePath = filePath;
}
// 整个访问者模式的关键代码:在抽象接待方法里,让访问者访问自己。从而提供了自己、访问者的两个扩展维度。或者说对象和操作的两个扩展维度。
abstract public void accept(Visitor vistor);
}
public class PdfFile extends ResourceFile {
public PdfFile(String filePath) {
super(filePath);
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this); // 整个访问者模式的关键代码:在接待方法里,让访问者访问自己。
}
//...
}
//...PPTFile、WordFile跟PdfFile类似,这里就省略了...
public interface Visitor {
void visit(PdfFile pdfFile);
void visit(PPTFile pdfFile);
void visit(WordFile pdfFile);
}
public class Extractor implements Visitor {
@Override
public void visit(PPTFile pptFile) {
//...
System.out.println("Extract PPT.");
}
@Override
public void visit(PdfFile pdfFile) {
//...
System.out.println("Extract PDF.");
}
@Override
public void visit(WordFile wordFile) {
//...
System.out.println("Extract WORD.");
}
}
public class Compressor implements Visitor {
@Override
public void visit(PPTFile pptFile) {
//...
System.out.println("Compress PPT.");
}
@Override
public void visit(PdfFile pdfFile) {
//...
System.out.println("Compress PDF.");
}
@Override
public void visit(WordFile wordFile) {
//...
System.out.println("Compress WORD.");
}
}
public class ToolApplication {
public static void main(String[] args) {
Extractor extractor = new Extractor();
List<ResourceFile> resourceFiles = listAllResourceFiles(args[0]);
for (ResourceFile resourceFile : resourceFiles) {
resourceFile.accept(extractor);
}
Compressor compressor = new Compressor();
for(ResourceFile resourceFile : resourceFiles) {
resourceFile.accept(compressor);
}
}
private static List<ResourceFile> listAllResourceFiles(String resourceDirectory) {
List<ResourceFile> resourceFiles = new ArrayList<>();
//...根据后缀(pdf/ppt/word)由工厂方法创建不同的类对象(PdfFile/PPTFile/WordFile)
resourceFiles.add(new PdfFile("a.pdf"));
resourceFiles.add(new WordFile("b.word"));
resourceFiles.add(new PPTFile("c.ppt"));
return resourceFiles;
}
}
扩展:双分派语言
Single Dispatch 之所以称为“Single”,是因为执行哪个对象的哪个方法,只跟“对象”的运行时类型有关。Double Dispatch 之所以称为“Double”,是因为执行哪个对象的哪个方法,跟“对象”和“方法参数”两者的运行时类型有关。Java只支持单分派。
- 单分派语言:执行哪个对象的方法,由对象的实际类型决定
- 双分派语言:执行对象的哪个方法,由参数对象的声明类型决定
单分派、双分派差别示意:
public class ParentClass {
public void f() {
System.out.println("I am ParentClass's f().");
}
}
public class ChildClass extends ParentClass {
public void f() {
System.out.println("I am ChildClass's f().");
}
}
public class SingleDispatchClass {
public void polymorphismFunction(ParentClass p) {
p.f();
}
public void overloadFunction(ParentClass p) {
System.out.println("I am overloadFunction(ParentClass p).");
}
public void overloadFunction(ChildClass c) {
System.out.println("I am overloadFunction(ChildClass c).");
}
}
public class DemoMain {
public static void main(String[] args) {
SingleDispatchClass demo = new SingleDispatchClass();
ParentClass p = new ChildClass();
demo.polymorphismFunction(p);//执行哪个对象的方法,由对象的实际类型决定
demo.overloadFunction(p);//执行对象的哪个方法,由参数对象的声明类型决定
}
}
//代码执行结果:
I am ChildClass's f().
I am overloadFunction(ParentClass p).
访问者模式是为了弥补单分派语言的不足,因为拿到一个父类声明的对象时,语言本身无法判断这个对象的子类类型,只能用访问者模式来巧妙的实现。如果是双分配语言,直接用对象执行即可,不用再用访问者绕一圈。
双分派不需要使用访问者模式,因为可以识别方法形参的实际类型,以此进行方法重载:
public abstract class ResourceFile {
protected String filePath;
public ResourceFile(String filePath) {
this.filePath = filePath;
}
}
public class PdfFile extends ResourceFile {
public PdfFile(String filePath) {
super(filePath);
}
//...
}
//...PPTFile、WordFile代码省略...
public class Extractor {
public void extract2txt(PPTFile pptFile) {
//...
System.out.println("Extract PPT.");
}
public void extract2txt(PdfFile pdfFile) {
//...
System.out.println("Extract PDF.");
}
public void extract2txt(WordFile wordFile) {
//...
System.out.println("Extract WORD.");
}
}
public class ToolApplication {
public static void main(String[] args) {
Extractor extractor = new Extractor();
List<ResourceFile> resourceFiles = listAllResourceFiles(args[0]);
for (ResourceFile resourceFile : resourceFiles) {
// 关键代码:双分派语言可以自动感知形参是什么类型的子类,以此定位要执行的方法。而单分派具体执行哪个对象的哪个方法,只跟对象的运行时类型有关,跟参数的运行时类型无关。即只支持编译期的方法重载。这里就直接报错了。
extractor.extract2txt(resourceFile);
}
}
private static List<ResourceFile> listAllResourceFiles(String resourceDirectory) {
List<ResourceFile> resourceFiles = new ArrayList<>();
//...根据后缀(pdf/ppt/word)由工厂方法创建不同的类对象(PdfFile/PPTFile/WordFile)
resourceFiles.add(new PdfFile("a.pdf"));
resourceFiles.add(new WordFile("b.word"));
resourceFiles.add(new PPTFile("c.ppt"));
return resourceFiles;
}
}
备忘录模式
也叫快照(Snapshot)模式。
使用用途
用于保存对象状态并后续恢复。
就是在不违背封装原则的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便之后恢复对象为先前的状态。
实现方案
- 一个snapshot类,用于封装快照。
- 一个SnapshotHolder类,用于存储快照。存储的容器用栈或队列都可以。
- 一个操作类,用于操作快照类和holder类存储快照,以及业务处理。
代码示例
编写一个小程序,可以接收命令行的输入。用户输入文本时,程序将其追加存储在内存文本中;用户输入“:list”,程序在命令行中输出内存文本的内容;用户输入“:undo”,程序会撤销上一次输入的文本,也就是从内存文本中将上次输入的文本删除掉:
public class InputText {
private StringBuilder text = new StringBuilder();
public String getText() {
return text.toString();
}
public void append(String input) {
text.append(input);
}
public Snapshot createSnapshot() {
return new Snapshot(text.toString());
}
public void restoreSnapshot(Snapshot snapshot) {
this.text.replace(0, this.text.length(), snapshot.getText());
}
}
public class Snapshot {
private String text;
public Snapshot(String text) {
this.text = text;
}
public String getText() {
return this.text;
}
}
public class SnapshotHolder {
private Stack<Snapshot> snapshots = new Stack<>();
public Snapshot popSnapshot() {
return snapshots.pop();
}
public void pushSnapshot(Snapshot snapshot) {
snapshots.push(snapshot);
}
}
public class ApplicationMain {
public static void main(String[] args) {
InputText inputText = new InputText();
SnapshotHolder snapshotsHolder = new SnapshotHolder();
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()) {
String input = scanner.next();
if (input.equals(":list")) {
System.out.println(inputText.toString());
} else if (input.equals(":undo")) {
Snapshot snapshot = snapshotsHolder.popSnapshot();
inputText.restoreSnapshot(snapshot);
} else {
snapshotsHolder.pushSnapshot(inputText.createSnapshot());
inputText.append(input);
}
}
}
}
命令模式
命令模式、解释器模式、中介模式。这 3 个模式使用频率低、理解难度大,只在非常特定的应用场景下才会用到,所以,不是我们学习的重点,你只需要稍微了解,见了能认识就可以了。
使用用途
命令模式将请求(命令)封装为一个对象,这样可以使用不同的请求参数化其他对象(将不同请求依赖注入到其他对象),并且能够支持请求(命令)的排队执行、记录日志、撤销等(附加控制)功能。
命令模式用的最核心的实现手段,是将函数封装成对象。具体来说就是,设计一个包含这个函数的类,实例化一个对象传来传去,这样就可以实现把函数像对象一样使用。
命令模式和策略模式的不同:前者不同命令有不同的目的,对应不同处理逻辑,不可替换。后者不同策略有相同的目的,不过实现的方式不一样而已,可以替换。两种模式虽然代码相近,但设计模式最重要的是处理思路,两者的处理思路完全不同。
实现方案
把请求包含的数据和处理逻辑封装为命令对象,在程序中直接拿来执行即可。
把函数封装成对象,以实现函数的传递。
代码示例1
处理一个游戏的的各种指令:
public interface Command {
void execute();
}
public class GotDiamondCommand implements Command {
// 省略成员变量
public GotDiamondCommand(/*数据*/) {
//...
}
@Override
public void execute() {
// 执行相应的逻辑
}
}
//GotStartCommand/HitObstacleCommand/ArchiveCommand类省略
public class GameApplication {
private static final int MAX_HANDLED_REQ_COUNT_PER_LOOP = 100;
private Queue<Command> queue = new LinkedList<>();
public void mainloop() {
while (true) {
List<Request> requests = new ArrayList<>();
//省略从epoll或者select中获取数据,并封装成Request的逻辑,
//注意设置超时时间,如果很长时间没有接收到请求,就继续下面的逻辑处理。
for (Request request : requests) {
Event event = request.getEvent();
Command command = null;
if (event.equals(Event.GOT_DIAMOND)) {
command = new GotDiamondCommand(/*数据*/); // 核心代码
} else if (event.equals(Event.GOT_STAR)) {
command = new GotStartCommand(/*数据*/);
} else if (event.equals(Event.HIT_OBSTACLE)) {
command = new HitObstacleCommand(/*数据*/);
} else if (event.equals(Event.ARCHIVE)) {
command = new ArchiveCommand(/*数据*/);
} // ...一堆else if...
queue.add(command);
}
int handledCount = 0;
while (handledCount < MAX_HANDLED_REQ_COUNT_PER_LOOP) {
if (queue.isEmpty()) {
break;
}
Command command = queue.poll();
command.execute(); // 核心代码
}
}
}
}
代码示例2
买卖股票:
// 命令
public interface Command {
void execute();
}
// 请求类
public class StockReq {
private String name = "ABC";
private int quantity = 10;
public void buy(){
System.out.println("Stock [ Name: "+name+",
Quantity: " + quantity +" ] bought");
}
public void sell(){
System.out.println("Stock [ Name: "+name+",
Quantity: " + quantity +" ] sold");
}
}
// 命令实现类
public class BuyStockCommand implements Command {
private StockReq stockReq;
public BuyStockCommand(Stock stockReq){
this.stockReq = stockReq;
}
public void execute() {
stockReq.buy();
}
}
public class SellStockCommand implements Command {
private StockReq stockReq;
public SellStock(StockReq stockReq){
this.stockReq = stockReq;
}
public void execute() {
stockReq.sell();
}
}
// 命令调用类
import java.util.ArrayList;
import java.util.List;
public class CommendExcuter {
private List<Command> commandList = new ArrayList<Command>();
public void addCommand(Command command){
commandList.add(command);
}
public void executeCommands(){
for (Command command : commandList) {
command.execute();
}
commandList.clear();
}
}
// 使用
public class CommandPatternDemo {
public static void main(String[] args) {
Stock abcStockReq = new Stock();
BuyStockCommand buyStockCommand = new BuyCommand(abcStockReq);
SellStockCommand sellStockCommand = new SellCommand(abcStockReq);
CommendExcuter commendExcuter = new CommendExcuter();
commendExcuter.addCommand(buyStockOrder);
commendExcuter.addCommand(sellStockOrder);
commendExcuter.executeCommands();
}
}
// 结果
Stock [ Name: ABC, Quantity: 10 ] bought
Stock [ Name: ABC, Quantity: 10 ] sold
解释器模式
使用用途
用来描述如何构建一个简单的“语言”解释器。比如编译器、规则引擎、正则表达式,或者自己定义的规则语言等。
平时用的很少,运用场景很固定,了解就行。Java中已经有现成的expression4J 。
实现方案
解释器模式为某个语言定义它的语法(或者叫文法)表示,并定义一个解释器用来处理这个语法。
解释器模式的代码实现比较灵活,没有固定的模板。一般的做法是,将语法规则拆分成一些小的独立的单元,然后对每个单元进行解析,最终合并为对整个语法规则的解析。
1. 定义一个表达式接口,以及它的解释方法。解释方法的入参是表达式。
2. 实现表达式接口,从而派生出多种表达式及其解析。
3. 使用Expression 表达式类来创建各种新的表达式规则,并解析它们。也可以直接调用之前的表达式实现类解析。
代码示例
// 1.定义一个表达式接口
public interface Expression {
public boolean interpret(String context); // 注意这里的解释器
}
// 2.实现表达式接口
public class TerminalExpression implements Expression {
private String data;
public TerminalExpression(String data){
this.data = data;
}
@Override
public boolean interpret(String context) {
if(context.contains(data)){
return true;
}
return false;
}
}
public class OrExpression implements Expression {
private Expression expr1 = null;
private Expression expr2 = null;
public OrExpression(Expression expr1, Expression expr2) {
this.expr1 = expr1;
this.expr2 = expr2;
}
@Override
public boolean interpret(String context) {
return expr1.interpret(context) || expr2.interpret(context);
}
}
public class AndExpression implements Expression {
private Expression expr1 = null;
private Expression expr2 = null;
public AndExpression(Expression expr1, Expression expr2) {
this.expr1 = expr1;
this.expr2 = expr2;
}
@Override
public boolean interpret(String context) {
return expr1.interpret(context) && expr2.interpret(context);
}
}
// 3.使用Expression 类来创建新的Expression,并解析它们
public class InterpreterPatternDemo {
//规则:Robert 和 John 是男性
public static Expression getMaleExpression(){
Expression robert = new TerminalExpression("Robert");
Expression john = new TerminalExpression("John");
return new OrExpression(robert, john);
}
//规则:Julie 是一个已婚的女性
public static Expression getMarriedWomanExpression(){
Expression julie = new TerminalExpression("Julie");
Expression married = new TerminalExpression("Married");
return new AndExpression(julie, married);
}
public static void main(String[] args) {
Expression isMale = getMaleExpression();
Expression isMarriedWoman = getMarriedWomanExpression();
System.out.println("John is male? " + isMale.interpret("John"));
System.out.println("Julie is a married women? "
+ isMarriedWoman.interpret("Married Julie"));
}
}
中介模式
使用用途
中介模式也很少用。
中介模式定义了一个单独的(中介)对象,来封装一组对象之间的交互。将这组对象之间的交互委派给与中介对象交互,来避免对象之间的直接交互。(注意和观察者模式的区别。)通过引入中介这个中间层,将一组对象之间的交互关系(或者说依赖关系)从多对多(网状关系)转换为一对多(星状关系)。
比如UI控制,点击一个按钮,出发和它相关的所有组件,就可以用中介模式(类似总线?)。好处是简化了控件之间的交互,坏处是中介类有可能会变成大而复杂的“上帝类”(God Class)。
实现方案
定义一个中介者接口。定义处理方法,入参是原始类和消息。
中介者接口的实现类实现接口即处理方法。处理方法针对不同的原始类、不同的消息,做不同的处理。
大家按需注册到中介接口实现类,方便中介调用资源。
发送事件时,把自己的引用和事件发给中介,中介去处理即可。
各个控件只跟中介对象交互,中介对象负责所有业务逻辑的处理。
- 观察者模式,观察者持有被观察者的引用,并调用被观察者的update方法,业务处理逻辑在被观察者处。
- 中介者模式:中介者持有原始类的对象。原始类主动调用中介者的处理方法,将自己的引用和事件通过形参传给中介者。由中间来调用各个原始类的对象和资源,业务处理逻辑。重点看下下面示例代码的Interface`就明白了。
代码示例
以下是UI操作示例:
// 注意到这个重点的类:各原始类吧自己
public interface Mediator {
void handleEvent(Component component, String event);
}
public class LandingPageDialog implements Mediator {
private Button loginButton;
private Button regButton;
private Selection selection;
private Input usernameInput;
private Input passwordInput;
private Input repeatedPswdInput;
private Text hintText;
// 统一处理各类资源
@Override
public void handleEvent(Component component, String event) {
if (component.equals(loginButton)) {
String username = usernameInput.text();
String password = passwordInput.text();
//校验数据...
//做业务处理...
} else if (component.equals(regButton)) {
//获取usernameInput、passwordInput、repeatedPswdInput数据...
//校验数据...
//做业务处理...
} else if (component.equals(selection)) {
String selectedItem = selection.select();
if (selectedItem.equals("login")) {
usernameInput.show();
passwordInput.show();
repeatedPswdInput.hide();
hintText.hide();
//...省略其他代码
} else if (selectedItem.equals("register")) {
//....
}
}
}
}
public class UIControl {
private static final String LOGIN_BTN_ID = "login_btn";
private static final String REG_BTN_ID = "reg_btn";
private static final String USERNAME_INPUT_ID = "username_input";
private static final String PASSWORD_INPUT_ID = "pswd_input";
private static final String REPEATED_PASSWORD_INPUT_ID = "repeated_pswd_input";
private static final String HINT_TEXT_ID = "hint_text";
private static final String SELECTION_ID = "selection";
public static void main(String[] args) {
Button loginButton = (Button)findViewById(LOGIN_BTN_ID);
Button regButton = (Button)findViewById(REG_BTN_ID);
Input usernameInput = (Input)findViewById(USERNAME_INPUT_ID);
Input passwordInput = (Input)findViewById(PASSWORD_INPUT_ID);
Input repeatedPswdInput = (Input)findViewById(REPEATED_PASSWORD_INPUT_ID);
Text hintText = (Text)findViewById(HINT_TEXT_ID);
Selection selection = (Selection)findViewById(SELECTION_ID);
Mediator dialog = new LandingPageDialog();
dialog.setLoginButton(loginButton);
dialog.setRegButton(regButton);
dialog.setUsernameInput(usernameInput);
dialog.setPasswordInput(passwordInput);
dialog.setRepeatedPswdInput(repeatedPswdInput);
dialog.setHintText(hintText);
dialog.setSelection(selection);
loginButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
dialog.handleEvent(loginButton, "click");
}
});
regButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
dialog.handleEvent(regButton, "click");
}
});
//....
}
}
扩展:中介模式 VS 观察者模式
观察者模式:在观察者模式中,尽管一个参与者既可以是观察者,同时也可以是被观察者,但是,大部分情况下,交互关系往往都是单向的,一个参与者要么是观察者,要么是被观察者,不会兼具两种身份。也就是说,在观察者模式的应用场景中,参与者之间的交互关系比较有条理。
中介模式:中介模式正好相反。只有当参与者之间的交互关系错综复杂,维护成本很高的时候,我们才考虑使用中介模式,但有可能会产生大而复杂的上帝类。除此之外,如果一个参与者状态的改变,其他参与者执行的操作有一定先后顺序的要求,这个时候,中介模式就可以利用中介类,通过先后调用不同参与者的方法,来实现顺序的控制。观察者则没有这个功能。
转载请注明来源