Java设计模式之状态模式详解
作者:蜀山剑客李沐白
1. 状态模式的概述
状态模式是一种通过将对象的状态转换逻辑分布到状态对象中来实现状态转换的设计模式。它将对象的行为与对应的状态分离,使得在修改对象状态时,不需要修改对象的行为方法。同时,状态模式可以通过将状态的转换逻辑包含在各个状态类中来简化代码,避免出现大量的条件判断语句,从而提高代码的可读性和可维护性。
根据 GoF 的定义,状态模式的三个核心角色分别是:
- 环境(Context):它定义了客户端所感兴趣的接口,并维护一个当前状态,在具体状态类中实现该接口的各个具体操作。
- 抽象状态(State):它定义了一个接口,用于封装环境对象中不同状态对应的行为。
- 具体状态(Concrete State):它实现了抽象状态接口,封装了不同状态下对环境对象的响应行为。
2. 状态模式的结构与实现
在 Java 中,状态模式的实现方法比较简单,通常可以按照以下步骤进行:
- 定义抽象状态接口(State),它包含了具体状态所对应的操作方法;
- 定义具体状态类(ConcreteState1、ConcreteState2等),它们实现了抽象状态接口,封装了具体的状态行为;
- 定义环境类(Context),它包含了当前状态以及对外提供的操作接口;
- 在环境类中,使用一个State类型的变量来表示当前状态,并在具体操作中调用该状态的方法;
- 当状态发生改变时,修改环境对象的状态即可。
下面是 Java 中状态模式的一个简单实现:
// 定义抽象状态接口 interface State { void handle(); } // 定义具体状态类 class ConcreteState1 implements State { @Override public void handle() { System.out.println("当前状态为 State1."); } } class ConcreteState2 implements State { @Override public void handle() { System.out.println("当前状态为 State2."); } } // 定义环境类 class Context { private State state; public void setState(State state) { this.state = state; } public void request() { state.handle(); } } // 示例程序 public class StatePatternDemo { public static void main(String[] args) { // 创建状态对象 State state1 = new ConcreteState1(); State state2 = new ConcreteState2(); // 创建环境对象 Context context = new Context(); context.setState(state1); context.request(); context.setState(state2); context.request(); } }
在上述代码中,我们首先定义了抽象状态接口State和两个具体状态类ConcreteState1、ConcreteState2,它们分别实现了State接口。然后,我们定义了一个包含状态切换逻辑的环境类Context,其中,使用状态对象来表示当前的状态,并在request方法中调用当前状态的handle方法。最后,我们创建一个示例程序,调用context的setState方法来改变状态,并观察其输出。
3. 状态模式的优缺点
状态模式具有如下优点:
- 结构清晰、封装性好:将状态的转换逻辑分布到独立的状态类中,使得状态之间的耦合度降低,并且可以将状态的行为封装在状态类中,提高了系统的可维护性和可读性。
- 扩展性好:对于新的状态,只需要创建一个具体状态类即可,同时也可以很方便地增加新的状态转换。
- 易于维护和调试:状态模式将各个状态进行了封装,每个状态对象都只关注自身的行为,使得代码易于维护和调试。
但是状态模式也存在一些缺点:
- 状态模式会导致系统类和对象的个数增加:状态模式将每个状态都封装成了独立的对象,因此会增加系统的复杂度和实现难度。
- 状态模式的使用条件较为苛刻:由于状态模式要求将状态转换逻辑包含在具体状态类中,因此只适合“状态不多”且“状态转换比较少”的情况,否则会导致系统的维护和扩展变得困难。
4. 状态模式的适用场景
状态模式通常适用于以下情形:
- 行为随状态改变而改变的场景:在状态模式中,行为是由状态决定的,因此当一个对象状态改变时,它所对应的行为也随之改变。
- 条件、分支语句多的场景:如果使用传统的if/else语句实现状态转换逻辑,通常会出现大量的条件语句,从而导致代码复杂度的提高,而状态模式可以很好地解决这个问题。
具体来说,状态模式通常适用于以下场景:
- 对象的行为取决于它的状态,并且它必须在运行时刻根据状态改变它的行为;
- 某个操作有多个状态,且这些状态之间可以相互转换;
- 在不同状态下执行的操作有大量重复代码时,可以将该重复代码封装在具体状态类中,从而提高代码的重用性和可维护性。
5. 示例程序的设计与实现
下面我们将使用一个简单示例来说明状态模式的具体实现方法。假设我们正在开发一个多线程下载器程序,该程序可以同时下载多个文件,并且可以监控每个文件的下载进度,当某个文件下载完成后,该程序需要自动关闭该文件的下载线程并向用户发送下载完成的提示信息。
为了实现上述功能,我们可以使用状态模式对下载器程序进行重构,具体设计如下:
- 定义抽象状态类,并声明抽象的 download 方法,用于封装不同状态下的共性操作。
- 定义具体状态类,并实现 download 方法,用于完成具体的状态操作逻辑。
- 在 ConcreteState 类中,定义一个静态变量来表示当前状态,在 download 方法中根据下载状态进行状态转换。
- 在 Context 类中,维护一个当前状态,并将 download 方法委托给当前状态对象来执行。
接下来我们来看一下示例程序的具体实现。在本示例程序中,我们使用了 Java 中的线程池和 FutureTask,实现了对多个文件的同时下载。需要注意的是,由于本文篇幅较长,为了让代码更加清晰,我们将代码拆分成了多个类来实现相应的功能。
(1)抽象状态类
public abstract class DownloadState { protected DownloadContext context; public void setContext(DownloadContext context) { this.context = context; } public abstract void download(String url, String filePath); }
在上述代码中,我们定义了一个抽象状态类 DownloadState,它包含了一个 DownloadContext 对象,以及一个 download 方法,用于封装不同状态下的下载操作。需要注意的是,该抽象方法不包含具体的下载逻辑,具体的下载逻辑需要在具体状态类中进行实现。
(2)具体状态类
public class DownloadingState extends DownloadState { private FutureTask<Integer> futureTask; @Override public void download(String url, String filePath) { System.out.println("开始下载文件:" + filePath); // 开始下载 DownloadTask task = new DownloadTask(url, filePath); futureTask = new FutureTask<>(task); ThreadPool.getInstance().execute(futureTask); // 状态转换 try { int result = futureTask.get(); if (result == 0) { context.setState(new FinishedState()); } else { context.setState(new ErrorState()); } } catch (Exception e) { e.printStackTrace(); context.setState(new ErrorState()); } } } public class FinishedState extends DownloadState { @Override public void download(String url, String filePath) { System.out.println("文件已下载完成,无需重复下载!"); context.closeDownloadThread(filePath); } } public class ErrorState extends DownloadState { @Override public void download(String url, String filePath) { System.out.println("下载文件出错,无法继续下载!"); context.closeDownloadThread(filePath); } }
在上述代码中,我们定义了三个具体状态类:DownloadingState、FinishedState 和 ErrorState,它们分别代表下载中、下载完成和下载出错三种状态。其中,我们使用了线程池和 FutureTask 来实现下载操作,并根据下载结果进行状态转换(对于下载成功的情况,我们转换到FinishedState;对于下载出错的情况,我们转换到ErrorState)。
需要注意的是,由于下载完成或下载出错后都需要关闭下载线程,因此我们在FinishedState和ErrorState中都调用了context对象的closeDownloadThread方法来实现该功能。
(3)环境类
public class DownloadContext { private DownloadState currentState; private Map<String, FutureTask<Integer>> taskMap; public DownloadContext() { this.currentState = new FinishedState(); this.taskMap = new HashMap<>(); } public void setState(DownloadState state) { this.currentState = state; this.currentState.setContext(this); } public void download(String url, String filePath) { FutureTask<Integer> task = this.taskMap.get(filePath); if (task == null || task.isDone() || task.isCancelled()) { this.taskMap.remove(filePath); this.currentState.download(url, filePath); } else { System.out.println("文件 " + filePath + " 正在下载中,无需重复下载!"); } } public void closeDownloadThread(String filePath) { FutureTask<Integer> task = this.taskMap.get(filePath); if (task != null) { task.cancel(true); this.taskMap.remove(filePath); System.out.println("已关闭文件 " + filePath + " 的下载线程。"); } } }
在上述代码中,我们定义了一个 DownloadContext 类,它包含了当前状态以及 download 和 closeDownloadThread 方法。
在 download 方法中,我们首先检查是否存在正在下载的任务(即 task 对象是否存在且未完成),如果不存在,则将当前状态转换为下载中状态,并启动下载任务;否则输出提示信息,防止重复下载。
在 closeDownloadThread 方法中,我们将传入的 filePath 对应的下载任务取消,并从 taskMap 中移除该任务,同时输出提示信息。
(4)线程池类
public class ThreadPool { private ExecutorService executor; private ThreadPool() { this.executor = Executors.newFixedThreadPool(5); } private static class Singleton { private static final ThreadPool INSTANCE = new ThreadPool(); } public static ThreadPool getInstance() { return Singleton.INSTANCE; } public void execute(Runnable task) { this.executor.execute(task); } }
在上述代码中,我们定义了一个 ThreadPool 类,它包含了一个静态的 ExecutorService 对象 executor,并封装了一个 execute 方法用于提交任务到线程池中。
需要注意的是,由于我们要将ThreadPool类设计为单例模式,因此我们在该类中定义了一个私有的静态内部类Singleton,用于实现懒汉式单例模式。这样可以保证线程池中只有一个实例对象,并且线程安全。
(5)示例程序
public class Main { public static void main(String[] args) { DownloadContext context = new DownloadContext(); String url1 = "https://cdn.pixabay.com/photo/2018/10/30/16/06/water-lily-3784022__340.jpg"; String filePath1 = "water-lily.jpg"; String url2 = "https://cdn.pixabay.com/photo/2020/07/14/13/10/excursion-5407227__340.jpg"; String filePath2 = "excursion.jpg"; context.download(url1, filePath1); context.download(url2, filePath2); System.out.println("------------------------------------"); context.download(url1, filePath1); context.download(url2, filePath2); } }
在上述代码中,我们创建了一个 DownloadContext 对象,并分别下载了两个文件。需要注意的是,在第二次下载同一个文件时,系统会输出提示信息“文件正在下载中,无需重复下载!”。
运行该程序,我们可以看到如下输出结果:
开始下载文件:water-lily.jpg
开始下载文件:excursion.jpg
------------------------------------
文件正在下载中,无需重复下载!
文件正在下载中,无需重复下载!
从输出结果中我们可以看出,根据不同的状态,下载器程序完成了不同的操作,并且顺利地将多线程下载操作与状态转换功能封装在了不同的状态类中。
以上就是Java设计模式之状态模式详解的详细内容,更多关于Java状态模式的资料请关注脚本之家其它相关文章!