1. 结构型设计模式概述
结构型设计模式是Java设计模式中专门处理对象组合关系的一类模式,它们通过不同的方式将类或对象组合成更大的结构。在实际开发中,我们经常会遇到需要将多个对象或类组织成特定结构的场景,这时候结构型模式就能大显身手。
我从业十多年来,见过太多因为对象关系处理不当导致的代码问题。比如两个不相关的类强行耦合在一起,或者该用组合的地方用了继承,最终导致系统难以维护。结构型模式正是为解决这类问题而生的,它们能让你的代码像搭积木一样灵活可扩展。
结构型模式主要分为两大类:类结构型模式和对象结构型模式。前者关心类的组合,通过继承机制实现;后者则更关注对象间的组合,通过对象间的引用关系来实现。在Java中,由于单继承的限制,我们更多使用对象结构型模式。
2. 适配器模式(Adapter)
2.1 模式定义与适用场景
适配器模式就像是一个转接头,让原本不兼容的接口能够一起工作。想象你从国外带回来的电器插头在国内无法直接使用,这时候一个转换插头就能解决问题,适配器模式起的就是这个作用。
在Java开发中,这种场景太常见了。比如你正在使用一个第三方库,但它的接口与你的系统不匹配;或者你需要升级系统,但新老接口需要共存一段时间。这些都是适配器模式的用武之地。
2.2 实现方式与代码示例
适配器模式主要有两种实现方式:类适配器和对象适配器。由于Java不支持多重继承,我们通常使用对象适配器。
java复制// 目标接口
public interface MediaPlayer {
void play(String audioType, String fileName);
}
// 被适配者
public class AdvancedMediaPlayer {
public void playVlc(String fileName) {
System.out.println("Playing vlc file: " + fileName);
}
public void playMp4(String fileName) {
System.out.println("Playing mp4 file: " + fileName);
}
}
// 适配器
public class MediaAdapter implements MediaPlayer {
private AdvancedMediaPlayer advancedMusicPlayer;
public MediaAdapter(String audioType) {
if(audioType.equalsIgnoreCase("vlc")) {
advancedMusicPlayer = new AdvancedMediaPlayer();
} else if(audioType.equalsIgnoreCase("mp4")) {
advancedMusicPlayer = new AdvancedMediaPlayer();
}
}
@Override
public void play(String audioType, String fileName) {
if(audioType.equalsIgnoreCase("vlc")) {
advancedMusicPlayer.playVlc(fileName);
} else if(audioType.equalsIgnoreCase("mp4")) {
advancedMusicPlayer.playMp4(fileName);
}
}
}
2.3 实战经验与注意事项
在实际项目中,适配器模式有几个需要注意的点:
-
不要过度使用适配器。如果发现系统中适配器太多,可能是接口设计有问题,应该考虑重构而不是继续添加适配器。
-
适配器会增加系统复杂性。每增加一个适配器,就多了一个需要维护的类,所以要权衡利弊。
-
对象适配器比类适配器更灵活。因为对象适配器采用组合方式,可以适配被适配者及其子类。
提示:在Spring框架中,很多地方都使用了适配器模式,比如HandlerAdapter就是典型的例子。理解这个模式有助于你更好地理解框架设计。
3. 装饰器模式(Decorator)
3.1 模式核心思想
装饰器模式允许向一个现有的对象添加新的功能,同时又不改变其结构。这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。
我经常把它比作给手机加壳:手机本身功能不变,但加个防水壳就有了防水功能,加个电池壳就增加了续航。装饰器模式也是这样,在不修改原有对象的情况下动态地扩展功能。
3.2 Java IO中的经典实现
Java IO包是装饰器模式的经典应用。看看下面这个例子:
java复制InputStream inputStream = new FileInputStream("test.txt");
// 添加缓冲功能
inputStream = new BufferedInputStream(inputStream);
// 添加读取基本Java数据类型的功能
inputStream = new DataInputStream(inputStream);
每一层装饰都添加了新的功能,而且可以灵活组合。这种设计让Java IO非常灵活,但也导致API有些复杂。
3.3 自定义装饰器实现
让我们自己实现一个简单的装饰器:
java复制// 组件接口
public interface Coffee {
double getCost();
String getDescription();
}
// 具体组件
public class SimpleCoffee implements Coffee {
@Override
public double getCost() {
return 1;
}
@Override
public String getDescription() {
return "Simple coffee";
}
}
// 装饰器基类
public abstract class CoffeeDecorator implements Coffee {
protected final Coffee decoratedCoffee;
public CoffeeDecorator(Coffee coffee) {
this.decoratedCoffee = coffee;
}
public double getCost() {
return decoratedCoffee.getCost();
}
public String getDescription() {
return decoratedCoffee.getDescription();
}
}
// 具体装饰器
public class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
@Override
public double getCost() {
return super.getCost() + 0.5;
}
@Override
public String getDescription() {
return super.getDescription() + ", with milk";
}
}
3.4 装饰器与继承的抉择
很多新手会困惑:装饰器模式和继承都能扩展功能,该如何选择?根据我的经验:
-
当扩展功能是可选的时候,优先考虑装饰器。比如咖啡的配料,不是每杯咖啡都需要加牛奶。
-
当需要在运行时动态添加或移除功能时,必须使用装饰器。继承是编译时决定的,无法动态改变。
-
当扩展功能可能有多种组合时,装饰器更合适。如果用继承,会产生大量子类(加牛奶的咖啡、加糖的咖啡、既加牛奶又加糖的咖啡...)。
4. 代理模式(Proxy)
4.1 代理模式的应用场景
代理模式为其他对象提供一种代理以控制对这个对象的访问。在实际开发中,代理模式应用非常广泛:
- 远程代理:为远程对象提供本地代表,如RMI
- 虚拟代理:创建开销大的对象时使用,如图片预加载
- 保护代理:控制对原始对象的访问权限
- 智能引用:在访问对象时执行额外操作,如引用计数
4.2 静态代理实现
静态代理是最简单的代理形式,需要为每个被代理类创建一个代理类:
java复制public interface Image {
void display();
}
public class RealImage implements Image {
private String fileName;
public RealImage(String fileName) {
this.fileName = fileName;
loadFromDisk();
}
private void loadFromDisk() {
System.out.println("Loading " + fileName);
}
@Override
public void display() {
System.out.println("Displaying " + fileName);
}
}
public class ProxyImage implements Image {
private RealImage realImage;
private String fileName;
public ProxyImage(String fileName) {
this.fileName = fileName;
}
@Override
public void display() {
if(realImage == null) {
realImage = new RealImage(fileName);
}
realImage.display();
}
}
4.3 动态代理进阶
Java的动态代理更加强大,可以在运行时动态创建代理类。JDK动态代理是常用的实现方式:
java复制public interface Subject {
void doSomething();
}
public class RealSubject implements Subject {
@Override
public void doSomething() {
System.out.println("RealSubject do something");
}
}
public class DynamicProxyHandler implements InvocationHandler {
private Object target;
public DynamicProxyHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Before method " + method.getName());
Object result = method.invoke(target, args);
System.out.println("After method " + method.getName());
return result;
}
}
// 使用动态代理
Subject realSubject = new RealSubject();
Subject proxyInstance = (Subject) Proxy.newProxyInstance(
realSubject.getClass().getClassLoader(),
realSubject.getClass().getInterfaces(),
new DynamicProxyHandler(realSubject)
);
proxyInstance.doSomething();
4.4 代理模式实战技巧
-
Spring AOP就是基于动态代理实现的。理解代理模式是掌握AOP的基础。
-
性能敏感的场景要谨慎使用代理,特别是动态代理,因为反射调用会有额外开销。
-
CGLIB可以在代理类没有实现接口时使用,它通过继承方式实现代理。
-
代理模式容易过度使用,只有当直接访问对象不合适或不方便时,才应该考虑代理。
5. 组合模式(Composite)
5.1 树形结构处理利器
组合模式用于表示"部分-整体"的层次结构,使得客户端对单个对象和组合对象的使用具有一致性。文件系统是组合模式的经典例子:文件和文件夹都是文件系统条目,但文件夹又可以包含其他条目。
在GUI开发中,组合模式也很常见。比如Swing中的Component和Container,Container也是一种Component,但可以包含其他Component。
5.2 透明式与安全式实现
组合模式有两种实现方式:
- 透明式:在Component中声明所有子类共有的方法,包括管理子组件的方法。这样客户端可以一致地对待所有对象,但叶子节点需要实现不需要的方法。
java复制public abstract class Component {
public void add(Component component) {
throw new UnsupportedOperationException();
}
public void remove(Component component) {
throw new UnsupportedOperationException();
}
public abstract void operation();
}
- 安全式:只在Composite中定义管理子组件的方法。这样叶子节点不需要实现无关方法,但客户端需要区分叶子节点和组合节点。
5.3 组合模式实战示例
让我们实现一个简单的文件系统:
java复制public interface FileSystemComponent {
void showDetails();
}
public class File implements FileSystemComponent {
private String name;
public File(String name) {
this.name = name;
}
@Override
public void showDetails() {
System.out.println("File: " + name);
}
}
public class Directory implements FileSystemComponent {
private String name;
private List<FileSystemComponent> components = new ArrayList<>();
public Directory(String name) {
this.name = name;
}
public void addComponent(FileSystemComponent component) {
components.add(component);
}
@Override
public void showDetails() {
System.out.println("Directory: " + name);
for(FileSystemComponent component : components) {
component.showDetails();
}
}
}
5.4 组合模式使用心得
-
组合模式非常适合处理递归或层次结构数据。如果你的数据是一棵树,考虑使用组合模式。
-
透明式实现更符合里氏替换原则,但会导致叶子节点有不必要的方法。安全式实现更实际,但失去了透明性。
-
组合模式可能会让设计过于一般化,难以限制组合中的组件。有时你希望一个组合只能有特定类型的子组件,这在组合模式中难以实现。
-
在遍历组合结构时,可以考虑使用迭代器模式来统一遍历方式。
6. 桥接模式(Bridge)
6.2 桥接模式结构解析
桥接模式将抽象部分与实现部分分离,使它们可以独立变化。它通过组合代替继承来实现,避免了多层继承带来的复杂性。
桥接模式包含四个角色:
- Abstraction:抽象类,维护对Implementor的引用
- RefinedAbstraction:扩展Abstraction
- Implementor:实现类接口
- ConcreteImplementor:具体实现类
6.3 形状绘制示例
考虑一个图形绘制库,需要支持不同形状和不同渲染方式:
java复制// 实现部分接口
public interface Renderer {
void renderCircle(float radius);
void renderSquare(float side);
}
// 具体实现:矢量渲染
public class VectorRenderer implements Renderer {
@Override
public void renderCircle(float radius) {
System.out.println("Drawing a circle of radius " + radius + " using vectors");
}
@Override
public void renderSquare(float side) {
System.out.println("Drawing a square with side " + side + " using vectors");
}
}
// 抽象部分
public abstract class Shape {
protected Renderer renderer;
public Shape(Renderer renderer) {
this.renderer = renderer;
}
public abstract void draw();
}
// 扩展抽象:圆形
public class Circle extends Shape {
private float radius;
public Circle(Renderer renderer, float radius) {
super(renderer);
this.radius = radius;
}
@Override
public void draw() {
renderer.renderCircle(radius);
}
}
6.4 桥接模式优势分析
-
分离抽象和实现,使得两者可以独立扩展而不会相互影响。
-
避免了多层继承带来的复杂性。使用组合关系代替继承关系,提高了灵活性。
-
符合开闭原则。新增抽象或实现都很方便,不需要修改现有代码。
-
符合单一职责原则。抽象专注于高层逻辑,实现专注于底层细节。
注意:桥接模式在初期设计时需要识别出抽象和实现两个维度,这对设计者要求较高。如果维度识别错误,可能会导致设计复杂化。
7. 外观模式(Facade)
7.1 复杂系统的简化接口
外观模式为子系统中的一组接口提供一个统一的接口。它定义了一个高层接口,使得子系统更容易使用。简单说,外观模式就是为复杂系统提供一个简单的使用方式。
我经常把外观模式比作餐厅的服务员:厨房里有厨师、帮厨、洗碗工等复杂的协作关系,但顾客只需要跟服务员打交道即可。
7.2 计算机启动过程示例
让我们模拟计算机启动过程:
java复制public class CPU {
public void freeze() { /* ... */ }
public void jump(long position) { /* ... */ }
public void execute() { /* ... */ }
}
public class Memory {
public void load(long position, byte[] data) { /* ... */ }
}
public class HardDrive {
public byte[] read(long lba, int size) { /* ... */ }
}
// 外观类
public class ComputerFacade {
private CPU processor;
private Memory ram;
private HardDrive hd;
public ComputerFacade() {
this.processor = new CPU();
this.ram = new Memory();
this.hd = new HardDrive();
}
public void start() {
processor.freeze();
ram.load(BOOT_ADDRESS, hd.read(BOOT_SECTOR, SECTOR_SIZE));
processor.jump(BOOT_ADDRESS);
processor.execute();
}
}
7.3 外观模式最佳实践
-
在设计阶段就应该考虑使用外观模式。当发现系统越来越复杂,客户端需要了解太多子系统细节时,就该引入外观了。
-
外观模式可以减少客户端与子系统的耦合。当子系统发生变化时,只需要修改外观类即可。
-
一个系统可以有多个外观类,针对不同的客户端提供不同的简化接口。
-
外观模式并不禁止客户端直接访问子系统类。如果需要更精细的控制,可以考虑将子系统类设为包私有,只允许外观类访问。
8. 享元模式(Flyweight)
8.1 对象共享的智慧
享元模式通过共享技术有效地支持大量细粒度对象的复用。它通过区分内部状态(可共享)和外部状态(不可共享)来减少内存使用。
Java中的String常量池就是享元模式的经典实现。相同的字符串字面量会指向同一个String对象,从而节省内存。
8.2 文本编辑器中的字符处理
考虑一个文本编辑器,每个字符都有字体、大小、颜色等属性。如果为每个字符都创建一个独立对象,内存消耗会很大。使用享元模式可以优化:
java复制public class CharacterStyle {
private String font;
private int size;
private int colorRGB;
public CharacterStyle(String font, int size, int colorRGB) {
this.font = font;
this.size = size;
this.colorRGB = colorRGB;
}
@Override
public boolean equals(Object o) {
// 实现equals和hashCode用于比较样式
}
}
public class CharacterStyleFactory {
private static final Map<String, CharacterStyle> styles = new HashMap<>();
public static CharacterStyle getStyle(String font, int size, int colorRGB) {
String key = font + size + colorRGB;
if(!styles.containsKey(key)) {
styles.put(key, new CharacterStyle(font, size, colorRGB));
}
return styles.get(key);
}
}
public class Character {
private char value;
private CharacterStyle style;
public Character(char value, CharacterStyle style) {
this.value = value;
this.style = style;
}
}
8.3 享元模式使用要点
-
只有当程序需要创建大量相似对象,且这些对象造成了很大的存储开销时,才考虑使用享元模式。
-
享元对象的内部状态必须是不可变的。因为它们是共享的,任何修改都会影响所有使用该享元的上下文。
-
享元工厂通常使用单例模式实现,确保享元对象的唯一性。
-
在使用享元模式前,一定要做性能测试。有时候对象创建和垃圾回收的开销可能比共享带来的收益更大。
-
享元模式会增加系统复杂性,特别是需要区分内部状态和外部状态时。只有当性能收益明显大于复杂性成本时才使用。