Java 设计模式概述

设计模式的重要性

在软件开发领域,变化是永恒的主题。从项目启动到后期维护,需求变更、技术演进以及环境变化无处不在。我们面临的关键挑战,是如何以最小的成本和最大的灵活性来适应这些变化。

幸运的是,前人在应对各种设计问题时积累了丰富的经验,并形成了被广泛认可的最佳实践,这就是“设计模式”(Design Patterns)。设计模式提供了经过验证的解决方案,能够帮助我们构建更优质、更具扩展性的软件系统。接下来,我们将深入探讨两种经典的设计模式:适配器模式单例模式

适配器模式(Adapter Pattern)

1. 应用场景

假设存在一个旧系统,原本使用特定接口与系统其他部分交互。当需要引入一个新的第三方库时,问题出现了:新库采用了完全不同的 API,与旧系统适用的接口不兼容。

此时,直接修改旧系统的代码以适应新接口是一种高风险且不推荐的做法,尤其是对于大型复杂的旧系统,可能会引发一系列难以预料的问题。适配器模式提供了一种优雅的解决方案:它允许我们创建一个适配器(新的封装类),使不兼容的类能够协同工作。

2. 核心原理

适配器模式利用接口,将不兼容的接口进行转换,使其能够被客户端正确解析。通过这种方式,它在不兼容的类之间架起了一座桥梁,实现了不同接口之间的通信,而无需修改原有客户端代码。

3. 代码实战

旧系统接口与客户端代码

旧系统使用 LegacyVideoController 接口来控制视频系统:

public interface LegacyVideoController {
    /**
     * Begins the playback after startTimeTicks
     * from the beginning of the video
     * @param startTimeTicks time in milliseconds
     */
    public void startPlayback(long startTimeTicks);
    // 其他可能的方法...
}

客户端使用该控制器的方式如下:

public void playBackVideo(long timeToStart, LegacyVideoController controller) {
    if (controller != null) {
        controller.startPlayback(timeToStart);
    }
}

用户需求变更与新接口

随着业务发展,新的视频控制器接口 AdvancedVideoController 被引入:

public interface AdvancedVideoController {
    /**
     * Places the controller head after time
     * from the beginning of the track
     * @param time time defines how much seek is required
     */
    public void seek(Time time);

    /**
     * Plays the track
     */
    public void play();
}

这导致原有的客户端代码失效,因为新接口与旧接口不兼容。

适配器类的实现

为解决接口不兼容问题,我们创建一个适配器类 AdvancedVideoControllerAdapter

public class AdvancedVideoControllerAdapter implements LegacyVideoController {
    private AdvancedVideoController advancedVideoController;

    public AdvancedVideoControllerAdapter(AdvancedVideoController advancedVideoController) {
        this.advancedVideoController = advancedVideoController;
    }

    @Override
    public void startPlayback(long startTimeTicks) {
        // 将 long 类型转换为 Time 类型(假设存在相应转换方法)
        Time startTime = getTime(startTimeTicks);

        // 适配操作
        advancedVideoController.seek(startTime);
        advancedVideoController.play();
    }
}

适配器实现了目标接口 LegacyVideoController,这样无需更改客户端代码。适配器类持有需要兼容的接口 AdvancedVideoController 的实例,通过 has-a 关系将客户端请求转发给实际实例。

适配器的使用与优势

现在,我们可以将新对象封装到适配器中,无需修改客户端代码,因为新对象已通过适配器兼容了旧接口:

AdvancedVideoController advancedController = controllerFactory.createController();
// 适配
LegacyVideoController controllerAdapter = new AdvancedVideoControllerAdapter(advancedController);
playBackVideo(20, controllerAdapter);

适配器不仅可以简单地传递值,还能根据需要支持的接口复杂度提供扩展功能。如果目标接口复杂,需要多个类来实现新功能,适配器也可以封装多个对象。

4. 与其他模式的比较

模式核心目的接口变化结构关系
适配器 (Adapter)使不兼容接口能够协同工作改变接口供客户端使用持有被适配者实例
装饰器 (Decorator)动态给对象添加新功能增强接口功能包装对象,接口通常一致
外观 (Facade)为复杂子系统提供统一接口简化接口,隐藏复杂性持有子系统多个实例
代理 (Proxy)控制对目标对象的访问接口通常与目标一致持有目标对象实例
桥梁 (Bridge)分离抽象与实现,使其独立变化抽象与实现分离组合抽象与实现部分
  • 装饰模式:主要用于给对象添加新功能,会改变对象的接口行为,通过封装对象来实现功能增强。
  • 外观模式:将一个或多个复杂子系统的接口抽象为一个更简单的统一接口,隐藏子系统的复杂性。
  • 代理模式:为目标对象提供代理,通常用于控制访问、增强功能或资源管理,代理与目标实现相同接口。
  • 桥梁模式:核心是将抽象部分与实现部分分离,使它们可以独立变化,不涉及接口适配。

单例模式(Singleton Pattern)

1. 概念与应用场景

单例模式旨在确保一个类只有一个实例,并提供全局访问点。在许多应用场景中,如应用层缓存、线程池、数据库连接等,只需要一个实例就足以满足需求。若存在多个实例,可能会导致资源浪费、数据不一致等问题,甚至影响系统的稳定性和功能实现。

2. 实现方式

基本实现框架(Java)

public class ApplicationCache {
    private Map<String, Object> attributeMap;
    // 静态实例
    private static ApplicationCache instance;

    // 静态访问方法
    public static ApplicationCache getInstance() {
        if (instance == null) {
            instance = new ApplicationCache();
        }
        return instance;
    }

    // 私有构造函数
    private ApplicationCache() {
        attributeMap = createCache(); // 初始化缓存
    }
}

在这个例子中,类中有一个与类类型相同的静态成员,通过静态方法 getInstance() 获取实例。采用了延迟初始化(Lazy Initialization)策略,即直到运行时需要实例时才创建。构造函数被设为私有,防止通过 new 关键字创建多个实例。

多线程环境下的考虑 - 双重检查锁定

在多线程环境中,上述基本实现可能会出现问题。为确保初始化代码只执行一次,可使用双重检查锁定机制(Double-Checked Locking,适用于 Java 5.0 及以上版本):

public class ApplicationCache {
    private Map<String, Object> attributeMap;
    // 使用 volatile 关键字,防止 JVM 乱序写操作
    private static volatile ApplicationCache instance;

    public static ApplicationCache getInstance() {
        // 第一次检查
        if (instance == null) {
            // 同步类级锁
            synchronized (ApplicationCache.class) {
                // 第二次检查
                if (instance == null) {
                    instance = new ApplicationCache();
                }
            }
        }
        return instance;
    }

    private ApplicationCache() {
        attributeMap = createCache(); // 初始化缓存
    }
}

通过将 instance 变量声明为 volatile,避免了 JVM 的指令重排序问题。在初始化时进行两次 null 检查,有效防止多个线程创建多个实例。虽然也可以选择同步整个静态方法,但这样会增加不必要的性能开销,因为在初始化完成后,后续访问不再需要同步。

另一种实现方式 - 饿汉式(不使用延迟初始化)

如果不考虑延迟初始化的好处,还可以采用更简单的实现方式:

public class ApplicationCache {
    private Map<String, Object> attributeMap;
    // 在声明时初始化
    private static ApplicationCache instance = new ApplicationCache();

    public static ApplicationCache getInstance() {
        return instance;
    }

    // 私有构造函数
    private ApplicationCache() {
        attributeMap = createCache(); // 初始化缓存
    }
}

在类加载时就初始化变量,调用私有构造函数创建实例,保证只有一个实例存在。这种方式代码更简洁,但失去了延迟初始化的特性,可根据项目实际需求选择合适的实现方式。

3. 注意事项

  • 反射(Reflection)问题:Java 的反射 API 可以调用私有构造函数,这可能导致创建多个实例。为防止这种情况,可以在构造函数中抛出异常来阻止反射创建额外的实例。
  • 序列化(Serialization)问题:序列化和反序列化过程可能会创建两个不同的实例。可以通过重写序列化 API 中的 readResolve() 方法来解决,确保反序列化时返回的是单例实例。

4. 设计模式与语言无关性

虽然本文以 Java 为例介绍设计模式,但实际上设计模式是与编程语言无关的。它们是解决软件设计中常见问题的通用最佳实践方法。例如,在 JavaScript 中实现单例模式的概念与 Java 相同,但具体实现方式会因语言特性而异:

var applicationCache = function() {
    // 私有变量
    var instance;

    function initCache() {
        return {
            proxyUrl: "/bin/getCache.json",
            cachePurgeTime: 5000,
            permissions: {
                read: "everyone",
                write: "admin"
            }
        };
    }

    // 公共接口
    return {
        getInstance: function() {
            if (!instance) instance = initCache();
            return instance;
        },
        purgeCache: function() {
            instance = null;
        }
    };
};

此外,像 jQuery 等库也大量使用了设计模式,如 Facade 设计模式,用于隐藏子系统的复杂性,为用户提供更简单易用的接口。

总结与建议

1. 设计模式的合理运用

设计模式是软件开发中的强大工具,但并非所有问题都需要使用设计模式来解决。在实际应用中,应根据具体情况仔细分析,避免过度使用设计模式。过度使用可能会导致代码过于复杂,增加维护成本。

2. 学习设计模式的意义

学习设计模式有助于深入理解其他类库和框架的设计思想。许多流行的类库和框架,如 jQuery、Spring 等,都广泛应用了各种设计模式。掌握设计模式能够更好地阅读和使用这些类库,提高开发效率和软件质量。

希望通过本文的介绍,读者能对设计模式有更深入的理解和认识。如果在学习或使用设计模式过程中有任何疑问或需要进一步了解的内容,欢迎留言交流。

说明:文中关于单例模式的双重检查锁定(DCL)实现依赖于 Java 5.0 及以上版本的内存模型(JSR-133),在早期版本中 volatile 语义可能不足以保障线程安全。