关注对象之间的通信方式,以及如何合作共同完成任务。这些模式涉及到对象之间的交互、责任分配等。常见的行为型模式有观察者模式、策略模式、命令模式等。
责任链模式
如果有多个对象有机会处理请求,责任链可使请求的发送者和接受者解耦,请求沿着责任链传递,直到有一个对象处理了它为止,就是将链中的每一个结点看做是一个对象,每个结点处理请求均不同,且内部自动维护一个下一个结点对象。当请求从链条的首端出发时,会沿着链的路径依次传递给每一个结点的对象,直到有对象处理这个请求为止。
优点:
- 降低耦合度:发送者不需要知道哪个对象会处理请求,只需将请求发送到链的起始点。
- 灵活性:当工作流程发生变化,可以根据需要动态地改变处理链中处理者的顺序,以及每个处理者的职责。
- 可扩展性:可以根据业务需求添加新的处理者,而不会影响现有代码。
- 可维护性:明确责任范围,每个处理者关注单一的责任,使得代码更易于理解和维护,符合类的单一责任原则。
缺点:
- 不能保证每个请求一定被处理。由于一个请求没有明确的接收者,所以不能保证它一定会被处理,该请求可能一直传到链的末端都得不到处理。
- 对比较长的职责链,请求的处理可能涉及多个处理对象,系统性能将受到一定影响。
- 职责链建立的合理性要靠客户端来保证,增加了客户端的复杂性,可能会由于职责链的错误设置而导致系统出错,如可能会造成循环调用。
结构:
职责链模式主要包含以下角色:
- 抽象处理者(Handler)角色:定义一个处理请求的接口,包含抽象处理方法和一个后继连接。
- 具体处理者(ConcreteHandler)角色:实现抽象处理者的处理方法,判断能否处理本次请求,如果可以处理请求则处理,否则将该请求转给它的后继者。
- 客户类(Client)角色:创建处理链,并向链头的具体处理者对象提交请求,它不关心处理细节和请求的传递过程。
场景:
- 有多个对象可以处理一个请求,哪个对象处理该请求由运行时刻自动确定。
- 可动态指定一组对象处理请求,或添加新的处理者。
- 在不明确指定请求处理者的情况下,向多个处理者中的一个提交请求。
实例:
这里举例一个报销的请求,首先创建一个报销请求,然后根据结构创建抽象处理类,具体处理类以及客户端创建处理链(调用)。
/*责任链模式 首先创建一个请求类 (报销对象)*/
public class ReimbursementRequest {
/*报销金额*/
private double amount;
/*报销内容*/
private String description;
public ReimbursementRequest(double amount, String description) {
this.amount = amount;
this.description = description;
}
public double getAmount() {
return amount;
}
public String getDescription() {
return description;
}
}
/*抽象处理者 具体处理者设置三个ManagerHandler(主管),DepartmentHeadHandler(部门主管)FinanceHandler(财务部门)*/
public abstract class ReimbursementHandler {
protected ReimbursementHandler successor;
public void setSuccessor(ReimbursementHandler successor) {
this.successor = successor;
}
/*处理请求*/
public abstract void handleRequest(ReimbursementRequest request);
}
/*具体处理者 (经理)*/
public class ManagerHandler extends ReimbursementHandler{
@Override
public void handleRequest(ReimbursementRequest request) {
if (request.getAmount() <= 5000) {
System.out.println("经理处理报销请求:" + request.getDescription());
} else if (successor != null) {
successor.handleRequest(request);
}
}
}
/*具体处理者 (部门主管)*/
public class DepartmentHeadHandler extends ReimbursementHandler{
@Override
public void handleRequest(ReimbursementRequest request) {
if (request.getAmount() <= 1000) {
System.out.println("经理处理报销请求:" + request.getDescription());
} else if (successor != null) {
successor.handleRequest(request);
}
}
}
/*具体处理者 (财务部门)*/
public class FinanceHandler extends ReimbursementHandler {
@Override
public void handleRequest(ReimbursementRequest request) {
System.out.println("财务部门处理报销请求:" + request.getDescription());
}
}
客户端调用测试
/*Handler 里面聚合它自己,在 HandlerRequest 里判断是否合适,如果没达到条件则向下传递,向谁传递之前 set 进去。*/
public static void main(String[] args) {
ReimbursementHandler manager = new ManagerHandler();
ReimbursementHandler departmentHead = new DepartmentHeadHandler();
ReimbursementHandler finance = new FinanceHandler();
manager.setSuccessor(departmentHead);
departmentHead.setSuccessor(finance);
ReimbursementRequest request1 = new ReimbursementRequest(800, "购买办公用品");
ReimbursementRequest request2 = new ReimbursementRequest(3000, "部门聚会");
ReimbursementRequest request3 = new ReimbursementRequest(10000, "举办团建活动");
manager.handleRequest(request1);
manager.handleRequest(request2);
manager.handleRequest(request3);
}
在上面实例中,可以看到报销请求会依次被经理、部门主管和财务部门处理。根据报销金额的不同,请求会被传递到适当的处理者。
命令模式
将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。这样两者之间通过命令对象进行沟通,这样方便将命令对象进行储存、传递、调用、增加与管理。
优点:
- 降低系统的耦合度。命令模式能将调用操作的对象与实现该操作的对象解耦。
- 增加或删除命令非常方便。采用命令模式增加与删除命令不会影响其他类,它满足“开闭原则”,对扩展比较灵活。
- 可以实现宏命令。命令模式可以与组合模式结合,将多个命令装配成一个组合命令,即宏命令。 方便实现 Undo 和 Redo操作。命令模式可以与后面介绍的备忘录模式结合,实现命令的撤销与恢复。
缺点:
可能产生大量具体命令类。因为设计每一个具体操作都需要设计一个具体命令类,这将增加系统的复杂性。
结构:
- 命令(Command):抽象命令类,定义了执行命令的接口。它通常包含一个执行方法,以及可能的其他方法(例如,撤消)。
- 具体命令(ConcreteCommand):实现了抽象命令类的具体子类,将一个接收者与一个动作绑定。它实现了执行方法,该方法调用接收者的特定操作。
- 接收者(Receiver):执行实际工作的类。命令模式将命令传递给接收者,由接收者执行实际的操作。
- 调用者/请求者(Invoker):负责将命令传递给合适的接收者并触发命令的执行。它并不关心具体的命令细节。
场景:
- 当系统需要将请求调用者与请求接收者解耦时,命令模式使得调用者和接收者不直接交互。
- 当系统需要随机请求命令或经常增加或删除命令时,命令模式比较方便实现这些功能。
- 当系统需要执行一组操作时,命令模式可以定义宏命令来实现该功能。
- 当系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作时,可以将命令对象存储起来,采用备忘录模式来实现。
实例:
这里举例通过命令实现设备的开启以及关闭。
/*命令抽象类 */
public interface Command {
public void execute();
}
/*具体命令类 调用接收类执行关闭*/
public class DeviceOffConcreteCommand implements Command {
private DeviceReceiver deviceReceiver;
public DeviceOffConcreteCommand(DeviceReceiver deviceReceiver){
this.deviceReceiver = deviceReceiver;
}
@Override
public void execute() {
deviceReceiver.turnOff();
}
}
/*具体命令类 调用接收类执行开启*/
public class DeviceOnConcreteCommand implements Command {
private DeviceReceiver deviceReceiver;
public DeviceOnConcreteCommand(DeviceReceiver deviceReceiver){
this.deviceReceiver = deviceReceiver;
}
@Override
public void execute() {
deviceReceiver.turnOn();
}
}
/*命令接收类 执行实际的操作*/
public class DeviceReceiver {
/*开启*/
public void turnOn(){
System.out.println("设备开启");
}
/*关闭*/
public void turnOff(){
System.out.println("设备关闭");
}
}
/*调用/请求类 */
public class InvokerControl {
private Command command;
public void setCommand(Command command){
this.command = command;
}
/*传递命令*/
public void pressButton() {
command.execute();
}
}
调用测试
public static void main(String[] args) {
DeviceReceiver deviceReceiver = new DeviceReceiver();
DeviceOnConcreteCommand deviceOnConcreteCommand = new DeviceOnConcreteCommand(deviceReceiver);
DeviceOffConcreteCommand deviceOffConcreteCommand = new DeviceOffConcreteCommand(deviceReceiver);
InvokerControl invokerControl = new InvokerControl();
invokerControl.setCommand(deviceOnConcreteCommand);
invokerControl.pressButton();
invokerControl.setCommand(deviceOffConcreteCommand);
invokerControl.pressButton();
}
在上面实例中,我们使用命令模式创建了两种具体的命令:开启设备和关闭设备。
调用者可以设置不同的命令,这里仅设置啦开启和关闭,然后按下按钮触发相应的操作。
这样,命令发送者(调用类)和命令接收者(设备)之间实现了解耦。
迭代器模式
迭代器模式提供了一种统一的方法来遍历不同类型的集合,而无需暴露集合内部的表示细节。它包括两个主要组件:迭代器和集合。迭代器负责遍历集合并提供统一的访问接口,而集合负责实际存储元素。迭代器和集合之间的解耦使得可以独立地改变它们的实现,而不会影响到客户端代码。
优点:
- 访问一个聚合对象的内容而无须暴露它的内部表示。
- 遍历任务交由迭代器完成,这简化了聚合类。
- 它支持以不同方式遍历一个聚合,甚至可以自定义迭代器的子类以支持新的遍历。
- 增加新的聚合类和迭代器类都很方便,无须修改原有代码。
- 封装性良好,为遍历不同的聚合结构提供一个统一的接口。
缺点:
在扩展性增强的同时,一旦添加新的遍历方法,则需要增加新的聚合类和迭代器类,使类的个数成倍增加,会对系统本身的程序在一定程度上增加了复杂度。
结构:
迭代器模式是通过将聚合对象的遍历行为分离出来,抽象成迭代器类来实现的,其目的是在不暴露聚合对象的内部结构的情况下,让外部代码透明地访问聚合的内部数据。
迭代器模式主要包含以下角色:
- 抽象聚合(Aggregate)角色:定义存储、添加、删除聚合对象以及创建迭代器对象的接口。
- 具体聚合(ConcreteAggregate)角色:实现抽象聚合类,返回一个具体迭代器的实例。
- 抽象迭代器(Iterator)角色:定义访问和遍历聚合元素的接口,通常包含 hasNext()、first()、next() 等方法。
- 具体迭代器(Concretelterator)角色:实现抽象迭代器接口中所定义的方法,完成对聚合对象的遍历,记录遍历的当前位置。
场景:
- 当需要为聚合对象提供多种遍历方式时。
- 当需要为遍历不同的聚合结构提供一个统一的接口时。
- 当访问一个聚合对象的内容而无须暴露其内部细节的表示时。
实例:
这里举例一个通用代码
/*抽象聚合 定义一个可迭代的集合接口*/
public interface IterableCollection<T> {
public Iterator<T> createIterator();
}
/*具体聚合*/
public class ConcreteCollection<T> implements IterableCollection<T> {
private List<T> items = new ArrayList<>();
public void addItem(T item) {
items.add(item);
}
@Override
public Iterator<T> createIterator() {
return new ConcreteIterator<>(items);
}
}
/*抽象迭代器 */
public interface Iterator<T> {
public boolean hasNext();
public T next();
}
/*具体迭代器*/
public class ConcreteIterator<T> implements Iterator<T>{
private List<T> items;
private int position = 0;
public ConcreteIterator(List<T> items){
this.items = items;
}
@Override
public boolean hasNext() {
return position < items.size();
}
@Override
public T next() {
if (hasNext()){
T item = items.get(position);
position++;
return item;
}
throw new IndexOutOfBoundsException("No more elements");
}
}
调用测试
public static void main(String[] args) {
ConcreteCollection<String> collection = new ConcreteCollection<>();
collection.addItem("Item 1");
collection.addItem("Item 2");
collection.addItem("Item 3");
Iterator<String> iterator = collection.createIterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
在上面的实例中,我们定义了一个IterableCollection接口来表示可迭代的集合,一个具体的集合类ConcreteCollection实现了这个接口,并提供了一个用于创建迭代器的方法。
迭代器接口Iterator定义了hasNext(判断是否有下一个元素)和next(获取下一个元素)方法,具体的迭代器类ConcreteIterator实现了这个接口,并通过内部的位置追踪来遍历集合。根据自己的需求定义访问和遍历聚合的方法。
中介者模式
定义一个中介对象来封装一系列对象之间的交互,使原有对象之间的耦合松散,且可以独立地改变它们之间的交互。中介者模式又叫调停模式,它是迪米特法则的典型应用。
优点:
- 降低耦合度:对象之间的通信逻辑被集中在中介者中,从而降低了对象之间的直接依赖,减少了耦合度,使系统更加灵活和可维护。
- 集中管理:所有对象的交互逻辑都集中在中介者中,使得系统的交互逻辑更加清晰可见,便于管理和修改。
- 复用性:中介者模式将交互逻辑与对象本身的业务逻辑分离,可以更容易地复用这些交互逻辑。
- 可扩展性:通过增加或修改中介者对象,可以相对容易地扩展系统,而不需要修改对象之间的通信逻辑。
缺点:
中介者模式在一定程度上引入了中介者对象作为中心化的调度者,可能会增加系统的复杂性。
结构:
- 抽象中介者(Mediator)角色:它是中介者的接口,提供了同事对象注册与转发同事对象信息的抽象方法。
- 具体中介者(ConcreteMediator)角色:实现中介者接口,定义一个 List
- 来管理同事对象,协调各个同事角色之间的交互关系,因此它依赖于同事角色。
- 抽象同事类(Colleague)角色:定义同事类的接口,保存中介者对象,提供同事对象交互的抽象方法,实现所有相互影响的同事类的公共功能。
- 具体同事类(Concrete Colleague)角色:是抽象同事类的实现者,当需要与其他同事对象交互时,由中介者对象负责后续的交互。
场景:
- 当对象之间存在复杂的网状结构关系而导致依赖关系混乱且难以复用时。
- 当想创建一个运行于多个类之间的对象,又不想生成新的子类时。
实例:
/*抽象中介者*/
public interface Mediator {
/*注册同事类*/
public void register(Colleague colleague);
/*处理接收逻辑*/
public void operation(Colleague colleague);
}
/*具体中介者*/
public class ConcreteMediator implements Mediator {
private List<Colleague> colleagues = new ArrayList<Colleague>();
@Override
public void register(Colleague colleague) {
if (!colleagues.contains(colleague)) {
colleagues.add(colleague);
colleague.setMediator(this);
}
}
@Override
public void operation(Colleague colleague) {
for (Colleague coll : colleagues) {
if (!coll.equals(colleague)) {
coll.receive();
}
}
}
/*抽象同事类*/
public abstract class Colleague {
protected Mediator mediator;
public void setMediator(Mediator mediator){
this.mediator = mediator;
};
/*接收请求*/
public abstract void receive();
/*发送请求*/
public abstract void send();
}
/*具体同事类 1*/
public class ConcreteColleague1 extends Colleague{
@Override
public void receive() {
System.out.println("具体同事类 ConcreteColleague1 接收请求");
}
@Override
public void send() {
System.out.println("具体同事类 ConcreteColleague1 发送请求");
/*中介者进行转发*/
mediator.operation(this);
}
}
/*具体同事类 2*/
public class ConcreteColleague2 extends Colleague{
@Override
public void receive() {
System.out.println("具体同事类 ConcreteColleague2 接收到请求");
}
@Override
public void send() {
System.out.println("具体同事类 ConcreteColleague2 发送请求");
mediator.operation(this);
}
}
调用测试
public static void main(String[] args) {
Mediator concreteMediator = new ConcreteMediator();
Colleague concreteColleague1 = new ConcreteColleague1();
Colleague concreteColleague2 = new ConcreteColleague2();
concreteMediator.register(concreteColleague1);
concreteMediator.register(concreteColleague2);
concreteColleague1.send();
concreteColleague2.send();
}
备忘录模式
又叫快照模式,在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后当需要时能将该对象恢复到原先保存的状态。
二、结构
优点:
- 提供了一种可以恢复状态的机制。当用户需要时能够比较方便地将数据恢复到某个历史的状态。
- 实现了内部状态的封装。除了创建它的发起人之外,其他对象都不能够访问这些状态信息。
- 简化了发起人类。发起人不需要管理和保存其内部状态的各个备份,所有状态信息都保存在备忘录中,并由管理者进行管理,这符合单一职责原则。
缺点:
源消耗大。如果要保存的内部状态信息过多或者特别频繁,将会占用比较大的内存资源。
结构:
-
发起人(Originator)角色:记录当前时刻的内部状态信息,提供创建备忘录和恢复备忘录数据的功能,实现其他业务功能,它可以访问备忘录里的所有信息。
-
备忘录(Memento)角色:负责存储发起人的内部状态,在需要的时候提供这些内部状态给发起人。
-
管理者(Caretaker)角色:对备忘录进行管理,提供保存与获取备忘录的功能,但其不能对备忘录的内容进行访问与修改。
备忘录有两个等效的接口:
窄接口:管理者(Caretaker)对象(和其他发起人对象之外的任何对象)看到的是备忘录的窄接口(narror
Interface),这个窄接口只允许他把备忘录对象传给其他的对象。 •
宽接口:与管理者看到的窄接口相反,发起人对象可以看到一个宽接口(wide
Interface),这个宽接口允许它读取所有的数据,以便根据这些数据恢复这个发起人对象的内部状态。
场景:
- 需要保存与恢复数据的场景,如玩游戏时的中间结果的存档功能。
- 需要提供一个可回滚操作的场景,如Word、记事本、Photoshop,idea等软件在编辑时按 Ctrl+Z 组合键,还有数据库中事务操作。
实例:
/*备忘录*/
public class Memento {
private String state;
public Memento(String state) {
this.state = state;
}
public String getState() {
return state;
}
}
/*发起人*/
public class Originator {
private String state;
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
/*创建备忘录*/
public Memento createMemento(){
return new Memento(state);
}
public void restoreMemento(Memento memento){
state = memento.getState();
}
}
/*管理者*/
public class Caretaker {
private Memento memento;
public Memento getMemento(){
return memento;
}
public void setMemento(Memento memento) {
this.memento = memento;
}
}
调用测试
public static void main(String[] args) {
Originator originator = new Originator();
Caretaker caretaker = new Caretaker();
/*设置初始化状态*/
originator.setState("State 1");
System.out.println("Current State: " + originator.getState());
/*创建备忘录并保存*/
Memento memento = originator.createMemento();
caretaker.setMemento(memento);
// 修改状态
originator.setState("State 2");
System.out.println("Updated State: " + originator.getState());
// 恢复之前的状态
originator.restoreMemento(caretaker.getMemento());
System.out.println("Restored State: " + originator.getState());
}
在这个示例中,Originator 类表示原始对象,它具有状态并能够创建和恢复备忘录。
Memento 类表示备忘录对象,保存了特定时刻的状态。Caretaker 类负责保存和获取备忘录对象。
通过设置初始状态、创建备忘录、修改状态、然后恢复状态。
也就是将原始类当前的属性值保存到备忘录类里面,并将备忘录类放到备忘录管理类中,当改变当前原始属性值后想要恢复之前的属性值则通过备忘录管理类中获取备忘录将值重新赋值给原始类的属性,我们可以看到备忘录模式的工作方式。
观察者模式
指多个对象间存在一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。这种模式有时又称作发布-订阅模式、模型-视图模式,它是对象行为型模式。
优点:
- 松耦合:主题和观察者之间的耦合度降低,使得它们可以独立地进行变化。
- 可扩展性:可以方便地增加新的观察者,而不会影响到已有的观察者和主题。
- 自动通知:主题状态改变时会自动通知观察者,减少手动维护通知的工作。
- 可重用性:主题和观察者可以在不同的场景中重复使用。
缺点:
- 可能引起性能问题:如果观察者过多或通知机制不合理,可能会导致性能下降。
- 更新顺序问题:观察者的更新顺序可能会影响到系统的行为,需要特别注意。
- 过度使用的风险:并不是所有的状态变化都适合使用观察者模式,过度使用可能导致代码复杂化。
结构:
- 抽象主题(Subject)角色:也叫抽象目标类,它提供了一个用于保存观察者对象的聚集类和增加、删除观察者对象的方法,以及通知所有观察者的抽象方法。
- 具体主题(Concrete Subject)角色:也叫具体目标类,它实现抽象目标中的通知方法,当具体主题的内部状态发生改变时,通知所有注册过的观察者对象。
- 抽象观察者(Observer)角色:它是一个抽象类或接口,它包含了一个更新自己的抽象方法,当接到具体主题的更改通知时被调用。
- 具体观察者(Concrete Observer)角色:实现抽象观察者中定义的抽象方法,以便在得到目标的更改通知时更新自身的状态。
场景:
- 对象间存在一对多关系,一个对象的状态发生改变会影响其他对象。
- 当一个抽象模型有两个方面,其中一个方面依赖于另一方面时,可将这二者封装在独立的对象中以使它们可以各自独立地改变和复用。
实例:
/*抽象观察着接口*/
public interface Observer {
public void update(int state);
}
/*具体观察者 1*/
public class ConcreteObserver1 implements Observer{
private String name;
public ConcreteObserver1(String name){
this.name = name;
}
@Override
public void update(int state) {
System.out.println(name + " 收到更新,新状态为: " + state);
}
}
/*具体观察者 2*/
public class ConcreteObserver2 implements Observer{
private String name;
public ConcreteObserver2(String name){
this.name = name;
}
@Override
public void update(int state) {
System.out.println(name + " 收到更新,新状态为: " + state);
}
}
/*抽象主题接口*/
public interface Subject {
/*添加观察者*/
public void addObserver(Observer observer);
/*删除观察者*/
public void removeObserer(Observer observer);
/*通知观察者*/
public void notifyObservers();
}
/*具体主题类*/
public class ConcreteSubject implements Subject {
private List<Observer> observerList = new ArrayList<Observer>();
private int state;
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
notifyObservers();
}
@Override
public void addObserver(Observer observer) {
observerList.add(observer);
}
@Override
public void removeObserer(Observer observer) {
observerList.remove(observer);
}
@Override
public void notifyObservers() {
for (Observer observer : observerList) {
observer.update(state);
}
}
}
调用测试
public static void main(String[] args) {
ConcreteSubject subject = new ConcreteSubject();
Observer observer1 = new ConcreteObserver1("观察者 1");
Observer observer2 = new ConcreteObserver2("观察者 2");
subject.addObserver(observer1);
subject.addObserver(observer2);
subject.setState(10);
subject.removeObserer(observer2);
subject.setState(20);
}
状态模式
对有状态的对象,把复杂的“判断逻辑”提取到不同的状态对象中,允许状态对象在其内部状态发生改变时改变其行为。
当一个对象的内在状态改变时,允许改变其行为,这个对象看起来像是变成了另外一个类的对象
在状态模式中,使用类来表示状态,可以通过切换类来改变对象的状态,当需要增加新的类时,也只需要增加新的类即可
优点:
- 清晰的状态切换: 状态模式将每个状态的行为集中在各自的状态类中,使得状态切换的逻辑变得清晰,易于管理和修改。
- 可维护性:将状态相关的代码分布在不同的状态类中,使得代码更加模块化和可维护。
- 扩展性:添加新的状态只需要创建新的状态类并实现相关操作,不会影响到其他状态类或上下文类的代码。 避免条件语句:状态模式避免了大量的条件语句,从而提高了代码的可读性和可维护性。
- 复用性: 状态类之间的逻辑可以被复用,因为它们是独立的实体。
缺点:
- 状态模式的使用必然会增加系统的类与对象的个数。
- 状态模式的结构与实现都较为复杂,如果使用不当会导致程序结构和代码的混乱。
结构:
状态模式包含以下主要角色:
- 环境(Context)角色:也称为上下文,它定义了客户感兴趣的接口,维护一个当前状态,并将与状态相关的操作委托给当前状态对象来处理。
- 抽象状态(State)角色:定义一个接口,用以封装环境对象中的特定状态所对应的行为。
- 具体状态(Concrete State)角色:实现抽象状态所对应的行为
场景:
- 当一个对象的行为取决于它的状态,并且它必须在运行时根据状态改变它的行为时,就可以考虑使用状态模式。
- 一个操作中含有庞大的分支结构,并且这些分支决定于对象的状态时。
实例:
这里举例一个电梯的开门状态以及关门状态
/*状态接口*/
public interface ElevatorState {
/*开门*/
void openDoors();
/*关门*/
void closeDoors();
}
/*具体状态类 开门状态时*/
public class OpenState implements ElevatorState{
@Override
public void openDoors() {
System.out.println("当前电梯门已是打开状态");
}
@Override
public void closeDoors() {
System.out.println("关闭电梯门");
}
}
/*具体状态类 关门状态时*/
public class CloseState implements ElevatorState{
@Override
public void openDoors() {
System.out.println("打开电梯门");
}
@Override
public void closeDoors() {
System.out.println("当前门已是关闭状态");
}
}
/*环境角色(上下文) 电梯*/
public class Elevator {
private ElevatorState elevatorState;
public Elevator(){
elevatorState = new CloseState();//初始化状态为关门状态
}
public ElevatorState getElevatorState() {
return elevatorState;
}
public void setElevatorState(ElevatorState elevatorState) {
this.elevatorState = elevatorState;
}
public void openDoors() {
elevatorState.openDoors();
}
public void closeDoors() {
elevatorState.closeDoors();
}
}
调用测试
public static void main(String[] args) {
Elevator elevator = new Elevator();
elevator.closeDoors();
elevator.openDoors();
elevator.setElevatorState(new OpenState());
elevator.openDoors();
elevator.closeDoors();
}
在上面实例中,我们创建了一个模拟电梯系统,其中有开门状态和关门状态两个具体状态类,以及电梯类作为上下文类。
通过切换状态,电梯在不同状态下有不同的行为表现。这就是状态模式的基本思想。
策略模式
该模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理。
优点:
- 多重条件语句不易维护,而使用策略模式可以避免使用多重条件语句。
- 策略模式提供了一系列的可供重用的算法族,恰当使用继承可以把算法族的公共代码转移到父类里面,从而避免重复的代码。
- 策略模式可以提供相同行为的不同实现,客户可以根据不同时间或空间要求选择不同的。
- 策略模式提供了对开闭原则的完美支持,可以在不修改原代码的情况下,灵活增加新算法。
- 策略模式把算法的使用放到环境类中,而算法的实现移到具体策略类中,实现了二者的分离。
缺点:
- 客户端必须理解所有策略算法的区别,以便适时选择恰当的算法类。
- 策略模式造成很多的策略类。
结构:
策略模式的主要角色:
- 抽象策略(Strategy)类:定义了一个公共接口,各种不同的算法以不同的方式实现这个接口,环境角色使用这个接口调用不同的算法,一般使用接口或抽象类实现。
- 具体策略(Concrete Strategy)类:实现了抽象策略定义的接口,提供具体的算法实现。
- 环境(Context)类:持有一个策略类的引用,最终给客户端调用
场景:
- 需要在不同算法之间灵活切换的情况。
- 有多个类似的条件语句,可以使用策略模式提取出这些条件逻辑。
- 需要封装一些具体的算法,使其独立于客户端而变化。
实例:
这里举例一个数据解析的策略:
/*抽象策略接口 我们定义一个接口 DataParsing,表示数据解析的策略*/
public interface DataParsing {
String dataParsing(List<String> datas);
}
/*具体策略,实现第一种数据解析方案*/
public class Parsing1 implements DataParsing{
@Override
public String dataParsing(List<String> datas) {
String data = datas.get(0);
return data;
}
}
/*具体策略,实现第二种数据解析方案*/
public class Parsing2 implements DataParsing{
@Override
public String dataParsing(List<String> datas) {
String data = datas.get(1);
return data;
}
}
/*环境类 系统环境类 它接受一个数据解析策略,并根据用户的选择执行相应的操作*/
public class SystemContext {
private DataParsing dataParsing;
public void setDataParsing(DataParsing dataParsing){
this.dataParsing = dataParsing;
}
/*执行操作*/
public String performOperation(List<String> datas) {
if (datas.size()>=2) {
return dataParsing.dataParsing(datas);
}
throw new IllegalStateException("数据不满足执行解析的条件");
}
}
public static void main(String[] args) {
DataParsing dataParsing1 = new Parsing1();
DataParsing dataParsing2 = new Parsing2();
SystemContext systemContext = new SystemContext();
List<String> list = new ArrayList<String>();
list.add("解析方式1");
list.add("解析方式2");
systemContext.setDataParsing(dataParsing1);
System.out.println(systemContext.performOperation(list));;
systemContext.setDataParsing(dataParsing2);
System.out.println(systemContext.performOperation(list));;
list.clear();
systemContext.performOperation(list);
}
在上面实例中,我们通过创建不同的数据解析策略类来实现不同解析逻辑,获取指定数据功能,这里省略具体的业务逻辑,并通过设置不同的策略来执行不同的操作,具体实现根据业务实际需求来实现不同的解析数据的逻辑。这就是策略模式的基本思想,
模板模式
定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。
优点:
- 代码复用: 核心算法结构在父类中定义,可以被多个子类共享,避免了重复的代码。
- 灵活性:子类可以通过实现特定的步骤来定制算法的行为,而不需要改变算法的整体结构。
- 可维护性: 将算法的核心结构集中在一个地方,易于维护和修改。
- 代码一致性: 所有子类共享相同的算法模板,确保了算法的一致性。
缺点:
- 对每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象。
- 父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构,它提高了代码阅读的难度
结构:
模板方法模式包含以下主要角色:
-
抽象类(Abstract Class):负责给出一个算法的轮廓和骨架。它由一个模板方法和若干个基本方法构成。这些方法的定义如下。
① 模板方法:定义了算法的骨架,按某种顺序调用其包含的基本方法。
② 基本方法:是整个算法中的一个步骤,包含以下几种类型。
-
抽象方法:在抽象类中申明,由具体子类实现。 具体方法:在抽象类中已经实现,在具体子类中可以继承或重写它。
钩子方法:在抽象类中已经实现,包括用于判断的逻辑方法和需要子类重写的空方法两种。 -
具体子类(Concrete Class):实现抽象类中所定义的抽象方法和钩子方法,它们是一个*逻辑的一个组成步骤。
场景:
- 算法的整体步骤很固定,但其中个别部分易变时,这时候可以使用模板方法模式,将容易变的部分抽象出来,供子类实现。
- 当多个子类存在公共的行为时,可以将其提取出来并集中到一个公共父类中以避免代码重复。首先,要识别现有代码中的不同之处,并且将不同之处分离为新的操作。最后,用一个调用这些新的操作的模板方法来替换这些不同的代码。
- 当需要控制子类的扩展时,模板方法只在特定点调用钩子操作,这样就只允许在这些点进行扩展。
实例:
/*抽象类*/
public abstract class AbstractClass {
/*模板方法 ,定义算法的骨架*/
public void templateMethod(){
step1();
step2();
step3();
}
// 基本方法,子类需要实现
abstract void step1();
abstract void step2();
abstract void step3();
}
/*具体子类实现抽象方法*/
public class ConcreteClass extends AbstractClass{
@Override
void step1() {
System.out.println("step1");
}
@Override
void step2() {
System.out.println("step2");
}
@Override
void step3() {
System.out.println("step3");
}
}
调用测试
public static void main(String[] args) {
AbstractClass template = new ConcreteClass();
template.templateMethod();
}
在上面的示例中,AbstractClass 是模板类,定义了一个包含三个步骤的模板方法 templateMethod这些步骤由抽象方法 step1、step2 和 step3 构成。ConcreteClass 是具体子类,继承自AbstractClass,它实现了基本方法来完成每个步骤的具体行为。
在 main 方法中,我们创建了一个 ConcreteClass 实例并调用了 templateMethod,这会按照模板的结构执行具体的步骤。
访问者模式
将作用于某种数据结构中的各元素的操作分离出来封装成独立的类,使其在不改变数据结构的前提下可以添加作用于这些元素的新的操作,为数据结构中的每个元素提供多种访问方式。它将对数据的操作与数据结构进行分离,是行为类模式中最复杂的一种模式。
优点:
- 分离关注点:访问者模式将元素类与具体操作分离,使得每个类可以专注于自身的职责,而操作则由访问者来实现。
- 易于扩展:添加新的操作只需要增加一个新的访问者,不需要修改已存在的元素类,因此对系统的扩展更加容易。
- 可维护性:由于每个操作被封装在独立的访问者中,使得代码更加清晰、易于维护。
- 灵活性:可以在不修改元素类的情况下,动态地添加新的操作。
缺点:
- 增加新的元素类很困难。在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”。
- 破坏封装。访问者模式中具体元素对访问者公布细节,这破坏了对象的封装性。 违反了依赖倒置原则。访问者模式依赖了具体类,而没有依赖抽象类。
结构:
访问者模式包含以下主要角色:
- 抽象访问者(Visitor)角色:定义一个访问具体元素的接口,为每个具体元素类对应一个访问操作 visit(),该操作中的参数类型标识了被访问的具体元素。
- 具体访问者(ConcreteVisitor)角色:实现抽象访问者角色中声明的各个访问操作,确定访问者访问一个元素时该做什么。
- 抽象元素(Element)角色:声明一个包含接受操作 accept() 的接口,被接受的访问者对象作为 accept() 方法的参数。
- 具体元素(ConcreteElement)角色:实现抽象元素角色提供的 accept() 操作,其方法体通常都是visitor.visit(this) ,另外具体元素中可能还包含本身业务逻辑的相关操作。
- 对象结构(ObjectStructure)角色:是一个包含元素角色的容器,提供让访问者对象遍历容器中的所有元素的方法,通常由 List、Set、Map等聚合类实现。
场景:
- 对象结构相对稳定,但其操作算法经常变化的程序。
- 对象结构中的对象需要提供多种不同且不相关的操作,而且要避免让这些操作的变化影响对象的结构。
- 对象结构包含很多类型的对象,希望对这些对象实施一些依赖于其具体类型的操作。
实例:
/*抽象元素类 这里举例使用设备作为元素*/
public interface DeviceElement {
void accept(DeviceVisitor deviceVisitor);
}
/*具体元素类 电脑*/
public class ConcreteElementComputer implements DeviceElement {
public void openComputer(){
System.out.println("打开电脑");
}
@Override
public void accept(DeviceVisitor deviceVisitor) {
deviceVisitor.visit(this);
}
}
/*具体元素类 音箱*/
public class ConcreteElementSpeaker implements DeviceElement {
public void openSpeaker(){
System.out.println("打开音箱");
}
@Override
public void accept(DeviceVisitor deviceVisitor) {
deviceVisitor.visit(this);
}
}
/*抽象访问者 启动设备操作的访问者*/
public interface DeviceVisitor {
void visit(ConcreteElementSpeaker concreteElementSpeaker);
void visit(ConcreteElementComputer concreteElementComputer);
}
/*具体访问者 主人*/
public class ConcreteDeviceVisitorOwner implements DeviceVisitor{
@Override
public void visit(ConcreteElementSpeaker concreteElementSpeaker) {
concreteElementSpeaker.openSpeaker();
System.out.println("主人打开音响");
}
@Override
public void visit(ConcreteElementComputer concreteElementComputer) {
concreteElementComputer.openComputer();
System.out.println("主人打开电脑");
}
}
/*具体访问者 其他人*/
public class ConcreteDeviceVisitorSomeOne implements DeviceVisitor{
@Override
public void visit(ConcreteElementSpeaker concreteElementSpeaker) {
concreteElementSpeaker.openSpeaker();
System.out.println("其他人打开音响");
}
@Override
public void visit(ConcreteElementComputer concreteElementComputer) {
concreteElementComputer.openComputer();
System.out.println("其他人打开电脑");
}
}
/*对象结构类 作为一个元素的容器并进行管理,提供让访问者对象遍历容器中的所有元素的方法*/
public class ObjectStructure {
//声明一个集合对象,用来存储元素对象
private List<DeviceElement> deviceElements = new ArrayList<>();
//添加元素
public void add(DeviceElement element) {
deviceElements.add(element);
}
public void action(DeviceVisitor deviceVisitor) {
//遍历集合,获取每一个元素,让访问者访问每一个元素
for (DeviceElement animal : deviceElements) {
animal.accept(deviceVisitor);
}
}
}
调用测试
public static void main(String[] args) {
//创建 ObjectStructure 对象
ObjectStructure objectStructure = new ObjectStructure();
//添加元素到 ObjectStructure 对象中
objectStructure.add(new ConcreteElementComputer());
objectStructure.add(new ConcreteElementSpeaker());
//创建主人对象
ConcreteDeviceVisitorOwner owner = new ConcreteDeviceVisitorOwner();
//让主人打开所有的设备
objectStructure.action(owner);
System.out.println("===============");
//创建其他人对象
ConcreteDeviceVisitorSomeOne someOne = new ConcreteDeviceVisitorSomeOne();
//让其他人打开所有的设备
objectStructure.action(someOne);
}
在上面的实例种,访问者模式允许我们在不修改设备类的情况下,通过实现不同的访问者来执行不同的操作,例如打开设备。
这样,我们可以轻松地添加新的访问者来执行其他操作,同时保持设备类的不变。
解释器模式
给分析对象定义一个语言,并定义该语言的文法表示,再设计一个解析器来解释语言中的句子。也就是说,用编译语言的方式来分析应用中的实例。这种模式实现了文法表达式处理的接口,该接口解释一个特定的上下文。
这里提到的文法和句子的概念同编译原理中的描述相同,“文法”指语言的语法规则,而“句子”是语言集中的元素。例如,汉语中的句子有很多,“我是中国人”是其中的一个句子,可以用一棵语法树来直观地描述语言中的句子。
优点:
- 扩展性好。由于在解释器模式中使用类来表示语言的文法规则,因此可以通过继承等机制来改变或扩展文法。
- 容易实现。在语法树中的每个表达式节点类都是相似的,所以实现其文法较为容易。
缺点:
- 执行效率较低。解释器模式中通常使用大量的循环和递归调用,当要解释的句子较复杂时,其运行速度很慢,且代码的调试过程也比较麻烦。
- 会引起类膨胀。解释器模式中的每条规则至少需要定义一个类,当包含的文法规则很多时,类的个数将急剧增加,导致系统难以管理与维护。
- 可应用的场景比较少。在软件开发中,需要定义语言文法的应用实例非常少,所以这种模式很少被使用到。
结构:
解释器模式包含以下主要角色。
- 抽象表达式(Abstract Expression)角色:定义解释器的接口,约定解释器的解释操作,主要包含解释方法interpret()。
- 终结符表达式(Terminal Expression)角色:是抽象表达式的子类,用来实现文法中与终结符相关的操作,文法中的每一个终结符都有一个具体终结表达式与之相对应。
- 非终结符表达式(NonterminalExpression)角色:也是抽象表达式的子类,用来实现文法中与非终结符相关的操作,文法中的每条规则都对应于一个非终结符表达式。
- 环境(Context)角色:通常包含各个解释器需要的数据或是公共的功能,一般用来传递被所有解释器共享的数据,后面的解释器可以从这里获取这些值。
- 客户端(Client):主要任务是将需要分析的句子或表达式转换成使用解释器对象描述的抽象语法树,然后调用解释器的解释方法,当然也可以通过环境角色间接访问解释器的解释方法。
场景:
- 当语言的文法较为简单,且执行效率不是关键问题时。
- 当问题重复出现,且可以用一种简单的语言来进行表达时。
- 当一个语言需要解释执行,并且语言中的句子可以表示为一个抽象语法树的时候,如 XML 文档解释。
实例:
这里举例数字的加减用表达式表示。
/*抽象表达式类*/
public interface AbstractExpression {
public int interpret(Context context);
}
/*终结符表达式类 //用于封装变量的类*/
public class TerminalExpression implements AbstractExpression{
//声明存储变量名的成员变量
private String name;
public TerminalExpression(String name) {
this.name = name;
}
@Override
public int interpret(Context context) {
//直接返回变量的值
return context.getValue(this);
}
@Override
public String toString() {
return name;
}
}
/*非终结表达式 加法表达式*/
public class Plus implements AbstractExpression{
// + 左边的表达式
private AbstractExpression left;
// + 右边的表达式
private AbstractExpression right;
public Plus(AbstractExpression left, AbstractExpression right) {
this.left = left;
this.right = right;
}
@Override
public int interpret(Context context) {
//将左边表达式的结果和右边的进行相加
return left.interpret(context) + right.interpret(context);
}
@Override
public String toString() {
return "(" + left.toString() + "+" + right.toString() + ")";
}
}
/*非终结表达式 减法表达式*/
public class Minus implements AbstractExpression {
// - 左边的表达式
private AbstractExpression left;
// - 右边的表达式
private AbstractExpression right;
public Minus(AbstractExpression left, AbstractExpression right) {
this.left = left;
this.right = right;
}
@Override
public int interpret(Context context) {
//将左边表达式的结果和右边的进行相减
return left.interpret(context) - right.interpret(context);
}
@Override
public String toString() {
return "(" + left.toString() + "-" + right.toString() + ")";
}
}
/*环境类*/
public class Context {
//定义一个 map 集合,用来存储变量以及对应的值
private Map<TerminalExpression,Integer> map = new HashMap<>();
//添加变量的功能
public void assign(TerminalExpression var, Integer value){
map.put(var, value);
}
//根据变量获取对应的值
public int getValue(TerminalExpression var){
return map.get(var);
}
}
调用测试
public static void main(String[] args) {
//创建环境对象
Context context = new Context();
//创建多个变量对象
TerminalExpression a = new TerminalExpression("a");
TerminalExpression b = new TerminalExpression("b");
TerminalExpression c = new TerminalExpression("c");
TerminalExpression d = new TerminalExpression("d");
//将变量存储到环境变量中
context.assign(a, 1);
context.assign(b, 2);
context.assign(c, 3);
context.assign(d, 4);
//获取抽象语法树 a + b - c + d
AbstractExpression expression = new Minus(a, new Plus(new Minus(b, c), d));
//解释,即计算
int result = expression.interpret(context);
System.out.println(expression + "=" + result);
}