设计模式——开发常用的设计模式梳理

基础学习:UML四种关系

耦合度大小关系

泛化 = 实现 > 组合 > 聚合 > 关联 > 依赖

依赖(Dependency)

[图片上传失败...(image-656a2e-1518499281051)]

一个人(Person)可以买车(car)和房子(House),那么就可以称:Person类依赖于Car类和House类
这里注意与下面的关联关系区分:Person类里并没有使用Car和House类型的属性,Car和House的实例是以参量的方式传入到buy()方法中。
依赖关系在Java语言中体现为局域变量、方法的形参,或者对静态方法的调用。

关联(Association)

[图片上传失败...(image-59b25d-1518499281051)]

它使一个类知道另一个类的属性和方法。
关联可以是双向的,也可以是单向的。
在Java语言中,关联关系一般使用成员变量来实现。

聚合(Aggregation)

[图片上传失败...(image-577fd0-1518499281051)]

聚合是关联关系的一种,是强的关联关系。
聚合是整体和个体之间的关系,但个体可以脱离整体而存在。
例如,汽车类与引擎类、轮胎类,以及其它的零件类之间的关系便整体和个体的关系。
与关联关系一样,聚合关系也是通过成员变量实现的。但是关联关系所涉及的两个类是处在同一层次上的,而在聚合关系中,两个类是处在不平等层次上的,一个代表整体,另一个代表部分。

组合(Composition)

[图片上传失败...(image-32b2cd-1518499281051)]

组合是关联关系的一种,是比聚合关系强的关系,也以成员变量的形式出现。
在某一个时刻,部分对象只能和一个整体对象发生组合关系,由后者排他地负责生命周期。
部分和整体的生命周期一样。
整体可以将部分传递给另一个对象,这时候该部分的生命周期由新整体控制,然后旧整体可以死亡。

策略模式

什么是策略模式

一个类中的一些行为,可能会随着系统的迭代而发生变化。为了使得该类满足开放-封闭原则(即:具备可扩展性 或 弹性),我们需要将这些未来会发生动态变化的行为从该类中剥离出来,并通过预测未来业务发展的方式,为这些行为抽象出共有的特征,封装在抽象类或接口中,并通过它们的实现类提供具体的行为。原本类中需要持有该抽象类/接口的引用。在使用时,将某一个具体的实现类对象注入给该类所持有的接口/抽象类的引用。

类图描述

[图片上传失败...(image-2289b1-1518499281051)]

如果类A中有两个行为X和Y会随着业务的发展而变化,那么,我们需要将这两个行为从类A中剥离出来,并形成各自的继承体系(策略体系)。每个继承体系(策略体系)的顶层父类/接口中定义共有行为的抽象函数,每个子类/实现类中定义该策略体系具体的实现。

其中,每一个被抽象出来的继承体系被称为一个策略体系,每个具体的实现类被称为策略。

此时,策略体系已经构建完成,接下来需要改造类A。
在类A中增加所需策略体系的顶层父类/接口,并向外暴露一个共有的函数action给调用者使用。

在Spring项目中,策略类和类A之间的依赖关系可以通过依赖注入来完成。

到此为止,策略模式已经构建完成,下面我们来看优缺点分析。

策略模式的优点

1. 满足开放封闭原则

如果类A需要更换一种策略的时候,只需修改Spring的XML配置文件即可,其余所有的代码均不需要修改。

比如,将类A的策略X_1更换成X_2的方法如下:

<bean id="a" class="类A">
    <!-- 将原本策略实现类X_1修改为策略实现类X_2即可 -->
    <property name="策略接口X" class="策略实现类X_2" />
</bean>

此外,如果需要新增一种策略,只需要为策略接口X添加一个新的实现类即可,并覆盖其中的commonAction函数。然后按照上面的方式修改XML文件即可。

在这个过程中,在保持原有Java代码不发生变化的前提下,扩展性了新的功能,从而满足开放封闭原则。

2. 可方便地创建具有不同策略的对象

如果我们需要根据不同的策略创建多种类A的对象,那么使用策略模式就能很容易地实现这一点。

比如,我们要创建三个A类的对象,a、b、c。其中,a使用策略X_1和Y_1,b使用策略X_2和Y_2,c使用策略X_3和Y_3。
要创建这三个对象,我们只需在XML中作如下配置即可:

<bean id="a" class="类A">
    <property name="策略接口X" class="策略实现类X_1" />
    <property name="策略接口Y" class="策略实现类Y_1" />
</bean>

<bean id="b" class="类A">
    <property name="策略接口X" class="策略实现类X_2" />
    <property name="策略接口Y" class="策略实现类Y_2" />
</bean>

<bean id="c" class="类A">
    <property name="策略接口X" class="策略实现类X_3" />
    <property name="策略接口Y" class="策略实现类Y_3" />
</bean>

答疑

问:如何实现部分继承?也就是类Son1只继承Father的一部分功能,Son2继承Father的另一部分功能。

这是设计上的缺陷,当出现这种情况时,应当将父类再次拆分成2个子类,保证任何一个父类的行为和特征均是该继承体系中共有的!

[图片上传失败...(image-c213f2-1518499281051)]

问:随着需求的变化,父类中需要增加共有行为时怎么办?这就破坏了“开放封闭原则”。

这并未破坏“开放封闭原则”!在系统迭代更新的过程中,修改原有的代码是在所难免的,这并不违背“开放封闭原则”。
“开放封闭原则”要求我们:当系统在迭代过程中,第一次出现某一类型的需求时,是允许修改的;在此时,应该对系统进行修改,并进行合理地设计,以保证对该类型需求的再次修改具备可扩展性。当再一次出现该类型的需求时,就不应该修改原有代码,只允许通过扩展来满足需求。

观察者模式

观察者模式是什么

如果出现如下场景需求时,就需要使用观察者模式。

如果存在一系列类,他们都需要向指定类获取指定的数据,当获取到数据后需要触发相应的业务逻辑。这种场景就可以用观察者模式来实现。

在观察者模式中,存在两种角色,分别是:观察者和被观察者。被观察者即为数据提供者。他们呈多对一的关系。

类图描述

[图片上传失败...(image-dd906-1518499281051)]

  • 被观察者是数据提供方,观察者是数据获取方

  • 一个普通的类,如果要成为观察者,获取指定的数据,一共需要如下几步:

    • 首先,需要实现Observer接口,并实现update函数;
    • 然后,在该函数中定义获取数据后的业务逻辑;
      update(Observable, Object)一共有两个参数:
      Observable:被观察者对象(数据提供方)
      Object:数据本身
    • 最后,通过调用 被观察者 的addObservable()或者通过Spring的XML配置文件完成观察者向被观察者的注入。此时,该观察者对象就会被添加进 被观察者 的List中。
  • 调用者才是真正的数据提供方。当调用者需要广播最新数据时,只需调用 被观察者 的notidyObservers()函数,该函数会遍历List集合,并依次调用每个Observer的update函数,从而完成数据的发送,并触发每个Observer收到数据后的业务逻辑。

两种注册观察者的方式

将Observer注册进Observable中有如下两种方式:

1. 运行前,通过Spring XML

在系统运行前,如果观察者数量可以确定,并在运行过程中不会发生变化,那么就可以在XML中完成List<Observer>对象的注入,这种方式代码将会比较简洁。

1、配置好所有 观察者 bean

<!-- 创建observerA -->
<bean name="observerA" class="ObservserA">
</bean>

<!-- 创建observerB-->
<bean name="observerB" class="ObservserB">
</bean>

2、 配置好 被观察者 bean,并将所有观察者bean注入给被观察者bean

<!-- 创建observable -->
<bean name="observable" class="Observable">
    <property name="observerList">
        <list>
            <ref bean="observerA" />
            <ref bean="observerB" />
        </list>
    </property>
</bean>

2. 运行中,通过addObserver()函数

在Spring初始化的时候,通过addObserver()函数将所有Observer对象注入Observable的observerList中。

@Component
public class InitConfiguration implements ApplicationListener<ContextRefreshedEvent>{

    @Override
    public void onApplicationEvent(ContextRefreshedEvent arg0) {
        if(event.getApplicationContext().getParent() == null){
            Observable observable = (Observable)event.getApplicationContext().getBean("observable");

            ObserverA observerA = (ObserverA)event.getApplicationContext().getBean("observerA");
            ObserverB observerB = (ObserverB)event.getApplicationContext().getBean("observerB");

            observable.setObserverList(Arrays.asList(observerA, observerB));
        }  
    }
}

建议使用第一种方式初始化所有的观察者,此外,被观察者仍然需要提供addObserver()函数供系统在运行期间动态地添加、删除观察者对象。

JDK提供的观察者模式工具包

JDK已经提供了观察者模式的工具包,包括Observable类和Observer接口。若要实现观察者模式,直接使用这两个工具包即可。

装饰模式

何时使用

  • 需要增强一个对象中某些函数的功能。

  • 需要动态地给一个对象增加功能,这些功能可以再动态地撤销。

  • 需要增加 由一些基本功能排列组合 而产生的大量功能,从而使继承体系大爆炸。

类图描述

[图片上传失败...(image-9a55f4-1518499281051)]
在装饰模式中的各个角色有:

  • 抽象构件(Component)角色:给出一个抽象接口,以规范准备接收附加责任的对象。

  • 具体构件(Concrete Component)角色:定义一个将要接收附加责任的类。

  • 装饰(Decorator)角色:持有一个构件(Component)对象的实例,并定义一个与抽象构件接口一致的接口。

  • 具体装饰(Concrete Decorator)角色:负责给构件对象”贴上”附加的责任。

Decorator中包含Component的成员变量,每个Concrete Decorator实现类均需要实现operation()函数,该函数大致过程如下:

class ConcreteDecorator {
    private Component component;

    返回类型 operation(){
        // 执行上一层的operation(),并获取返回结果
        返回结果 = component.operation();
        // 拿到返回结果后,再做额外的处理
        处理返回结果
        return 返回结果;
    }
}

使用装饰类的过程如下:

// 准备好所有装饰类
DecoratorA decoratorA = new DecoratorA();
DecoratorB decoratorB = new DecoratorB();
DecoratorC decoratorC = new DecoratorC();

// 准备好 被装饰的类
Component component = new Component();

// 组装装饰类
decoratorC.setComponent(decoratorB);
decoratorB.setComponent(decoratorA);
decoratorA.setComponent(component);

// 执行
decoratorC.operation();

执行过程如下:

[图片上传失败...(image-f46797-1518499281051)]

优点

1、Decorator模式与继承关系的目的都是要扩展对象的功能,但是Decorator可以提供比继承更多的灵活性。继承通过覆盖的方式重写需要扩展的函数,当然也可以通过super.xxx()获取原本的功能,然后在该功能基础上扩展新功能,但它只能增加某一项功能;如果要通过继承实现增加多种功能,那么需要多层继承多个类来实现;而Decorator模式可以在原有功能的基础上通过组合来增加新功能,这些新功能已经被封装成一个个独立的装饰类,在运行期间通过搭积木的方式选择装饰类拼凑即可。

2、通过使用不同的具体装饰类以及这些装饰类的排列组合,设计师可以创造出很多不同行为的组合。

缺点

1、这种比继承更加灵活机动的特性,也同时意味着更加多的复杂性。
2、装饰模式会导致设计中出现许多小类,如果过度使用,会使程序变得很复杂。

3、装饰模式是针对抽象组件(Component)类型编程。但是,如果你要针对具体组件编程时,就应该重新思考你的应用架构,以及装饰者是否合适。当然也可以改变Component接口,增加新的公开的行为,实现“半透明”的装饰者模式。在实际项目中要做出最佳选择。

设计原则

  • 多用组合,少用继承。
    利用继承设计子类的行为,是在编译时静态决定的,而且所有的子类都会继承到相同的行为。然而,如果能够利用组合的做法扩展对象的行为,就可以在运行时动态地进行扩展。

单例模式

Java中单例(Singleton)模式是一种广泛使用的设计模式。单例模式的主要作用是保证在Java程序中,某个类只有一个实例存在。一些管理器和控制器常被设计成单例模式。

单例模式有很多好处,它能够避免实例对象的重复创建,不仅可以减少每次创建对象的时间开销,还可以节约内存空间;能够避免由于操作多个实例导致的逻辑错误。如果一个对象有可能贯穿整个应用程序,而且起到了全局统一管理控制的作用,那么单例模式也许是一个值得考虑的选择。

单例模式有很多种写法,大部分写法都或多或少有一些不足。下面将分别对这几种写法进行介绍。

饿汉模式

public class Singleton{  
    private static Singleton instance = new Singleton();  
    private Singleton(){}  
    public static Singleton newInstance(){  
        return instance;  
    }  
}  

  • 类的构造函数定义为private,保证其他类不能实例化此类;
  • 然后提供了一个静态实例并返回给调用者;
  • 饿汉模式在类加载的时候就对实例进行创建,实例在整个程序周期都存在
  • 优点:只在类加载的时候创建一次实例,不会存在多个线程创建多个实例的情况,避免了多线程同步的问题。
  • 缺点:即使这个单例没有用到也会被创建,而且在类加载之后就被创建,内存就被浪费了。
  • 适用场景:这种实现方式适合单例占用内存比较小,在初始化时就会被用到的情况。但是,如果单例占用的内存比较大,或单例只是在某个特定场景下才会用到,使用饿汉模式就不合适了,这时候就需要用到懒汉模式进行延迟加载。

懒汉模式(存在线程安全性问题)

public class Singleton{  
    private static Singleton instance = null;  
    private Singleton(){}  
    public static Singleton newInstance(){  
        if(null == instance){  
            instance = new Singleton();  
        }  
        return instance;  
    }  
}  
  • 懒汉模式中单例是在需要的时候才去创建的,如果单例已经创建,再次调用获取接口将不会重新创建新的对象,而是直接返回之前创建的对象。
  • 如果某个单例使用的次数少,并且创建单例消耗的资源较多,那么就需要实现单例的按需创建,这个时候使用懒汉模式就是一个不错的选择。
  • 但是这里的懒汉模式并没有考虑线程安全问题,在多个线程可能会并发调用它的getInstance()方法,导致创建多个实例,因此需要加锁解决线程同步问题,实现如下。

懒汉模式(线程安全,但效率低)

public class Singleton{  
    private static Singleton instance = null;  
    private Singleton(){}  
    public static synchronized Singleton newInstance(){  
        if(null == instance){  
            instance = new Singleton();  
        }  
        return instance;  
    }  
}

加锁的懒汉模式看起来即解决了线程并发问题,又实现了延迟加载,然而它存在着性能问题,依然不够完美。synchronized修饰的同步方法比一般方法要慢很多,如果多次调用getInstance(),累积的性能损耗就比较大了。

懒汉模式(线程安全,效率高)

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

这种方式比上一种方式只多加了一行代码,那就是在synchronized之上又加了一层判断if (instance == null)。这样当单例创建完毕后,不用每次都进入同步代码块,从而能提升效率。当然,除了初始化单例对象的线程ThreadA外,可能还存在少数线程,在ThreadA创建完单例后,刚释放锁的时候进入同步代码块,但此时有第二道if (instance == null)判断,因此也就避免了创建多个对象。而且进入同步代码块的线程相对较少。

静态内部类(懒汉+无锁)


public class Singleton{  
    private static class SingletonHolder{  
        public static Singleton instance = new Singleton();  
    }  
    private Singleton(){}  
    public static Singleton newInstance(){  
        return SingletonHolder.instance;  
    }  
}  

这种方式同样利用了类加载机制来保证只创建一个instance实例。它与饿汉模式一样,也是利用了类加载机制,因此不存在多线程并发的问题。不一样的是,它是在内部类里面去创建对象实例。这样的话,只要应用中不使用内部类,JVM就不会去加载这个单例类,也就不会创建单例对象,从而实现懒汉式的延迟加载。也就是说这种方式可以同时保证延迟加载和线程安全。

枚举

public enum Singleton{  
    instance;  
    public void whateverMethod(){}      
}

上面提到的四种实现单例的方式都有共同的缺点:

1、需要额外的工作来实现序列化,否则每次反序列化一个序列化的对象时都会创建一个新的实例。
2、可以使用反射强行调用私有构造器(如果要避免这种情况,可以修改构造器,让它在创建第二个实例的时候抛异常)。

而枚举类很好的解决了这两个问题,使用枚举除了线程安全和防止反射调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。因此,《Effective Java》作者推荐使用的方法。不过,在实际工作中,很少看见有人这么写。

模板方法模式

定义

在父类中定义算法的流程,而算法的某些无法确定的细节,通过抽象函数的形式,在子类中去实现。

也可以理解为,一套算法的某些步骤可能随着业务的发展而改变,那么我们可以将确定的步骤在父类中实现,而可变的步骤作为抽象函数让其在子类中实现。

  • 在模板方法模式中,父类是一个抽象类,算法的每一步都被封装成一个函数,templateMethod函数将所有算法步骤串联起来。
  • 对于不变的步骤,用private修饰,防止子类重写;
  • 对于可变的步骤,用abstract protected修饰,必须要求子类重写;
  • 子类重写完所有抽象函数后,调用templateMethod即可执行算法。

外观模式

外观模式这种思想在项目中普遍存在,也极其容易理解,大家一定用过,只是没有上升到理论的层面。这里对这种思想进行介绍。

外观模式他屏蔽了系统功能实现的复杂性,向客户端提供一套极其简单的接口。客户端只需要知道接口提供什么功能,如何调用就行了,不需要管这些接口背后是如何实现的。从而使得客户端和系统之间的耦合度大大降低,客户端只需跟一套简单的Facade接口打交道即可。

适配器模式

定义

作为一个基金交易平台,需要提供一套接口规范,供各个基金公司接入。然而,各个基金公司的接口各不相同,没有办法直接和平台接口对接。此时,各个基金公司需要自行实现一个适配器,适配器完成不同接口的转换工作,使得基金公司的接口和平台提供的接口对接上。

三种适配器

适配器模式有三种实现方式,下面都以基金交易平台的例子来解释。

  • 基金公司的交易接口:
/**
 * 基金公司的交易接口
 */
class FundCompanyTrade{
    /**
     * 买入函数
     */
    public void mairu() {
        // ……
    }

    /**
     * 卖出函数
     */
    public void maichu() {
        // ……
    }
}

  • 基金交易平台的交易接口
/**
 * 基金交易平台的交易接口
 */
interface FundPlatformTrade {
    // 买入接口
    void buy();

    // 卖出接口
    void sell();
}
  • 基金交易平台均通过如下代码调用各个基金公司的交易接口:
class Client {
    @Autowired
    private FundPlatformTrade fundPlatformTrade;

    /**
     * 买入基金
     */
     public void buy() {
        fundPlatformTrade.buy();
     }


    /**
     * 卖出基金
     */
     public void sell() {
        fundPlatformTrade.sell();
     }
}

方式1:类适配器

通过继承来实现接口的转换。

  • 基金交易适配器:
class Adapter extends FundCompanyTrade implements FundPlatformTrade {

    void buy() {
        mairu();
    }

    void sell(){
        maichu();
    }
}

适配器Adapter继承了FundCompanyTrade,因此拥有了FundCompanyTrade买入和卖出的能力;适配器Adapter又实现了FundPlatformTrade,因此需要实现其中的买入和卖出接口,这个过程便完成了基金公司交易接口向基金平台交易接口的转换。

使用时,只需将Adapter通过Spring注入给Client类的fundPlatformTrade成员变量即可:

<!-- 声明Adapter对象 -->
<bean name="adapter" class="Adapter">
</bean>

<!-- 将adapter注入给Client -->
<bean class="Client">
    <property name="fundPlatformTrade" ref="adapter" />
</bean>

方式2:对象适配器

通过组合来实现接口的转换。

  • 基金交易适配器:
class Adapter implements FundPlatformTrade {
    @Autowired
    private FundCompanyTrade fundCompanyTrade;

    void buy() {
        fundCompanyTrade.mairu();
    }

    void sell(){
        fundCompanyTrade.maichu();
    }
}

这种方式中,适配器Adapter并未继承FundCompanyTrade,而是将该对象作为成员变量注入进来,一样可以达到同样的效果。

方式3:接口适配器

当存在这样一个接口,其中定义了N多的方法,而我们现在却只想使用其中的一个到几个方法,如果我们直接实现接口,那么我们要对所有的方法进行实现,哪怕我们仅仅是对不需要的方法进行置空(只写一对大括号,不做具体方法实现)也会导致这个类变得臃肿,调用也不方便,这时我们可以使用一个抽象类作为中间件,即适配器,用这个抽象类实现接口,而在抽象类中所有的方法都进行置空,那么我们在创建抽象类的继承类,而且重写我们需要使用的那几个方法即可。

  • 目标接口:A
public interface A {
    void a();
    void b();
    void c();
    void d();
    void e();
    void f();
}

  • 适配器:Adapter
    实现所有函数,将所有函数先置空。
public abstract class Adapter implements A {
    public void a(){
        throw new UnsupportedOperationException("对象不支持这个功能");
    }
    public void b(){
        throw new UnsupportedOperationException("对象不支持这个功能");
    }
    public void c(){
        throw new UnsupportedOperationException("对象不支持这个功能");
    }
    public void d(){
        throw new UnsupportedOperationException("对象不支持这个功能");
    }
    public void e(){
        throw new UnsupportedOperationException("对象不支持这个功能");
    }
    public void f(){
        throw new UnsupportedOperationException("对象不支持这个功能");
    }
}

  • 实现类:Ashili
    继承适配器类,选择性地重写相应函数。
public class Ashili extends Adapter {
    public void a(){
        System.out.println("实现A方法被调用");
    }
    public void d(){
        System.out.println("实现d方法被调用");
    }
}

迭代器模式

定义

迭代器模式用于在无需了解容器内部细节的情况下,实现容器的迭代。

容器用于存储数据,而容器的存储结构种类繁多。在不使用适配器模式的情况下,如果要迭代容器中的元素,就需要充分理解容器的存储结构。存储结构不同,导致了不同容器的迭代方式都不一样。这无疑增加了我们使用容器的成本。

而迭代器模式提出了一种迭代容器元素的新思路,迭代器规定了一组迭代容器的接口,作为容器使用者,只需会用这套迭代器即可。容器本身需要实现这套迭代器接口,并实现其中的迭代函数。也就是容器提供方在提供容器的同时,还需要提供迭代器的实现。因为容器本身是了解自己的存储结构的,由它来实现迭代函数非常合适。而我们作为容器的使用者,只需知道怎么用迭代器即可,无需了解容器内部的存储结构。

类图描述

好好学java

在迭代器模式中,一共有两种角色:迭代器 和 容器

  • 迭代器 Iterator:封装了迭代容器的接口

  • 容器 Container:存储元素的东西

     - 容器若要具备迭代的能力,就必须拥有getIterator()函数,该函数将会返回一个迭代器对象
     - 每个容器都有属于自己的迭代器内部类,该内部类实现了Iterator接口,并实现了其中用于迭代的两个函数hasNext()和next()
     - boolean hasNext():用于判断当前容器是否还有尚未迭代完的元素
     - Object next():用于获取下一个元素
    

代码实现

  • 迭代器接口:
public interface Iterator {
   public boolean hasNext();
   public Object next();
}
  • 容器接口:
public interface Iterator {
   public boolean hasNext();
   public Object next();
}
  • 具体的容器(必须实现Container接口):
public class NameRepository implements Container {
   public String names[] = {"Robert" , "John" ,"Julie" , "Lora"};

   @Override
   public Iterator getIterator() {
      return new NameIterator();
   }

   private class NameIterator implements Iterator {

      int index;

      @Override
      public boolean hasNext() {
         if(index < names.length){
            return true;
         }
         return false;
      }

      @Override
      public Object next() {
         if(this.hasNext()){
            return names[index++];
         }
         return null;
      }        
   }
}

  • 具体的容器实现了Container接口,并实现了其中的getIterator()函数,该函数用于返回该容器的迭代器对象。
  • 容器内部需要实现自己的迭代器内部类,该内部类实现Iterator接口,并实现了其中的hasNext()和next()函数。

当容器和容器的迭代器创建完毕后,接下来就轮到用户使用了,使用就非常简单了:

public class IteratorPatternDemo {

   public static void main(String[] args) {
      NameRepository namesRepository = new NameRepository();

      for(Iterator iter = namesRepository.getIterator(); iter.hasNext();){
         String name = (String)iter.next();
         System.out.println("Name : " + name);
      }     
   }
}

  • 对于使用者而言,只要知道Iterator接口,就能够迭代所有不同种类的容器了。

组合模式

定义

组合模式定义了树形结构的物理存储方式。

现实世界中树形结构的东西,在代码实现中,都可以用组合模式来表示。

比如:多级菜单、公司的组织结构等等。

下面就以多级菜单为例,介绍组合模式。

例子

假设我们要实现一个多级菜单,并实现多级菜单的增删改查操作。菜单如下:

一级菜单A
    二级菜单A_1
        三级菜单A_1_1
        三级菜单A_1_2
        三级菜单A_1_3
    二级菜单A_2
一级菜单B
    二级菜单B_1
    二级菜单B_2
    二级菜单B_3
    二级菜单B_4
        三级菜单B_4_1
        三级菜单B_4_2
        三级菜单B_4_3
一级菜单C
    二级菜单C_1
    二级菜单C_2
    二级菜单C_3

菜单的特点如下:

  • 深度不限,可以有无限级菜单
  • 每层菜单数量不限

类图描述

  • Item表示树中的节点;
  • Item中包含两个成员变量:
    • parent:指向当前节点的父节点
    • childList:当前节点的子节点列表
  • 这种Item中又包含Item的关系就构成了组合模式。

注意:循环引用

在构建树的过程中,可能会出现循环引用,从而在遍历树的时候可能就会出现死循环。因此,我们需要在添加节点的时候避免循环引用的出现。

我们可以在Item中再添加一个List成员变量,用于记录根节点到当前节点的路径。该路径可以用每个节点的ID表示。一旦新加入的节点ID已经出现在当前路径中的时候,就说明出现了循环引用,此时应该给出提示。

状态模式

使用场景

如果一个函数中出现大量的、复杂的if-else判断,这时候就要考虑使用状态模式了。

因为大量的if-else中往往包含了大量的业务逻辑,很有可能会随着业务的发展而变化。如果将这些业务逻辑都写死在一个类中,那么当业务逻辑发生变化的时候就需要修改这个类,从而违反了开放封闭原则。而状态模式就能很好地解决这一问题。

状态模式将每一个判断分支都封装成一个独立的类,每一个判断分支成为一种“状态”,因此每一个独立的类就成为一个“状态类”。并且由一个全局状态管理者Context来维护当前的状态。

类图描述

  • 在状态模式中,每一个判断分支被成为一种状态,每一种状态,都会被封装成一个单独的状态类;
  • 所有的状态类都有一个共同的接口——State
  • State接口中有一个doAction函数,每个状态类的状态处理逻辑均在该函数中完成;必须将Context对象作为doAction函数的参数传入。该函数的结构如下:

class StateA implements State{
    public void doAction(Context context){
        if (满足条件) {
            // 执行相应的业务逻辑
        }
        else {
            // 设置下一跳状态
            context.setState(new StateB());
            // 执行下一跳状态
            context.doCurState();
        }
    }
}

  • 每个状态类的doAction函数中都有且仅有一对if-else,if中填写满足条件时的业务逻辑,而else中填写不满足条件时的业务逻辑。

  • else中的代码都一样,有且仅有这两步:
    首先将context的state设为下一个状态对象;
    然后调用context的doCurState()执行;

  • Context类其实就是原本包含那个巨大、复杂的if-else的类。该类中持有了State对象,表示当前要执行的状态对象。

  • Context类必须要有一个doCurState函数,该函数的代码都一样:state.doAction()

  • 开启状态判断过程的代码如下:

// 准备好第一个状态
StateA stateA = new StateA();
// 设置第一个状态
context.setState(stateA);
// 开始执行
context.doCurState();

优点

状态模式将原本在一个类中的庞大的if-else拆分成一个个独立的状态类。原本这个包含庞大if-else的类成为Context,包含了当前的状态。Context只需要知道起始状态类即可,不需要知道其他状态类的存在。也就是Context只与第一个状态类发生耦合。而每一个状态类只和下一个状态类发生耦合,从而形成一条状态判断链。状态类之间的耦合通过Spring XML文件配置。这样,当判断逻辑发生变化的时候,只需要新增状态类,并通过修改XML的方式将新的状态类插入到判断逻辑中。从而满足了开放封闭原则。
代理模式

代理模式

好好学java

代理模式是在不改变目标类和使用者的前提下,扩展该类的功能。

代理模式中存在『目标对象』和『代理对象』,它们必须实现相同的接口。用户直接使用代理对象,而代理对象会将用户的请求交给目标对象处理。代理对象可以对用户的请求增加额外的处理。

Java动态代理的使用

  • 首先你得拥有一个目标对象,该对象必须要实现一个接口:
public interface Subject   
{   
  public void doSomething();   
}  
public class RealSubject implements Subject   
{   
  public void doSomething()   
  {   
    System.out.println( "call doSomething()" );   
  }   
}   
  • 其次,为目标对象增加额外的逻辑:
    • 自定义一个类,并实现InvocationHandler接口;
    • 实现invoke函数,并将需要增加的逻辑写在该函数中;
public class ProxyHandler implements InvocationHandler   
{   
  private Object proxied;   

  public ProxyHandler( Object proxied )   
  {   
    this.proxied = proxied;   
  }   

  public Object invoke( Object proxy, Method method, Object[] args ) throws Throwable   
  {   
    //在转调具体目标对象之前,可以执行一些功能处理

    //转调具体目标对象的方法
    return method.invoke( proxied, args);  

    //在转调具体目标对象之后,可以执行一些功能处理
  }    
} 

  • 创建代理对象,调用者直接使用该对象即可:
RealSubject real = new RealSubject();   
Subject proxySubject = (Subject)Proxy.newProxyInstance(Subject.class.getClassLoader(), 
     new Class[]{Subject.class}, 
     new ProxyHandler(real));

    proxySubject.doSomething();

转自:http://blog.csdn.net/u010425776/article/details/79211117
文章有不当之处,欢迎指正,你也可以关注我的微信公众号:好好学java,获取java等优质视频教程、学习资源。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容

  • 设计模式汇总 一、基础知识 1. 设计模式概述 定义:设计模式(Design Pattern)是一套被反复使用、多...
    MinoyJet阅读 3,903评论 1 15
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,599评论 18 139
  • 原文链接:http://blog.csdn.net/zhangerqing http://www.cnblogs....
    孤独杂货铺阅读 1,508评论 0 3
  • 在学薛兆丰老师经济学的第二课就是马粪案的故事,故事大致意思是,早起纽约大街上马粪很多,没有环卫工扫除,一位有眼光的...
    秦家炎阅读 759评论 2 4
  • 苏霍姆林斯基劳动教育简述 28、用劳动的爱好来教育学生 66、劳动和智力发展 67、通过劳动发展学生的个人才能和爱...
    小达摩和小天狗阅读 491评论 0 1