Java常用设计模式

设计原则

  • 找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起,把会变化的部分取出并”封装”起来,以便以后可以轻易地改动或扩充此部分,好让其他部分不会受到影响。

  • 针对接口编成,而不是针对实现编成
    这样的做法异于以往,以前的做法是:行为来自超类的具体实现,或是继承某个接口并由子类自行实现而来。这两个做法都是依赖于”实现”,我们被实现绑得死死的,没办法更改行为(除非写更多的代码)

“针对接口编程”真正的意思是”针对超类型(supertype)编程”

这里所谓的”接口”有多个含义,接口是一个”概念”,也是一种Java的interface构造,你可以在不涉及Java interface的情况下,”针对接口编程”,关键就在多态。利用多态,程序可以针对超类型编程,执行时会根据实际状况执行到真正的行为,不会被绑死在超类型的行为上。”针对超类型编程”这句话,可以更明确地说成”变量的声明类型应该是超类型”,通常是一个抽象类或者是一个接口,如此,只要是具体实现此超类型的类所产生的对象,都可以指定给这个变量。这也意味着,声明类时不用理会以后执行时的真正对象类型

  • 多用组合,少用继承

当你将两个类结合起来使用,这就是组合(composition)。这种做法和”继承”不同的地方在于,鸭子的行为不是继承来的,而是和适当的行为对象”组合”来的,如你所见,使用组合建立系统具有很大的弹性,不仅可将算法族封装成类,更可以”在运行时动态地改变行为”,只要组合的行为对象符合正确的接口标准即可利用继承设计子类的行为,是在编译时静态决定的,而且所有的子类都会继承到相同的行为。然而,如果能够利用组合的做法扩展对象的行为,就可以在运行时动态地进行扩展

策略模式

定义了算法族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。

设计原则

为了交互对象之间的松耦合设计而努力
松耦合的设计之所以能让我们建立有弹性的OO系统,能够应对变化,是因为对象之间的互相依赖降到了最低。

观察者模式

定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。关于观察者的一切,主题只知道观察者实现了某个接口(也就是Observer接口),主体不需要知道观察者的具体类是谁,做了些什么或其他任何细节。
任何时候我们都可以增加新的观察者,因为主题唯一依赖的东西是一个实现Observer接口的对象列表,所以我们可以随时增加观察者。事实上,在运行时我们可以用新的观察者取代现有的观察者,主题不会受到任何影响。同样的,也可以在任何时候删除某些观察者。

有新类型的观察者出现时,主题的代码不需要修改。假如我们有个新的具体类需要当观察者,我们不需要为了兼容新类型而修改主题的代码,所有要做的就是在新的类里实现此观察者接口,然后注册为观察者即可。主题不在乎别的,它只会发送通知给所有实现了观察者接口的对象。

改变主题或观察者其中一方,并不会影响另一方,因为两者是松耦合的,所以只要它们之间的接口仍被遵守,我们就可以自由地改变它们。

关于Java自带的观察者模式 java.util.Observable

  • java.util.Observable的黑暗面
    可观察者是一个”类”而不是一个”接口”,更糟的是,它甚至没有实现一个接口。不幸的是,java.util.Observable的实现有许多问题,
    限制了它的使用和复用,这并不是说它没有提供有用的功能,我们只是想提醒大家注意一些事实

  • Observable是一个类
    首先,因为Observable是一个”类”,你必须设计一个类继承它。如果某类想同时具有Observable类和另一个超类的行为,就会陷入两难,
    毕竟Java是不支持多继承的,这限制了Observable的复用能力。

  • 再者,因为没有Observable接口,所以你无法建立自己的实现。和java内置的Observable API搭配使用,也无法将java.uril的实现换成
    另一套做法的实现。比如说下面这个

  • Observable将关键的方法保护起来
    如果你看看Observable API,你会发现setChanged()方法被保护起来了,这意味着,除非你继承自Observable,否则你无法创建Observable实
    例并组合到你自己的对象中来,这个设计违反了第二个设计原则”多用组合,少用继承”

Summary

  • 1)观察者模式定义了对象之间一对多的个系
  • 2)主题(也就是可观察者)用一个共同的接口来更新观察者
  • 3)观察者和可观察者之间用耦合方式结合(loosecoupling),可观察者不知道观察者的细节,只知道观察者实现了观察者接口
  • 4)使用此模式时,你可从被观察者处推(push)或拉(pull)数据(然而,推的方式被认为更”正确”)
  • 5)有多个观察者时,不可以依赖特定的通知次序
  • 6)Java有多种观察者模式的实现,包括了通用的java.util.Observable
  • 7)要注意java.util.Observable实现上所带来的一些问题
  • 8)如果有必要的话,可以实现自己的Observable,这并不难,不要害怕
  • 9)Swing大量使用观察者模式,许多GUI框架也是如此
  • 10)此模式也被应用在许多地方,例如:JavaBeans,RMI

设计原则

1)类应该对扩展开放,对修改关闭(开放-关闭原则)
装饰者模式完全遵循开放-关闭原则,

装饰者模式

动态地将责任附加到对象上。若要扩展功能,装饰者提供了比继承更有弹性的替代方案

Java IO里的装饰者模式:
+ 1) LineNumberInputStream 加上了计算行数的能力
+ 2) BufferedInputStream 加入两种行为:利用缓冲输入来改进性能;用一个readLine()方法(用来一次读取一行文本输入数据)
来增强接口

BufferedInputStream和LineNumberInputStream都扩展自FilterInputStream,而FilterInputStream是一个抽象的装饰类

FAQ

F:如果我将代码针对特定种类的具体组件(例如House-Blend),做一些特殊的事(例如,打折),我担心这样的设计是否恰当。
因为一旦用装饰者包装HouseBlend,就会造成类型改变

A:的确是这样,如果你把代码写成依赖于具体的组件类型,那么装饰者就会导致程序出现问题。只有在针对抽象组件类型编程时,
才不会因为装饰者而受到影响。但是,如果的确针对特定的具体组件编程,就应该重新思考你的应用架构,以及装饰者是否适合

Summary

1)继承属于扩展形式之一,但不见得是达到弹性设计的最佳方式

2)在我们的设计中,应该允许行为可以被扩展,而无须修改现有的代码

3)组合和委托可用于在运行时动态地加上新的行为

4)除了继承,装饰者模式也可以让我们扩展行为

5)装饰者模式意味着一群装饰者类,这些类用来包装具体组件

6)装饰者反映出被装饰的组件类型(事实上,他们具有相同的类型,都经过接口或继承实现)

7)装饰者可以在被装饰者的行为前面与/或后面加上自己的行为,甚至将被装饰者的行为整个取代掉,而达到特定的目的

8)你可以用无数个装饰者包装一个组件

9)装饰者一般对组件的客户是透明的,除非客户程序依赖于组件的具体类型

10)装饰者会导致设计中出现很多小对象,如果过度使用,会让程序变得很复杂

门面模式

意图是将复杂的业务逻辑封装到高层接口中,从而简化了对子系统的访问。其实现方式通常是将相关联的方法调用组织到一起,然后在一个方法中按照顺序来调用这些方法。从高层角度来看,每个API都可以看作是门面模式的一种实现,因为它们都提供了用于隐藏复杂性的简单接口。对API方法的任何调用都会导致隐藏在其后的众多子系统方法的调用。javas.servlet.http.HttpSession接口就是门面的一个示例。它将维护会话相关的复杂逻辑隐藏起来,同时又通过少量易于使用的方法来公开其功能。

使用情形

门面模式常常用于如下目的与情形:
+ 为遗留的后端系统提供简单且统一的访问
+ 为类创建一个公开的API,如驱动程序
+ 为可用服务提供粗粒度的访问。服务会被组合,就像洗衣机一样
+ 减少网络调用。门面会对子系统发起多次调用,而远程客户端只对门面进行一次调用
+ 将应用的流程与内部细节封装起来,从而提升安全性并简化操作

注意,门面有时也被实现为单例抽象工厂

实现门面模式并不复杂。它并没有强制要求严格的结构或是规则集。为复杂流程提供简单访问的任何方法都可以看成是门面模式的实现。

作用及好处

  • 降低了耦合度,因为客户端对子系统一无所知
  • 在变更时增强了可维护性与可管理性
  • 实现功能重用,因为门面模式鼓励控制重用以及细粒度逻辑
  • 每次方法调用都会调用相同的方法,从而确保了服务执行的一致性
  • 分组相关方法并从一个方法调用中来调用它们,从而降低业务逻辑复杂度
  • 中央化的安全与事务控制管理
  • 可测试性与可模拟性的模式实现

这种实现称为POJO门面,它与本章后面将要介绍的有状态和无状态实现是不同的

何时以及何处该使用门面模式

门面模式应该用于高层封装复杂业务逻辑,并通过一个API提供简洁的访问单点。如果要为其他人提供一个接口或API,请首先考虑逻辑的复杂性以及可能会出现的变更。门面模式在提供一个整洁的API的同时又隐藏掉可能会发生变化的部分方面很有优势。不过,毫无必要地将方法包装到门面中却不是好的做法,还会增加不必要的层次。不当的封装可能会导致太多的调用以及毫无价值的层次。在实现会话门面时,你需要确定用例是否需要维持状态。只调用门面的一个方法来接受所需服务的用例是非对话式的,因此没必要在方法调用间保持会话状态。你应该将这种门面实现为无状态会话Bean。
另一方面,如果必须在方法调用间维持会话状态,那么最恰当的方式就是将这种门面实现为有状态会话Bean。你需要注意到有状态会话门面的使用,因为它会占用服务器资源,直到客户端触发会话将其释放掉或是超时。这意味着在大多数时间内,有状态会话Bean门面会绑定到客户端,但却什么都没做;它仅维持着状态并使用资源。与无状态会话Bean门面不同的是,它无法在其他客户端之间重用和共享,因为每个请求都会创建无状态门面的新实例,并且维持着该客户端会话的状态。
因此,在使用该模式的时候要小心,请仔细分析用例并做恰当的决定。

小结

可将门面模式实现为POJO,无状态会话Bean,或是有状态会话Bean。实现门面模式的各种方法解决了不同用例场景下的不同问题。不过,各种实现并没有背离其主要意图:为复杂的子系统提供一个高层次,简单的接口。
在决定将门面实现为有状态会话Bean时要小心,请确保它导致资源消耗的问题。
设计良好的应用会充分利用门面模式来封装复杂逻辑,并将子系统与客户端解耦:不过,对门面模式的不当使用和过度使用会导致更加复杂,拥有多个层次的系统。
会话门面模式类似与实体-控制-边界架构模式中的边界,并且也与适配器和包装模式相关。

单例模式

单例类可以保证其类型只会生成一个实例。只拥有一个实例在很多时候是很有用的,比如说全局访问以及缓存代价高昂的资源;不过,如果在多线程环境下使用单例,那就有可能引入一些竞态条件问题。由于大多数编程语言并未提供创建单利的内置机制,因此开发者需要自己实现。不过,javaEE提供了一种内建机制,开发者可以通过为类添加注解来方便的创建单例。

使用场景

  • 跨越整个应用程序域来访问共享数据,比如配置数据
  • 只加载并缓存代价高昂的资源一次,这样可以做到全局共享访问并改进性能
  • 创建应用日志实例,因为通常情况下只需要一个即可
  • 管理实现了工厂模式的类中的对象
  • 创建门面对象,因为通常情况下只需要一个即可
  • 延迟创建静态类,单例可以做到延迟实例化

Spring在创建Bean时使用了单例(默认情况下,Spring Bean是单例的),JavaEE在内部会使用单例,比如说在服务定位器中。JavaSE也在Runtime类的实现中使用了单例模式。因此,如果在正确上下文中使用了单例,那么单利还是非常有用的。
不过,过度使用单例模式意味着不必要的资源缓存,无法让垃圾收集器回收对象并释放宝贵的内存资源。此外,这么做意味着你无法充分利用对象创建与继承的好处。大量使用单例模式其实是一种糟糕的面向对象设计的信号,这会导致内存与性能问题。另一个问题就是单例对於单元测试来说并不友好。

单例模式的几种实现方法

private static MySingleton instance;
private MySingleton(){}
public static MySingleton getInstance(){
    if (null == instance){
        instance = new MySingleton();
    }
    return instance;
}

要解决竞态条件问题,你需要获得一把锁,并且在实例返回后才释放。

private static MySingleton instance;
private MySingleton(){}
public static synchronized MySingleton getInstance(){
    if (null == instance){
        instance = new MySingleton();
    }
    return instance;
}

另一种手段就是在加载类的同时创建单例实例,这样就不必同步单例实例的创建,并在JVM加载完所有类时就创建好单例对象(因此,这是在类调用getInstance()方法前发生的)。之所以可以这样,是因为静态成员与静态块是在类加载时执行的

private final static MySingleton instance = new MySingleton();
private MySingleton();
public static MySingleton getInstance(){
    return instance;
}

还可以使用静态块,不过,这会导致延迟初始化,因为静态块是在构造方法调用前执行的

private static MySingleton instance = null;
static {
    instance = new MySingleton();
}
private MySingleton(){}
public static MySingleton getInstance(){
    return instance;
}

双重检测锁是另一种非常流行的创建单例的机制,它要比其他方法更加安全,因为它会在锁定单例类之前检查一次单例的创建,在对象创建之前再一次检查

private volatile MySingleton instance;
private MySingleton(){}
public MySingleton getInstance(){
    if (null == instance){
        synchronized(MySingleton.class){
        if (null == instance){
            instance = new MySingleton();
        }
    }
    }
    return instance;
}

不过,这些方法都不是绝对安全的。比如说,开发者可以通过Java Reflection API将构造方法的访问修饰符改为public的,这样就可以再次创建单例了。在Java中,创建单例的最佳方式是使用Java5中引入的枚举类型,如下代码所示,也是Joshua Bloch在其著作Effective Java中所极力推荐的方法。枚举类型本质上就是单例的,因此JVM会处理创建单例所需的大部分工作。这样,通过使用枚举类型,你就无需再处理同步对象创建与提供等工作了,还能避免与初始化相关的问题

public enum MySingletonEnum {
    INSTANCE;
    public void doSomethingInteresting(){}
}

在该示例中,对单例对象的引用是通过如下方式获得的

MySingletonEnum mse = MySingleEnum.INSTANCE;

一旦拥有了单例的引用,你就可以像下面这样调用它的任何方法

mse.doSomethingInteresting();:w

依赖(Dependency Injection, DI)注入与CDI

JavaEE旨在处理最复杂的系统,不过它造成了开发的过于复杂。即便对于小型系统也是如此,因此未能达成所愿。J2EE最初的设计依赖于极高的复杂性与紧耦合,这导致了诸如Spring与Pico容器等的逐步流行。大多数厂商并不支持和鼓励开发者使用J2EE容器。然而,不久之后轻量级容器横空出世,官方对其提供了支持,更有甚者,Spring成为了非官方事实上的标准,并导致企业级Java完全的重新设计。

依赖注入的优势

  • 客户端不必关注被注入资源的不同实现,这使得设计变更变得更加容易
  • 可以轻松使用模拟对象实现单元测试
  • 配置外化,降低了变更的影响
  • 松耦合的架构支持可插拔的结构

DI背后的基本思想是改变对象创建的地点,使用注入器在恰当的时刻将指定实现注入目标对象中。这看起来像是工厂模式的一种实现,不过整个概念并不止于简单的对象创建。控制反转(IoC)改变了对象之间的整个连接,并且让注入器完成这些工作(很多时候都是很神奇的)。相对于采用工厂将实现提供给调用者,注入器会更主动的确认目的对象何时需要目标对象,并以恰当的方式执行注入。

使用普通代码实现DI

在没有EJB容器的帮助下,Java并未提供标准的DI实现,知道上下文与依赖注入(CDI)的引入才改变了这一现状。虽然有多种DI框架,比如说Spring和Guice,但编写一个基本的实现也不是难事。最简单的DI实现是一个工厂,它通过getInstance()方法在请求到来时创建依赖。

class UserService {
    private UserDataRepository udr;

    UserService(){
        this.udr = new UserDataRepositoryImpl();
    }

    public void persisUser(User user){
        udr.save(user);
    }
}
public interface UserDataRepository{
    public void save(User user);
}
public class UserDataRepositoryImpl implement UserDataRepository {
        @Overside
    public void save(User user){
        // Persistence code here
    }
    }
}
public class User {
    //User Specific Code Here
}

以上代码中,UserService类提供了用于用户管理的业务逻辑服务,比如说将用户持久化到数据库中。在该实例中,对象创建工作是在构造方法中完成的。这耦合了业务逻辑(类的行为)与对象的创建。接下来进行重构,将对象创建从类中剥离出来放到工厂中。以下代码创建了UserDataRepository的一个实现并将其传递给UserService类的构造方法。修改UserDataRepository类的构造方法以接受这个参数

public class UserServiceFactory{
    public UserService getInstance(){
        return new UserService(new UserDataRepositoryImpl());
    }
}

以下代码中,UserService构造方法需要一个UserDataRepository实例注入到构造方法中。UserService类实现了与UserDataRepositoryImpl类的解耦。工厂现在负责对象的创建,并将实现注入到UserService的构造方法中,现在成功实现了业务逻辑与对象创建的解耦。

class UserService {
    private UserDataRepository udr;
    UserService (UserDataRepository udr){
        this.udr = udr;
    }

    public void persistUser(User user){
        udr.save(user);
    }
}

使用JavaEE实现DI

JavaEE 5之前,J2EE并未提供开箱即用的DI。相反,在J2EE中,Bean与资源是通过JNDI(Java命名与目录接口)上下文查找来访问的。这种方式导致了硬连接,并且依赖于重量级的基于服务器的容器,这使得测试要比编写实际代码还要困难。随着JavaEE 5于EJB 3的发布,DI成为了企业级Java平台的不可分割的一部分。为了摆脱XML的配置,JavaEE新增了几个注解来执行注入:
+ @Resource(JSR250),用于注入数据源,Java消息服务(JMS),URL,邮件与环境变量
+ @EJB(JSR220),用于注入EJB
+ @WebServiceRef,用于注入Web Service

随着JavaEE 6,CDI与EJB 3.1的发布,DI成为了Java EE中功能更强,也更有趣的一个主题。在EJB 3.1中,接口对于EJB来说不再是强制的了。此外,新引入的EJB Web Profile提供了简化,量级更轻的EJB容器。新引入的改进的注入注解@Inject(JSR229与JSR330)也为Java领域中其他DI框架间的注入提供了公共接口。@Inject注解DI是类型安全的,因为它会根据对象引用的类型注入依赖。如果要重构以上代码,则需要删除掉构造方法,并将@Inject注解添加到UserDataRepository字段,如下代码所示

class UserService {
    @Inject
    private UserDataRepository udr;

    public void persistUser(User user){
        udr.save(user);
    }
}

CDI容器构建了一UserDataRepositoryImpl实例作为容器管理的Bean,如果在类型为UserDataRepository的字段上发现了@Inject注解,那么就将其注入进去。你可以将容器管理器的bean注入到构造方法,方法与字段中,无论其访问修饰符是什么,不过字段不可以是final,方法也不能是抽象的。这引发一个问题。如果UserDataRepository接口有多个实现会出现什么情况?CDI容器如何识别出要注入的正确实现?为了消除UserDataRepository接口具体实现间的歧义,你可以使用开发者定义的限定符来注解具体的类。假设有UserDataRepository接口的两个实现:一个用于MongoDB集合,另一个用于MySQL数据库。你需要创建两个限定符(一个用于Mongo实现,另一个用于MySQL实现),具体类要在类级别上使用相关的限定符进行注解,同时在注入UserDataRepository的类中要有一个字段使用相同的限定符进行注解。如果重构代码如上所示中的UserService类以使用UserDataRepository的Mongo实现,那么需要像下面这样将@Mongo注解添加到udr字段上

@Inject Mongo

工厂模式

工厂模式有两种形式:工厂方法和抽象工厂。它们的意图是一样的:提供一个接口,在不指定具体类的情况下创建相关或依赖的一系列对象。

何为工厂

作为一种创建型模式,工厂的目的在于创建对象。创建的逻辑被封装在工厂中,要么提供一个方法来返回新创建的对象(工厂方法模式),要么将对象的创建委托给子类(抽象工厂模式)。无论哪种情况,对象的创建都从对象的使用中抽离出来了。客户端不必考虑接口或类的不同实现,它只需通工厂(工厂方法或抽象工厂)获取接口实现的一个实例即可,这样客户端与对象的创建就实现了解耦。解耦是应用依赖反转原则的结果,这带来了很多好处,其中最重要的好处就是实现了高层类与底层类之间的解耦。通过解耦,具体类实现的变化不会影响到客户端,这降低了类与类之间的耦合,并提升了灵活性。通过将创建对象的代码封装起来,我们可以借助工厂模式实现对象创建与底层系统间的解耦。通过这种方式,我们在重构时就会轻松很多,因为重构的改变只会对一个点产生影响。
通常情况下,工厂本身会被实现为单例或是静态类,因为一般来说只需要一个工厂实例即可。这么做会将工厂对象的创建集中到一个地方,在对代码进行修改和更新时有利于更好地组织和维护,并减少了错误。

依赖反转原则:
+ 高层模块不应该依赖于底层模块,它们都应该依赖于抽象
+ 抽象不应该依赖于细节,细节应该依赖于抽象

工厂方法

定义一个用于创建对象的接口,不过让子类决定实例化哪个类。工厂方法将类的实例化推迟到了子类。工厂极大降低了new关键字的使用次数,并且将初始化过程与不同的具体实现封装起来。将这些需求中心化可以极大减少向系统中添加或删除具体类的影响以及具体的类依赖的影响。

普通代码实现工厂方法

public abstract class DrinksMachine {
    public abstract Drink dispenseDrink();
    public String displayMessage(){
        return "Thank for your custom.";
    }
}
public class CoffeeMachine extends DrinksMachine {
    public Drink dispenseDrink(){
        return new Coffee();
    }
}
public class SoftDrinksMachine extends DrinksMachine {
    public Drink dispenseDrink(){
        return new SoftDrink();
    }
}
public interface Drink{}
public class SoftDrink implements Drink {
     SoftDrink() {
         System.out.println("Soft Drink");
     }
}
public class Coffee implements Drink {
    Coffee(){
        System.out.println("Coffee");
    }
}

何时该使用工厂模式

抽象工厂被认为是一种隐藏对象创建的有效手段,特别是在对象创建很复杂的情况下。对象创建越复杂,使用工厂来创建对象就越有道理。如果以一致的方式创建对象并且其创建要被严格控制是很重要的,那么你就应该考虑工厂模式的实现。不过,在CDI环境这个美丽的世界中,容器会实例化托管对象,这时使用抽象工厂的意义就不大了。这种情况下,实现工厂模式的最好方式就是使用@Produce注解,它可以将复杂的创建逻辑隐藏到创建者方法中,并将生成的对象注入客户端。此外,你还可以利用CDI环境的功能,让容器创建对象,然后从相似对象的池中选择需要使用的实例。不过,你只能使用简单对象,即可以通过调用默认构造方法来实例化的对象。

面向切面编程(拦截器)

Aspectj与Spring已经广为接受,并且用在Java项目中很长时间了。Java也通过Servlet过滤器提供了一个类似但更加基本的方式,不过它只能用于处理Web请求。借助于Servlet过滤器,任何请求和相应都可以被拦截,并且可以添加任何额外的行为。
AOP并不是一种设计模式,它其实是一种编程范式。AOP在编译时或运行时会使用依赖注入,它会比较既有代码库与给定注入标准,然后向每一个匹配点添加所需行为或功能。执行编译时注入的框架通常都很不错,不过它们所生成的class文件并不会与源代码逐行匹配,这是因为被注入的代码在起作用。运行时注入并不会修改源代码或class文件,它通过拦截调用执行注入,并在原来的执行顺序前后执行所需代码。如果要添加一些重复性动作,那么AOP就是非常有用的,比如说向代码库中添加日志或安全等。可以根据环境或是项目的不同阶段打开和关闭方面。方面可以动态向运行中的代码添加所需行为。它们会动态装饰方式调用,就好像装饰模式装饰对象一样。
AOP非常适合于封装通用的非业务关注点。不过,如果向业务逻辑添加行为,那么AOP就会令人感到困惑。这种实现会导致去中心化,分布式,以及难以测试和调试的业务逻辑的结果。生成的代码也难以维护。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章