工具篇-Java中的设计模式积累(二)
目录
行为型设计模式
------------------------------------------行为型设计模式
1. 模版模式
为什么有模版模式:
定义一个算法的骨架,而将一些步骤的实现延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中某些步骤的具体实现;
优点:
- 模板方法模式通过把不变的行为搬到父类,去除了子类中的重复代码;
- 子类实现算法的某些细节,有助于算法的扩展;
- 父类调用子类实现的操作,通过子类扩展增加新的行为,符合“开放-封闭原则”
缺点:
-
在模版模式中,子类执行的结果影响了父类的结果,会增加代码阅读的难度;
应用场景:
- 多个子类存在共有方法,且逻辑相同;
- 重要、复杂的算法,可以把核心算法设计为模板方法
实现:
模版模式中,基类方法的默认实现被退化为钩子Hooks的概念,他们被设计在子类中被重写,如果你期望一些方法在子类中不被重写,可以让他们为final。例如,
模版抽象类
1 public abstract class HouseTemplate { 2 3 protected HouseTemplate(String name){ 4 this.name = name; 5 } 6 7 protected String name; 8 9 protected abstract void buildDoor(); 10 11 protected abstract void buildWindow(); 12 13 protected abstract void buildWall(); 14 15 protected abstract void buildBase(); 16 17 protected abstract void buildToilet(); 18 19 //钩子方法 20 protected boolean isBuildToilet(){ 21 return true; 22 } 23 24 //公共逻辑 25 public final void buildHouse(){ 26 27 buildBase(); 28 buildWall(); 29 buildDoor(); 30 buildWindow(); 31 if(isBuildToilet()){ 32 buildToilet(); 33 } 34 } 35 36 }
子类1
1 public class HouseOne extends HouseTemplate { 2 3 HouseOne(String name){ 4 super(name); 5 } 6 7 HouseOne(String name, boolean isBuildToilet){ 8 this(name); 9 this.isBuildToilet = isBuildToilet; 10 } 11 12 public boolean isBuildToilet; 13 14 @Override 15 protected void buildDoor() { 16 System.out.println(name +"的门要采用防盗门"); 17 } 18 19 @Override 20 protected void buildWindow() { 21 System.out.println(name + "的窗户要面向北方"); 22 } 23 24 @Override 25 protected void buildWall() { 26 System.out.println(name + "的墙使用大理石建造"); 27 } 28 29 @Override 30 protected void buildBase() { 31 System.out.println(name + "的地基使用钢铁地基"); 32 } 33 34 @Override 35 protected void buildToilet() { 36 System.out.println(name + "的厕所建在东南角"); 37 } 38 39 @Override 40 protected boolean isBuildToilet(){ 41 return isBuildToilet; 42 } 43 44 }
子类2
1 public class HouseTwo extends HouseTemplate { 2 3 HouseTwo(String name){ 4 super(name); 5 } 6 7 @Override 8 protected void buildDoor() { 9 System.out.println(name + "的门采用木门"); 10 } 11 12 @Override 13 protected void buildWindow() { 14 System.out.println(name + "的窗户要向南"); 15 } 16 17 @Override 18 protected void buildWall() { 19 System.out.println(name + "的墙使用玻璃制造"); 20 } 21 22 @Override 23 protected void buildBase() { 24 System.out.println(name + "的地基使用花岗岩"); 25 } 26 27 @Override 28 protected void buildToilet() { 29 System.out.println(name + "的厕所建在西北角"); 30 } 31 32 }
客户端
1 public class Clienter { 2 3 public static void main(String[] args){ 4 HouseTemplate houseOne = new HouseOne("房子1", false); 5 HouseTemplate houseTwo = new HouseTwo("房子2"); 6 houseOne.buildHouse(); 7 houseTwo.buildHouse(); 8 } 9 10 }
可以看到,通过重写钩子方法自定义了房子1不需要建造厕所(fasle),另外可以参考这篇文章:设计模式学习笔记之九:模板方法模式。
2. 策略模式
为什么有策略模式:
Define a family of algorithms,encapsulate each one,and make them interchangeable.
定义一组算法(我觉得应该是一个行为的不同实现吧),将每个算法都封装起来,并且使它们之间可以互换。
通俗点讲是利用继承和多态机制,从而实现同一行为在不同场景下的不同实现吧。
优点:
- 算法可以自由切换;
- 避免使用多重条件判断;
- 扩展性良好,策略类遵顼里氏替换原则,增加一个策略只需要实现一个接口就可以了。
缺点:
- 策略模式造成很多的策略类,每个具体策略类都会产生一个新类;
- 所有的策略类都需要对外暴露,客户端必须知道所有的策略类,并自行决定使用哪一个策略类,违反了迪米特法,可使用工厂方法模式、代理模式修整这个缺陷。
应用场景:
- 如果在一个系统里面有许多类,它们之间的区别仅在于它们的行为,在运行时动态选择具体要执行的行为;
- 在不同情况下使用不同的策略(算法),或者策略还可能在未来用其它方式来实现;
- 对客户隐藏具体策略(算法)的实现细节,彼此完全独立。
实现:
策略模式有三种角色:
抽象策略角色这个是一个抽象的角色,通常使用接口或者抽象类去实现。比如Comparator接口;
具体策略角色包装了具体的算法和行为。就是实现了Comparator接口的实现一组实现类;
环境角色内部会持有一个抽象角色的引用,比如内部一定会有一个策略类的一个成员变量,这样在构建函数中就可以接收外部传递的具体的策略类。
例如,
//抽象策略类 Strategy interface IStrategy { void algorithm(); } //具体策略类 ConcreteStrategyA static class ConcreteStrategyA implements IStrategy { @Override public void algorithm() { System.out.println("Strategy A"); } } //具体策略类 ConcreteStrategyB static class ConcreteStrategyB implements IStrategy { @Override public void algorithm() { System.out.println("Strategy B"); } }
客户端,
1 //上下文环境角色 2 static class Context { 3 private IStrategy mStrategy; // 引用 4 5 public Context(IStrategy strategy) { 6 this.mStrategy = strategy; 7 } 8 9 public void algorithm() { 10 this.mStrategy.algorithm(); 11 } 12 }
3. 命令模式
为什么有命令模式:
将一个请求封装成一个对象,从而使请求参数化。
优点:
- 降低"行为请求者"与"行为实现者"的耦合;
- 新的命令可以很容易添加到系统中。
缺点:
- 可能会导致某些系统有过多的具体命令类。
应用场景:
在一些对行为进行"记录、撤销/重做、事务"等处理的场合,比如:线程池、工作队列、日志请求等
- 工作队列:
在某一端添加命令,另一端则是线程进行下面的动作:从队列中取出一个命令,调用它的execute()方法,等待这个调用完成,然后将此命令对象丢弃,再取出下一个命令......这里工作队列和命令对象之间是完全解耦的,线程可能此刻在进行财务运算,下一刻却在读取网络数据。工作队列对象不在乎到底做些什么,它们只知道取出命令对象,然后调用其execute()方法。类似地,它们只要实现命令模式的对象,就可以放入队列里,当线程可用时,就调用此对象的execute()方法。
- 日志请求
多用于数据库管理系统的实现,我们需要把一系列的操作记录下来(如写在硬盘上),在遇到系统故障时读出来以恢复数据,如何实现?利用对象的序列化把对象保存起来(记录日志),在需要的时候反序列化(恢复事务)。
注:这里的可撤销的操作就是:放弃该操作,回到未执行该操作前的状态。
有两种基本的思路来实现可撤销的操作:
① 一种是补偿式,又称反操作式
比如被撤销的操作是加的功能, 那撤消的实现就变成减的功能;比如被撤销的操作是打开的功能,那么撤销的实现就变成关闭的功能。
② 另外一种方式是存储恢复式
意思就是把操作前的状态记录下来,然后要撤销操作的时候就直接恢复回去就可以了。
实现:
该模式具有以下角色:
抽象命令接口Command:定义接口,声明执行的方法。
具体的命令对象ConcreteCommand:持有接受者对象,完成具体的命令。
接受者对象Receiver:接受者对象,真正执行命令的对象。
传递命令对象Invoker:调用者,要求命令对象执行请求。
客户端对象Client:创建具体命令对象并且设置接受者。
比如:顾客来到餐馆点一碗面(发出请求) -> 柜台服务员记录下来(创建命令) -> 服务员把订单扔给厨房 -> 厨师很快做好了一碗面(请求被执行)。顾客不知道将由谁来做这碗面,柜台服务员也不知道,厨师不知道是谁点了这碗面,其中,订单就起解耦的作用。
首先,写命令接口
1 public interface Command { 2 public abstract void execute();//只需要定义一个统一的执行方法 3 }
然后是执行者
1 public abstract class Chef { 2 //在此定义厨师的公共属性 3 4 //定义烹饪方法 5 public abstract void cook(); 6 }
具体执行者,做面的厨师和做饼的厨师
1 public class NoodlesChef extends Chef{ 2 3 @Override 4 public void cook() { 5 System.out.println("做好了一碗美味的拉面"); 6 } 7 }
1 public class PieChef extends Chef{ 2 3 @Override 4 public void cook() { 5 System.out.println("做好了一块香喷喷的大饼"); 6 } 7 }
各种具体命令
1 public class NoodlesCommand implements Command{ 2 private NoodlesChef chef;//专业做面的厨师 3 4 public NoodlesCommand(){ 5 chef = new NoodlesChef(); 6 } 7 8 @Override 9 public void execute() { 10 chef.cook(); 11 //调用其它需要的方法 12 } 13 }
1 public class PieCommand implements Command{ 2 private PieChef chef;//专业做饼的厨师 3 4 public PieCommand(){ 5 chef = new PieChef(); 6 } 7 8 @Override 9 public void execute() { 10 chef.cook(); 11 //调用其它需要的方法 12 } 13 }
调用者invoker
1 public class Waiter { 2 3 private Command command; 4 5 // 设置具体的命令 6 public void setCommand(Command command) { 7 this.command = command; 8 } 9 10 // 执行命令 11 public void doCommand() { 12 command.execute(); 13 } 14 }
客户端,店馆开张了
public class Test { public static void main(String[] args) { System.out.println("第一位客户X先生"); System.out.println("X先生:你好,我需要一碗面,我饿极了"); NoodlesCommand nCmd = new NoodlesCommand(); System.out.println("柜台服务员:好的厨房~~,接单"); Waiter.doCommand(); //安排厨师做 System.out.println("第二位客户XX先生"); System.out.println("XX先生:你好,我需要一块饼,20分钟后来取"); PieCommand pCmd = new PieCommand(); System.out.println("柜台服务员:好的厨房~~,接单"); Waiter.doCommand(); // 安排厨师做 } }
4. 责任链模式
为什么有责任链模式:
搞一条对象链,这样请求不用知道具体执行的是哪一个对象,就实现了请求与对象之间的解耦。
优点:
- 降低了对象之间的耦合度,上边已经说了;
- 增强了系统的可扩展性。可以根据需要增加删除请求处理类,满足开闭原则;
- 增强了给对象指派职责的灵活性。可以动态地改变链内的成员的次序;
- 责任分担。每个类只需要处理自己该处理的工作,符合类的单一职责原则。
缺点(针对不纯责任链模式):
- 不能保证每个请求一定被处理。,请求可能一直传到链的末端都得不到处理;
- 对比较长的职责链,系统性能将受到一定影响;
- 职责链建立的合理性要靠客户端来保证,可能会造成循环调用。
应用场景:
我觉得好多if/else,switch/case操作可以考虑这个吧,还是能充分利用这种模式的优点呗,比如:
Netty 中的 Pipeline 和 ChannelHandler 通过责任链设计模式来组织代码逻辑
Spring Security 使用责任链模式,可以动态地添加或删除责任(处理 request 请求)
Spring AOP 通过责任链模式来管理 Advisor
实现(具体参考:责任链模式妙用):
该模式具有以下角色:
抽象处理者(Handler):定义一个处理请求的接口(或抽象类),包含抽象处理方法和一个后继连接。
具体处理者(Concrete Handler):实现抽象处理者的处理方法,判断能否处理本次请求,如果可以则处理,否则将该请求转给它的后继者。
客户类(Client):创建处理链,并向链头的具体处理者对象提交请求,它不关心处理细节和请求的传递过程。
抽象接口
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 public abstract class BaseCase { 2 // 为 true 表明自己可以处理该 case 3 private boolean isConsume; 4 public BaseCase(boolean isConsume) { 5 this.isConsume = isConsume; 6 } 7 // 下一个责任节点 8 private BaseCase nextCase; 9 public void setNextCase(BaseCase nextCase) { 10 this.nextCase = nextCase; 11 } 12 public void handleRequest() { 13 if (isConsume) { 14 // 如果当前节点可以处理,直接处理 15 doSomething(); 16 } else { 17 // 如果当前节点不能处理,并且有下个节点,交由下个节点处理 18 if (null != nextCase) { 19 nextCase.handleRequest(); 20 } 21 } 22 } 23 abstract protected void doSomething(); 24 }
具体处理者,其中一个为例
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 public class OneCase extends BaseCase { 2 public OneCase(boolean isConsume) { 3 super(isConsume); 4 } 5 6 @Override protected void doSomething() { 7 // TODO do something 8 System.out.println(getClass().getName()); 9 } 10 }
客户端
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 String input = "1"; 2 OneCase oneCase = new OneCase("1".equals(input)); 3 TwoCase twoCase = new TwoCase("2".equals(input)); 4 DefaultCase defaultCase = new DefaultCase(true); 5 oneCase.setNextCase(twoCase); 6 twoCase.setNextCase(defaultCase); 7 oneCase.handleRequest();
5. 状态模式
为什么有状态模式:
让一个对象在其内部状态改变的时候改变其行为
优点:
- 将不同状态的行为分割开来,满足“单一职责原则”;
- 将不同的状态引入独立的对象中会使状态转换更加明确,减少对象间的相互依赖;
- 有利于扩展,通过定义新的子类很容易地增加新的状态和转换
缺点:
- 每一个状态都对应一个子类,当你的状态非常多的时候就会发生类膨胀。
应用场景:
行为随着状态的改变而改变的时候,例如权限设计,人员的状态不同即使执行相同的行为结果也会不同,在这种情况下需要考虑使用状态模式。
实现:
该模式具有以下角色
环境(Context)角色:也称为上下文,它将与状态相关的操作委托给当前状态对象来处理;
抽象状态(State)角色:接口,用来封装对象中的特定状态所对应的行为;
具体状态(Concrete State)角色:实现抽象状态所对应的行为。
比如:用户登录系统,同一个操作在不同状态下会有不同的操作,先定义一个用户状态的接口
1 public interface LoginState { 2 public void loadUser(); 3 public void loginIn(); 4 public void loginOut(); 5 }
登录成功状态
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 public class LoginInState implements LoginState{ 2 3 @Override 4 public void loadUser() { 5 // TODO Auto-generated method stub 6 System.out.println("用户数据载入成功"); 7 8 } 9 10 @Override 11 public void loginIn() { 12 // TODO Auto-generated method stub 13 System.out.println("已经登录成功了"); 14 } 15 16 @Override 17 public void loginOut() { 18 // TODO Auto-generated method stub 19 System.out.println("已经登录成功了,请按退出登录按钮"); 20 } 21 22 }
退出登录状态
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 public class LoginErrorState implements LoginState{ 2 @Override 3 public void loadUser() { 4 // TODO Auto-generated method stub 5 System.out.println("已经退出,请按登录按钮"); 6 } 7 8 @Override 9 public void loginIn() { 10 // TODO Auto-generated method stub 11 System.out.println("已经退出,请先登录"); 12 } 13 14 @Override 15 public void loginOut() { 16 // TODO Auto-generated method stub 17 System.out.println("退出登录成功"); 18 } 19 }
用LoginContext管理这两个状态
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 /** 2 * 登录环境上下文 3 * @author ccj 4 * 5 */ 6 public class LoginContext { 7 private LoginState loginState=new LoginErrorState(); 8 9 /** 10 * 可以根据需求,进行单例化,工厂化 11 */ 12 public void loginIn(){ 13 loginState=new LoginInState(); 14 loginState.loginIn(); 15 } 16 17 public void loginOut(){ 18 loginState=new LoginErrorState(); 19 loginState.loginOut(); 20 } 21 22 public void loadUser(){ 23 loginState.loadUser(); 24 } 25 }
客户端按键
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 public class Test { 2 /** 3 * 将登录登出两个动作, 分别对应两个按钮 4 * @param args 5 */ 6 public static void main(String[] args) { 7 LoginContext context =new LoginContext(); 8 System.out.println("=======点击登录按钮===>>=状态:登录=====>>选择加载用户数据==========="); 9 context.loginIn(); 10 context.loadUser(); 11 12 System.out.println("=====点击退出按钮>>======状态:退出=======>>选择加载用户数据========="); 13 context.loginIn(); 14 context.loginOut(); 15 context.loadUser(); 16 } 17 }
测试结果
1 =======点击登录按钮===>>=状态:登录=====>>选择加载用户数据=========== 2 已经登录成功了 3 用户数据载入成功 4 =====点击退出按钮>>======状态:退出=======>>选择加载用户数据========= 5 已经登录成功了 6 退出登录成功 7 已经退出,请按登录按钮
⚠️注意:在行为受状态约束的时候使用状态模式,而且状态不超过5个。
策略模式和状态模式对比:
策略模式需要客户端必须完全知晓所有的策略方法,才能够知道究竟哪一个策略是当前需要的。而状态模式客户端在使用的时候,可以不必关心有哪些状态,他只需要去调用环境的行为就可以了,在环境的内部维护了这些状态,就是策略模式服务的对象是不固定的,但是状态模式服务的对象是固定的,每次都是那一个。
6. 观察者模式
为什么有观察者模式:
主要是松解耦(对应Kafka依赖broker发布订阅的这种完全的解耦方式),对于对象间一对多的依赖关系,使得每当一个对象改变状态,则所有依赖它的对象都会得到通知并自动更新。
优点:
-
降低了目标与观察者之间的耦合关系;
- 目标与观察者之间建立了一套触发机制
缺点:
- 目标与观察者之间的依赖关系并没有完全解除,而且有可能出现循环引用。
- 当观察者对象很多时,通知的发布会花费很多时间。
应用场景:
- 对象间存在一对多关系,一个对象的状态改变会影响其他对象,而不知道具体有多少对象需要被改变;
- 当一个抽象模型有两个方面,其中一个方面依赖于另一方面时,可将这二者封装在独立的对象中以使它们可以各自独立地改变和复用
实现:
该模式具有以下角色
抽象主题(Subject):提供一个用于保存观察者对象的聚集类和增加、删除观察者对象的方法,以及通知所有观察者的抽象方法;
具体主题(Concrete Subject):在具体主题内部状态改变时,给所有登记过的观察者发出通知。
抽象观察者(Observer):为所有的具体观察者定义一个接口,在得到主题更改通知时更新自己。
具体观察者(Concrete Observer):实现抽象观察者中定义的抽象方法,以便在得到目标的更改通知时更新自身的状态。
让耦合的双方依赖于抽象,而不是依赖于具体,从而使各自的变化不会影响另一边的变化,这是依赖倒转原则的最佳体现。
首先是一个观察者接口
1 public interface Subscriber { 2 int receive(String publisher, String articleName); 3 }
有一个具体的观察者(微信客户端),实现receive方法
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 public class WeChatClient implements Subscriber { 2 private String username; 3 4 public WeChatClient(String username) { 5 this.username = username; 6 } 7 8 @Override 9 public int receive(String publisher, String articleName) { 10 // 接收到推送时的操作 11 System.out.println(String.format("用户<%s> 接收到 <%s>微信公众号 的推送,文章标题为 <%s>", username, publisher, articleName)); 12 return 0; 13 } 14 }
接着是一个抽象主题,该类维护了一个订阅者列表,实现了订阅、取消订阅、通知所有订阅者等功能
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 public class Publisher { 2 private List<Subscriber> subscribers; 3 private boolean pubStatus = false; 4 5 public Publisher() { 6 subscribers = new ArrayList<Subscriber>(); 7 } 8 9 protected void subscribe(Subscriber subscriber) { 10 this.subscribers.add(subscriber); 11 } 12 13 protected void unsubscribe(Subscriber subscriber) { 14 if (this.subscribers.contains(subscriber)) { 15 this.subscribers.remove(subscriber); 16 } 17 } 18 19 protected void notifySubscribers(String publisher, String articleName) { 20 if (this.pubStatus == false) { 21 return; 22 } 23 for (Subscriber subscriber : this.subscribers) { 24 subscriber.receive(publisher, articleName); 25 } 26 this.clearPubStatus(); 27 } 28 29 protected void setPubStatus() { 30 this.pubStatus = true; 31 } 32 33 protected void clearPubStatus() { 34 this.pubStatus = false; 35 } 36 }
最后是一个具体主题微信公众号类,该类提供了 publishArticles
方法,用于发布推送,当文章发布完毕时调用父类的通知所有订阅者方法
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 public class WeChatAccounts extends Publisher { 2 private String name; 3 4 public WeChatAccounts(String name) { 5 this.name = name; 6 } 7 8 public void publishArticles(String articleName, String content) { 9 System.out.println(String.format("\n<%s>微信公众号 发布了一篇推送,文章名称为 <%s>,内容为 <%s> ", this.name, articleName, content)); 10 setPubStatus(); 11 notifySubscribers(this.name, articleName); 12 } 13 }
客户端使用
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 public class Test { 2 public static void main(String[] args) { 3 WeChatAccounts accounts = new WeChatAccounts("小旋锋"); 4 5 WeChatClient user1 = new WeChatClient("张三"); 6 WeChatClient user2 = new WeChatClient("李四"); 7 WeChatClient user3 = new WeChatClient("王五"); 8 9 accounts.subscribe(user1); 10 accounts.subscribe(user2); 11 accounts.subscribe(user3); 12 13 accounts.publishArticles("设计模式 | 观察者模式及典型应用", "观察者模式的内容..."); 14 15 accounts.unsubscribe(user1); 16 accounts.publishArticles("设计模式 | 单例模式及典型应用", "单例模式的内容...."); 17 } 18 }
输出,当公众号发布一篇推送时,订阅该公众号的用户可及时接收到推送的通知
1 <小旋锋>微信公众号 发布了一篇推送,文章名称为 <设计模式 | 观察者模式及典型应用>,内容为 <观察者模式的内容...> 2 用户<张三> 接收到 <小旋锋>微信公众号 的推送,文章标题为 <设计模式 | 观察者模式及典型应用> 3 用户<李四> 接收到 <小旋锋>微信公众号 的推送,文章标题为 <设计模式 | 观察者模式及典型应用> 4 用户<王五> 接收到 <小旋锋>微信公众号 的推送,文章标题为 <设计模式 | 观察者模式及典型应用> 5 6 <小旋锋>微信公众号 发布了一篇推送,文章名称为 <设计模式 | 单例模式及典型应用>,内容为 <单例模式的内容....> 7 用户<李四> 接收到 <小旋锋>微信公众号 的推送,文章标题为 <设计模式 | 单例模式及典型应用> 8 用户<王五> 接收到 <小旋锋>微信公众号 的推送,文章标题为 <设计模式 | 单例模式及典型应用>
7. 中介者模式
为什么有中介者模式:
把原来对象之间多对多的交互变成一对多的交互;减少各个对象之间的耦合,可以更方便的增加减少对象。
优点:
-
简化了对象之间的交互,将网状结构转换成相对简单的星型结构,一对多关系更容易理解、维护和扩展;
- 松耦合,可以独立的改变和复用每一个同事和中介者,增加新的中介者和新的同事类都比较方便,更好地符合 “开闭原则”
缺点:
- 具体中介者类中包含了大量同事之间的交互细节,会导致具体中介者类非常复杂,系统不好维护
应用场景:
- 一组定义良好的对象,现在要进行复杂的相互通信;
- 想通过一个中间类来封装多个类中的行为,而又不想生成太多的子类。
示例很多,比如
MVC模式中,Controller 是中介者,根据View 层的请求来操作Model 层
- 比如组成原理中的主板,上边插了CPU、内存、网卡、声卡、硬盘各种东西
实现(参考https://blog.csdn.net/wwwdc1012/article/details/83389158):
该模式具有以下角色
抽象中介者(Mediator)角色:它是中介者的接口,提供了同事对象注册与转发同事对象信息的抽象方法。
具体中介者(ConcreteMediator)角色:实现中介者接口,定义一个 List 来管理同事对象,协调各个同事角色之间的交互关系,因此它依赖于同事角色。
抽象同事类(Colleague)角色:定义同事类的接口,保存中介者对象,提供同事对象交互的抽象方法,实现所有相互影响的同事类的公共功能。
具体同事类(Concrete Colleague)角色:实现了在抽象同事类中声明的抽象方法,每个同事都知道中介者对象,要与同事通信则把通信告诉中介者。
比如,具体同事类,两个自己写的定时任务
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 public class MyOneTask extends TimerTask { 2 private static int num = 0; 3 @Override 4 public void run() { 5 System.out.println("I'm MyOneTask " + ++num); 6 } 7 } 8 9 public class MyTwoTask extends TimerTask { 10 private static int num = 1000; 11 @Override 12 public void run() { 13 System.out.println("I'm MyTwoTask " + num--); 14 } 15 }
客户端
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 public class TimerTest { 2 public static void main(String[] args) { 3 // 注意:多线程并行处理定时任务时,Timer运行多个TimeTask时,只要其中之一没有捕获抛出的异常, 4 // 其它任务便会自动终止运行,使用ScheduledExecutorService则没有这个问题 5 Timer timer = new Timer(); 6 timer.schedule(new MyOneTask(), 3000, 1000); // 3秒后开始运行,循环周期为 1秒 7 timer.schedule(new MyTwoTask(), 3000, 1000); 8 } 9 }
输出
1 I'm MyOneTask 1 2 I'm MyTwoTask 1000 3 I'm MyTwoTask 999 4 I'm MyOneTask 2 5 I'm MyOneTask 3 6 I'm MyTwoTask 998 7 I'm MyTwoTask 997 8 I'm MyOneTask 4 9 I'm MyOneTask 5 10 I'm MyTwoTask 996 11 I'm MyTwoTask 995 12 I'm MyOneTask 6 13 ...
具体中介者Timer
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 public class Timer { 2 private final TaskQueue queue = new TaskQueue(); 3 private final TimerThread thread = new TimerThread(queue); 4 public void schedule(TimerTask task, long delay) { 5 if (delay < 0) 6 throw new IllegalArgumentException("Negative delay."); 7 sched(task, System.currentTimeMillis()+delay, 0); 8 } 9 10 public void schedule(TimerTask task, Date time) { 11 sched(task, time.getTime(), 0); 12 } 13 14 private void sched(TimerTask task, long time, long period) { 15 if (time < 0) 16 throw new IllegalArgumentException("Illegal execution time."); 17 18 if (Math.abs(period) > (Long.MAX_VALUE >> 1)) 19 period >>= 1; 20 21 // 获取任务队列的锁(同一个线程多次获取这个锁并不会被阻塞,不同线程获取时才可能被阻塞) 22 synchronized(queue) { 23 // 如果定时调度线程已经终止了,则抛出异常结束 24 if (!thread.newTasksMayBeScheduled) 25 throw new IllegalStateException("Timer already cancelled."); 26 27 // 再获取定时任务对象的锁(为什么还要再加这个锁呢?想不清) 28 synchronized(task.lock) { 29 // 判断线程的状态,防止多线程同时调度到一个任务时多次被加入任务队列 30 if (task.state != TimerTask.VIRGIN) 31 throw new IllegalStateException( 32 "Task already scheduled or cancelled"); 33 // 初始化定时任务的下次执行时间 34 task.nextExecutionTime = time; 35 // 重复执行的间隔时间 36 task.period = period; 37 // 将定时任务的状态由TimerTask.VIRGIN(一个定时任务的初始化状态)设置为TimerTask.SCHEDULED 38 task.state = TimerTask.SCHEDULED; 39 } 40 41 // 将任务加入任务队列 42 queue.add(task); 43 // 如果当前加入的任务是需要第一个被执行的(也就是他的下一次执行时间离现在最近) 44 // 则唤醒等待queue的线程(对应到上面提到的queue.wait()) 45 if (queue.getMin() == task) 46 queue.notify(); 47 } 48 } 49 50 // cancel会等到所有定时任务执行完后立刻终止定时线程 51 public void cancel() { 52 synchronized(queue) { 53 thread.newTasksMayBeScheduled = false; 54 queue.clear(); 55 queue.notify(); // In case queue was already empty. 56 } 57 } 58 // ... 59 }
Timer 中在 schedulexxx 方法中通过 TaskQueue 协调各种 TimerTask 定时任务,Timer 是中介者,TimerTask 是抽象同事类,而我们自己写的任务则是具体同事类
TimerThread 是 Timer 中定时调度线程类的定义,这个类会做为一个线程一直运行来执行 Timer 中任务队列中的任务。
Timer 这个中介者的功能就是定时调度我们写的各种任务,将任务添加到 TaskQueue 任务队列中,给 TimerThread 执行,让任务与执行线程解耦
注:
- 实现过程中可能存在一些问题,比如如果中介者只有一个的话,而且预计中也没有扩展的要求,那么就可以不定义Mediator接口,把中介者对象设为单例;
- 实现同事和中介者的通信:一种实现方式是在Mediator接口中定义一个通用的方法,让各个同事类来调用这个方法 ;另外一种实现方式是可以采用观察者模式,把Mediator实现成为观察者,而各个同事类实现成为Subject,这样同事类发生了改变,会通知Mediator。
- 通常会去掉同事对象的父类,这样可以让任意的对象,只要需要相互交互就可以成为同事;
- 中介者对象可以不把同事作为自己的属性,可以在处理方法中创建或者获取需要的同事对象;
- 同事类需要知道中介者对象,在改变时通知它,但没必要作为属性这么强依赖,可以把中介对象做成单例,直接在同事类的方法里面去调用中介者对象。
8. 迭代器模式
为什么有迭代器模式:
将遍历和实现分开,顺序的访问集合内部的对象,而又不暴露集合内部的表示。
优点:
- 遍历由迭代器完成,简化了集合类;
- 支持以不同方式遍历一个集合,可以自定义迭代器的子类以支持新的遍历;
- 增加新的集合类和迭代器类都很方便,无须修改原有代码;
- 封装性良好,为遍历不同的集合结构提供一个统一的接口。
缺点:
- 增加类的个数,比如增加新的集合类需要增加新的具体迭代器
应用场景:
这个JDK已经提供了Iterator接口,我们直接使用就行,现在很少自己写迭代器了吧
实现
该模式具有以下角色:
抽象迭代器(Iterator):抽象迭代器负责定义访问和遍历元素的接口,而且基本上是有一些固定的方法:
- first()获得第一个元素
- next()访问下一个元素
- hasNext()是否已经访问到底部
具体迭代器(ConcreteIterator):具体迭代器角色要实现迭代器接口,完成容器元素的遍历
抽象容器(Aggregate):容器角色负责提供创建具体迭代器角色的接口,必然提供一个类似createIterator()这样的方法,在Java中一般是iterator()方法
具体容器(Concrete Aggregate):具体容器实现容器接口定义的方法,创建出容纳迭代器的对象
比如,首先iterator,Iterator中最重要的两个方法就是next方法和hasNext方法,构成了遍历整个容器数据的两个方法。remove方法如果没有实现的话默认是会抛出一个不支持该操作的异常。
1 public interface Iterator<E> { 2 boolean hasNext(); 3 E next(); 4 default void remove() { 5 throw new UnsupportedOperationException(); 6 } 7 }
然后ConcreteIterator
1 private class Itr<E> implements Iterator<E> { 2 private int position = -1; 3 private Object[] data = elements; 4 @Override 5 public boolean hasNext() { 6 return ++ position < data.length; 7 } 8 9 @Override 10 public E next() { 11 return (E) data[position]; 12 } 13 }
然后是aggregate
1 public interface Iterable<E> { 2 Iterator<E> createIterator(); 3 }
concrete aggregate
1 public class MyContainer<E> implements Iterable<E>{ 2 3 Object[] elements; 4 public MyContainer() { 5 elements = new Byte[10]; 6 for (int i =0; i < 10; i ++) 7 elements[i] = (byte) i; 8 } 9 @Override 10 public Iterator<E> createIterator() { 11 return new Itr(); 12 } 13 14 }
客户端
1 iterator it = new MyContainer<Byte>().createIterator(); 2 while (elements.hasNext()) { 3 System.out.println(elements.next()); 4 }
注:为什么不让容器直接继承Iterator接口,还要整个Iterable接口?
1 为了提供容器内数据安全性。假设容器实现了Iterator接口,那么我们所有通过Iterator接口进行的数据访问修改操作都会直接影响容器内的数据,因为我们访问的数据和容器维护的数据是同一份数据。所以不如让Iterator接口访问数据副本来的安全。自己用自己的,互不影响。
9. 访问者模式
为什么有访问者模式:
将稳定的数据结构和易变的数据访问操作分离,满足开闭原则。
优点:
- 将有关元素对象的访问行为集中到一个访问者对象中,而不是分散在一个个的元素类中,符合单一职责原则;
- 可以通过接受新的访问者增加新的访问操作十分方便,对扩展开放,符合开闭原则。
缺点:
- 访问者模式中具体元素对访问者公布细节,破坏了对象的封装性,违反了迪米特原则;
- 每增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,违反了开闭原则;
- 违反了依赖倒置原则,依赖了具体类,没有依赖抽象
应用场景:
最复杂的设计模式,并且使用频率不高。一般场景如下:
1、对象结构中对象对应的元素类很少改变,但经常需要在此对象结构上定义新的操作;
2、需要对一个对象结构中的元素进行很多不同的并且不相关的操作,而需要避免让这些操作"污染"
实现:
抽象访问者(Vistor):抽象访问者为对象结构中每一个具体元素类声明一个访问操作,从这个操作的名称或参数类型可以清楚知道需要访问的具体元素的类型,具体访问者需要实现这些操作方法,定义对这些元素的访问操作;
具体访问者(ConcreteVisitor):具体访问者实现了每个由抽象访问者声明的操作;
抽象元素(Element):抽象元素一般是抽象类或者接口,它定义一个accept()方法,该方法通常以一个抽象访问者作为参数;
具体元素(ConcreteElement):具体元素实现了accept()方法,在accept()方法中调用访问者的访问方法以便完成对一个元素的操作;
对象结构(ObjectStructure):对象结构是一个元素的集合,它用于存放元素对象,并且提供了遍历其内部元素的方法。
它其实是对不同数据结构的访问封装,例如
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 // 抽象元素 2 public interface Element { 3 void accept(Visitor visitor); 4 } 5 // 具体元素A 6 public class ElementA implements Element { 7 @Override 8 public void accept(Visitor visitor) { 9 visitor.visit(this); 10 } 11 } 12 // 具体元素B 13 public class ElementB implements Element { 14 @Override 15 public void accept(Visitor visitor) { 16 visitor.visit(this); 17 } 18 } 19 // 抽象访问者 20 public abstract class Visitor { 21 public abstract void visit(ElementA elementA); 22 public abstract void visit(ElementB elementB); 23 } 24 // 具体访问者A 25 public class VisitorA extends Visitor { 26 @Override 27 public void visit(ElementA elementA) { 28 System.out.println(this + ":elementA"); 29 } 30 31 @Override 32 public void visit(ElementB elementB) { 33 System.out.println(this + ":elementB"); 34 } 35 } 36 // 对象结构 37 public class ObjectStructure { 38 private List<Element> elementList; 39 40 public ObjectStructure() { 41 this.elementList = new ArrayList<>(); 42 } 43 44 public void addElement(Element element) { 45 elementList.add(element); 46 } 47 48 public void removeElement(Element element) { 49 elementList.remove(element); 50 } 51 52 public void accept(Visitor visitor) { 53 elementList.forEach(element -> element.accept(visitor)); 54 } 55 } 56 // 客户端 57 @Test 58 public void VisitorTest() { 59 ObjectStructure objectStructure = new ObjectStructure(); 60 objectStructure.addElement(new ElementA()); 61 objectStructure.addElement(new ElementB()); 62 objectStructure.accept(new VisitorA()); 63 }
输出:
com.test.visitor.visitorA@33a10788:ElementA
com.test.visitor.visitorA@33a10788:ElementB
10. 备忘录模式
为什么有备忘录模式:
保存对象的状态,恢复对象的状态
优点:
- 给用户提供了一种可以恢复状态的机制,可以是用户能够方便的回到某个历史的状态;
- 简化了发起人类。发起人不需要管理和保存其内部状态的各个备份,所有状态信息都保存在备忘录中,并由管理者进行管理,符合单一职责原则
缺点:
- 资源消耗过大,如果需要保存的发起人类的成员变量太多,就不可避免需要占用大量的存储空间,每保存一次对象的状态都需要消耗一定的系统资源
应用场景:
JDK、Spring很少用备忘录的场景,一般常用于需要保存/恢复数据的相关状态场景,比如:
浏览器回退:当我们可以在浏览器左上角点击左箭头回退到上一次的页面,也可以点击右箭头重新回到当前页面
数据库备份与还原:备份当前已有的数据或者记录保留,还原已经保留的数据
编辑器撤销与重做:写错时可以按快捷键 Ctrl + z 撤销,撤销后可以按 Ctrl + y 重做(md 竟然现在才知道......)
虚拟机生成快照与恢复:虚拟机可以生成一个快照,当虚拟机发生错误时可以恢复到快照的样子
Git版本管理:Git是最常见的版本管理软件,每提交一个新版本,实际上Git就会把它们自动串成一条时间线,每个版本都有一个版本号,使用 git reset --hard 版本号 即可回到指定的版本
棋牌游戏悔棋:在棋牌游戏中可以悔棋,回退到上一步状态
实现:
该模式具有以下角色:
发起人角色(Originator):当前时刻的内部状态信息,提供创建备忘录和恢复备忘录数据的功能,实现其他业务功能;
备忘录角色(Memento):负责存储发起人的内部状态,在需要的时候提供这些内部状态给发起人。
管理者角色(Caretaker):对备忘录进行管理,提供保存与获取备忘录的功能,但其不能对备忘录的内容进行访问与修改。
还需要注意下宽接口和窄接口:
窄接口:只允许它把备忘录对象传给其他的对象,针对的是负责人对象和其他除发起人对象之外的任何对象。
宽接口: 允许它读取所有的数据,以便根据这些数据恢复这个发起人对象的内部状态,针对发起人。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 // 备忘录类 2 public class Memento { 3 private String state; 4 5 public Memento(String state){ 6 this.state = state; 7 } 8 9 public String getState(){ 10 return state; 11 } 12 }
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 // 发起人类 2 public class Originator { 3 private String state; 4 5 public void setState(String state){ 6 this.state = state; 7 } 8 9 public String getState(){ 10 return state; 11 } 12 13 public Memento saveStateToMemento(){ 14 return new Memento(state); 15 } 16 17 public void getStateFromMemento(Memento Memento){ 18 state = Memento.getState(); 19 } 20 }
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 // 管理者类 2 import java.util.ArrayList; 3 import java.util.List; 4 5 public class CareTaker { 6 private List<Memento> mementoList = new ArrayList<Memento>(); 7 8 public void add(Memento state){ 9 mementoList.add(state); 10 } 11 12 public Memento get(int index){ 13 return mementoList.get(index); 14 } 15 }
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 // 客户端 2 public class MementoPatternDemo { 3 public static void main(String[] args) { 4 Originator originator = new Originator(); 5 CareTaker careTaker = new CareTaker(); 6 originator.setState("State #1"); 7 originator.setState("State #2"); 8 careTaker.add(originator.saveStateToMemento()); 9 originator.setState("State #3"); 10 careTaker.add(originator.saveStateToMemento()); 11 originator.setState("State #4"); 12 13 System.out.println("Current State: " + originator.getState()); 14 originator.getStateFromMemento(careTaker.get(0)); 15 System.out.println("First saved State: " + originator.getState()); 16 originator.getStateFromMemento(careTaker.get(1)); 17 System.out.println("Second saved State: " + originator.getState()); 18 } 19 }
// 输出
Current State: State #4 First saved State: State #2 Second saved State: State #3
11. 解释器模式
为什么有解释器模式:
对于一些固定文法构建一个解释句子的解释器,其中文法是文法是用于描述语言语法结构的形式规则
优点:
- 扩展性,修改语法规则只要修改相应的非终结符表达式就可以了;若扩展语法,则只要增加非终结符类就可以了
缺点:
- 执行效率较低。解释器模式中通常使用大量的循环和递归调用,当要解释的句子较复杂时,其运行速度很慢,且代码的调试过程也比较麻烦;
- 会引起类膨胀。解释器模式中的每条规则至少需要定义一个类,当包含的文法规则很多时,类的个数将急剧增加,导致系统难以管理与维护;
- 可应用的场景比较少。在软件开发中,需要定义语言文法的应用实例非常少,所以这种模式很少被使用到。
应用场景:
解释器模式在实际的软件开发中使用比较少,因为它会引起效率、性能以及维护等问题。如果碰到对表达式的解释,在JAVA 中可以用 Expression4J 或 Jep 等来设计。该模式主要应用在:
- 当语言的文法较为简单,且执行效率不是关键问题时;
- 当问题重复出现,且可以用一种简单的语言来进行表达时;
- 当一个语言需要解释执行,并且语言中的句子可以表示为一个抽象语法树的时候,如XML文档解释
实现:
该模式具有以下角色:
抽象解释器(AbstractExpression):它包含了解释方法 interpret;
终结符表达式(TerminalExpression):实现与文法中的元素相关联的解释操作,通常一个解释器模式中只有一个终结表达式,但对应不同的终结符,比如四则运算中的终结符就是运算元素;
非终结符表达式(NonterminalExpression):文法中的每条规则对应于一个非终结表达式,比如四则运算中的加法运算、减法运算等
上下文(Context): 上下文环境类,包含解释器之外的全局信息,一般是 HashMap
实现一个简单的计算器
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 // 解释器接口 2 public interface Expression { 3 int interpreter(Context context);//一定会有解释方法 4 } 5 6 // 终结符表达式(在这个例子,用来存放数字,或者代表数字的字符) 7 public class TerminalExpression implements Expression{ 8 9 String variable; 10 public TerminalExpression(String variable){ 11 12 this.variable = variable; 13 } 14 @Override 15 public int interpreter(Context context) { 16 return context.lookup(this); 17 } 18 } 19 20 // 抽象非终结符表达式 21 public abstract class NonTerminalExpression implements Expression{ 22 Expression e1,e2; 23 public NonTerminalExpression(Expression e1, Expression e2){ 24 25 this.e1 = e1; 26 this.e2 = e2; 27 } 28 } 29 30 // 加法运算 31 public class PlusOperation extends NonTerminalExpression { 32 33 public PlusOperation(Expression e1, Expression e2) { 34 super(e1, e2); 35 } 36 37 //将两个表达式相加 38 @Override 39 public int interpreter(Context context) { 40 return this.e1.interpreter(context) + this.e2.interpreter(context); 41 } 42 } 43 44 // 减法表达式实现类 45 public class MinusOperation extends NonTerminalExpression { 46 47 public MinusOperation(Expression e1, Expression e2) { 48 super(e1, e2); 49 } 50 51 //将两个表达式相减 52 @Override 53 public int interpreter(Context context) { 54 return this.e1.interpreter(context) - this.e2.interpreter(context); 55 } 56 } 57 58 59 // 上下文类(这里主要用来将变量解析成数字【当然一开始要先定义】 60 public class Context { 61 private Map<Expression, Integer> map = new HashMap<>(); 62 63 //定义变量 64 public void add(Expression s, Integer value){ 65 map.put(s, value); 66 } 67 //将变量转换成数字 68 public int lookup(Expression s){ 69 return map.get(s); 70 } 71 } 72 73 // 测试类 74 public class Test { 75 public static void main(String[] args) { 76 77 Context context = new Context(); 78 TerminalExpression a = new TerminalExpression("a"); 79 TerminalExpression b = new TerminalExpression("b"); 80 TerminalExpression c = new TerminalExpression("c"); 81 context.add(a, 4); 82 context.add(b, 8); 83 context.add(c, 2); 84 85 System.out.println(new MinusOperation(new PlusOperation(a,b), c).interpreter(context)); 86 } 87 } 88 89 运行结果如下 90 ----------------------------------- 91 10