引言

生命中唯一不变的事实就是世事时刻在变,这在软件开发的每一个阶段都不可避免。我们所要面对的挑战是:如何以最小的延迟和最大的灵活性来适应变化。

令人欣慰的是,许多设计问题前人已经解决,并且其方法已形成了最佳实践。这些公认为最佳实践的方法就是"设计模式"。今天我们将研究两个最流行的设计模式,学习怎样使用好的设计让你的代码更干净、扩展性更好。

适配器模式 (Adapter Design Pattern)

假设你有一个旧系统,现在需要让它适应新的第三方库,但是这个库使用的是完全不同的 API。旧系统适用的接口与新库的接口完全不同。当然,若你够勇敢,可以修改旧的代码以适用新的接口。但对于所有旧系统来说,千万不要这么做。

mismatch.jpg

适配器模式可以解决这一问题!你可以简单地写一个适配器(新的封装类)。

好的设计不仅是可以重复使用,还要具有可扩展性。

适配器使用了接口,并且将其转换成客户端可以解析的接口,使不兼容的类联系在一起。

adapterWorks.jpg

实战适配器设计模式

闲话少说,我们来实战演习。假设旧系统使用的是下面的 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);
    }
}

用户需求变了!

用户需求改变并不是什么新鲜事——这时常发生。用户需求总是在变,而我们的系统需要适应新的视频控制器,接口如下:

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();
}

此时客户端代码失效了,因为新的接口不再兼容。

适配器救了我们

那么我们如何处理这个改变了的接口,而不改变我们旧的代码呢?你知道答案了,不是吗?我们写个简单的适配器类,就像这样:

public class AdvancedVideoControllerAdapter implements LegacyVideoController {

    private AdvancedVideoController advancedVideoController;

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

    @Override
    public void startPlayback(long startTimeTicks) {
        // Convert long into DateTime
        Time startTime = getTime(startTimeTicks);

        // Adapt
        advancedVideoController.seek(startTime);
        advancedVideoController.play();
    }
}

适配器实现了目标的接口,所以不需要更改客户端代码。我们的适配器类中含有需要兼容的接口(AdvancedVideoController)的实例。

这种"has-a"的关系让适配器将客户端的请求发给实际的实例。

适配器也能够减少客户端和实现代码之间的耦合。

现在我们可以简单地将新对象封装到适配器中去,而且不需要更改客户端代码,因为这个新对象已经兼容了以前的接口。

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

适配器可以简单地传值,也可以提供一些扩展,取决于需要支持的接口的复杂度。类似地,如果目标接口很复杂,新的功能需要切分成多个类,适配器也可以封装不止一个对象。

和其他模式的比较

  • 装饰模式 (Decorator):装饰模式改变了接口,将对象封装起来,加上新的功能。而适配器模式是将被适配的接口转换成能够被客户端代码理解的目标接口。
  • 外观模式 (Facade):外观模式是将之前的接口的复杂性抽象化形成的全新的接口,而适配器模式是将一个接口转换成另外一个,可以让不兼容的接口之间可以互相沟通。
  • 代理模式 (Proxy):代理模式提供同样的接口,而适配器模式提供不一样的接口。
  • 桥梁模式 (Bridge):桥梁模式使得抽象和实现独立起来,而适配器是用来将客户端的请求转发到被适配者,以适应已有的接口。

单例模式 (Singleton Design Pattern)

虽然有许多创建对象的模式存在,但有一个模式为大家所熟知。今天我们来看看最简单也是容易弄错的单例模式。

就如同它的名字,单例只创建类的一个实例并提供全局化的访问。应用的例子可以是应用层的缓存、线程池、连接等。对这些应用而言,有且只有一个实例已经足够,而且如果多过一个会影响稳定性,甚至不能实现应用的功能。

实现单例模式

用 Java 实现的最基本的框架如下:

public class ApplicationCache {

    private Map<String, Object> attributeMap;
    // Static instance
    private static ApplicationCache instance;

    // Static accessor method
    public static ApplicationCache getInstance() {
        if (instance == null) {
            instance = new ApplicationCache();
        }
        return instance;
    }

    // private Constructor
    private ApplicationCache() {
        attributeMap = createCache(); // Initialize the cache
    }
}

我们的例子中有一个和类同样类型的静态成员,可以由静态方法 (getInstance()) 获得。我们使用了延迟初始化 (Lazy Initialization),直到运行时需要时才实例化 cache。构造器是 private 的,所以不可以通过 new 来创建类的实例。为了获得 cache,我们调用:

ApplicationCache cache = ApplicationCache.getInstance();
// use cache to improve performance

如果是单线程,上面的代码没什么问题。但事情没有那么简单。在多线程环境下,你要么同步延迟初始化 (lazy initialization),或者不用延迟初始化,在加载类的时候就创建 cache。通过使用静态程序块 (static block) 或者初始的时候声明 cache 可以做到。

双重检查锁定

我们通过延迟初始化来确保初始化的代码段只运行了一次。下面的代码在 Java 5.0 及以上版本运行得很好,因为 synchronizedvolatile 特性已经实现了正确的内存语义。

public class ApplicationCache {

    private Map<String, Object> attributeMap;
    // volatile so that JVM out-of-order writes do not happen
    private static volatile ApplicationCache instance;

    public static ApplicationCache getInstance() {
        // Checked once
        if (instance == null) {
            // Synchronized on Class level lock
            synchronized (ApplicationCache.class) {
                // Checked again
                if (instance == null) {
                    instance = new ApplicationCache();
                }
            }
        }
        return instance;
    }

    private ApplicationCache() {
        attributeMap = createCache(); // Initialize the cache
    }
}

我们让 instance 变量 volatile,这样 JVM 避免了乱序写操作 (out-of-order writes)。在对初始化作同步时,对 instance 是否是 null 检查了两次,这样就避免了两个以上的线程创建多过一个 cache 的实例。我们也可以同步整个静态方法,但这种做法又太过了,因为我们在初始化之前只会调用一次,之后就不需要再同步了。

不用延迟初始化

更简单的方法是不使用延迟初始化,下面的代码看起来更简洁些:

public class ApplicationCache {

    private Map<String, Object> attributeMap;
    // Initialized while declaration
    private static ApplicationCache instance = new ApplicationCache();

    public static ApplicationCache getInstance() {
        return instance;
    }

    // private Constructor
    private ApplicationCache() {
        attributeMap = createCache(); // Initialize the cache
    }
}

加载类的时候变量会初始化,这样会调用私有的构造器来创建实例,使得只有一个 cache 实例。我们虽然没有了延迟初始化所带来的好处,但我们的代码更简洁些。两种方法都是线程安全的,你可以根据你的项目环境任选一种。

小心反射 (Reflection) 和序列化 (Serialization)

根据你的需求,你可能要小心:

  • 反射 (Reflection):API 会调用私有的构造器,所以为了防止创建超过一个实例,可以通过从构造器抛出异常来解决。
  • 序列化 (Serialization):序列化和反序列化可能会创建两个不同的实例,可以通过重写序列化 API 中的 readResolve() 方法来解决。

设计模式与语言无关

我承认这个教程的标题有点误导,因为设计模式是与编程语言无关的。它们是那些解决软件设计中的重复问题的最佳方法的集合。

举个例子,下面是 Javascript 对单例模式的实现。概念是一样的:对创建对象进行控制和一个全局的访问,但实现随着语言的不同而不同。

var applicationCache = function() {

    // Private stuff
    var instance;

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

    // Public
    return {
        getInstance: function() {
            if (!instance) instance = initCache();
            return instance;
        },
        purgeCache: function() {
            instance = null;
        }
    };
};

另一个例子是 jQuery 也使用了大量的 Facade 设计模式,隐藏了子系统的复杂性,将更简化的接口呈现给用户。

结束语

不是所有的问题都需要某个设计模式来解决

要提醒一句:不要过度使用设计模式!不是所有的问题都需要某个设计模式来解决。你需要在使用之前仔细分析。学习设计模式有助于理解其他类库如 jQuery、Spring 等等,它们都大量使用了设计模式。

我希望读了这篇文章之后,你能更了解设计模式。如果你有什么问题或者想知道更多的设计模式,请留言,我会尽力解答!

英文原文:tutsplus


说明:文中关于 Java 单例模式的双重检查锁定(Double-Checked Locking)涉及 volatile 关键字的内存语义,该特性在 Java 5.0 及以上版本中得到正确支持。若使用更低版本,请注意线程安全问题。