且听风吟

Don't panic! I'm a programmer.

面向对象设计-原则与模式

| Comments

多态

多态是面向对象思想的三大特征中最核心的元素。多态有两方面的含义:

  1. 允许将子类类型的指针赋值给父类类型的指针
  2. 同样的消息发给不同类型的对象可能会使对象表现出不同的行为

方法在运行时绑定,也就是动态绑定才叫多态。重载不叫多态,重载时方法在编译期就已经绑定。 多态复用了接口。

策略模式

策略模式定义了一系列的算法,并将每一个算法封装起来,而且使它们还可以相互替换。策略模式让算法独立于使用它的客户而独立变化。

当在业务上下文中有多个if-else来决定流程的话,就需要考虑使用策略模式了。它可以将if-else中各种平等的实现封装到单独的策略实现类中,这样使得系统更容易扩展。

/images/dp-strategy.png

策略模式很容易和模板方法模式结合使用。
策略模式的思想是用一个通用接口将不同的实现算法抽象出来,达到在运行时自由切换算法的目的,同时使得代码更容易应对变化。策略模式的典型应用场景比如说,一个应用需要保存文件,这个文件可以保存成很多种格式,每种格式的保存逻辑都不同,应用策略模式就可以让用户在运行随意选择哪种格式进行保存。

设计原则

  • 开闭原则
  • 封装变化

    找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起。 在软件开发过程中,可能无法预知所有的变化,这就需要不断重构。

  • 针对接口编程,而不是针对实现编程

    如何分离变化?
    将变化抽象到一个接口,通过这个接口的不同实现来表现不同的变化。

  • 组合优先继承

    如何达到复用的目的?最直接的做法就是利用继承,然后出于这个目的的继承,往往会被过度使用。考虑两种情况下的继承:

    1. 和具体业务无关的继承 比如说工具方法等。这些方法和具体业务无关,业务的变更不会影响到这些方法的实现,因此使用继承来达到复用是可能的。
    2. 业务相关的继承 如果一个方法或属性与具体业务实现相关,那么就必须慎重考虑是否要被继承。因为业务逻辑很可能经常会发生变化,继承会导致所有的子类都会拥有这个特性,一旦子类的业务逻辑发生变化,将变得难以维护。而且,并不是所有的子类都要有这种特性。

    而通过组合,每个客户端对象通过组合的方式持有这些接口的引用,在运行时将这些变化(依赖)注入进来,这也是Spring中依赖注入的思想。注入的方式包括:构造方法注入,setter方法注入和接口注入。这样,我们达到了复用的目的—通过动态的组合这些变化对象来达到我们的需求。而且,更容易扩展和维护。

模板方法模式

定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

模板方法模式多用于高层框架设计。

设计原则

  • 好莱坞原则:Do not call us, we’ll call you.

装饰者模式

装饰者模式可以动态将职责附加到对象上,若要扩展功能,装饰者提供了比继承更具弹性的代替方案。

要点

  1. 装饰者和被装饰对象有相同的超类型。
  2. 可以用一个或多个装饰者包装一个对象。
  3. 装饰者可以在所委托被装饰者的行为之前或之后,加上自己的行为,以达到特定的目的。
  4. 对象可以在任何时候被装饰,所以可以在运行时动态的,不限量的用你喜欢的装饰者来装饰对象。
  5. 装饰模式中使用继承的关键是想达到装饰者和被装饰对象的类型匹配,而不是获得其行为。

设计原则

  • 组合优先继承
  • 对修改关闭,对扩展开放

适配器模式

将一个类的接口,转换成客户期望的另一个接口。

/images/dp-adapter.png

外观模式

外观模式提供了一个统一的接口,用来访问子系统中的一群接口。外观定义了一个高层接口,让子系统更容易使用。

/images/dp-facade.png

外观模式最大的作用,是对外提供一个简单的接口,同时避免客户代码与子系统之间有太多耦合。

实现一个外观,需要将子系统组合进外观中,然后将客户端的请求委托给子系统执行。

适配器模式将一个对象包装起来用以改变其接口;
装饰者模式将一个对象包装起来用以增加新的行为和责任;
外观模式将一群对象包装起来用以简化接口。
所有包装都通过组合的方式来实现。

命令模式

将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。

/images/dp-command.png

所谓参数化,指的是可以用不同的命令对象去配置客户的请求。客户发起的是同一个请求,但最好执行的是什么功能,要看配置的具体是什么命令对象。
命令模式的本质是将请求封装成命令对象。命令模式使用之前有一个命令组装的过程,调用顺序为:

  1. Client组装者创建命令执行者对象
  2. Client组装者创建命令对象,设置命令对象和命令执行者之间的关系
  3. Client组装者创建Invoker对象,将命令对象设置到Invoker对象中,让Invoker对象持有该命令对象
  4. Client组装者调用Invoker对象的方法,出发命令对象开始执行

状态模式

当一个对象的内在状态改变时允许改变其行为,这个对象看起来像是改变了其类。 

/images/dp-state.png

状态模式应用的主要场景在于,如果对同一个请求,一个对象表现出来的行为取决与它的状态,而且对象需要在任意两种状态之间进行自由切换。状态模式主要重构的目标是针对不同的状态充斥了大量的if/else判断,造成代码维护困难,严重违反开闭原则。
状态模式的典型应用场景包括:网上商城的订单流程, TCP协议的状态转换等等。
进行状态转换的地方可以是在Context中(可以定义一个nextState()方法驱动状态改变),也可以是在状态对象中(这时状态对象需要持有Context对象)。

状态模式与策略模式的区别

状态模式与策略模式十分相像,策略模式适用于这样的场景:

if (which == 1){
    applyThis();
} else if (which == 2) {
    applyThat();
} else if (which == 3) {
    ...
}

而状态模式适用于:

if (state == initial) {
    doSomething();
    state = processing;
} else if (state == processing) {
    doSomethingElse();
    state = done;
} else {
    ...
}

很明显对象面对客户的请求,都会触发自身内部状态的迁移。每当对象接受一个客户的请求,上述逻辑都要判断一下,对同一个请求,不同的对象状态下表现出来的行为也会不一样。
http://www.jdon.com/designpatterns/designpattern_State.htm

代理模式

代理模式为一个对象一个替身以**控制**对这个对象的访问。

/images/dp-proxy.png

代理模式使得客户对被代理对象的访问细节一无所知,代理对象控制了访问过程,代理模式的应用场景包括:

  1. 对被代理对象的访问进行拦截并控制访问(例如AOP机制)
  2. 控制资源加载,如让被代理对象在后台加载资源,而让代理对象显示提示信息,资源加载完毕后显示给用户
  3. 提供访问授权检查

代理模式与装饰者模式

装饰者模式和代理模式都会通过包装原有的对象,但装饰者模式会对原有的对象添加新的功能,而且这些功能可以任意组合;代理模式不会添加新的功能,原有的服务没有改变。

动态代理

从上面的类图可见,Proxy对象必须实现Subject这个业务接口,使得这个Proxy对象与具体的业务逻辑耦合起来了,对不同的业务接口,必须创建不同的Proxy对象。
Java提供了动态代理的支持,结构如下:

/images/dp-dynamic-proxy.png

java.lang.reflect.Proxy类实际上只是一个辅助类,真正的代理逻辑被委托到InvocationHandler的invoke方法中.
动态代理机制使得不用再手工编写代理类,只需要指定一个业务接口和一个委托对象,便能动态的获取代理对象,这个代理对象将来自客户端的调用分派到委托对象上执行,而委托对象可以根据需要添加必要的控制逻辑。

模式的最终目的是封装变化,不变的是接口,变化的是实现