【Java实战㉒】Java设计模式实战:解锁结构型模式的奥秘 - 实践
目录
一、代理模式实战
1.1 代理模式概念
代理模式是一种结构型设计模式,它为其他对象提供一种代理以控制对这个对象的访问。在代理模式中,代理对象作为访问对象和目标对象之间的中介,客户端通过代理对象间接访问目标对象,这样可以在不改变目标对象的情况下,对其访问进行控制和增强。
举个例子,明星在日常生活中会有很多事务需要处理,如商业演出、广告代言等。但明星本人可能无法亲自处理所有这些事务,这时就需要一个经纪人作为代理。经纪人负责与外界沟通、洽谈业务,控制对明星的访问。只有通过经纪人的安排,才能与明星进行合作。经纪人还可以在明星参与活动之前,对活动进行筛选和评估,确保活动的质量和明星的形象,这就是对目标对象(明星)功能的扩展。
1.2 静态代理
静态代理是指在编译期就已经确定代理类的代码,代理类和目标类实现相同的接口,代理类持有目标类的引用,通过调用目标类的方法来实现代理功能。
以售票系统为例,假设有一个售票接口SellTickets,火车站是真实的售票对象,代售点是代理对象。
// 售票接口
public interface SellTickets {
void sell();
}
// 火车站,实现售票接口
public class TrainStation
implements SellTickets {
@Override
public void sell() {
System.out.println("火车站卖票");
}
}
// 代售点,代理对象
public class ProxyPoint
implements SellTickets {
private TrainStation station = new TrainStation();
@Override
public void sell() {
System.out.println("代理点收取一些服务费用");
station.sell();
}
}
测试代码如下:
public class Client
{
public static void main(String[] args) {
ProxyPoint pp = new ProxyPoint();
pp.sell();
}
}
上述代码中,ProxyPoint类作为代理类,实现了SellTickets接口,并持有TrainStation类的实例。在sell方法中,先打印代理点收取服务费用的信息,然后调用TrainStation的sell方法,实现了对目标对象功能的增强。
静态代理的优点是实现简单,容易理解。缺点是代理类和目标类实现相同的接口,当接口方法较多时,代理类的代码会比较繁琐。而且每增加一个目标类,就需要创建一个对应的代理类,代码的维护性较差。
1.3 动态代理
动态代理是指在运行时动态生成代理类的字节码,并加载到内存中,从而实现代理功能。动态代理分为 JDK 动态代理和 CGLIB 动态代理。
1.3.1 JDK 动态代理
JDK 动态代理是 Java 自带的动态代理实现方式,它利用反射机制在运行时动态生成代理类。JDK 动态代理要求目标对象必须实现接口,代理类实现与目标对象相同的接口,并通过InvocationHandler接口来处理方法调用。
下面以租房为例,展示 JDK 动态代理的实现过程。
// 租房接口
public interface Rent {
void rent();
}
// 房东,实现租房接口
public class Landlord
implements Rent {
@Override
public void rent() {
System.out.println("房东出租房子");
}
}
// 代理工厂,用于创建代理对象
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class ProxyFactory
{
private Landlord landlord;
public ProxyFactory(Landlord landlord) {
this.landlord = landlord;
}
public Rent getProxyObject() {
return (Rent) Proxy.newProxyInstance(
landlord.getClass().getClassLoader(),
landlord.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("中介收取中介费");
return method.invoke(landlord, args);
}
});
}
}
测试代码如下:
public class Client
{
public static void main(String[] args) {
Landlord landlord = new Landlord();
ProxyFactory factory = new ProxyFactory(landlord);
Rent proxy = factory.getProxyObject();
proxy.rent();
}
}
上述代码中,ProxyFactory类通过Proxy.newProxyInstance方法创建代理对象。Proxy.newProxyInstance方法接收三个参数:类加载器、目标对象实现的接口数组和InvocationHandler实例。在InvocationHandler的invoke方法中,先打印中介收取中介费的信息,然后通过反射调用目标对象的方法。
JDK 动态代理的实现原理是:Proxy.newProxyInstance方法会在运行时动态生成一个实现了目标接口的代理类,该代理类继承自Proxy类。当调用代理对象的方法时,实际上是调用InvocationHandler的invoke方法,在invoke方法中通过反射调用目标对象的方法,从而实现代理功能。
1.3.2 CGLIB 动态代理
CGLIB(Code Generation Library)是一个强大的高性能的代码生成类库,它可以在运行期扩展 Java 类与实现 Java 接口。CGLIB 动态代理基于字节码生成技术,在运行时动态生成目标类的子类作为代理类,代理类重写目标类的方法,在方法调用前后添加增强逻辑。
还是以租房为例,展示 CGLIB 动态代理的实现过程。
// 房东类,无需实现接口
public class Landlord
{
public void rent() {
System.out.println("房东出租房子");
}
}
// CGLIB代理类,实现MethodInterceptor接口
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class CglibProxy
implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("中介收取中介费");
return proxy.invokeSuper(obj, args);
}
public Object getProxy(Class<
?> clazz) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(clazz);
enhancer.setCallback(this);
return enhancer.create();
}
}
测试代码如下:
public class Client
{
public static void main(String[] args) {
CglibProxy proxy = new CglibProxy();
Landlord landlord = (Landlord) proxy.getProxy(Landlord.class)
;
landlord.rent();
}
}
上述代码中,CglibProxy类实现了MethodInterceptor接口,重写了intercept方法。在intercept方法中,先打印中介收取中介费的信息,然后通过MethodProxy调用目标对象的方法。Enhancer类用于创建代理对象,它设置目标类为父类,并将CglibProxy实例作为回调函数。
CGLIB 动态代理的实现原理是:Enhancer类通过字节码技术生成目标类的子类,在子类中重写目标类的方法。当调用代理对象的方法时,会先调用MethodInterceptor的intercept方法,在intercept方法中可以添加增强逻辑,然后通过MethodProxy调用目标类的方法。
CGLIB 动态代理与 JDK 动态代理的区别在于:JDK 动态代理要求目标对象必须实现接口,而 CGLIB 动态代理可以代理没有实现接口的类;JDK 动态代理是基于接口的代理,代理类实现目标对象的接口,而 CGLIB 动态代理是基于类的代理,代理类是目标类的子类;在性能方面,CGLIB 动态代理由于避免了反射调用,性能略优于 JDK 动态代理,但 CGLIB 动态代理生成代理类的速度较慢。在实际应用中,需要根据具体情况选择合适的动态代理方式。
1.4 代理模式实战案例
1.4.1 日志代理
在实际开发中,经常需要记录用户的操作日志,以便于系统的维护和问题排查。使用代理模式可以在不修改原有业务代码的基础上,实现日志记录功能。
假设有一个用户服务接口UserService,其中包含一个注册用户的方法registerUser。
// 用户服务接口
public interface UserService {
void registerUser(String username, String password);
}
// 用户服务实现类
public class UserServiceImpl
implements UserService {
@Override
public void registerUser(String username, String password) {
System.out.println("注册用户:" + username + ",密码:" + password);
}
}
// 日志代理类
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Date;
public class LogProxy
implements InvocationHandler {
private Object target;
public LogProxy(Object target) {
this.target = target;
}
public Object getProxy() {
return Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("[" + new Date() + "] 开始调用方法:" + method.getName());
Object result = method.invoke(target, args);
System.out.println("[" + new Date() + "] 方法调用结束:" + method.getName());
return result;
}
}
测试代码如下:
public class Client
{
public static void main(String[] args) {
UserService userService = new UserServiceImpl();
LogProxy logProxy = new LogProxy(userService);
UserService proxy = (UserService) logProxy.getProxy();
proxy.registerUser("张三", "123456");
}
}
上述代码中,LogProxy类实现了InvocationHandler接口,在invoke方法中记录方法调用的开始时间和结束时间。通过Proxy.newProxyInstance方法创建代理对象,当调用代理对象的registerUser方法时,会先打印开始调用方法的日志,然后调用目标对象的registerUser方法,最后打印方法调用结束的日志。
1.4.2 权限代理
在系统中,不同用户可能具有不同的权限,有些功能只允许特定用户访问。使用代理模式可以实现权限控制,确保只有具有相应权限的用户才能访问特定功能。
假设有一个系统功能接口SystemFunction,其中包含一个敏感操作方法sensitiveOperation。
// 系统功能接口
public interface SystemFunction {
void sensitiveOperation();
}
// 系统功能实现类
public class SystemFunctionImpl
implements SystemFunction {
@Override
public void sensitiveOperation() {
System.out.println("执行敏感操作");
}
}
// 权限代理类
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class PermissionProxy
implements InvocationHandler {
private Object target;
private String currentUser;
public PermissionProxy(Object target, String currentUser) {
this.target = target;
this.currentUser = currentUser;
}
public Object getProxy() {
return Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("admin".equals(currentUser)) {
System.out.println("用户 " + currentUser + " 具有权限,允许执行操作");
return method.invoke(target, args);
} else {
System.out.println("用户 " + currentUser + " 没有权限,禁止执行操作");
return null;
}
}
}
测试代码如下:
public class Client
{
public static void main(String[] args) {
SystemFunction systemFunction = new SystemFunctionImpl();
PermissionProxy permissionProxy = new PermissionProxy(systemFunction, "普通用户");
SystemFunction proxy = (SystemFunction) permissionProxy.getProxy();
proxy.sensitiveOperation();
permissionProxy = new PermissionProxy(systemFunction, "admin");
proxy = (SystemFunction) permissionProxy.getProxy();
proxy.sensitiveOperation();
}
}
上述代码中,PermissionProxy类实现了InvocationHandler接口,在invoke方法中判断当前用户是否为admin,如果是则允许执行操作,否则禁止执行操作。通过Proxy.newProxyInstance方法创建代理对象,当调用代理对象的sensitiveOperation方法时,会先进行权限校验,然后根据校验结果决定是否执行目标方法。
二、适配器模式与装饰器模式
2.1 适配器模式
适配器模式(Adapter Pattern)是一种结构型设计模式,它的作用是将一个类的接口转换成客户期望的另一个接口,使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。适配器模式就像是一个转换器,在两个不兼容的接口之间架起一座桥梁,让它们能够协同工作。
以音频播放为例,假设我们有一个音频播放器AudioPlayer,它只能播放 MP3 格式的音频文件,其接口定义如下:
public interface AudioPlayer {
void playMP3(String fileName);
}
public class DefaultAudioPlayer
implements AudioPlayer {
@Override
public void playMP3(String fileName) {
System.out.println("正在播放MP3文件:" + fileName);
}
}
随着业务的发展,我们需要支持播放 VLC 和 MP4 格式的音频文件,但AudioPlayer接口并不支持这些格式的播放方法。此时,我们可以使用适配器模式来解决这个问题。
首先,定义 VLC 和 MP4 播放器的接口:
public interface VlcPlayer {
void playVlc(String fileName);
}
public interface Mp4Player {
void playMp4(String fileName);
}
public class VlcPlayerImpl
implements VlcPlayer {
@Override
public void playVlc(String fileName) {
System.out.println("正在播放VLC文件:" + fileName);
}
}
public class Mp4PlayerImpl
implements Mp4Player {
@Override
public void playMp4(String fileName) {
System.out.println("正在播放MP4文件:" + fileName);
}
}
然后,创建适配器类,将VlcPlayer和Mp4Player的接口适配成AudioPlayer的接口:
public class MediaAdapter
implements AudioPlayer {
private VlcPlayer vlcPlayer;
private Mp4Player mp4Player;
public MediaAdapter(String audioType) {
if ("vlc".equalsIgnoreCase(audioType)) {
vlcPlayer = new VlcPlayerImpl();
} else if ("mp4".equalsIgnoreCase(audioType)) {
mp4Player = new Mp4PlayerImpl();
}
}
@Override
public void playMP3(String fileName) {
// 不支持MP3播放,直接返回
}
public void play(String audioType, String fileName) {
if ("vlc".equalsIgnoreCase(audioType)) {
vlcPlayer.playVlc(fileName);
} else if ("mp4".equalsIgnoreCase(audioType)) {
mp4Player.playMp4(fileName);
}
}
}
最后,修改AudioPlayer接口,添加通用的播放方法,并在DefaultAudioPlayer中实现对 MP3 的播放,在MediaAdapter中实现对 VLC 和 MP4 的播放:
public interface AudioPlayer {
void play(String audioType, String fileName);
}
public class DefaultAudioPlayer
implements AudioPlayer {
@Override
public void play(String audioType, String fileName) {
if ("mp3".equalsIgnoreCase(audioType)) {
System.out.println("正在播放MP3文件:" + fileName);
}
}
}
测试代码如下:
public class Client
{
public static void main(String[] args) {
AudioPlayer audioPlayer = new DefaultAudioPlayer();
audioPlayer.play("mp3", "song.mp3");
AudioPlayer mediaAdapter = new MediaAdapter("vlc");
mediaAdapter.play("vlc", "movie.vlc");
mediaAdapter = new MediaAdapter("mp4");
mediaAdapter.play("mp4", "clip.mp4");
}
}
上述代码中,MediaAdapter类就是适配器,它实现了AudioPlayer接口,并在内部持有VlcPlayer和Mp4Player的实例。通过MediaAdapter,我们可以使用AudioPlayer接口来播放 VLC 和 MP4 格式的音频文件,解决了接口不兼容的问题。
2.2 适配器模式实战案例
假设我们正在开发一个音频播放器应用,最初的设计只支持播放 MP3 格式的音频文件。随着市场需求的变化,用户希望能够播放其他格式的音频文件,如 WAV 和 OGG。为了满足用户需求,同时又不希望大规模修改现有的音频播放核心代码,我们可以使用适配器模式来扩展音频播放器的功能。
首先,定义现有的 MP3 播放器接口和实现类:
// MP3播放器接口
public interface Mp3Player {
void playMp3(String filePath);
}
// MP3播放器实现类
public class RealMp3Player
implements Mp3Player {
@Override
public void playMp3(String filePath) {
System.out.println("正在播放MP3文件: " + filePath);
}
}
然后,定义新的音频格式(WAV 和 OGG)的播放器接口和实现类:
// WAV播放器接口
public interface WavPlayer {
void playWav(String filePath);
}
// WAV播放器实现类
public class RealWavPlayer
implements WavPlayer {
@Override
public void playWav(String filePath) {
System.out.println("正在播放WAV文件: " + filePath);
}
}
// OGG播放器接口
public interface OggPlayer {
void playOgg(String filePath);
}
// OGG播放器实现类
public class RealOggPlayer
implements OggPlayer {
@Override
public void playOgg(String filePath) {
System.out.println("正在播放OGG文件: " + filePath);
}
}
接下来,创建适配器类,将 WAV 和 OGG 播放器的接口适配成 MP3 播放器的接口:
// 音频播放器适配器类
public class AudioPlayerAdapter
implements Mp3Player {
private WavPlayer wavPlayer;
private OggPlayer oggPlayer;
public AudioPlayerAdapter(String audioType) {
if ("wav".equalsIgnoreCase(audioType)) {
wavPlayer = new RealWavPlayer();
} else if ("ogg".equalsIgnoreCase(audioType)) {
oggPlayer = new RealOggPlayer();
}
}
@Override
public void playMp3(String filePath) {
if (wavPlayer != null) {
wavPlayer.playWav(filePath);
} else if (oggPlayer != null) {
oggPlayer.playOgg(filePath);
}
}
}
最后,在客户端代码中使用适配器来播放不同格式的音频文件:
public class AudioPlayerApp
{
public static void main(String[] args) {
// 播放MP3文件
Mp3Player mp3Player = new RealMp3Player();
mp3Player.playMp3("music.mp3");
// 播放WAV文件,通过适配器
Mp3Player wavAdapter = new AudioPlayerAdapter("wav");
wavAdapter.playMp3("sound.wav");
// 播放OGG文件,通过适配器
Mp3Player oggAdapter = new AudioPlayerAdapter("ogg");
oggAdapter.playMp3("tune.ogg");
}
}
在上述案例中,AudioPlayerAdapter类充当了适配器的角色,它将WavPlayer和OggPlayer的接口适配成了Mp3Player的接口。这样,在不修改RealMp3Player类和客户端代码中对Mp3Player接口调用方式的前提下,实现了对新音频格式的支持,体现了适配器模式在解决接口不兼容问题,以及在系统扩展时复用现有代码方面的优势。
2.3 装饰器模式
装饰器模式(Decorator Pattern)是一种结构型设计模式,它允许向一个现有的对象添加新的功能,同时又不改变其结构。这种模式通过创建一个装饰类,将原始对象包装起来,并在保持原始对象方法签名完整的情况下,为其提供额外的功能。装饰器模式遵循开闭原则,即对扩展开放,对修改关闭,在不修改原有代码的基础上,实现对象功能的动态扩展。
我们以咖啡店为例来理解装饰器模式。假设咖啡店提供基础的咖啡,顾客可以根据自己的口味选择添加不同的配料,如糖、牛奶、巧克力等。每添加一种配料,咖啡的功能(口味)就会得到扩展。
首先,定义咖啡接口和基础咖啡实现类:
// 咖啡接口
public interface Coffee {
String getDescription();
double getCost();
}
// 基础咖啡实现类
public class SimpleCoffee
implements Coffee {
@Override
public String getDescription() {
return "简单咖啡";
}
@Override
public double getCost() {
return 1.0;
}
}
然后,定义装饰器抽象类,它实现了咖啡接口,并持有一个咖啡对象的引用:
// 咖啡装饰器抽象类
public abstract class CoffeeDecorator
implements Coffee {
protected Coffee coffee;
public CoffeeDecorator(Coffee coffee) {
this.coffee = coffee;
}
@Override
public String getDescription() {
return coffee.getDescription();
}
@Override
public double getCost() {
return coffee.getCost();
}
}
接着,定义具体的装饰器类,如加糖、加牛奶的装饰器:
// 加糖装饰器
public class SugarDecorator
extends CoffeeDecorator {
public SugarDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + ", 加糖";
}
@Override
public double getCost() {
return coffee.getCost() + 0.5;
}
}
// 加牛奶装饰器
public class MilkDecorator
extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + ", 加牛奶";
}
@Override
public double getCost() {
return coffee.getCost() + 1.0;
}
}
最后,在客户端代码中使用装饰器来创建不同口味的咖啡:
public class CoffeeShop
{
public static void main(String[] args) {
// 基础咖啡
Coffee coffee = new SimpleCoffee();
System.out.println(coffee.getDescription() + ",价格:" + coffee.getCost());
// 加牛奶的咖啡
coffee = new MilkDecorator(coffee);
System.out.println(coffee.getDescription() + ",价格:" + coffee.getCost());
// 加牛奶和糖的咖啡
coffee = new SugarDecorator(coffee);
System.out.println(coffee.getDescription() + ",价格:" + coffee.getCost());
}
}
上述代码中,SimpleCoffee是基础对象,CoffeeDecorator是抽象装饰器,SugarDecorator和MilkDecorator是具体装饰器。通过装饰器模式,我们可以在不修改SimpleCoffee类的情况下,动态地为咖啡添加不同的配料,扩展其功能。每个装饰器都可以独立地为对象添加新的行为,并且可以根据需要组合多个装饰器,实现更加复杂的功能扩展。
2.4 装饰器模式实战案例
2.4.1 IO 流装饰器
在 Java 的 IO 流中,装饰器模式被广泛应用。以文件读取为例,FileInputStream是一个基础的文件输入流,它提供了基本的读取文件的功能。而BufferedInputStream是一个装饰器,它可以为FileInputStream添加缓冲功能,提高读取效率。
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public class FileReadingExample
{
public static void main(String[] args) {
try {
// 创建基础的文件输入流
InputStream fileInputStream = new FileInputStream("example.txt");
// 使用BufferedInputStream装饰文件输入流,添加缓冲功能
InputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
int data;
while ((data = bufferedInputStream.read()) != -1) {
System.out.print((char) data);
}
// 关闭流,先关闭外层装饰器流,它会自动关闭内层被装饰的流
bufferedInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
在上述代码中,首先创建了一个FileInputStream对象来读取文件。然后,将FileInputStream对象传递给BufferedInputStream的构造函数,创建了一个BufferedInputStream对象,它装饰了FileInputStream,为其添加了缓冲功能。通过BufferedInputStream的read方法读取数据时,实际上是先从缓冲区中读取数据,如果缓冲区中没有数据,才会从文件中读取数据到缓冲区,这样减少了磁盘 I/O 操作的次数,提高了读取效率。
除了BufferedInputStream,Java 的 IO 流中还有很多其他的装饰器,如DataInputStream可以为输入流添加读取基本数据类型的功能,ObjectInputStream可以为输入流添加读取对象的功能等。这些装饰器可以根据实际需求进行组合使用,以实现更复杂的 IO 操作。
2.4.2 日志增强
假设我们有一个用户服务类UserService,其中包含一些用户操作的方法,如登录、注册等。现在我们希望在这些方法执行前后添加日志记录功能,以记录用户的操作行为。使用装饰器模式可以在不修改UserService类的基础上,实现日志增强功能。
首先,定义用户服务接口和实现类:
// 用户服务接口
public interface UserService {
void login(String username, String password);
void register(String username, String password);
}
// 用户服务实现类
public class UserServiceImpl
implements UserService {
@Override
public void login(String username, String password) {
System.out.println("用户 " + username + " 登录成功");
}
@Override
public void register(String username, String password) {
System.out.println("用户 " + username + " 注册成功");
}
}
然后,定义日志装饰器类,它实现了用户服务接口,并持有一个用户服务对象的引用:
// 日志装饰器类
public class LoggingUserServiceDecorator
implements UserService {
private UserService userService;
public LoggingUserServiceDecorator(UserService userService) {
this.userService = userService;
}
@Override
public void login(String username, String password) {
System.out.println("开始记录用户登录操作,用户名:" + username);
userService.login(username, password);
System.out.println("用户登录操作记录结束,用户名:" + username);
}
@Override
public void register(String username, String password) {
System.out.println("开始记录用户注册操作,用户名:" + username);
userService.register(username, password);
System.out.println("用户注册操作记录结束,用户名:" + username);
}
}
最后,在客户端代码中使用日志装饰器来增强用户服务的功能:
public class Client
{
public static void main(String[] args) {
UserService userService = new UserServiceImpl();
// 使用日志装饰器增强用户服务
UserService loggingUserService = new LoggingUserServiceDecorator(userService);
loggingUserService.login("张三", "123456");
loggingUserService.register("李四", "654321");
}
}
在上述代码中,UserServiceImpl是基础的用户服务实现类,LoggingUserServiceDecorator是日志装饰器类。通过将UserServiceImpl对象传递给LoggingUserServiceDecorator的构造函数,创建了一个装饰后的用户服务对象loggingUserService。当调用loggingUserService的login和register方法时,会先打印日志记录开始的信息,然后调用UserServiceImpl的相应方法,最后打印日志记录结束的信息,实现了对用户服务方法的日志增强功能,同时保持了UserServiceImpl类的独立性和可维护性。
三、外观模式与组合模式
3.1 外观模式
外观模式(Facade Pattern)是一种结构型设计模式,它为子系统中的一组接口提供一个统一的高层接口,使得子系统更加容易使用。外观模式通过引入一个外观类,将子系统的复杂性封装起来,客户端只需要与外观类进行交互,而不需要了解子系统内部的具体实现细节。
举个生活中的例子,当我们启动电脑时,电脑内部会进行一系列复杂的操作,如硬件自检、加载 BIOS、初始化操作系统等。对于用户来说,只需要按下电源按钮,这一个简单的操作就相当于外观模式中的外观类提供的统一接口,用户不需要了解电脑内部各个硬件和软件组件是如何协同工作的,就可以完成电脑启动的操作。在这个例子中,电脑内部的各个硬件和软件组件就是子系统,电源按钮的操作封装了这些子系统的复杂交互,提供了一个简单易用的接口给用户。
外观模式主要包含以下角色:
- 外观类(Facade):提供一个统一的接口,用来访问子系统中的一群接口,负责协调子系统的调用,将客户端的请求转发给相应的子系统对象。
- 子系统类(Subsystem Classes):实现子系统的功能,处理外观对象指派的任务,子系统类之间可以相互交互,但它们并不知晓外观类的存在,对子系统而言,外观类只是另一个客户端。
3.2 外观模式实战案例
以订单系统服务外观为例,假设一个电商订单系统涉及多个子系统,如库存系统、支付系统、物流系统等。当用户下单时,需要与这些子系统进行交互,完成库存检查、支付处理、物流安排等操作。如果没有外观模式,客户端代码需要直接与各个子系统进行交互,代码会变得复杂且难以维护。
首先,定义各个子系统的接口和实现类:
// 库存系统接口
public interface InventorySystem {
boolean checkStock(int productId, int quantity);
void deductStock(int productId, int quantity);
}
// 库存系统实现类
public class InventorySystemImpl
implements InventorySystem {
@Override
public boolean checkStock(int productId, int quantity) {
// 实际实现中查询数据库或其他存储方式检查库存
System.out.println("检查商品 " + productId + " 的库存是否足够 " + quantity + " 件");
return true;
}
@Override
public void deductStock(int productId, int quantity) {
// 实际实现中更新数据库或其他存储方式扣除库存
System.out.println("扣除商品 " + productId + " 的库存 " + quantity + " 件");
}
}
// 支付系统接口
public interface PaymentSystem {
boolean processPayment(double amount, String paymentMethod);
}
// 支付系统实现类
public class PaymentSystemImpl
implements PaymentSystem {
@Override
public boolean processPayment(double amount, String paymentMethod) {
// 实际实现中调用支付接口进行支付处理
System.out.println("使用 " + paymentMethod + " 支付 " + amount + " 元");
return true;
}
}
// 物流系统接口
public interface LogisticsSystem {
void shipOrder(String address);
}
// 物流系统实现类
public class LogisticsSystemImpl
implements LogisticsSystem {
@Override
public void shipOrder(String address) {
// 实际实现中调用物流接口安排发货
System.out.println("发货至地址 " + address);
}
}
然后,创建外观类OrderFacade,封装各个子系统的操作,提供一个统一的下单接口:
public class OrderFacade
{
private InventorySystem inventorySystem;
private PaymentSystem paymentSystem;
private LogisticsSystem logisticsSystem;
public OrderFacade() {
this.inventorySystem = new InventorySystemImpl();
this.paymentSystem = new PaymentSystemImpl();
this.logisticsSystem = new LogisticsSystemImpl();
}
public boolean placeOrder(int productId, int quantity, double price, String paymentMethod, String address) {
// 检查库存
if (!inventorySystem.checkStock(productId, quantity)) {
System.out.println("下单失败:库存不足");
return false;
}
// 扣除库存
inventorySystem.deductStock(productId, quantity);
// 处理支付
if (!paymentSystem.processPayment(price * quantity, paymentMethod)) {
System.out.println("下单失败:支付失败");
return false;
}
// 安排发货
logisticsSystem.shipOrder(address);
System.out.println("下单成功");
return true;
}
}
最后,在客户端代码中使用外观类进行下单操作:
public class Client
{
public static void main(String[] args) {
OrderFacade orderFacade = new OrderFacade();
boolean result = orderFacade.placeOrder(1001, 2, 99.9, "支付宝", "北京市朝阳区 XX 路");
}
}
在上述案例中,OrderFacade类作为外观类,封装了库存系统、支付系统和物流系统的复杂操作,为客户端提供了一个简单的下单接口placeOrder。客户端只需要调用placeOrder方法,就可以完成下单的整个流程,而不需要了解各个子系统的具体实现细节,降低了客户端与子系统之间的耦合度,提高了系统的可维护性和可扩展性。
3.3 组合模式
组合模式(Composite Pattern)是一种结构型设计模式,它将对象组织成树形结构以表示 “部分 - 整体” 的层次结构,使客户端对单个对象和对象组合的使用具有一致性。组合模式的核心在于通过递归组合的方式,将叶子对象(不包含子对象的对象)和容器对象(包含子对象的对象)统一对待,使得客户端可以用相同的方式处理单个对象和组合对象。
以文件系统的目录结构为例,在文件系统中,文件和文件夹都可以看作是组件。文件是叶子节点,它没有子节点,只包含具体的数据;文件夹是容器节点,它可以包含文件和其他文件夹。当我们需要遍历文件系统时,无论是访问单个文件还是整个文件夹及其包含的所有文件,都可以使用相同的操作方式。比如,我们可以对文件和文件夹都执行打开、删除等操作,而不需要区分它们是文件还是文件夹,这就是组合模式的应用。
组合模式主要包含以下角色:
- 抽象组件(Component):声明组合中对象的接口,定义了叶子节点和容器节点的共同行为,它可以是一个抽象类或接口。在文件系统的例子中,抽象组件可以定义诸如open(打开)、delete(删除)等方法,这些方法既适用于文件(叶子节点),也适用于文件夹(容器节点)。
- 叶子节点(Leaf):表示树的叶节点,没有子节点,负责实现抽象组件的行为。在文件系统中,文件就是叶子节点,它实现了抽象组件定义的具体操作,比如文件的打开就是读取文件内容,文件的删除就是从存储介质中移除文件。
- 容器节点(Composite):包含子节点,负责实现管理子节点的操作,如添加、移除子节点,并实现抽象组件的行为。在文件系统中,文件夹就是容器节点,它不仅实现了抽象组件定义的操作,如文件夹的打开就是展示文件夹内的文件和子文件夹列表,文件夹的删除就是删除文件夹及其包含的所有文件和子文件夹,还提供了管理子节点的方法,如添加文件或子文件夹到文件夹中,从文件夹中移除文件或子文件夹。
- 客户端(Client):通过抽象组件与树形结构进行交互,不关心是单个对象还是组合结构。客户端可以对抽象组件进行操作,而不需要了解具体是叶子节点还是容器节点,从而简化了客户端的代码。
3.4 组合模式实战案例
以文件目录结构处理为例,我们来实现一个简单的文件系统目录结构管理程序,展示组合模式的应用。
首先,定义抽象组件FileSystemComponent:
// 文件系统组件抽象类
public abstract class FileSystemComponent
{
protected String name;
public FileSystemComponent(String name) {
this.name = name;
}
public abstract void display();
public abstract void add(FileSystemComponent component);
public abstract void remove(FileSystemComponent component);
public abstract FileSystemComponent getChild(int index);
}
然后,定义叶子节点File:
// 文件类,叶子节点
public class File
extends FileSystemComponent {
public File(String name) {
super(name);
}
@Override
public void display() {
System.out.println("文件: " + name);
}
@Override
public void add(FileSystemComponent component) {
throw new UnsupportedOperationException("文件不能添加子组件");
}
@Override
public void remove(FileSystemComponent component) {
throw new UnsupportedOperationException("文件不能移除子组件");
}
@Override
public FileSystemComponent getChild(int index) {
throw new UnsupportedOperationException("文件没有子组件");
}
}
接着,定义容器节点Folder:
// 文件夹类,容器节点
import java.util.ArrayList;
import java.util.List;
public class Folder
extends FileSystemComponent {
private List<
FileSystemComponent> components = new ArrayList<
>();
public Folder(String name) {
super(name);
}
@Override
public void display() {
System.out.println("文件夹: " + name);
for (FileSystemComponent component : components) {
component.display();
}
}
@Override
public void add(FileSystemComponent component) {
components.add(component);
}
@Override
public void remove(FileSystemComponent component) {
components.remove(component);
}
@Override
public FileSystemComponent getChild(int index) {
return components.get(index);
}
}
最后,在客户端代码中使用组合模式操作文件目录结构:
public class Client
{
public static void main(String[] args) {
// 创建根文件夹
Folder rootFolder = new Folder("根目录");
// 创建子文件夹和文件
Folder subFolder1 = new Folder("子文件夹1");
File file1 = new File("文件1.txt");
File file2 = new File("文件2.txt");
// 将文件添加到子文件夹
subFolder1.add(file1);
subFolder1.add(file2);
// 将子文件夹添加到根文件夹
rootFolder.add(subFolder1);
// 创建另一个子文件夹和文件
Folder subFolder2 = new Folder("子文件夹2");
File file3 = new File("文件3.txt");
subFolder2.add(file3);
rootFolder.add(subFolder2);
// 显示文件目录结构
rootFolder.display();
}
}
在上述案例中,FileSystemComponent是抽象组件,File是叶子节点,Folder是容器节点,Client是客户端。通过组合模式,我们可以方便地创建和管理文件目录结构,客户端可以用统一的方式操作文件和文件夹,而不需要关心它们的具体类型。当需要添加新的文件或文件夹,或者删除、遍历文件目录结构时,代码的实现变得更加简洁和灵活,体现了组合模式在处理树形结构对象时的优势。

浙公网安备 33010602011771号