设计模式

1. 创建型模式

这些设计模式提供了一种在创建对象的同时隐藏创建逻辑的方式,而不是使用 new 运算符直接实例化对象。这使得程序在判断针对某个给定实例需要创建哪些对象时更加灵活。

1.1 简单工厂模式

小总结:

简单工厂模式实现了生成产品类的代码跟客户端代码分离。

简单类图```

class Circle: Shape{
    override fun draw(): String{
        System.out.println("circle:draw()")
        return "circle:draw()"
    }
}
class Rectangle : Shape{
    override fun draw(): String {
        System.out.println("Rectangle:draw()")
        return "Rectangle:draw()"
    }
}
class Squre : Shape{
    override fun draw(): String {
        System.out.println("squre:draw()")
        return "squre:draw()"
    }
}
interface  Shape{
    fun draw(): String
}
class ShapeFactory{
    fun getShape(shapeType: String): Shape? {
        if (shapeType == null){
            return null
        }
        when(shapeType){
            "circle" -> return Circle()
            "rectangle" -> return Rectangle()
            "squre" -> return Squre()
        }
        return null
    }
}

1.2 工厂模式(factory pattern)

小总结

在工厂类中你可以添加所需的生成产品的逻辑代码,但是问题来了,优秀的java代码是符合“开放-封闭”原则的,也就是说对扩展开发,对修改关闭,如果你要加一个产品类C,你就要修改工厂类里面的生成产品的代码,在这里你就要增加if-else判断。对于这个问题,我们的工厂方法模式就可以解决这个问题。
定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行。

优点: 1、一个调用者想创建一个对象,只要知道其名称就可以了。 2、扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。 3、屏蔽产品的具体实现,调用者只关心产品的接口。
缺点:每次增加一个产品时,都需要增加一个具体类和对象实现工厂,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。这并不是什么好事。

使用场景: 1、日志记录器:记录可能记录到本地硬盘、系统事件、远程服务器等,用户可以选择记录日志到什么地方。 2、数据库访问,当用户不知道最后系统采用哪一类数据库,以及数据库可能有变化时。 3、设计一个连接服务器的框架,需要三个协议,"POP3"、"IMAP"、"HTTP",可以把这三个作为产品类,共同实现一个接口。

注意事项:作为一种创建类模式,在任何需要生成复杂对象的地方,都可以使用工厂方法模式。有一点需要注意的地方就是复杂对象适合使用工厂模式,而简单对象,特别是只需要通过 new 就可以完成创建的对象,无需使用工厂模式。如果使用工厂模式,就需要引入一个工厂类,会增加系统的复杂度。

如下所示:

工厂模式```

class Circle: Shape{
    override fun draw(): String{
        System.out.println("circle:draw()")
        return "circle:draw()"
    }
}
class Rectangle : Shape{
    override fun draw(): String {
        System.out.println("Rectangle:draw()")
        return "Rectangle:draw()"
    }
}
class Squre : Shape{
    override fun draw(): String {
        System.out.println("squre:draw()")
        return "squre:draw()"
    }
}
interface  Shape{
    fun draw(): String
}
interface Factory{
	fun getShape(): Shape
}
class CircleFactory: Factory{
  override fun getShape(): Shape{
	return Circle()
	}
}
class RectangleFactory: Factory{
  override fun getShape(): Shape{
	return Rectangle()
	}
}
class SqureFactory: Factory{
  override fun getShape(): Shape{
	return Squre()
	}
}

1.3 抽象工厂模式

小总结

工厂方法模式中我们把生成产品类的时间延迟,就是通过对应的工厂类来生成对应的产品类,在这里我们就可以实现“开发-封闭”原则,无论加多少产品类,我们都不用修改原来类中的代码,而是通过增加工厂类来实现。但是这还是有缺点的,如果产品类过多,我们就要生成很多的工厂类。假如我们要实现的产品接口不止一个,也就是有多个产品接口,不同产品接口有对应的产品族。什么是产品族呢?简单的理解就是,不同牌子产的车里面会有跑车类型,家庭类型,商用类型等的车,不同牌子的车的跑车类型的车可以组成一个产品族。对于这种情况我们可以采用抽象工厂模式。

优点:当一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象。
缺点:产品族扩展非常困难,要增加一个系列的某一产品,既要在抽象的 Creator 里加代码,又要在具体的里面加代码。

使用场景: 1、QQ 换皮肤,一整套一起换。 2、生成不同操作系统的程序。

注意事项:产品族难扩展,产品等级易扩展。
如下所示:

抽象工厂模式```

class Circle: Shape{
    override fun draw(): String{
        System.out.println("circle:draw()")
        return "circle:draw()"
    }
}
class Rectangle : Shape{
    override fun draw(): String {
        System.out.println("Rectangle:draw()")
        return "Rectangle:draw()"
    }
}
class Squre : Shape{
    override fun draw(): String {
        System.out.println("squre:draw()")
        return "squre:draw()"
    }
}
interface  Shape{
    fun draw(): String
}
interface Color{
	 fun fill(): String
}
class Green: Color{
    override fun fill(): String {
        return "green"
    }
}
class Blue: Color{
    override fun getShape(shape: String): Shape? {
        return null
    }override fun fill(): String {
        return "blue"
    }
}
interface Factory{
	fun getShape(): Shape
	fun getColor(): Color
}
class FactoryA: Factory{
	//获取blue和circle对象的工厂
	override fun getShape(): Shape {
        return Circle()
    }
   override fun getColor(): Color {
        return Blue()
    }
}
class FactoryB: Factory{
	//获取green和rectangle对象的工厂
	override fun getShape(): Shape {
        return Rectangle()
    }
   override fun getColor(): Color {
        return Green()
    }
}

1.4 单例模式

要注意哦:静态成员属于整个类,当系统第一次使用该类时,就会为其分配内存空间直到该类被卸载才会进行资源回收!~~

Java 中被 static 修饰的成员称为静态成员或类成员。它属于整个类所有,而不是某个对象所有,即被类的所有对象所共享。静态成员可以使用类名直接访问,也可以使用对象名进行访问。

静态方法中不能直接调用非静态方法,需要通过对象来访问非静态方法。如:
hello.name
注意:
1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。

1.4.1 懒汉式,多线程不安全

public class SingleTon {

    /*
   懒汉式,线程不安全
   是否 Lazy 初始化:是
   
   是否多线程安全:否

   描述:这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。
   这种方式 lazy loading 很明显,不要求线程安全,在多线程不能正常工作。
    */
    private static SingleTon instance;
    private SingleTon(){}
    public static SingleTon getInstance(){
        if (instance == null){
            instance = new SingleTon();
        }
        return instance;
    }
}

1.4.2 懒汉式,多线程安全

public class SingleTonb {

    /*
    懒汉式,线程安全
    是否 Lazy 初始化:是

    是否多线程安全:是

    描述:这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步。
    优点:第一次调用才初始化,避免内存浪费。
    缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。
    getInstance() 的性能对应用程序不是很关键(该方法使用不太频繁)。
     */
    private static SingleTonb instance;
    private SingleTonb(){}
    public static synchronized SingleTonb getInstance(){
        if (instance == null){
            instance =  new SingleTonb();
        }
        return instance;
    }
}

1.4.3 饿汉式

public class SingleTonc {

    /*
    饿汉式
    是否 Lazy 初始化:否

    是否多线程安全:是
    
    描述:这种方式比较常用,但容易产生垃圾对象。
    优点:没有加锁,执行效率会提高。
    缺点:类加载时就初始化,浪费内存。
    它基于 classloader 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,
    在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,
    这时候初始化 instance 显然没有达到 lazy loading 的效果。
     */
    private static final SingleTonc instance = new SingleTonc();
    private SingleTonc(){}
    public static SingleTonc getInstance(){
        return instance;
    }
}

1.4.4 双检锁/双重校验锁

public class SingleTond {

    /*
    双检锁/双重校验锁(DCL,即 double-checked locking)
    JDK 版本:JDK1.5 起

    是否 Lazy 初始化:是

    是否多线程安全:是

    描述:这种方式采用双锁机制,安全且在多线程情况下能保持高性能。
    getInstance() 的性能对应用程序很关键。
     */
    private static SingleTond instance ;
    private SingleTond() {}
    public static SingleTond getInstance(){
        if (instance == null){
            synchronized (SingleTond.class){
            if (instance == null){
           	 instance = new SingleTond();
			}        
          }
        }
        return instance;
    }
}

1.4.5 登记式/静态内部类

public class SingleTone {
    /*
    登记式/静态内部类
    是否 Lazy 初始化:是

    是否多线程安全:是

    实现难度:一般

    描述:这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。
    这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。

    这种方式同样利用了 classloader 机制来保证初始化 instance 时只有一个线程,它跟第 3 种方式不同的是:
    第 3 种方式只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果)
    而这种方式是 Singleton 类被装载了,instance 不一定被初始化。
    因为 SingletonHolder 类没有被主动使用,只有通过显式调用 getInstance 方法时,
    才会显式装载 SingletonHolder 类,从而实例化 instance。想象一下,如果实例化 instance 很消耗资源,所以想让它延迟加载,
    另外一方面,又不希望在 Singleton 类加载时就实例化,因为不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载,
    那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比第 3 种方式就显得很合理。
 */
    private static class SingleTonHolder {
        private static final SingleTone instance = new SingleTone();
    }

    private SingleTone() {
    }

    public static SingleTone getInstance() {
        return SingleTonHolder.instance;
    }
}

1.4.6 枚举

public enum SingleTonf {
    /*
    枚举
    JDK 版本:JDK1.5 起

    是否 Lazy 初始化:否

    是否多线程安全:是

    实现难度:易

    描述:这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。
    这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,
    防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,
    在实际工作中,也很少用。
    不能通过 reflection attack 来调用私有构造方法。
     */

    INSTANCE;

    public void whateverMethod() {
    }
}
一般情况下,不建议使用第 1 种和第 2 种懒汉方式,建议使用第 3 种饿汉方式。只有在要明确实现 lazy loading 效果时,
才会使用第 5 种登记方式。如果涉及到反序列化创建对象时,可以尝试使用第 6 种枚举方式。如果有其他特殊的需求,
可以考虑使用第 4 种双检锁方式。

1.4.7 防止反射攻击

1.4.7.1 饿汉式修改版
public class SingleTonNotAttackByReflect {
    private static boolean flag = false;
    private static final SingleTonNotAttackByReflect instance = new SingleTonNotAttackByReflect();

    private SingleTonNotAttackByReflect() {
        synchronized (SingleTonNotAttackByReflect.class) {
            if (!flag) {
                flag = !flag;
            } else {
                throw new RuntimeException("单例模式正在被攻击");
            }
        }
    }
    public static SingleTonNotAttackByReflect getInstance(){
        return instance;
    }
}

public static void modifiedByAttack()
    {
        try
        {
            Class<SingleTonNotAttackByReflect> classType = SingleTonNotAttackByReflect.class;
            Constructor<SingleTonNotAttackByReflect> constructor = classType.getDeclaredConstructor((Class<?>) null);
            constructor.setAccessible(true);
            SingleTonNotAttackByReflect singleton = constructor.newInstance();
            SingleTonNotAttackByReflect singleton2 = SingleTonNotAttackByReflect.getInstance();

            System.out.println(singleton == singleton2);
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }

    }
1.4.7.2 枚举类
枚举类型的单例模式也可以防止被JAVA反射攻击。
1.4.7.3 反射攻击类测试代码
public static void modifiedByAttack()
    {
        try
        {
            Class<SingleTonNotAttackByReflect> classType = SingleTonNotAttackByReflect.class;
            Constructor<SingleTonNotAttackByReflect> constructor = classType.getDeclaredConstructor((Class<?>) null);
            constructor.setAccessible(true);
            SingleTonNotAttackByReflect singleton = constructor.newInstance();
            SingleTonNotAttackByReflect singleton2 = SingleTonNotAttackByReflect.getInstance();

            System.out.println(singleton == singleton2);
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }

    }
1.4.8 防序列化攻击
饿汉式修改版的基础上再加上防序列化攻击 = 防反射攻击 + 防序列化攻击

主要添加了一下代码:

private static boolean flag = false;
public Object readResolve(){
        return instance;
 }

完整代码如下:

public class SingleTonNotAttackByReflect implements Serializable{
    private static boolean flag = false;
//    private static final long serialVersionUID = 29292228;
    private static final SingleTonNotAttackByReflect instance = new SingleTonNotAttackByReflect();

    private SingleTonNotAttackByReflect() {
        synchronized (SingleTonNotAttackByReflect.class) {
            if (!flag) {
                flag = !flag;
            } else {
                throw new RuntimeException("单例模式正在被攻击");
            }
        }
    }
    public static SingleTonNotAttackByReflect getInstance(){
        return instance;
    }

    public Object readResolve(){
        return instance;
    }
}

防序列化攻击测试代码:

public  void test1(){
        SingleTonNotAttackByReflect singleTonNotAttackByReflect = SingleTonNotAttackByReflect.getInstance();
        Log.d("SingleTonNotAttack:",singleTonNotAttackByReflect.toString());

		//先序列化
        File file = new File(getCacheDir().getPath() + "/SingleTonNotAttackByReflect.txt");
        ObjectOutputStream oos = null;
        try {
            oos = new ObjectOutputStream(new FileOutputStream(file));
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            oos.writeObject(singleTonNotAttackByReflect);
        } catch (IOException e) {
            e.printStackTrace();
        }

		//再反序列化
        ObjectInputStream ois = null;
        try {
            ois = new ObjectInputStream(new FileInputStream(file));
        } catch (IOException e) {
            e.printStackTrace();
        }
        SingleTonNotAttackByReflect user = null;
        try {
            user = (SingleTonNotAttackByReflect) ois.readObject();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        System.out.println(user);

        if(user==singleTonNotAttackByReflect){
            System.out.println("同一个实例");
        }else{
            System.out.println("不同的实例");
        }
    }

1.5 建造者模式

建造者模式(Builder Pattern):将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。建造者模式是一种对象创建型模式。

Builder 类会一步一步构造最终的对象。该 Builder 类是独立于其他对象的。

建造者模式一步一步创建一个复杂的对象,它允许用户只通过指定复杂对象的类型和内容就可以构建它们,用户不需要知道内部的具体构建细节。
如下图所示:

建造者模式类图```

在建造者模式结构图中包含如下几个角色:

● Builder(抽象建造者):它为创建一个产品Product对象的各个部件指定抽象接口,在该接口中一般声明两类方法,一类方法是buildPartX(),它们用于创建复杂对象的各个部件;另一类方法是getResult(),它们用于返回复杂对象。Builder既可以是抽象类,也可以是接口。

● ConcreteBuilder(具体建造者):它实现了Builder接口,实现各个部件的具体构造和装配方法,定义并明确它所创建的复杂对象,也可以提供一个方法返回创建好的复杂产品对象。

● Product(产品角色):它是被构建的复杂对象,包含多个组成部件,具体建造者创建该产品的内部表示并定义它的装配过程。

● Director(指挥者):指挥者又称为导演类,它负责安排复杂对象的建造次序,指挥者与抽象建造者之间存在关联关系,可以在其construct()建造方法中调用建造者对象的部件构造与装配方法,完成复杂对象的建造。客户端一般只需要与指挥者进行交互,在客户端确定具体建造者的类型,并实例化具体建造者对象(也可以通过配置文件和反射机制),然后通过指挥者类的构造函数或者Setter方法将该对象传入指挥者类中。
class Product  {

       private  String partA; //定义部件,部件可以是任意类型,包括值类型和引用类型

       private  String partB;

       private  String partC;

       //partA的Getter方法和Setter方法省略

       //partB的Getter方法和Setter方法省略

       //partC的Getter方法和Setter方法省略

}
abstract class Builder {

     //创建产品对象
       protected  Product product=new Product();
       public  abstract void buildPartA();
       public  abstract void buildPartB();
       public  abstract void buildPartC();
     //返回产品对象
       public  Product getResult() {
              return  product;
       }
}

class ConcreateBuilder {
        //创建产品对象
        protected  Product product=new Product();

        @Override
        public  void buildPartA(){
            product.partA = "partA";
        }
        @Override
        public  void buildPartB(){
            product.partB = "partB";
        }
        @Override
        public  void buildPartC(){
            product.partC = "partC";
        }
    }
    class Director {

       private  Builder builder;
       public  Director(Builder builder) {
              this.builder=builder;
       }
       
       public  void setBuilder(Builder builder) {
              this.builder=builer;
       }
       
     //产品构建与组装方法
       public Product construct() {

              builder.buildPartA();

              builder.buildPartB();

              builder.buildPartC();

              return builder.getResult();
       }
}

更换新的建造者时无须修改源代码,系统扩展更为方便。在客户端代码中,无须关心产品对象的具体组装过程,只需指定具体建造者的类型即可。

建造者模式与抽象工厂模式有点相似,但是建造者模式返回一个完整的复杂产品,而抽象工厂模式返回一系列相关的产品;在抽象工厂模式中,客户端通过选择具体工厂来生成所需对象,而在建造者模式中,客户端通过指定具体建造者类型并指导Director类如何去生成对象,侧重于一步步构造一个复杂对象,然后将结果返回。如果将抽象工厂模式看成一个汽车配件生产厂,生成不同类型的汽车配件,那么建造者模式就是一个汽车组装厂,通过对配件进行组装返回一辆完整的汽车。

1.主要优点
(1) 在建造者模式中,客户端不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象。

(2) 每一个具体建造者都相对独立,而与其他的具体建造者无关,因此可以很方便地替换具体建造者或增加新的具体建造者,用户使用不同的具体建造者即可得到不同的产品对象。由于指挥者类针对抽象建造者编程,增加新的具体建造者无须修改原有类库的代码,系统扩展方便,符合“开闭原则”

(3) 可以更加精细地控制产品的创建过程。将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰,也更方便使用程序来控制创建过程。

2.主要缺点
(1) 建造者模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,例如很多组成部分都不相同,不适合使用建造者模式,因此其使用范围受到一定的限制。

(2) 如果产品的内部变化复杂,可能会导致需要定义很多具体建造者类来实现这种变化,导致系统变得很庞大,增加系统的理解难度和运行成本。

3.适用场景
(1) 需要生成的产品对象有复杂的内部结构,这些产品对象通常包含多个成员属性。

(2) 需要生成的产品对象的属性相互依赖,需要指定其生成顺序。

(3) 对象的创建过程独立于创建该对象的类。在建造者模式中通过引入了指挥者类,将创建过程封装在指挥者类中,而不在建造者类和客户类中。

(4) 隔离复杂对象的创建和使用,并使得相同的创建过程可以创建不同的产品。

1.6 原型模式

原型模式,是用于创建重复的对象,同时又能保证性能。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式是实现了一个原型接口,该接口用于创建当前对象的克隆。当直接创建对象的代价比较大时,则采用这种模式。例如,一个对象需要在一个高代价的数据库操作之后被创建。我们可以缓存该对象,在下一个请求时返回它的克隆,在需要的时候更新数据库,以此来减少数据库调用。

关键代码:
 1、实现克隆操作,在 JAVA 继承 Cloneable,重写 clone(),在 .NET 中可以使用 Object 类的 MemberwiseClone() 方法来实现对象的浅拷贝或通过序列化的方式来实现深拷贝。 
 2、原型模式同样用于隔离类对象的使用者和具体类型(易变类)之间的耦合关系,它同样要求这些"易变类"拥有稳定的接口。

使用场景:
  1、资源优化场景。
  2、类初始化需要消化非常多的资源,这个资源包括数据、硬件资源等。
  3、性能和安全要求的场景。 
  4、通过 new 产生一个对象需要非常繁琐的数据准备或访问权限,则可以使用原型模式。 
  5、一个对象多个修改者的场景。 
  6、一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时,可以考虑使用原型模式拷贝多个对象供调用者使用。 
  7、在实际项目中,原型模式很少单独出现,一般是和工厂方法模式一起出现,通过 clone 的方法创建一个对象,然后由工厂方法提供给调用者。原型模式已经与 Java 融为浑然一体,大家可以随手拿来使用。

优点: 
1、性能提高。 
2、逃避构造函数的约束。

缺点:
 1、配备克隆方法需要对类的功能进行通盘考虑,这对于全新的类不是很难,但对于已有的类不一定很容易,特别当一个类引用不支持串行化的间接对象,或者引用含有循环结构的时候。 
 2、必须实现 Cloneable 接口。

注意事项:与通过对一个类进行实例化来构造新对象不同的是,原型模式是通过拷贝一个现有对象生成新对象的。浅拷贝实现 Cloneable,重写,深拷贝是通过实现 Serializable 读取二进制流。

原型模式类图```
代码如下:

public abstract class Shape implements Cloneable {
   
   private String id;
   protected String type;
   
   abstract void draw();
   
   public String getType(){
      return type;
   }
   
   public String getId() {
      return id;
   }
   
   public void setId(String id) {
      this.id = id;
   }
   
   public Object clone() {
      Object clone = null;
      try {
         clone = super.clone();
      } catch (CloneNotSupportedException e) {
         e.printStackTrace();
      }
      return clone;
   }
}
public class Rectangle extends Shape {
 
   public Rectangle(){
     type = "Rectangle";
   }
 
   @Override
   public void draw() {
      System.out.println("Inside Rectangle::draw() method.");
   }
}

public class Square extends Shape {
 
   public Square(){
     type = "Square";
   }
 
   @Override
   public void draw() {
      System.out.println("Inside Square::draw() method.");
   }
}
public class Circle extends Shape {
 
   public Circle(){
     type = "Circle";
   }
 
   @Override
   public void draw() {
      System.out.println("Inside Circle::draw() method.");
   }
}
import java.util.Hashtable;
 
public class ShapeCache {
    
   private static Hashtable<String, Shape> shapeMap 
      = new Hashtable<String, Shape>();
 
   public static Shape getShape(String shapeId) {
      Shape cachedShape = shapeMap.get(shapeId);
      return (Shape) cachedShape.clone();
   }
 
   // 对每种形状都运行数据库查询,并创建该形状
   // shapeMap.put(shapeKey, shape);
   // 例如,我们要添加三种形状
   public static void loadCache() {
      Circle circle = new Circle();
      circle.setId("1");
      shapeMap.put(circle.getId(),circle);
 
      Square square = new Square();
      square.setId("2");
      shapeMap.put(square.getId(),square);
 
      Rectangle rectangle = new Rectangle();
      rectangle.setId("3");
      shapeMap.put(rectangle.getId(),rectangle);
   }
public class PrototypePatternDemo {
   public static void main(String[] args) {
      ShapeCache.loadCache();
 
      Shape clonedShape = (Shape) ShapeCache.getShape("1");
      System.out.println("Shape : " + clonedShape.getType());        
 
      Shape clonedShape2 = (Shape) ShapeCache.getShape("2");
      System.out.println("Shape : " + clonedShape2.getType());        
 
      Shape clonedShape3 = (Shape) ShapeCache.getShape("3");
      System.out.println("Shape : " + clonedShape3.getType());        
   }
}

小总结

  1. 深拷贝和浅拷贝都是重新开辟了空间。 但是浅拷贝对基本数据类型是复制,对引用类型是指向了同一个引用。深拷贝对基本数据类型是复制,对引用类型是并没有指向同一个引用,而是重新建了一个属性值对象。所以他们指向的并不是同一个地方。
  2. java中除了基本类型的其他类型都是引用类型。java中有8中基本类型:Byte、short、int、long、float、double、boolean、char。
  3. 引用类型变量由类的构造函数创建,可以使用它们访问所引用的对象。这些变量在声明时被指定为一个特定的类型,比如Employee、Pubby等。变量一旦声明后,类型就不能被改变了。对象、数组都是引用数据类型。所有引用类型的默认值都是null。一个引用变量可以用来引用与任何与之兼容的类型。例:Animalanimal = new Animal(“giraffe”)。
  4. String是特殊的引用类型,它在使用时跟基本类型相似。

2. 结构型模式

结构型模式,关注与类和对象的组合,继承的概念被用来组合接口和定义组合对象获得新功能的方式。

2.1 适配器模式

适配器模式(Adapter Pattern)是作为两个不兼容的接口之间的桥梁。这种类型的设计模式属于结构型模式,它结合了两个独立接口的功能。

这种模式涉及到一个单一的类,该类负责加入独立的或不兼容的接口功能。
举个真实的例子,读卡器是作为内存卡和笔记本之间的适配器。您将内存卡插入读卡器,再将读卡器插入笔记本,这样就可以通过笔记本来读取内存卡。

使用场景:有目的性地修改一个正常运行的系统的接口,这时应该考虑使用适配器模式。
注意事项:适配器不是在详细设计时添加的,而是解决正在服役的项目的问题。
优点: 
1、可以让任何两个没有关联的类一起运行。 
2、提高了类的复用。 
3、增加了类的透明度。 
4、灵活性好。

缺点: 
1. 过多地使用适配器,会让系统非常零乱,不易整体进行把握。比如,明明看到调用的是 A 接口,其实内部被适配成了 B 接口的实现,一个系统如果太多出现这种情况,无异于一场灾难。因此如果不是很有必要,可以不使用适配器,而是直接对系统进行重构。 
2. 由于 JAVA 至多继承一个类,所以至多只能适配一个适配者类,而且目标类必须是抽象类。

何时使用: 
1、系统需要使用现有的类,而此类的接口不符合系统的需要。 
2、想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作,这些源类不一定有一致的接口。 
3、通过接口转换,将一个类插入另一个类系中。(比如老虎和飞禽,现在多了一个飞虎,在不增加实体的需求下,增加一个适配器,在里面包容一个虎对象,实现飞的接口。)

如果要实现飞虎,且老虎的类不能够修改。那么就应该使用一个适配器adapter,让适配器继承老虎类,并实现飞的接口。

适配器模式类图

public interface MediaPlayer {
   public void play(String audioType, String fileName);
}
public interface AdvancedMediaPlayer { 
   public void playVlc(String fileName);
   public void playMp4(String fileName);
}
public class VlcPlayer implements AdvancedMediaPlayer{
   @Override
   public void playVlc(String fileName) {
      System.out.println("Playing vlc file. Name: "+ fileName);      
   }
 
   @Override
   public void playMp4(String fileName) {
      //什么也不做
   }
}
public class Mp4Player implements AdvancedMediaPlayer{
 
   @Override
   public void playVlc(String fileName) {
      //什么也不做
   }
 
   @Override
   public void playMp4(String fileName) {
      System.out.println("Playing mp4 file. Name: "+ fileName);      
   }
}
public class MediaAdapter implements MediaPlayer {
 
   AdvancedMediaPlayer advancedMusicPlayer;
 
   public MediaAdapter(String audioType){
      if(audioType.equalsIgnoreCase("vlc") ){
         advancedMusicPlayer = new VlcPlayer();       
      } else if (audioType.equalsIgnoreCase("mp4")){
         advancedMusicPlayer = new Mp4Player();
      }  
   }
 
   @Override
   public void play(String audioType, String fileName) {
      if(audioType.equalsIgnoreCase("vlc")){
         advancedMusicPlayer.playVlc(fileName);
      }else if(audioType.equalsIgnoreCase("mp4")){
         advancedMusicPlayer.playMp4(fileName);
      }
   }
}
public class AudioPlayer implements MediaPlayer {
   MediaAdapter mediaAdapter; 
 
   @Override
   public void play(String audioType, String fileName) {    
 
      //播放 mp3 音乐文件的内置支持
      if(audioType.equalsIgnoreCase("mp3")){
         System.out.println("Playing mp3 file. Name: "+ fileName);         
      } 
      //mediaAdapter 提供了播放其他文件格式的支持
      else if(audioType.equalsIgnoreCase("vlc") 
         || audioType.equalsIgnoreCase("mp4")){
         mediaAdapter = new MediaAdapter(audioType);
         mediaAdapter.play(audioType, fileName);
      }
      else{
         System.out.println("Invalid media. "+
            audioType + " format not supported");
      }
   }   
}
public class AdapterPatternDemo {
   public static void main(String[] args) {
      AudioPlayer audioPlayer = new AudioPlayer();
 
      audioPlayer.play("mp3", "beyond the horizon.mp3");
      audioPlayer.play("mp4", "alone.mp4");
      audioPlayer.play("vlc", "far far away.vlc");
      audioPlayer.play("avi", "mind me.avi");
   }
}

输出结果:

Playing mp3 file. Name: beyond the horizon.mp3
Playing mp4 file. Name: alone.mp4
Playing vlc file. Name: far far away.vlc
Invalid media. avi format not supported

类适配器和对象适配器的区别

  1. 类适配器:(首先还是得看目标是接口还是类,若是接口,则满足后面的操作特点)adapter继承了一个适配者,实现了某个或者某些目标接口。直接实现目标接口中的方法,并在该方法中调用所继承的适配者的方法。
  2. 对象适配器:adapter中含有某个或者某些适配者的实例,实现了某个或者某些目标接口。直接实现目标接口中的方法,并在该方法中利用适配者实例调用适配者的方法。
    类适配器和对象适配器结构图
    参与者
    a. 类适配器结构实现
 public interface ITarget
    {
        void Request();
    }
public class Adaptee
    {
        public void SpecificRequest()
        {
            Console.WriteLine("Called SpecificRequest()");
        }
    }
public class Adapter : Adaptee, ITarget
    {
        public void Request()
        {
            this.SpecificRequest();
        }
    }
public class Client
    {
        static void Main(string[] args)
        {
            ITarget t = new Adapter();
            t.Request();
        }
    }

运行结果

Called SpecificRequest()

b. 对象适配器结构的实现

 public interface ITarget
    {
        void Request();
    }
public class Target : ITarget
    {
        public virtual void Request()
        {
            Console.WriteLine("Called Target Request()");
        }
    }
public class Adaptee
    {
        public void SpecificRequest()
        {
            Console.WriteLine("Called SpecificRequest()");
        }
    }
public class Adapter : Target
    {
        private Adaptee _adaptee = new Adaptee();

        public override void Request()
        {
            _adaptee.SpecificRequest();
        }
    }
public class Client
    {
        static void Main(string[] args)
        {
            ITarget t = new Adapter();
            t.Request();
        }
    }

适配器模式应用分析

1. 适配器模式适用情形:

◊ 当适用一个已存在的类,而它的接口不符合所要求的情况;

◊ 想要创建一个可以复用的类,该类可以与原接口的类协调工作;

◊ 在对象适配中,当要匹配数个子类的时候,对象适配器可以适配它们的父类接口。

2. 适配器模式特点:

2.1 类适配器

◊ 使得Adapter可以重定义Adaptee的部分行为。因为Adapter是Adaptee的一个子类;

◊ 仅仅引入了一个对象,并不需要额外的指针间接得到Adaptee。

2.2 对象适配器

◊ 允许一个Adapter与多个Adaptee同时工作。Adapter也可以一次给所有的Adaptee添加功能;

◊ 使得重定义Adaptee的行为比较困难。需要生成一个Adaptee的子类,然后使Adapter引入这个子类而不是引用Adaptee本身。

小总结
总而言之,需不需要适配器就看:方不方便修改原有的代码。即对原有的代码进行增强或修改。若不能修改源码,则可以用到适配器。

2.2 桥接模式

桥接(Bridge)是用于把抽象化与实现化解耦,使得二者可以独立变化。这种类型的设计模式属于结构型模式,它通过提供抽象化和实现化之间的桥接结构,来实现二者的解耦。

系统中存在2个或者多个独立变化的纬度时:就可以通过桥接模式将这2个或者多个纬度分离出来,使得两者或者多者可以独立扩展,让系统更加符合“单一职责原则”。

2.2.1什么是桥接模式?

用代码来进行解释,as follows:
为了减少所需生成的子类数目,实现将操作系统和图像文件格式两个维度分离,使它们可以独立改变。
Image充当抽象类,其子类JPGImage、PNGImage、BMPImage和GIFImage充当扩充抽象类;ImageImp充当实现类接口,其子类WindowsImp、LinuxImp和UnixImp充当具体实现类。完整代码如下所示:

//像素矩阵类:辅助类,各种格式的文件最终都被转化为像素矩阵,不同的操作系统提供不同的方式显示像素矩阵
class Matrix {
	//此处代码省略
}
 
//抽象图像类:抽象类
abstract class Image {
	protected ImageImp imp;
 
	public void setImageImp(ImageImp imp) {
		this.imp = imp;
	} 
 
	public abstract void parseFile(String fileName);
}
 
//抽象操作系统实现类:实现类接口
interface ImageImp {
	public void doPaint(Matrix m);  //显示像素矩阵m
} 
 
//Windows操作系统实现类:具体实现类
class WindowsImp implements ImageImp {
    public void doPaint(Matrix m) {
    	//调用Windows系统的绘制函数绘制像素矩阵
    	System.out.print("在Windows操作系统中显示图像:");
    }
}
 
//Linux操作系统实现类:具体实现类
class LinuxImp implements ImageImp {
    public void doPaint(Matrix m) {
    	//调用Linux系统的绘制函数绘制像素矩阵
    	System.out.print("在Linux操作系统中显示图像:");
    }
}
 
//Unix操作系统实现类:具体实现类
class UnixImp implements ImageImp {
    public void doPaint(Matrix m) {
    	//调用Unix系统的绘制函数绘制像素矩阵
    	System.out.print("在Unix操作系统中显示图像:");
    }
}
 
//JPG格式图像:扩充抽象类
class JPGImage extends Image {
	public void parseFile(String fileName) {
        //模拟解析JPG文件并获得一个像素矩阵对象m;
        Matrix m = new Matrix(); 
        imp.doPaint(m);
        System.out.println(fileName + ",格式为JPG。");
    }
}
 
//PNG格式图像:扩充抽象类
class PNGImage extends Image {
	public void parseFile(String fileName) {
        //模拟解析PNG文件并获得一个像素矩阵对象m;
        Matrix m = new Matrix(); 
        imp.doPaint(m);
        System.out.println(fileName + ",格式为PNG。");
    }
}
 
//BMP格式图像:扩充抽象类
class BMPImage extends Image {
	public void parseFile(String fileName) {
        //模拟解析BMP文件并获得一个像素矩阵对象m;
        Matrix m = new Matrix(); 
        imp.doPaint(m);
        System.out.println(fileName + ",格式为BMP。");
    }
}
 
//GIF格式图像:扩充抽象类
class GIFImage extends Image {
	public void parseFile(String fileName) {
        //模拟解析GIF文件并获得一个像素矩阵对象m;
        Matrix m = new Matrix(); 
        imp.doPaint(m);
        System.out.println(fileName + ",格式为GIF。");
    }
}

2.2.2 桥接模式怎么体现出来的?

我认为,首先:通过将抽象和实现分离。其次:需要在抽象层建立一个关联关系。通过以上2点得以体现。

2.2.3 桥接模式是拿来解决什么的?

其实,桥接模式它为多维度变化的系统提供了一套完整的解决方案,降低了系统的复杂度。

2.2.4 桥接模式使用场景?

(1)如果一个系统需要在抽象化和具体化之间增加更多的灵活性,避免在两个层次之间建立静态的继承关系,通过桥接模式可以使它们在抽象层建立一个关联关系。
(2)“抽象部分”和“实现部分”可以以继承的方式独立扩展而互不影响,在程序运行时可以动态将一个抽象化子类的对象和一个实现化子类的对象进行组合,即系统需要对抽象化角色和实现化角色进行动态耦合。
(3)一个类存在两个(或多个)独立变化的维度,且这两个(或多个)维度都需要独立进行扩展。
(4)对于那些不希望使用继承或因为多层继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。

2.2.5 桥接模式的优缺点?

桥接模式的主要优点如下:

(1)分离抽象接口及其实现部分。桥接模式使用“对象间的关联关系”解耦了抽象和实现之间固有的绑定关系,使得抽象和实现可以沿着各自的维度来变化。所谓抽象和实现沿着各自维度的变化,也就是说抽象和实现不再在同一个继承层次结构中,而是“子类化”它们,使它们各自都具有自己的子类,以便任何组合子类,从而获得多维度组合对象。

(2)在很多情况下,桥接模式可以取代多层继承方案,多层继承方案违背了“单一职责原则”,复用性较差,且类的个数非常多,桥接模式是比多层继承方案更好的解决方法,它极大减少了子类的个数。

(3)桥接模式提高了系统的可扩展性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统,符合“开闭原则”。

2.主要缺点
桥接模式的主要缺点如下:

(1)桥接模式的使用会增加系统的理解与设计难度,由于关联关系建立在抽象层,要求开发者一开始就针对抽象层进行设计与编程。
    
(2)桥接模式要求正确识别出系统中两个独立变化的维度,因此其使用范围具有一定的局限性,如何正确识别两个独立维度也需要一定的经验积累。

2.2.6 目前的使用方式

常用于适配器模式和桥接模式的联用:
在软件开发中,适配器模式通常可以与桥接模式联合使用。适配器模式可以解决两个已有接口间不兼容问题,在这种情况下被适配的类往往是一个黑盒子,有时候我们不想也不能改变这个被适配的类,也不能控制其扩展。适配器模式通常用于现有系统与第三方产品功能的集成,采用增加适配器的方式将第三方类集成到系统中。桥接模式则不同,用户可以通过接口继承或类继承的方式来对系统进行扩展。

桥接模式和适配器模式用于设计的不同阶段,桥接模式用于系统的初步设计,对于存在两个独立变化维度的类可以将其分为抽象化和实现化两个角色,使它们可以分别进行变化;而在初步设计完成之后,当发现系统与已有类无法协同工作时,可以采用适配器模式。但有时候在设计初期也需要考虑适配器模式,特别是那些涉及到大量第三方应用接口的情况。

2.2.7 目前使用方式的用例

下面通过一个实例来说明适配器模式和桥接模式的联合使用:

在某系统的报表处理模块中,需要将报表显示和数据采集分开,系统可以有多种报表显示方式也可以有多种数据采集方式,如可以从文本文件中读取数据,也可以从数据库中读取数据,还可以从Excel文件中获取数据。如果需要从Excel文件中获取数据,则需要调用与Excel相关的API,而这个API是现有系统所不具备的,该API由厂商提供。使用适配器模式和桥接模式设计该模块。

在设计过程中,由于存在报表显示和数据采集两个独立变化的维度,因此可以使用桥接模式进行初步设计;为了使用Excel相关的API来进行数据采集则需要使用适配器模式。系统的完整设计中需要将两个模式联用。

2.3 组合模式

2.3.1 什么是组合模式?

  1. 组合模式,就是一个对象中包含其他对象,这些被包含的对象可能是终点对象(不再包含其他对象),也有可能是非终点对象(内部还包含其他对象),我们将对象称为节点,即一个根节点包含许多子节点,这些子节点有的不再包含子节点,而有的仍然包含子节点。
  2. 组合模式使用面向对象的思想来实现树形结构的构建与处理,描述了如何将容器对象和叶子对象进行递归组合,实现简单,灵活性好。由于在软件开发中存在大量的树形结构,因此组合模式是一种使用频率较高的结构型设计模式。

2.3.2 组合模式它解决了什么问题?

组合模式,它就是为处理树形结构提供了一种较为完美的解决方案,它描述了如何将容器和叶子进行递归组合,使得用户在使用时无须对他们进行区分,可以一致地对待容器和叶子。

2.3.3 用代码解释

AbstractFile充当抽象构件类,Folder充当容器构件类,ImageFile、TextFile和VideoFile充当叶子构件类。完整代码如下所示:

import java.util.*;
 
//抽象文件类:抽象构件
abstract class AbstractFile {
	public abstract void add(AbstractFile file);
	public abstract void remove(AbstractFile file);
	public abstract AbstractFile getChild(int i);
	public abstract void killVirus();
}
 
//图像文件类:叶子构件
class ImageFile extends AbstractFile {
	private String name;
	
	public ImageFile(String name) {
		this.name = name;
	}
	
	public void add(AbstractFile file) {
	   System.out.println("对不起,不支持该方法!");
	}
	
	public void remove(AbstractFile file) {
		System.out.println("对不起,不支持该方法!");
	}
	
	public AbstractFile getChild(int i) {
		System.out.println("对不起,不支持该方法!");
		return null;
	}
	
	public void killVirus() {
		//模拟杀毒
		System.out.println("----对图像文件'" + name + "'进行杀毒");
	}
}
 
//文本文件类:叶子构件
class TextFile extends AbstractFile {
	private String name;
	
	public TextFile(String name) {
		this.name = name;
	}
	
	public void add(AbstractFile file) {
	   System.out.println("对不起,不支持该方法!");
	}
	
	public void remove(AbstractFile file) {
		System.out.println("对不起,不支持该方法!");
	}
	
	public AbstractFile getChild(int i) {
		System.out.println("对不起,不支持该方法!");
		return null;
	}
	
	public void killVirus() {
		//模拟杀毒
		System.out.println("----对文本文件'" + name + "'进行杀毒");
	}
}
 
//视频文件类:叶子构件
class VideoFile extends AbstractFile {
	private String name;
	
	public VideoFile(String name) {
		this.name = name;
	}
	
	public void add(AbstractFile file) {
	   System.out.println("对不起,不支持该方法!");
	}
	
	public void remove(AbstractFile file) {
		System.out.println("对不起,不支持该方法!");
	}
	
	public AbstractFile getChild(int i) {
		System.out.println("对不起,不支持该方法!");
		return null;
	}
	
	public void killVirus() {
		//模拟杀毒
		System.out.println("----对视频文件'" + name + "'进行杀毒");
	}
}
 
//文件夹类:容器构件
class Folder extends AbstractFile {
	//定义集合fileList,用于存储AbstractFile类型的成员
	private ArrayList<AbstractFile> fileList=new ArrayList<AbstractFile>();
	private String name;
		
	public Folder(String name) {
		this.name = name;
	}
	
	public void add(AbstractFile file) {
	   fileList.add(file);	
	}
	
	public void remove(AbstractFile file) {
		fileList.remove(file);
	}
	
	public AbstractFile getChild(int i) {
		return (AbstractFile)fileList.get(i);
	}
	
	public void killVirus() {
		System.out.println("****对文件夹'" + name + "'进行杀毒");  //模拟杀毒
		
		//递归调用成员构件的killVirus()方法
		for(Object obj : fileList) {
			((AbstractFile)obj).killVirus();
		}
	}
}

2.3.4 透明组合模式和安全组合模式

  1. 透明组合模式:
    透明组合模式中,抽象构件Component中声明了所有用于管理成员对象的方法,包括add()、remove()以及getChild()等方法,这样做的好处是确保所有的构件类都有相同的接口。在客户端看来,叶子对象与容器对象所提供的方法是一致的,客户端可以相同地对待所有的对象。透明组合模式也是组合模式的标准形式。透明组合模式中,抽象构件Component中声明了所有用于管理成员对象的方法,包括add()、remove()以及getChild()等方法,这样做的好处是确保所有的构件类都有相同的接口。在客户端看来,叶子对象与容器对象所提供的方法是一致的,客户端可以相同地对待所有的对象。透明组合模式也是组合模式的标准形式。
    透明组合模式的缺点是不够安全,因为叶子对象和容器对象在本质上是有区别的。叶子对象不可能有下一个层次的对象,即不可能包含成员对象,因此为其提供add()、remove()以及getChild()等方法是没有意义的,这在编译阶段不会出错,但在运行阶段如果调用这些方法可能会出错(如果没有提供相应的错误处理代码)。
  2. 安全组合模式:
    安全组合模式中,在抽象构件Component中没有声明任何用于管理成员对象的方法,而是在Composite类中声明并实现这些方法。这种做法是安全的,因为根本不向叶子对象提供这些管理成员对象的方法,对于叶子对象,客户端不可能调用到这些方法。
    安全组合模式的缺点是不够透明,因为叶子构件和容器构件具有不同的方法,且容器构件中那些用于管理成员对象的方法没有在抽象构件类中定义,因此客户端不能完全针对抽象编程,必须有区别地对待叶子构件和容器构件。在实际应用中,安全组合模式的使用频率也非常高。

2.3.5 组合模式优缺点

  1. 优点:
    (1)组合模式可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,它让客户端忽略了层次的差异,方便对整个层次结构进行控制。
    (2)增加新的容器构件和叶子构件都很方便,无需对现有类库进行任何修改,符合“开闭原则”。
    (3)组合模式为树形结构的面向对象实现提供了一种灵活的解决方案,通过叶子对象和容器对象的递归组合。

  2. 缺点:
    在增加新构件时很难对容器中的构件类型进行限制。有时候我们希望一个容器中只能有某些特定类型的对象,例如在某个文件夹中只能包含文本文件,使用组合模式时,不能依赖类型系统来施加这些约束,因为它们都来自于相同的抽象层,在这种情况下,必须通过在运行时进行类型检查来实现,这个实现过程较为复杂。

2.3.6 组合模式使用场景

(1) 在具有整体和部分的层次结构中,希望通过一种方式忽略整体与部分的差异,客户端可以一致地对待它们。
(2) 在一个使用面向对象语言开发的系统中需要处理一个树形结构。
(3) 在一个系统中能够分离出叶子对象和容器对象,而且它们的类型不固定,需要增加一些新的类型。

2.4 装饰模式

2.4.1 装饰模式的解释

装饰模式可以在不改变一个对象本身功能的基础上给对象增加额外的新行为,在现实生活中,这种情况也到处存在,例如一张照片,我们可以不改变照片本身,给它增加一个相框,使得它具有防潮的功能,而且用户可以根据需要给它增加不同类型的相框,甚至可以在一个小相框的外面再套一个大相框。

装饰模式是一种用于替代继承的技术,它通过一种无须定义子类的方式来给对象动态增加职责,使用对象之间的关联关系取代类之间的继承关系。在装饰模式中引入了装饰类,在装饰类中既可以调用待装饰的原有类的方法,还可以增加新的方法,以扩充原有类的功能。

装饰模式(Decorator Pattern):动态地给一个对象增加一些额外的职责,就增加对象功能来说,装饰模式比生成子类实现更为灵活。装饰模式是一种对象结构型模式。

2.4.2 装饰模式中的角色

在装饰模式结构图中包含如下几个角色:

● Component(抽象构件):它是具体构件和抽象装饰类的共同父类,声明了在具体构件中实现的业务方法,它的引入可以使客户端以一致的方式处理未被装饰的对象以及装饰之后的对象,实现客户端的透明操作。

● ConcreteComponent(具体构件):它是抽象构件类的子类,用于定义具体的构件对象,实现了在抽象构件中声明的方法,装饰器可以给它增加额外的职责(方法)。

● Decorator(抽象装饰类):它也是抽象构件类的子类,用于给具体构件增加职责,但是具体职责在其子类中实现。它维护一个指向抽象构件对象的引用,通过该引用可以调用装饰之前构件对象的方法,并通过其子类扩展该方法,以达到装饰的目的。

● ConcreteDecorator(具体装饰类):它是抽象装饰类的子类,负责向构件添加新的职责。每一个具体装饰类都定义了一些新的行为,它可以调用在抽象装饰类中定义的方法,并可以增加新的方法用以扩充对象的行为。

2.4.3 上代码

Component充当抽象构件类,其子类Window、TextBox、ListBox充当具体构件类,Component类的另一个子类ComponentDecorator充当抽象装饰类,ComponentDecorator的子类ScrollBarDecorator和BlackBorderDecorator充当具体装饰类。完整代码如下所示:

//抽象界面构件类:抽象构件类,为了突出与模式相关的核心代码,对原有控件代码进行了大量的简化

abstract class Component

{

       public  abstract void display();

}

 

//窗体类:具体构件类

class Window extends Component

{

       public  void display()

       {

              System.out.println("显示窗体!");

       }

}

 

//文本框类:具体构件类

class TextBox extends Component

{

       public  void display()

       {

              System.out.println("显示文本框!");

       }

}

 

//列表框类:具体构件类

class ListBox extends Component

{

       public  void display()

       {

              System.out.println("显示列表框!");

       }

}

 

//构件装饰类:抽象装饰类

class ComponentDecorator extends Component

{

       private Component component;  //维持对抽象构件类型对象的引用

 

       public ComponentDecorator(Component  component)  //注入抽象构件类型的对象

       {

              this.component = component;

       }

 

       public void display()

       {

              component.display();

       }

}

 

//滚动条装饰类:具体装饰类

class ScrollBarDecorator extends  ComponentDecorator

{

       public ScrollBarDecorator(Component  component)

       {

              super(component);

       }

 

       public void display()

       {

              this.setScrollBar();

              super.display();

       }

 

       public  void setScrollBar()

       {

              System.out.println("为构件增加滚动条!");

       }

}

 

//黑色边框装饰类:具体装饰类

class BlackBorderDecorator extends  ComponentDecorator

{

       public BlackBorderDecorator(Component  component)

       {

              super(component);

       }

 

       public void display()

       {

              this.setBlackBorder();

              super.display();

       }

 

       public  void setBlackBorder()

       {

              System.out.println("为构件增加黑色边框!");

       }

}

只装饰一次的客户端代码:

class Client

{

       public  static void main(String args[])

       {

              Component component,componentSB;  //使用抽象构件定义

              component = new Window(); //定义具体构件

              componentSB = new  ScrollBarDecorator(component); //定义装饰后的构件

              componentSB.display();

       }

}

运行结果:

为构件增加滚动条!

显示窗体!

多次装饰的客户端代码:

class Client

{

       public  static void main(String args[])

       {

              Component  component,componentSB,componentBB; //全部使用抽象构件定义

              component = new Window();

              componentSB = new  ScrollBarDecorator(component);

              componentBB = new  BlackBorderDecorator(componentSB); //将装饰了一次之后的对象继续注入到另一个装饰类中,进行第二次装饰

              componentBB.display();

       }

}

运行结果:

为构件增加黑色边框!

为构件增加滚动条!

显示窗体!

我们可以将装饰了一次之后的componentSB对象注入另一个装饰类BlackBorderDecorator中实现第二次装饰,得到一个经过两次装饰的对象componentBB,再调用componentBB的display()方法即可得到一个既有滚动条又有黑色边框的窗体。

如果需要在原有系统中增加一个新的具体构件类或者新的具体装饰类,无须修改现有类库代码,只需将它们分别作为抽象构件类或者抽象装饰类的子类即可。与图12-2所示的继承结构相比,使用装饰模式之后将大大减少了子类的个数,让系统扩展起来更加方便,而且更容易维护,是取代继承复用的有效方式之一。

2.4.4 透明装饰和不透明装饰模式

Document充当抽象构件类,PurchaseRequest和LeaveRequest充当具体构件类,Decorator充当抽象装饰类,Approver和Deleter充当具体装饰类。其中Decorator类和Approver类的示例代码如下所示:

//抽象装饰类

class Decorator implements  Document

{

      private Document  document;

      

      public Decorator(Document  document)

      {

             this.  document = document;

      }

      

      public void display()

      {

             document.display();

      }

}

 

//具体装饰类

class Approver extends  Decorator

{

      public Approver(Document document)

      {

             super(document);

             System.out.println("增加审批功能!");

      }

      

      public void approve()

      {

             System.out.println("审批文件!");

      }

}

透明装饰模式:在透明装饰模式中,要求客户端完全针对抽象编程,装饰模式的透明性要求客户端程序不应该将对象声明为具体构件类型或具体装饰类型,而应该全部声明为抽象构件类型。对于客户端而言,具体构件对象和具体装饰对象没有任何区别。也就是应该使用如下代码:

Component component,componentSB,componentBB; //全部使用抽象构件定义

component = new Window();

componentSB = new ScrollBarDecorator(component);

componentBB = new BlackBorderDecorator(componentSB);

componentBB.display();

透明装饰模式:

  1. 使用抽象构件类型定义全部具体构件对象和具体装饰对象,客户端可以一致地使用这些对象,因此符合透明装饰模式的要求。
  2. 可以对一个已经装饰过了的对象进行多次装饰,得到更复杂、功能更强大的对象。

半透明装饰模式:透明装饰模式的设计难度较大,而且有时我们需要单独调用新增的业务方法。为了能够调用到新增方法,我们不得不用具体装饰类型来定义装饰之后的对象,而具体构件类型还是可以使用抽象构件类型来定义,这种装饰模式即为半透明装饰模式,也就是说,对于客户端而言,具体构件类型无须关心,是透明的;但是具体装饰类型必须指定,这是不透明的。

Document  doc; //使用抽象构件类型定义

doc = new PurchaseRequest();

Approver newDoc; //使用具体装饰类型定义

newDoc = new Approver(doc);

半透明装饰模式:

  1. 最大的缺点在于,不能实现对同一个对象的多次装饰,而且,客户端需要有区别地对待装饰前和装饰后的对象。
  2. 在实现半透明的装饰模式时,我们只需在具体装饰类在实现半透明的装饰模式时,我们只需在具体装饰类中增加一个独立的方法来封装相应的业务处理,由于客户端使用具体装饰类来定义装饰后的对象,因此可以单独调用新增的方法来扩展系统功能。

装饰模式的注意事项:

  1. 尽量保持装饰类的接口与被装饰类的接口相同,这样,对于客户端而言,无论是装饰之前的对象还是装饰之后的对象都可以一致对待。这也就是说,在可能的情况下,我们应该尽量使用透明装饰模式。
  2. 尽量保持具体构件类ConcreteComponent是一个“轻”类,也就是说不要把太多的行为放在具体构件类中,我们可以通过装饰类对其进行扩展。
  3. 如果只有一个具体构件类,那么抽象装饰类可以作为该具体构件类的直接子类。

装饰模式总结:
优点:

  1. 对于扩展一个对象的功能,装饰模式比继承更加灵活性,不会导致类的个数急剧增加。
  2. 可以对一个对象进行多次装饰,通过使用不同的具体装饰类以及这些装饰类的排列组合,可以创造出很多不同行为的组合,得到功能更为强大的对象。
  3. 具体构件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构件类和具体装饰类,原有类库代码无须改变,符合“开闭原则”。

缺点:

  1. 使用装饰模式进行系统设计时将产生很多小对象,这些对象的区别在于它们之间相互连接的方式有所不同,而不是它们的类或者属性值有所不同,大量小对象的产生势必会占用更多的系统资源,在一定程序上影响程序的性能。
  2. 装饰模式提供了一种比继承更加灵活机动的解决方案,但同时也意味着比继承更加易于出错,排错也很困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为繁琐。

适用场景:

  1. 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
  2. 当不能采用继承的方式对系统进行扩展或者采用继承不利于系统扩展和维护时可以使用装饰模式。不能采用继承的情况主要有两类:第一类是系统中存在大量独立的扩展,为支持每一种扩展或者扩展之间的组合将产生大量的子类,使得子类数目呈爆炸性增长;第二类是因为类已定义为不能被继承(如Java语言中的final类)。

2.5 外观模式

2.5.1 模式结构


外观模式包含如下两个角色:

(1) Facade(外观角色):在客户端可以调用它的方法,在外观角色中可以知道相关的(一个或者多个)子系统的功能和责任;在正常情况下,它将所有从客户端发来的请求委派到相应的子系统去,传递给相应的子系统对象处理。

(2) SubSystem(子系统角色):在软件系统中可以有一个或者多个子系统角色,每一个子系统可以不是一个单独的类,而是一个类的集合,它实现子系统的功能;每一个子系统都可以被客户端直接调用,或者被外观角色调用,它处理由外观类传过来的请求;子系统并不知道外观的存在,对于子系统而言,外观角色仅仅是另外一个客户端而已。

2.5.2 模式实现

外观模式的主要目的在于降低系统的复杂程度。引入外观模式之后,增加新的子系统或者移除子系统都非常方便,客户类无须进行修改(或者极少的修改),只需要在外观类中增加或移除对子系统的引用即可。从这一点来说,外观模式在一定程度上并不符合开闭原则,增加新的子系统需要对原有系统进行一定的修改,虽然这个修改工作量不大。

外观模式中所指的子系统是一个广义的概念,它可以是一个类、一个功能模块、系统的一个组成部分或者一个完整的系统。子系统类通常是一些业务类,实现了一些具体的、独立的业务功能。

上代码:

class SubSystemA
{
    public void MethodA()
    {
        //业务实现代码
    }
}
 
class SubSystemB
{
    public void MethodB()
    {
        //业务实现代码
     }
}
 
class SubSystemC
{
    public void MethodC()
    {
        //业务实现代码
    }
}

class Facade
{
    private SubSystemA obj1 = new SubSystemA();
    private SubSystemB obj2 = new SubSystemB();
    private SubSystemC obj3 = new SubSystemC();
 
    public void Method()
    {
        obj1.MethodA();
        obj2.MethodB();
        obj3.MethodC();
    }
}

客户端代码:

class Program
{
    static void Main(string[] args)
    {
        Facade facade = new Facade();
        facade.Method();
    }
}

2.5.3 外观模式的优化

主要是:通过引入抽象外观类。
在标准的外观模式结构图中,如果需要增加、删除或更换与外观类交互的子系统类,必须修改外观类或客户端的源代码,这将违背开闭原则,因此可以通过引入抽象外观类来对系统进行改进,在一定程度上可以解决该问题。在引入抽象外观类之后,客户端可以针对抽象外观类进行编程,对于新的业务需求,不需要修改原有外观类,而对应增加一个新的具体外观类,由新的具体外观类来关联新的子系统对象,同时通过修改配置文件来达到不修改任何源代码并更换外观类的目的。

抽象外观类代码如下:

namespace FacadeSample
{
    abstract class AbstractEncryptFacade
    {
        public abstract void FileEncrypt(string fileNameSrc, string fileNameDes);
    }
}

具体外观类代码如下:

namespace FacadeSample
{
    class NewEncryptFacade : AbstractEncryptFacade
    {
        private FileReader reader;
        private NewCipherMachine cipher;
        private FileWriter writer;
 
        public NewEncryptFacade()
        {
            reader = new FileReader();
            cipher = new NewCipherMachine();
            writer = new FileWriter();
        }
 
        public override void FileEncrypt(string fileNameSrc, string fileNameDes)
        {
            string plainStr = reader.Read(fileNameSrc);
            string encryptStr = cipher.Encrypt(plainStr);
            writer.Write(encryptStr, fileNameDes);
        }
    }
}

这样的话,就符合开闭原则了。

2.5.4 外观模式的优缺点和适用场景

a.优点:

  1. 它对客户端屏蔽了子系统组件,减少了客户端所需处理的对象数目,并使得子系统使用起来更加容易。通过引入外观模式,客户端代码将变得很简单,与之关联的对象也很少。
  2. 它实现了子系统与客户端之间的松耦合关系,这使得子系统的变化不会影响到调用它的客户端,只需要调整外观类即可。
  3. 一个子系统的修改对其他子系统没有任何影响,而且子系统内部变化也不会影响到外观对象。

b. 缺点:

  1. 不能很好地限制客户端直接使用子系统类,如果对客户端访问子系统类做太多的限制则减少了可变性和灵活 性。
  2. 如果设计不当,增加新的子系统可能需要修改外观类的源代码,违背了开闭原则。

c. 适用场景:

  1. 当要为访问一系列复杂的子系统提供一个简单入口时可以使用外观模式。
  2. 客户端程序与多个子系统之间存在很大的依赖性。引入外观类可以将子系统与客户端解耦,从而提高子系统的独立性和可移植性。
  3. 在层次化结构中,可以使用外观模式定义系统中每一层的入口,层与层之间不直接产生联系,而通过外观类建立联系,降低层之间的耦合度。

2.6 享元模式

2.6.1 享元模式的理解?

例如在一个文本字符串中存在很多重复的字符,如果每一个字符都用一个单独的对象来表示,将会占用较多的内存空间,那么我们如何去避免系统中出现大量相同或相似的对象,同时又不影响客户端程序通过面向对象的方式对这些对象进行操作?

享元模式正为解决这一类问题而诞生。享元模式通过共享技术实现相同或相似对象的重用,在逻辑上每一个出现的字符都有一个对象与之对应,然而在物理上它们却共享同一个享元对象,这个对象可以出现在一个字符串的不同地方,相同的字符对象都指向同一个实例,在享元模式中,存储这些共享实例对象的地方称为享元池(Flyweight Pool)。我们可以针对每一个不同的字符创建一个享元对象,将其放在享元池中,需要时再从享元池取出。

2.6.2 享元模式的内部和外部状态?

享元模式以共享的方式高效地支持大量细粒度对象的重用,享元对象能做到共享的关键是区分了内部状态(Intrinsic State)和外部状态(Extrinsic State)。

内部状态和外部状态:
内部状态: 内部状态是存储在享元对象内部并且不会随环境改变而改变的状态,内部状态可以共享。如字符的内容,不会随外部环境的变化而变化,无论在任何环境下字符“a”始终是“a”,都不会变成“b”。

外部状态:外部状态是随环境改变而改变的、不可以共享的状态。享元对象的外部状态通常由客户端保存,并在享元对象被创建之后,需要使用的时候再传入到享元对象内部。一个外部状态与另一个外部状态之间是相互独立的。如字符的颜色,可以在不同的地方有不同的颜色,例如有的“a”是红色的,有的“a”是绿色的,字符的大小也是如此,有的“a”是五号字,有的“a”是四号字。而且字符的颜色和大小是两个独立的外部状态,它们可以独立变化,相互之间没有影响,客户端可以在使用时将外部状态注入享元对象中。

2.6.3 享元模式的结构

享元模式结构较为复杂,一般结合工厂模式一起使用,在它的结构图中包含了一个享元工厂类。
在享元模式结构图中包含如下几个角色:

  ● Flyweight(抽象享元类):通常是一个接口或抽象类,在抽象享元类中声明了具体享元类公共的方法,这些方法可以向外界提供享元对象的内部数据(内部状态),同时也可以通过这些方法来设置外部数据(外部状态)。

  ● ConcreteFlyweight(具体享元类):它实现了抽象享元类,其实例称为享元对象;在具体享元类中为内部状态提供了存储空间。通常我们可以结合单例模式来设计具体享元类,为每一个具体享元类提供唯一的享元对象。

  ● UnsharedConcreteFlyweight(非共享具体享元类):并不是所有的抽象享元类的子类都需要被共享,不能被共享的子类可设计为非共享具体享元类;当需要一个非共享具体享元类的对象时可以直接通过实例化创建。

  ● FlyweightFactory(享元工厂类):享元工厂类用于创建并管理享元对象,它针对抽象享元类编程,将各种类型的具体享元对象存储在一个享元池中,享元池一般设计为一个存储“键值对”的集合(也可以是其他类型的集合),可以结合工厂模式进行设计;当用户请求一个具体享元对象时,享元工厂提供一个存储在享元池中已创建的实例或者创建一个新的实例(如果不存在的话),返回新创建的实例并将其存储在享元池中。

  在享元模式中引入了享元工厂类,享元工厂类的作用在于提供一个用于存储享元对象的享元池,当用户需要对象时,首先从享元池中获取,如果享元池中不存在,则创建一个新的享元对象返回给用户,并在享元池中保存该新增对象。

2.6.4 享元模式围棋棋子实例

在围棋棋盘中包含大量的黑子和白子,它们的形状、大小都一模一样,只是出现的位置不同而已。如果将每一个棋子都作为一个独立的对象存储在内存中,将导致该围棋软件在运行时所需内存空间较大,如何降低运行代价、提高系统性能?

IgoChessman充当抽象享元类,BlackIgoChessman和WhiteIgoChessman充当具体享元类,IgoChessmanFactory充当享元工厂类。
代码如下:

import java.util.*;
 
//围棋棋子类:抽象享元类
abstract class IgoChessman {
	public abstract String getColor();
 
	public void display() {
		System.out.println("棋子颜色:" + this.getColor());	
	}
}
 
//黑色棋子类:具体享元类
class BlackIgoChessman extends IgoChessman {
	public String getColor() {
		return "黑色";
	}	
}
 
//白色棋子类:具体享元类
class WhiteIgoChessman extends IgoChessman {
	public String getColor() {
		return "白色";
	}
}
 
//围棋棋子工厂类:享元工厂类,使用单例模式进行设计
class IgoChessmanFactory {
	private static IgoChessmanFactory instance = new IgoChessmanFactory();
	private static Hashtable ht; //使用Hashtable来存储享元对象,充当享元池
	
	private IgoChessmanFactory() {
		ht = new Hashtable();
		IgoChessman black,white;
		black = new BlackIgoChessman();
		ht.put("b",black);
		white = new WhiteIgoChessman();
		ht.put("w",white);
	}
	
    //返回享元工厂类的唯一实例
	public static IgoChessmanFactory getInstance() {
		return instance;
	}
	
    //通过key来获取存储在Hashtable中的享元对象
	public static IgoChessman getIgoChessman(String color) {
		return (IgoChessman)ht.get(color);	
	}
}

客户端代码:

class Client {
	public static void main(String args[]) {
		IgoChessman black1,black2,black3,white1,white2;
		IgoChessmanFactory factory;
        
        //获取享元工厂对象
		factory = IgoChessmanFactory.getInstance();
 
        //通过享元工厂获取三颗黑子
		black1 = factory.getIgoChessman("b");
		black2 = factory.getIgoChessman("b");
		black3 = factory.getIgoChessman("b");
		System.out.println("判断两颗黑子是否相同:" + (black1==black2));
 
        //通过享元工厂获取两颗白子
		white1 = factory.getIgoChessman("w");
		white2 = factory.getIgoChessman("w");
		System.out.println("判断两颗白子是否相同:" + (white1==white2));
 
        //显示棋子
		black1.display();
		black2.display();
		black3.display();
		white1.display();
		white2.display();
	}
}

输出结果:

判断两颗黑子是否相同:true

判断两颗白子是否相同:true

棋子颜色:黑色

棋子颜色:黑色

棋子颜色:黑色

棋子颜色:白色

棋子颜色:白色

从输出结果可以看出,虽然我们获取了三个黑子对象和两个白子对象,但是它们的内存地址相同,也就是说,它们实际上是同一个对象。在实现享元工厂类时我们使用了单例模式和简单工厂模式,确保了享元工厂对象的唯一性,并提供工厂方法来向客户端返回享元对象。

2.6.5 带外部状态的解决方案

虽然黑色棋子和白色棋子可以共享,但是它们将显示在棋盘的不同位置,如何让相同的黑子或者白子能够多次重复显示且位于一个棋盘的不同地方?解决方法就是将棋子的位置定义为棋子的一个外部状态,在需要时再进行设置。增加了一个新的类Coordinates(坐标类),用于存储每一个棋子的位置。

除了增加一个坐标类Coordinates以外,抽象享元类IgoChessman中的display()方法也将对应增加一个Coordinates类型的参数,用于在显示棋子时指定其坐标,Coordinates类和修改之后的IgoChessman类的代码如下所示:

//坐标类:外部状态类
class Coordinates {
	private int x;
	private int y;
	
	public Coordinates(int x,int y) {
		this.x = x;
		this.y = y;
	}
	
	public int getX() {
		return this.x;
	}
	
	public void setX(int x) {
		this.x = x;
	}
	
	public int getY() {
		return this.y;
	}
	
	public void setY(int y) {
		this.y = y;
	}
} 
 
//围棋棋子类:抽象享元类
abstract class IgoChessman {
	public abstract String getColor();
	
	public void display(Coordinates coord){
		System.out.println("棋子颜色:" + this.getColor() + ",棋子位置:" + coord.getX() + "," + coord.getY() );	
	}
}

客户端代码:

class Client {
	public static void main(String args[]) {
		IgoChessman black1,black2,black3,white1,white2;
		IgoChessmanFactory factory;
        
        //获取享元工厂对象
		factory = IgoChessmanFactory.getInstance();
 
        //通过享元工厂获取三颗黑子
		black1 = factory.getIgoChessman("b");
		black2 = factory.getIgoChessman("b");
		black3 = factory.getIgoChessman("b");
		System.out.println("判断两颗黑子是否相同:" + (black1==black2));
 
        //通过享元工厂获取两颗白子
		white1 = factory.getIgoChessman("w");
		white2 = factory.getIgoChessman("w");
		System.out.println("判断两颗白子是否相同:" + (white1==white2));
 
        //显示棋子,同时设置棋子的坐标位置
		black1.display(new Coordinates(1,2));
		black2.display(new Coordinates(3,4));
		black3.display(new Coordinates(1,3));
		white1.display(new Coordinates(2,5));
		white2.display(new Coordinates(2,4));
	}
}

输出结果:

判断两颗黑子是否相同:true

判断两颗白子是否相同:true

棋子颜色:黑色,棋子位置:12

棋子颜色:黑色,棋子位置:34

棋子颜色:黑色,棋子位置:13

棋子颜色:白色,棋子位置:25

棋子颜色:白色,棋子位置:24

从输出结果可以看到,在每次调用display()方法时,都设置了不同的外部状态——坐标值,因此相同的棋子对象虽然具有相同的颜色,但是它们的坐标值不同,将显示在棋盘的不同位置。

2.6.6 享元模式总结

a. 与其他模式联用
几种常见的联用方式如下:
(1) 在享元模式的享元工厂类中通常提供一个静态的工厂方法用于返回享元对象,使用简单工厂模式来生成享元对象。

(2) 在一个系统中,通常只有唯一一个享元工厂,因此可以使用单例模式进行享元工厂类的设计。

(3) 享元模式可以结合组合模式形成复合享元模式,统一对多个享元对象设置外部状态。

b. 享元模式与String类
JDK类库中的String类使用了享元模式。

c. 享元模式优点
(1) 可以极大减少内存中对象的数量,使得相同或相似对象在内存中只保存一份,从而可以节约系统资源,提高系统性能。

(2) 享元模式的外部状态相对独立,而且不会影响其内部状态,从而使得享元对象可以在不同的环境中被共享。
d. 享元模式缺点
(1) 享元模式使得系统变得复杂,需要分离出内部状态和外部状态,这使得程序的逻辑复杂化。

(2) 为了使对象可以共享,享元模式需要将享元对象的部分状态外部化,而读取外部状态将使得运行时间变长。

e. 适用场景
(1) 一个系统有大量相同或者相似的对象,造成内存的大量耗费。

(2) 对象的大部分状态都可以外部化,可以将这些外部状态传入对象中。

(3) 在使用享元模式时需要维护一个存储享元对象的享元池,而这需要耗费一定的系统资源,因此,应当在需要多次重复使用享元对象时才值得使用享元模式。

2.7 代理模式

2.7.1 代理模式的解释?

代理模式是常用的结构型设计模式之一,当无法直接访问某个对象或访问某个对象存在困难时可以通过一个代理对象来间接访问,为了保证客户端使用的透明性,所访问的真实对象与代理对象需要实现相同的接口。根据代理模式的使用目的不同,代理模式又可以分为多种类型,例如保护代理、远程代理、虚拟代理、缓冲代理等,它们应用于不同的场合,满足用户的不同需求。

2.7.2 模式结构

代理模式的结构比较简单,其核心是代理类,为了让客户端能够一致性地对待真实对象和代理对象,在代理模式中引入了抽象层。

代理模式包含有三个角色:

(1) Subject(抽象主题角色):它声明了真实主题和代理主题的共同接口,这样一来在任何使用真实主题的地方都可以使用代理主题,客户端通常需要针对抽象主题角色进行编程。

(2) Proxy(代理主题角色):它包含了对真实主题的引用,从而可以在任何时候操作真实主题对象;在代理主题角色中提供一个与真实主题角色相同的接口,以便在任何时候都可以替代真实主题;代理主题角色还可以控制对真实主题的使用,负责在需要的时候创建和删除真实主题对象,并对真实主题对象的使用加以约束。通常,在代理主题角色中,客户端在调用所引用的真实主题操作之前或之后还需要执行其他操作,而不仅仅是单纯调用真实主题对象中的操作。

(3) RealSubject(真实主题角色):它定义了代理角色所代表的真实对象,在真实主题角色中实现了真实的业务操作,客户端可以通过代理主题角色间接调用真实主题角色中定义的操作。

2.7.3 模式实现

abstract class Subject
{
    public abstract void Request();
}
class RealSubject : Subject
{
    public override void Request()
    {
        //业务方法具体实现代码
    }
}
class Proxy : Subject
{
    private RealSubject realSubject = new RealSubject(); //维持一个对真实主题对象的引用
 
    public void PreRequest() 
    {...
    }
 
    public override void Request() 
    {
        PreRequest();
        realSubject.Request(); //调用真实主题对象的方法
         PostRequest();
    }
 
    public void PostRequest() 
    {
        ……
    }
}

2.7.4 常用代理模式

(1) 远程代理(Remote Proxy):为一个位于不同的地址空间的对象提供一个本地的代理对象,这个不同的地址空间可以是在同一台主机中,也可是在另一台主机中,远程代理又称为大使(Ambassador)。

(2) 虚拟代理(Virtual Proxy):如果需要创建一个资源消耗较大的对象,先创建一个消耗相对较小的对象来表示,真实对象只在需要时才会被真正创建。

(3) 保护代理(Protect Proxy):控制对一个对象的访问,可以给不同的用户提供不同级别的使用权限。

(4) 缓冲代理(Cache Proxy):为某一个目标操作的结果提供临时的存储空间,以便多个客户端可以共享这些结果。

(5) 智能引用代理(Smart Reference Proxy):当一个对象被引用时,提供一些额外的操作,例如将对象被调用的次数记录下来等。

2.7.5 代理模式实例

某软件公司承接了某信息咨询公司的收费商务信息查询系统的开发任务,该系统的基本需求如下:

   (1) 在进行商务信息查询之前用户需要通过身份验证,只有合法用户才能够使用该查询系统;

   (2) 在进行商务信息查询时系统需要记录查询日志,以便根据查询次数收取查询费用。

   该软件公司开发人员已完成了商务信息查询模块的开发任务,现希望能够以一种松耦合的方式向原有系统增加身份验证和日志记录功能,客户端代码可以无区别地对待原始的商务信息查询模块和增加新功能之后的商务信息查询模块,而且可能在将来还要在该信息查询模块中增加一些新的功能。
   试使用代理模式设计并实现该收费商务信息查询系统。

实例分析图如下:

实例类图:

业务类AccessValidator用于验证用户身份,业务类Logger用于记录用户查询日志,Searcher充当抽象主题角色,RealSearcher充当真实主题角色,ProxySearcher充当代理主题角色。

上代码:

//AccessValidator.cs
using System;
 
namespace ProxySample
{
    class AccessValidator
    {
        //模拟实现登录验证
        public bool Validate(string userId) 
        {
		    Console.WriteLine("在数据库中验证用户'" + userId + "'是否是合法用户?");
		    if (userId.Equals("杨过")) {
			    Console.WriteLine("'{0}'登录成功!",userId);
			    return true;
		    }
		    else {
                Console.WriteLine("'{0}'登录失败!", userId);
			    return false;
		    }
	    }
    }
}
//Logger.cs
using System;
 
namespace ProxySample
{
    class Logger
    {
        //模拟实现日志记录
        public void Log(string userId) {
            Console.WriteLine("更新数据库,用户'{0}'查询次数加1!",userId);
	    }
    }
}
//Searcher.cs
namespace ProxySample
{
    interface Searcher
    {
        string DoSearch(string userId, string keyword);
    }
}
//RealSearcher.cs
using System;
 
namespace ProxySample
{
    class RealSearcher : Searcher
    {
        //模拟查询商务信息
        public string DoSearch(string userId, string keyword) {
            Console.WriteLine("用户'{0}'使用关键词'{1}'查询商务信息!",userId,keyword);
		    return "返回具体内容";
	    }
    }
}
//ProxySearcher.cs
namespace ProxySample
{
    class ProxySearcher : Searcher
    {
        private RealSearcher searcher = new RealSearcher(); //维持一个对真实主题的引用
        private AccessValidator validator;
        private Logger logger;
 
        public string DoSearch(string userId, string keyword)
        {
            //如果身份验证成功,则执行查询
            if (this.Validate(userId))
            {
                string result = searcher.DoSearch(userId, keyword); //调用真实主题对象的查询方法
                this.Log(userId); //记录查询日志
                return result; //返回查询结果
            }
            else
            {
                return null;
            }
        }
 
        //创建访问验证对象并调用其Validate()方法实现身份验证
        public bool Validate(string userId)
        {
            validator = new AccessValidator();
            return validator.Validate(userId);
        }
 
        //创建日志记录对象并调用其Log()方法实现日志记录
        public void Log(string userId)
        {
            logger = new Logger();
            logger.Log(userId);
        }
    }
}

客户端测试类:

//Program.cs
using System;
using System.Configuration;
using System.Reflection;
 
namespace ProxySample
{
    class Program
    {
        static void Main(string[] args)
        {
            //读取配置文件
            string proxy = ConfigurationManager.AppSettings["proxy"];
 
            //反射生成对象,针对抽象编程,客户端无须分辨真实主题类和代理类
            Searcher searcher;
            searcher = (Searcher)Assembly.Load("ProxySample").CreateInstance(proxy);
 
            String result = searcher.DoSearch("杨过", "玉女心经");
            Console.Read();
        }
    }
}

运行结果如下:

在数据库中验证用户'杨过'是否是合法用户?

'杨过'登录成功!

用户'杨过'使用关键词'玉女心经'查询商务信息!

更新数据库,用户'杨过'查询次数加1!

实例分析:
** 本实例是保护代理和智能引用代理的应用实例,在代理类ProxySearcher中实现对真实主题类的权限控制和引用计数,如果需要在访问真实主题时增加新的访问控制机制和新功能,只需增加一个新的代理类,再修改配置文件,在客户端代码中使用新增代理类即可,源代码无须修改,符合开闭原则。**

2.7.6 远程代理

远程代理(Remote Proxy)是一种常用的代理模式,它使得客户端程序可以访问在远程主机上的对象,远程主机可能具有更好的计算性能与处理速度,可以快速响应并处理客户端的请求。远程代理可以将网络的细节隐藏起来,使得客户端不必考虑网络的存在。客户端完全可以认为被代理的远程业务对象是在本地而不是在远程,而远程代理对象承担了大部分的网络通信工作,并负责对远程业务方法的调用。

客户端对象不能直接访问远程主机中的业务对象,只能采取间接访问的方式。远程业务对象在本地主机中有一个代理对象,该代理对象负责对远程业务对象的访问和网络通信,它对于客户端对象而言是透明的。客户端无须关心实现具体业务的是谁,只需要按照服务接口所定义的方式直接与本地主机中的代理对象交互即可。

2.7.7 虚拟代理

虚拟代理(Virtual Proxy)也是一种常用的代理模式,对于一些占用系统资源较多或者加载时间较长的对象,可以给这些对象提供一个虚拟代理。在真实对象创建成功之前虚拟代理扮演真实对象的替身,而当真实对象创建之后,虚拟代理将用户的请求转发给真实对象。

通常,在以下两种情况下可以考虑使用虚拟代理:

(1) 由于对象本身的复杂性或者网络等原因导致一个对象需要较长的加载时间,此时可以用一个加载时间相对较短的代理对象来代表真实对象。通常在实现时可以结合多线程技术,一个线程用于显示代理对象,其他线程用于加载真实对象。这种虚拟代理模式可以应用在程序启动的时候,由于创建代理对象在时间和处理复杂度上要少于创建真实对象,因此,在程序启动时,可以用代理对象代替真实对象初始化,大大加速了系统的启动时间。当需要使用真实对象时,再通过代理对象来引用,而此时真实对象可能已经成功加载完毕,可以缩短用户的等待时间。

(2) 当一个对象的加载十分耗费系统资源的时候,也非常适合使用虚拟代理。虚拟代理可以让那些占用大量内存或处理起来非常复杂的对象推迟到使用它们的时候才创建,而在此之前用一个相对来说占用资源较少的代理对象来代表真实对象,再通过代理对象来引用真实对象。为了节省内存,在第一次引用真实对象时再创建对象,并且该对象可被多次重用,在以后每次访问时需要检测所需对象是否已经被创建,因此在访问该对象时需要进行存在性检测,这需要消耗一定的系统时间,但是可以节省内存空间,这是一种用时间换取空间的做法。

无论是以上哪种情况,虚拟代理都是用一个“虚假”的代理对象来代表真实对象,通过代理对象来间接引用真实对象,可以在一定程度上提高系统的性能。

2.7.8 缓冲代理

什么是缓冲代理?
它为某一个操作的结果提供了一个临时的缓存存储空间,以便在后续使用中能够共享这些结果,从而可以避免某些方法的重复执行,优化系统性能。

在引入代理模式后,实现了在缓存级别上对业务对象的封装,增强了对业务对象的控制,如果需要访问的数据在缓存中已经存在,则无须再重复执行获取数据的方法,直接返回存储在缓存中的数据即可。由于原有业务对象(真实对象)和新增代理对象暴露在外的方法是一致的,因而对于调用方即客户端而言,调用代理对象与真实对象并没有实质的区别。

2.7.9 代理总结

1. 优点:
代理模式的共同优点如下:

(1) 能够协调调用者和被调用者,在一定程度上降低了系统的耦合度。

(2) 客户端可以针对抽象主题角色进行编程,增加和更换代理类无须修改源代码,符合开闭原则,系统具有较好的灵活性和可扩展性。

此外,不同类型的代理模式也具有独特的优点,例如:

(1) 远程代理为位于两个不同地址空间对象的访问提供了一种实现机制,可以将一些消耗资源较多的对象和操作移至性能更好的计算机上,提高系统的整体运行效率。

(2) 虚拟代理通过一个消耗资源较少的对象来代表一个消耗资源较多的对象,可以在一定程度上节省系统的运行开销。

(3) 缓冲代理为某一个操作的结果提供临时的缓存存储空间,以便在后续使用中能够共享这些结果,优化系统性能,缩短执行时间。

(4) 保护代理可以控制对一个对象的访问权限,为不同用户提供不同级别的使用权限。

2. 缺点:

(1) 由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢,例如保护代理。

(2) 实现代理模式需要额外的工作,而且有些代理模式的实现过程较为复杂,例如远程代理。

3. 模式适用场景:

(1) 当客户端对象需要访问远程主机中的对象时可以使用远程代理。

(2) 当需要用一个消耗资源较少的对象来代表一个消耗资源较多的对象,从而降低系统开销、缩短运行时间时可以使用虚拟代理,例如一个对象需要很长时间才能完成加载时。

(3) 当需要为某一个被频繁访问的操作结果提供一个临时存储空间,以供多个客户端共享访问这些结果时可以使用缓冲代理。通过使用缓冲代理,系统无须在客户端每一次访问时都重新执行操作,只需直接从临时缓冲区获取操作结果即可。

(4) 当需要控制对一个对象的访问,为不同用户提供不同级别的访问权限时可以使用保护代理。

(5) 当需要为一个对象的访问(引用)提供一些额外的操作时可以使用智能引用代理。

3. 行为型模式

3.1 职责链模式

3.1.1 职责链应用实例分析

某企业系统的开发任务,其中包含一个采购审批子系统。该企业的采购审批是分级进行的,即根据采购金额的不同由不同层次的主管人员来审批,主任可以审批5万元以下(不包括5万元)的采购单,副董事长可以审批5万元至10万元(不包括10万元)的采购单,董事长可以审批10万元至50万元(不包括50万元)的采购单,50万元及以上的采购单就需要开董事会讨论决定。

很多情况下,在一个软件系统中可以处理某个请求的对象不止一个,例如SCM系统中的采购单审批,主任、副董事长、董事长和董事会都可以处理采购单,他们可以构成一条处理采购单的链式结构,采购单沿着这条链进行传递,这条链就称为职责链。职责链可以是一条直线、一个环或者一个树形结构,最常见的职责链是直线型,即沿着一条单向的链来传递请求。链上的每一个对象都是请求处理者,职责链模式可以将请求的处理者组织成一条链,并让请求沿着链传递,由链上的处理者对请求进行相应的处理,客户端无须关心请求的处理细节以及请求的传递,只需将请求发送到链上即可,实现请求发送者和请求处理者解耦。

职责链模式(Chain of Responsibility Pattern):避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止。职责链模式是一种对象行为型模式

3.1.2 职责链模式中包含哪些角色?

在职责链模式结构图中包含如下几个角色:

  ● Handler(抽象处理者):它定义了一个处理请求的接口,一般设计为抽象类,由于不同的具体处理者处理请求的方式不同,因此在其中定义了抽象请求处理方法。因为每一个处理者的下家还是一个处理者,因此在抽象处理者中定义了一个抽象处理者类型的对象(如结构图中的successor),作为其对下家的引用。通过该引用,处理者可以连成一条链。

  ● ConcreteHandler(具体处理者):它是抽象处理者的子类,可以处理用户请求,在具体处理者类中实现了抽象处理者中定义的抽象请求处理方法,在处理请求之前需要进行判断,看是否有相应的处理权限,如果可以处理请求就处理它,否则将请求转发给后继者;在具体处理者中可以访问链中下一个对象,以便请求的转发。

在职责链模式里,很多对象由每一个对象对其下家的引用而连接起来形成一条链。请求在这个链上传递,直到链上的某一个对象决定处理此请求。发出这个请求的客户端并不知道链上的哪一个对象最终处理这个请求,这使得系统可以在不影响客户端的情况下动态地重新组织链和分配责任

需要注意的是,职责链模式并不创建职责链,职责链的创建工作必须由系统的其他部分来完成,一般是在使用该职责链的客户端中创建职责链。职责链模式降低了请求的发送端和接收端之间的耦合,使多个对象都有机会处理这个请求。

3.1.3 上代码

抽象类Approver充当抽象处理者(抽象传递者),Director、VicePresident、President和Congress充当具体处理者(具体传递者),PurchaseRequest充当请求类。完整代码如下所示:

//采购单:请求类
class PurchaseRequest {
	private double amount;  //采购金额
	private int number;  //采购单编号
	private String purpose;  //采购目的
	
	public PurchaseRequest(double amount, int number, String purpose) {
		this.amount = amount;
		this.number = number;
		this.purpose = purpose;
	}
	
	public void setAmount(double amount) {
		this.amount = amount;
	}
	
	public double getAmount() {
		return this.amount;
	}
	
	public void setNumber(int number) {
		this.number = number;
	}
	
	public int getNumber() {
		return this.number;
	}
	
	public void setPurpose(String purpose) {
		this.purpose = purpose;
	}
	
	public String getPurpose() {
		return this.purpose;
	}
}
 
//审批者类:抽象处理者
abstract class Approver {
	protected Approver successor; //定义后继对象
	protected String name; //审批者姓名
	
	public Approver(String name) {
		this.name = name;
	}
 
	//设置后继者
	public void setSuccessor(Approver successor) { 
		this.successor = successor;
	}
 
    //抽象请求处理方法
    public abstract void processRequest(PurchaseRequest request);
}
 
//主任类:具体处理者
class Director extends Approver {
	public Director(String name) {
		super(name);
	}
	
    //具体请求处理方法
 	public void processRequest(PurchaseRequest request) {
 		if (request.getAmount() < 50000) {
 			System.out.println("主任" + this.name + "审批采购单:" + request.getNumber() + ",金额:" + request.getAmount() + "元,采购目的:" + request.getPurpose() + "。");  //处理请求
 		}
 		else {
 			this.successor.processRequest(request);  //转发请求
 		}	
 	}
}
 
//副董事长类:具体处理者
class VicePresident extends Approver {
	public VicePresident(String name) {
		super(name);
	}
	
    //具体请求处理方法
 	public void processRequest(PurchaseRequest request) {
 		if (request.getAmount() < 100000) {
 			System.out.println("副董事长" + this.name + "审批采购单:" + request.getNumber() + ",金额:" + request.getAmount() + "元,采购目的:" + request.getPurpose() + "。");  //处理请求
 		}
 		else {
 			this.successor.processRequest(request);  //转发请求
 		}	
 	}
}
 
//董事长类:具体处理者
class President extends Approver {
	public President(String name) {
		super(name);
	}
	
    //具体请求处理方法
 	public void processRequest(PurchaseRequest request) {
 		if (request.getAmount() < 500000) {
 			System.out.println("董事长" + this.name + "审批采购单:" + request.getNumber() + ",金额:" + request.getAmount() + "元,采购目的:" + request.getPurpose() + "。");  //处理请求
 		}
 		else {
 			this.successor.processRequest(request);  //转发请求
 		}
 	}
}
 
//董事会类:具体处理者
class Congress extends Approver {
	public Congress(String name) {
		super(name);
	}
	
    //具体请求处理方法
 	public void processRequest(PurchaseRequest request) {
 		System.out.println("召开董事会审批采购单:" + request.getNumber() + ",金额:" + request.getAmount() + "元,采购目的:" + request.getPurpose() + "。");	    //处理请求
 	}    
}

客户端代码:

class Client {
	public static void main(String[] args) {
		Approver wjzhang,gyang,jguo,meeting;
		wjzhang = new Director("张无忌");
		gyang = new VicePresident("杨过");
		jguo = new President("郭靖");
		meeting = new Congress("董事会");
	
		//创建职责链
		wjzhang.setSuccessor(gyang);
		gyang.setSuccessor(jguo);
		jguo.setSuccessor(meeting);
		
		//创建采购单
		PurchaseRequest pr1 = new PurchaseRequest(45000,10001,"购买倚天剑");
		wjzhang.processRequest(pr1);
		
		PurchaseRequest pr2 = new PurchaseRequest(60000,10002,"购买《葵花宝典》");
		wjzhang.processRequest(pr2);
	
		PurchaseRequest pr3 = new PurchaseRequest(160000,10003,"购买《金刚经》");
		wjzhang.processRequest(pr3);
 
		PurchaseRequest pr4 = new PurchaseRequest(800000,10004,"购买桃花岛");
		wjzhang.processRequest(pr4);
	}
} 

运行结果:

主任张无忌审批采购单:10001,金额:45000.0元,采购目的:购买倚天剑。

副董事长杨过审批采购单:10002,金额:60000.0元,采购目的:购买《葵花宝典》。

董事长郭靖审批采购单:10003,金额:160000.0元,采购目的:购买《金刚经》。

召开董事会审批采购单:10004,金额:800000.0元,采购目的:购买桃花岛。

如果要在原系统中增加一个新的具体的处理者,只需要编写一个新的具体的处理者类Manager,作为抽象处理者Approver的子类,实现在Approver类中定义的抽象处理方法。代码如下所示:

//经理类:具体处理者
class Manager extends Approver {
	public Manager(String name) {
		super(name);
	}
	
    //具体请求处理方法
 	public void processRequest(PurchaseRequest request) {
 		if (request.getAmount() < 80000) {
 			System.out.println("经理" + this.name + "审批采购单:" + request.getNumber() + ",金额:" + request.getAmount() + "元,采购目的:" + request.getPurpose() + "。");  //处理请求
 		}
 		else {
 			this.successor.processRequest(request);  //转发请求
 		}	
 	}
}

客户端需要增加的代码:

Approver rhuang;
rhuang = new Manager("黄蓉");

客户端需要修改的代码:

//创建职责链
wjzhang.setSuccessor(rhuang); //将“黄蓉”作为“张无忌”的下家
rhuang.setSuccessor(gyang); //将“杨过”作为“黄蓉”的下家
gyang.setSuccessor(jguo);
jguo.setSuccessor(meeting);

运行结果:

主任张无忌审批采购单:10001,金额:45000.0元,采购目的:购买倚天剑。

经理黄蓉审批采购单:10002,金额:60000.0元,采购目的:购买《葵花宝典》。

董事长郭靖审批采购单:10003,金额:160000.0元,采购目的:购买《金刚经》。

召开董事会审批采购单:10004,金额:800000.0元,采购目的:购买桃花岛。

3.1.4 纯与不纯的职责链模式

(1) 纯的职责链模式

一个纯的职责链模式要求一个具体处理者对象只能在两个行为中选择一个:要么承担全部责任,要么将责任推给下家,不允许出现某一个具体处理者对象在承担了一部分或全部责任后又将责任向下传递的情况。而且在纯的职责链模式中,要求一个请求必须被某一个处理者对象所接收,不能出现某个请求未被任何一个处理者对象处理的情况。在前面的采购单审批实例中应用的是纯的职责链模式。

(2)不纯的职责链模式

在一个不纯的职责链模式中允许某个请求被一个具体处理者部分处理后再向下传递,或者一个具体处理者处理完某请求后其后继处理者可以继续处理该请求,而且一个请求可以最终不被任何处理者对象所接收

3.1.5 职责链模式总结

1. 优点
(1) 职责链模式使得一个对象无须知道是其他哪一个对象处理其请求,对象仅需知道该请求会被处理即可,接收者和发送者都没有对方的明确信息,且链中的对象不需要知道链的结构,由客户端负责链的创建,降低了系统的耦合度。

(2) 请求处理对象仅需维持一个指向其后继者的引用,而不需要维持它对所有的候选处理者的引用,可简化对象的相互连接。

(3) 在给对象分派职责时,职责链可以给我们更多的灵活性,可以通过在运行时对该链进行动态的增加或修改来增加或改变处理一个请求的职责。

(4) 在系统中增加一个新的具体请求处理者时无须修改原有系统的代码,只需要在客户端重新建链即可,从这一点来看是符合“开闭原则”的。

2. 缺点
(1) 由于一个请求没有明确的接收者,那么就不能保证它一定会被处理,该请求可能一直到链的末端都得不到处理;一个请求也可能因职责链没有被正确配置而得不到处理。

(2) 对于比较长的职责链,请求的处理可能涉及到多个处理对象,系统性能将受到一定影响,而且在进行代码调试时不太方便。

(3) 如果建链不当,可能会造成循环调用,将导致系统陷入死循环。

3. 适用场景
(1) 有多个对象可以处理同一个请求,具体哪个对象处理该请求待运行时刻再确定,客户端只需将请求提交到链上,而无须关心请求的处理对象是谁以及它是如何处理的。

(2) 在不明确指定接收者的情况下,向多个对象中的一个提交一个请求。

(3) 可动态指定一组对象处理请求,客户端可以动态创建职责链来处理请求,还可以改变链中处理者之间的先后次序。

3.2 策略模式

实现某一个功能有多条途径,每一条途径对应一种算法,此时我们可以使用一种设计模式来实现灵活地选择解决途径,也能够方便地增加新的解决途径。

3.2.1 策略模式应用实例介绍

软件公司为某电影院开发了一套影院售票系统,在该系统中需要为不同类型的用户提供不同的电影票打折方式,具体打折方案如下:

(1) 学生凭学生证可享受票价8折优惠;

(2) 年龄在10周岁及以下的儿童可享受每张票减免10元的优惠(原始票价需大于等于20元);

(3) 影院VIP用户除享受票价半价优惠外还可进行积分,积分累计到一定额度可换取电影院赠送的奖品。

该系统在将来可能还要根据需要引入新的打折方式。

3.2.2 策略模式概述

在策略模式中,我们可以定义一些独立的类来封装不同的算法,每一个类封装一种具体的算法,在这里,每一个封装算法的类我们都可以称之为一种策略(Strategy),为了保证这些策略在使用时具有一致性,一般会提供一个抽象的策略类来做规则的定义,而每种算法则对应于一个具体策略类。

策略模式的主要目的是将算法的定义与使用分开,也就是将算法的行为和环境分开,将算法的定义放在专门的策略类中,每一个策略类封装了一种实现算法,使用算法的环境类针对抽象策略类进行编程,符合“依赖倒转原则”。在出现新的算法时,只需要增加一个新的实现了抽象策略类的具体策略类即可。策略模式定义如下:

策略模式(Strategy Pattern):定义一系列算法类,将每一个算法封装起来,并让它们可以相互替换,策略模式让算法独立于使用它的客户而变化,也称为政策模式(Policy)。策略模式是一种对象行为型模式。

3.2.3 策略模式中有哪些角色?

在策略模式结构图中包含如下几个角色:

  ● Context(环境类):环境类是使用算法的角色,它在解决某个问题(即实现某个方法)时可以采用多种策略。在环境类中维持一个对抽象策略类的引用实例,用于定义所采用的策略。

  ● Strategy(抽象策略类):它为所支持的算法声明了抽象方法,是所有策略类的父类,它可以是抽象类或具体类,也可以是接口。环境类通过抽象策略类中声明的方法在运行时调用具体策略类中实现的算法。

  ● ConcreteStrategy(具体策略类):它实现了在抽象策略类中声明的算法,在运行时,具体策略类将覆盖在环境类中定义的抽象策略类对象,使用一种具体的算法实现某个业务处理。

在策略模式中,对环境类和抽象策略类的理解非常重要,环境类是需要使用算法的类。在一个系统中可以存在多个环境类,它们可能需要重用一些相同的算法

3.2.4 上代码

MovieTicket充当环境类角色,Discount充当抽象策略角色,StudentDiscount、 ChildrenDiscount 和VIPDiscount充当具体策略角色。

//电影票类:环境类
class MovieTicket {
	private double price;
	private Discount discount; //维持一个对抽象折扣类的引用
 
	public void setPrice(double price) {
		this.price = price;
	}
 
    //注入一个折扣类对象
	public void setDiscount(Discount discount) {
		this.discount = discount;
	}
 
	public double getPrice() {
        //调用折扣类的折扣价计算方法
		return discount.calculate(this.price);
	}
}
 
//折扣类:抽象策略类
interface Discount {
	public double calculate(double price);
}
 
//学生票折扣类:具体策略类
class StudentDiscount implements Discount {
	public double calculate(double price) {
		System.out.println("学生票:");
		return price * 0.8;
	}
} 
 
//儿童票折扣类:具体策略类
class ChildrenDiscount implements Discount {
	public double calculate(double price) {
		System.out.println("儿童票:");
		return price - 10;
	}
} 
 
//VIP会员票折扣类:具体策略类
class VIPDiscount implements Discount {
	public double calculate(double price) {
		System.out.println("VIP票:");
		System.out.println("增加积分!");
		return price * 0.5;
	}
}

客户端代码如下:

class Client {
	public static void main(String args[]) {
		MovieTicket mt = new MovieTicket();
		double originalPrice = 60.0;
		double currentPrice;
		
		mt.setPrice(originalPrice);
		System.out.println("原始价为:" + originalPrice);
		System.out.println("---------------------------------");
			
		Discount discount;
		discount = (Discount)XMLUtil.getBean(); //读取配置文件并反射生成具体折扣对象
		mt.setDiscount(discount); //注入折扣对象
		
		currentPrice = mt.getPrice();
		System.out.println("折后价为:" + currentPrice);
	}
}

运行结果如下:

原始价为:60.0

---------------------------------

学生票:

折后价为:48.0

如果需要增加新的打折方式,原有代码均无须修改,只要增加一个新的折扣类作为抽象折扣类的子类,实现在抽象折扣类中声明的打折方法,完全符合“开闭原则”。

3.2.5 策略模式总结

策略模式用于算法的自由切换和扩展,它是应用较为广泛的设计模式之一。策略模式对应于解决某一问题的一个算法族,允许用户从该算法族中任选一个算法来解决某一问题,同时可以方便地更换算法或者增加新的算法。只要涉及到算法的封装、复用和切换都可以考虑使用策略模式。

1. 优点:
(1) 策略模式提供了对“开闭原则”的完美支持,用户可以在不修改原有系统的基础上选择算法或行为,也可以灵活地增加新的算法或行为。

(2) 策略模式提供了管理相关的算法族的办法。策略类的等级结构定义了一个算法或行为族,恰当使用继承可以把公共的代码移到抽象策略类中,从而避免重复的代码。

(3) 策略模式提供了一种可以替换继承关系的办法。如果不使用策略模式,那么使用算法的环境类就可能会有一些子类,每一个子类提供一种不同的算法。但是,这样一来算法的使用就和算法本身混在一起,不符合“单一职责原则”,决定使用哪一种算法的逻辑和该算法本身混合在一起,从而不可能再独立演化;而且使用继承无法实现算法或行为在程序运行时的动态切换。

(4) 使用策略模式可以避免多重条件选择语句。多重条件选择语句不易维护,它把采取哪一种算法或行为的逻辑与算法或行为本身的实现逻辑混合在一起,将它们全部硬编码(Hard Coding)在一个庞大的多重条件选择语句中,比直接继承环境类的办法还要原始和落后。

(5) 策略模式提供了一种算法的复用机制,由于将算法单独提取出来封装在策略类中,因此不同的环境类可以方便地复用这些策略类。

2. 缺点:
(1) 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。这就意味着客户端必须理解这些算法的区别,以便适时选择恰当的算法。换言之,策略模式只适用于客户端知道所有的算法或行为的情况。

(2) 策略模式将造成系统产生很多具体策略类,任何细小的变化都将导致系统要增加一个新的具体策略类。

(3) 无法同时在客户端使用多个策略类,也就是说,在使用策略模式时,客户端每次只能使用一个策略类,不支持使用一个策略类完成部分功能后再使用另一个策略类来完成剩余功能的情况。

3. 适用场景:
(1) 一个系统需要动态地在几种算法中选择一种,那么可以将这些算法封装到一个个的具体算法类中,而这些具体算法类都是一个抽象算法类的子类。换言之,这些具体算法类均有统一的接口,根据“里氏代换原则”和面向对象的多态性,客户端可以选择使用任何一个具体算法类,并只需要维持一个数据类型是抽象算法类的对象。

(2) 一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重条件选择语句来实现。此时,使用策略模式,把这些行为转移到相应的具体策略类里面,就可以避免使用难以维护的多重条件选择语句。

(3) 不希望客户端知道复杂的、与算法相关的数据结构,在具体策略类中封装算法与相关的数据结构,可以提高算法的保密性与安全性。

3.3 模板方法模式

3.3.1 模板方法是什么?

某个方法的实现需要多个步骤(类似“请客”),其中有些步骤是固定的(类似“点单”和“买单”),而有些步骤并不固定,存在可变性(类似“吃东西”)。为了提高代码的复用性和系统的灵活性,可以使用一种称之为模板方法模式的设计模式来对这类情况进行设计。例如将模板方法“请客”以及基本方法“点单”和“买单”的实现放在父类中,而对于基本方法“吃东西”,在父类中只做一个声明,将其具体实现放在不同的子类中,在一个子类中提供“吃面条”的实现,而另一个子类提供“吃满汉全席”的实现。

模板方法模式:定义一个操作中算法的框架,而将一些步骤延迟到子类中。模板方法模式使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
模板方法模式是一种基于继承的代码复用技术,它是一种类行为型模式。

模板方法模式是结构最简单的行为型设计模式,在其结构中只存在父类与子类之间的继承关系。通过使用模板方法模式,可以将一些复杂流程的实现步骤封装在一系列基本方法中,在抽象父类中提供一个称之为模板方法的方法来定义这些基本方法的执行次序,而通过其子类来覆盖某些步骤,从而使得相同的算法框架可以有不同的执行结果。模板方法模式提供了一个模板方法来定义算法框架,而某些具体步骤的实现可以在其子类中完成。

3.3.2 模板方法中包含哪几个角色?


模板方法模式包含如下两个角色:

(1) AbstractClass(抽象类):在抽象类中定义了一系列基本操作(PrimitiveOperations),这些基本操作可以是具体的,也可以是抽象的,每一个基本操作对应算法的一个步骤,在其子类中可以重定义或实现这些步骤。同时,在抽象类中实现了一个模板方法(Template Method),用于定义一个算法的框架,模板方法不仅可以调用在抽象类中实现的基本方法,也可以调用在抽象类的子类中实现的基本方法,还可以调用其他对象中的方法。

(2) ConcreteClass(具体子类):它是抽象类的子类,用于实现在父类中声明的抽象基本操作以完成子类特定算法的步骤,也可以覆盖在父类中已经实现的具体基本操作。

3.3.3 模式实现

1. 模板方法
一个模板方法是定义在抽象类中的、把基本操作方法组合在一起形成一个总算法或一个总行为的方法。这个模板方法定义在抽象类中,并由子类不加以修改地完全继承下来。模板方法是一个具体方法,它给出了一个顶层逻辑框架,而逻辑的组成步骤在抽象类中可以是具体方法,也可以是抽象方法。由于模板方法是具体方法,因此模板方法模式中的抽象层只能是抽象类,而不是接口。

2. 基本方法
基本方法是实现算法各个步骤的方法,是模板方法的组成部分。基本方法又可以分为三种:抽象方法(Abstract Method)、具体方法(Concrete Method)和钩子方法(Hook Method)。

(1) 抽象方法:一个抽象方法由抽象类声明、由其具体子类实现。在C#语言里一个抽象方法以abstract关键字标识。

(2) 具体方法:一个具体方法由一个抽象类或具体类声明并实现,其子类可以进行覆盖也可以直接继承。

(3) 钩子方法:一个钩子方法由一个抽象类或具体类声明并实现,而其子类可能会加以扩展。通常在父类中给出的实现是一个空实现(可使用virtual关键字将其定义为虚函数),并以该空实现作为方法的默认实现,当然钩子方法也可以提供一个非空的默认实现。

在模板方法模式中,钩子方法有2种。
第一类钩子方法可以与一些具体步骤“挂钩”,以实现在不同条件下执行模板方法中的不同步骤,这类钩子方法的返回类型通常是bool类型的,这类方法名一般为IsXXX(),用于对某个条件进行判断,如果条件满足则执行某一步骤,否则将不执行,如下代码片段所示:

……
//模板方法
public void TemplateMethod() 
{
Open();
Display();
//通过钩子方法来确定某步骤是否执行
if (IsPrint()) 
{
    Print();
}
}
 
//钩子方法
public bool IsPrint()
{
    return true;
}
……

另一类钩子方法就是实现体为空的具体方法,子类可以根据需要覆盖或者继承这些钩子方法,与抽象方法相比,这类钩子方法的好处在于子类如果没有覆盖父类中定义的钩子方法,编译可以正常通过,但是如果没有覆盖父类中声明的抽象方法,编译将报错。

3.3.4 模板模式中——抽象类典型代码

abstract class AbstractClass 
{
//模板方法
public void TemplateMethod() 
{
        PrimitiveOperation1();
        PrimitiveOperation2();
        PrimitiveOperation3();
}
 
//基本方法—具体方法
public void PrimitiveOperation1() 
{
    //实现代码
}
 
//基本方法—抽象方法
    public abstract void PrimitiveOperation2();    
 
//基本方法—钩子方法
public virtual void PrimitiveOperation3()   
{  }
}

具体类代码如下:

class ConcreteClass : AbstractClass 
{
public override void PrimitiveOperation2() 
{
    //实现代码
}
 
public override void PrimitiveOperation3() 
{
    //实现代码
}
}

3.3.5 模板方法模式应用实例

1. 实例说明
某软件公司欲为某银行的业务支撑系统开发一个利息计算模块,利息计算流程如下:

  (1) 系统根据账号和密码验证用户信息,如果用户信息错误,系统显示出错提示;

  (2) 如果用户信息正确,则根据用户类型的不同使用不同的利息计算公式计算利息(如活期账户和定期账户具有不同的利息计算公式);

  (3) 系统显示利息。

  试使用模板方法模式设计该利息计算模块。

2. 上代码

Account充当抽象类角色,CurrentAccount和SavingAccount充当具体子类角色。

账户类,充当抽象类

//Account.cs 
using System;
 
namespace TemplateMethodSample
{
    abstract class Account
    {
        //基本方法——具体方法
        public bool Validate(string account, string password) 
        {
		    Console.WriteLine("账号:{0}", account);
            Console.WriteLine("密码:{0}", password);
            //模拟登录
            if (account.Equals("张无忌") && password.Equals("123456")) 
            {
			    return true;
		    }
		    else 
            {
			    return false;
		    }
	    }
 
        //基本方法——抽象方法
        public abstract void CalculateInterest();
 
        //基本方法——具体方法
        public void Display() 
        {
            Console.WriteLine("显示利息!");
	    }
 
        //模板方法
        public void Handle(string account, string password) 
        {
		    if (!Validate(account,password)) 
            {
                Console.WriteLine("账户或密码错误!");
			    return;
		    }
		    CalculateInterest();
		    Display();
	    }
    }
}

活期账户类,充当具体子类

//CurrentAccount.cs
using System;
 
namespace TemplateMethodSample
{
    class CurrentAccount : Account
    {
        //覆盖父类的抽象基本方法
        public override void CalculateInterest() 
        {
		    Console.WriteLine("按活期利率计算利息!");
	    }
    }
}

定期账户类,充当具体子类

//SavingAccount.cs
using System;
 
namespace TemplateMethodSample
{
    class SavingAccount : Account
    {
        //覆盖父类的抽象基本方法
        public override void CalculateInterest() 
        {
		    Console.WriteLine("按定期利率计算利息!");
	    }
    }
}

客户端测试类

//Program.cs
using System;
using System.Configuration;
using System.Reflection;
 
namespace TemplateMethodSample
{
    class Program
    {
        static void Main(string[] args)
        {
            Account account;
            //读取配置文件
            string subClassStr = ConfigurationManager.AppSettings["subClass"];
            //反射生成对象
            account = (Account)Assembly.Load("TemplateMethodSample").CreateInstance(subClassStr);
            account.Handle("张无忌", "123456");
            Console.Read();
        }
    }
}

运行结果:

账号:张无忌

密码:123456

按活期利率计算利息!

显示利息!

如果需要增加新的具体子类(新的账户类型),原有代码均无须修改,完全符合开闭原则。

3.3.6 模板方法模式总结

1. 优点

(1) 在父类中形式化地定义一个算法,而由它的子类来实现细节的处理,在子类实现详细的处理算法时并不会改变算法中步骤的执行次序。

(2) 模板方法模式是一种代码复用技术,它在类库设计中尤为重要,它提取了类库中的公共行为,将公共行为放在父类中,而通过其子类来实现不同的行为,它鼓励我们恰当使用继承来实现代码复用。

(3) 可实现一种反向控制结构,通过子类覆盖父类的钩子方法来决定某一特定步骤是否需要执行。

(4) 在模板方法模式中可以通过子类来覆盖父类的基本方法,不同的子类可以提供基本方法的不同实现,更换和增加新的子类很方便,符合单一职责原则和开闭原则。

2. 缺点
需要为每一个基本方法的不同实现提供一个子类,如果父类中可变的基本方法太多,将会导致类的个数增加,系统更加庞大,设计也更加抽象,此时,可结合桥接模式来进行设计。

3. 适用场景

(1) 对一些复杂的算法进行分割,将其算法中固定不变的部分设计为模板方法和父类具体方法,而一些可以改变的细节由其子类来实现。即:一次性实现一个算法的不变部分,并将可变的行为留给子类来实现。

(2) 各子类中公共的行为应被提取出来并集中到一个公共父类中以避免代码重复。

(3) 需要通过子类来决定父类算法中某个步骤是否执行,实现子类对父类的反向控制。

3.4 命令模式

3.4.1 命令模式概述

命令模式可以将请求发送者和接收者完全解耦,发送者与接收者之间没有直接引用关系,发送请求的对象只需要知道如何发送请求,而不必知道如何完成请求。

命令模式(Command Pattern):将一个请求封装为一个对象,从而让我们可用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。命令模式是一种对象行为型模式,其别名为动作(Action)模式或事务(Transaction)模式。

命令模式的定义比较复杂,提到了很多术语,例如“用不同的请求对客户进行参数化”、“对请求排队”,“记录请求日志”、“支持可撤销操作”等。

命令模式中有哪几个角色?
在命令模式结构图中包含如下几个角色:

   ● Command(抽象命令类):抽象命令类一般是一个抽象类或接口,在其中声明了用于执行请求的execute()等方法,通过这些方法可以调用请求接收者的相关操作。

   ● ConcreteCommand(具体命令类):具体命令类是抽象命令类的子类,实现了在抽象命令类中声明的方法,它对应具体的接收者对象,将接收者对象的动作绑定其中。在实现execute()方法时,将调用接收者对象的相关操作(Action)。

   ● Invoker(调用者):调用者即请求发送者,它通过命令对象来执行请求。一个调用者并不需要在设计时确定其接收者,因此它只与抽象命令类之间存在关联关系。在程序运行时可以将一个具体命令对象注入其中,再调用具体命令对象的execute()方法,从而实现间接调用请求接收者的相关操作。

   ● Receiver(接收者):接收者执行与请求相关的操作,它具体实现对请求的业务处理。

命令模式的本质是对请求进行封装,一个请求对应于一个命令,将发出命令的责任和执行命令的责任分割开。每一个命令都是一个操作:请求的一方发出请求要求执行一个操作;接收的一方收到请求,并执行相应的操作。命令模式允许请求的一方和接收的一方独立开来,使得请求的一方不必知道接收请求的一方的接口,更不必知道请求如何被接收、操作是否被执行、何时被执行,以及是怎么被执行的。

命令模式的关键在于引入了抽象命令类,请求发送者针对抽象命令类编程,只有实现了抽象命令类的具体命令才与请求接收者相关联。

3.4.2 上代码

为了降低功能键与功能处理类之间的耦合度,让用户可以自定义每一个功能键的功能。


FBSettingWindow是“功能键设置”界面类,FunctionButton充当请求调用者,Command充当抽象命令类,MinimizeCommand和HelpCommand充当具体命令类,WindowHanlder和HelpHandler充当请求接收者。完整代码如下所示:

import java.util.*;
 
//功能键设置窗口类
class FBSettingWindow {
	private String title; //窗口标题
    //定义一个ArrayList来存储所有功能键
	private ArrayList<FunctionButton> functionButtons = new ArrayList<FunctionButton>();
	
	public FBSettingWindow(String title) {
		this.title = title;
	}
	
	public void setTitle(String title) {
		this.title = title;
	}
	
	public String getTitle() {
		return this.title;
	}
	
	public void addFunctionButton(FunctionButton fb) {
		functionButtons.add(fb);
	}
	
	public void removeFunctionButton(FunctionButton fb) {
		functionButtons.remove(fb);
	}
	
    //显示窗口及功能键
	public void display() {
		System.out.println("显示窗口:" + this.title);
		System.out.println("显示功能键:");
		for (Object obj : functionButtons) {
			System.out.println(((FunctionButton)obj).getName());
		}
		System.out.println("------------------------------");
	}	
}
 
//功能键类:请求发送者
class FunctionButton {
	private String name; //功能键名称
	private Command command; //维持一个抽象命令对象的引用
	
	public FunctionButton(String name) {
		this.name = name;
	}
	
	public String getName() {
		return this.name;
	}
	
    //为功能键注入命令
	public void setCommand(Command command) {
		this.command = command;
	}
	
    //发送请求的方法
	public void onClick() {
		System.out.print("点击功能键:");
		command.execute();
	}
}
 
//抽象命令类
abstract class Command {
	public abstract void execute();
}
 
//帮助命令类:具体命令类
class HelpCommand extends Command {
	private HelpHandler hhObj; //维持对请求接收者的引用
	
	public HelpCommand() {
		hhObj = new HelpHandler();
	}
	
    //命令执行方法,将调用请求接收者的业务方法
	public void execute() {
		hhObj.display();
	}
}
 
//最小化命令类:具体命令类
class MinimizeCommand extends Command {
	private WindowHanlder whObj; //维持对请求接收者的引用
	
	public MinimizeCommand() {
		whObj = new WindowHanlder();
	}
	
//命令执行方法,将调用请求接收者的业务方法
	public void execute() {
		whObj.minimize();
	}
}
 
//窗口处理类:请求接收者
class WindowHanlder {
	public void minimize() {
		System.out.println("将窗口最小化至托盘!");
	}
}
 
//帮助文档处理类:请求接收者
class HelpHandler {
	public void display() {
		System.out.println("显示帮助文档!");
	}
}

客户端代码:

class Client {
	public static void main(String args[]) {
		FBSettingWindow fbsw = new FBSettingWindow("功能键设置");
			
		FunctionButton fb1,fb2;
		fb1 = new FunctionButton("功能键1");
		fb2 = new FunctionButton("功能键1");
		
		Command command1,command2;
        //通过读取配置文件和反射生成具体命令对象
		command1 = (Command)XMLUtil.getBean(0);
		command2 = (Command)XMLUtil.getBean(1);
	    
        //将命令对象注入功能键
		fb1.setCommand(command1);
		fb2.setCommand(command2);
		
		fbsw.addFunctionButton(fb1);
		fbsw.addFunctionButton(fb2);
		fbsw.display();
		
        //调用功能键的业务方法
		fb1.onClick();
		fb2.onClick();
	}
}

运行结果:

显示窗口:功能键设置

显示功能键:

功能键1

功能键1

------------------------------

点击功能键:显示帮助文档!

点击功能键:将窗口最小化至托盘!

如果需要修改功能键的功能,例如某个功能键可以实现“自动截屏”,只需要对应增加一个新的具体命令类,在该命令类与屏幕处理者(ScreenHandler)之间创建一个关联关系,然后将该具体命令类的对象通过配置文件注入到某个功能键即可,原有代码无须修改,符合“开闭原则”。在此过程中,每一个具体命令类对应一个请求的处理者(接收者),通过向请求发送者注入不同的具体命令对象可以使得相同的发送者对应不同的接收者,从而实现“将一个请求封装为一个对象,用不同的请求对客户进行参数化”,客户端只需要将具体命令对象作为参数注入请求发送者,无须直接操作请求的接收者。

3.4.3 命令队列的实现

有时候我们需要将多个请求排队,当一个请求发送者发送一个请求时,将不止一个请求接收者产生响应,这些请求接收者将逐个执行业务方法,完成对请求的处理。此时,我们可以通过命令队列来实现。

命令队列的实现方法有多种形式,其中最常用、灵活性最好的一种方式是增加一个CommandQueue类,由该类来负责存储多个命令对象,而不同的命令对象可以对应不同的请求接收者,CommandQueue类的典型代码如下所示:

import java.util.*;
 
class CommandQueue {
    //定义一个ArrayList来存储命令队列
	private ArrayList<Command> commands = new ArrayList<Command>();
	
	public void addCommand(Command command) {
		commands.add(command);
	}
	
	public void removeCommand(Command command) {
		commands.remove(command);
	}
	
    //循环调用每一个命令对象的execute()方法
	public void execute() {
		for (Object command : commands) {
			((Command)command).execute();
		}
	}
}

在增加了命令队列类CommandQueue以后,请求发送者类Invoker将针对CommandQueue编程,代码修改如下:

class Invoker {
	private CommandQueue commandQueue; //维持一个CommandQueue对象的引用
	
    //构造注入
	public Invoker(CommandQueue commandQueue) {
		this. commandQueue = commandQueue;
	}
	
    //设值注入
	public void setCommandQueue(CommandQueue commandQueue) {
		this.commandQueue = commandQueue;
	}
	
	//调用CommandQueue类的execute()方法
	public void call() {
		commandQueue.execute();
	}
}

命令队列与我们常说的“批处理”有点类似。可以对一组对象(命令)进行批量处理,当一个发送者发送请求后,将有一系列接收者对请求作出响应,命令队列可以用于设计批处理应用程序,如果请求接收者的接收次序没有严格的先后次序,我们还可以使用多线程技术来并发调用命令对象的execute()方法,从而提高程序的执行效率。

3.4.4 撤销操作的实现

在命令模式中,我们可以通过调用一个命令对象的execute()方法来实现对请求的处理,如果需要撤销(Undo)请求,可通过在命令类中增加一个逆向操作来实现,或者通过保存对象的历史状态来实现撤销。

如:公司欲开发一个简易计算器,该计算器可以实现简单的数学运算,还可以对运算实施撤销操作。

代码如下:

//加法类:请求接收者
class Adder {
	private int num=0; //定义初始值为0
	
    //加法操作,每次将传入的值与num作加法运算,再将结果返回
	public int add(int value) {
		num += value;
		return num;
	}
}
 
//抽象命令类
abstract class AbstractCommand {
	public abstract int execute(int value); //声明命令执行方法execute()
	public abstract int undo(); //声明撤销方法undo()
}
 
//具体命令类
class ConcreteCommand extends AbstractCommand {
	private Adder adder = new Adder();
	private int value;
		
	//实现抽象命令类中声明的execute()方法,调用加法类的加法操作
public int execute(int value) {
		this.value=value;
		return adder.add(value);
	}
	
    //实现抽象命令类中声明的undo()方法,通过加一个相反数来实现加法的逆向操作
	public int undo() {
		return adder.add(-value);
	}
}
 
//计算器界面类:请求发送者
class CalculatorForm {
	private AbstractCommand command;
	
	public void setCommand(AbstractCommand command) {
		this.command = command;
	}
	
    //调用命令对象的execute()方法执行运算
	public void compute(int value) {
		int i = command.execute(value);
		System.out.println("执行运算,运算结果为:" + i);
	}
	
    //调用命令对象的undo()方法执行撤销
	public void undo() {
		int i = command.undo();
		System.out.println("执行撤销,运算结果为:" + i);
	}
}

客户端代码:

class Client {
	public static void main(String args[]) {
		CalculatorForm form = new CalculatorForm();
		AbstractCommand command;
		command = new ConcreteCommand();
		form.setCommand(command); //向发送者注入命令对象
		
		form.compute(10);
		form.compute(5);
		form.compute(10);
		form.undo();
	}
}

运行结果:

执行运算,运算结果为:10

执行运算,运算结果为:15

执行运算,运算结果为:25

执行撤销,运算结果为:15

需要注意的是在本实例中只能实现一步撤销操作,因为没有保存命令对象的历史状态,可以通过引入一个命令集合或其他方式来存储每一次操作时命令的状态,从而实现多次撤销操作。除了Undo操作外,还可以采用类似的方式实现恢复(Redo)操作,即恢复所撤销的操作(或称为二次撤销)。

3.4.5 请求日志

请求日志就是将请求的历史记录保存下来,通常以日志文件(Log File)的形式永久存储在计算机中。请求日志文件可以实现很多功能,常用功能如下:

   (1) “天有不测风云”,一旦系统发生故障,日志文件可以为系统提供一种恢复机制,在请求日志文件中可以记录用户对系统的每一步操作,从而让系统能够顺利恢复到某一个特定的状态;

   (2) 请求日志也可以用于实现批处理,在一个请求日志文件中可以存储一系列命令对象,例如一个命令队列;

   (3) 可以将命令队列中的所有命令对象都存储在一个日志文件中,每执行一个命令则从日志文件中删除一个对应的命令对象,防止因为断电或者系统重启等原因造成请求丢失,而且可以避免重新发送全部请求时造成某些命令的重复执行,只需读取请求日志文件,再继续执行文件中剩余的命令即可。

在实现请求日志时,我们可以将命令对象通过序列化写到日志文件中,此时命令类必须实现java.io.Serializable接口。

3.4.6 宏命令

宏命令(Macro Command)又称为组合命令,它是组合模式和命令模式联用的产物。宏命令是一个具体命令类,它拥有一个集合属性,在该集合中包含了对其他命令对象的引用。通常宏命令不直接与请求接收者交互,而是通过它的成员来调用接收者的方法。当调用宏命令的execute()方法时,将递归调用它所包含的每个成员命令的execute()方法,一个宏命令的成员可以是简单命令,还可以继续是宏命令。执行一个宏命令将触发多个具体命令的执行,从而实现对命令的批处理。

3.4.7 命令模式总结

1. 优点:
(1) 降低系统的耦合度。由于请求者与接收者之间不存在直接引用,因此请求者与接收者之间实现完全解耦,相同的请求者可以对应不同的接收者,同样,相同的接收者也可以供不同的请求者使用,两者之间具有良好的独立性。

(2) 新的命令可以很容易地加入到系统中。由于增加新的具体命令类不会影响到其他类,因此增加新的具体命令类很容易,无须修改原有系统源代码,甚至客户类代码,满足“开闭原则”的要求。

(3) 可以比较容易地设计一个命令队列或宏命令(组合命令)。

(4) 为请求的撤销(Undo)和恢复(Redo)操作提供了一种设计和实现方案。

2. 缺点:
使用命令模式可能会导致某些系统有过多的具体命令类。因为针对每一个对请求接收者的调用操作都需要设计一个具体命令类,因此在某些系统中可能需要提供大量的具体命令类,这将影响命令模式的使用。

3. 适用场景:
在以下情况下可以考虑使用命令模式:

(1) 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。请求调用者无须知道接收者的存在,也无须知道接收者是谁,接收者也无须关心何时被调用。

(2) 系统需要在不同的时间指定请求、将请求排队和执行请求。一个命令对象和请求的初始调用者可以有不同的生命期,换言之,最初的请求发出者可能已经不在了,而命令对象本身仍然是活动的,可以通过该命令对象去调用请求接收者,而无须关心请求调用者的存在性,可以通过请求日志文件等机制来具体实现。

(3) 系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作。

(4) 系统需要将一组操作组合在一起形成宏命令。

3.5 迭代器模式

3.5.1 迭代器模式实例说明

公司为某商场开发了一套销售管理系统,在对该系统进行分析和设计时,Sunny软件公司开发人员发现经常需要对系统中的商品数据、客户数据等进行遍历,为了复用这些遍历代码,Sunny公司开发人员设计了一个抽象的数据集合类AbstractObjectList,而将存储商品和客户等数据的类作为其子类。

AbstractObjectList类结构如下所示:

方法说明如下表所示:

方法名说明
AbstractObjectList ()构造方法,用于给objects对象赋值
addObject()增加元素
removeObject ()删除元素
getObjects()获取所有元素
next()移至下一个元素
isLast ()判断当前元素是否是最后一个元素
previous()移至上一个元素
isFirst()判断当前元素是否是第一元素
getNextItem()获取下一个元素
getPreviousItem()获取上一个元素

AbstractObjectList类的子类ProductList和CustomerList分别用于存储商品数据和客户数据。
但这种设计方案存在几个问题:
(1) 在所示类图中,addObject()、removeObject()等方法用于管理数据,而next()、isLast()、previous()、isFirst()等方法用于遍历数据。这将导致聚合类的职责过重,它既负责存储和管理数据,又负责遍历数据,违反了“单一职责原则”,由于聚合类非常庞大,实现代码过长,还将给测试和维护增加难度。

(2) 如果将抽象聚合类声明为一个接口,则在这个接口中充斥着大量方法,不利于子类实现,违反了“接口隔离原则”。

(3) 如果将所有的遍历操作都交给子类来实现,将导致子类代码庞大,而且必须暴露AbstractObjectList的内部存储细节,向子类公开自己的私有属性,否则子类无法实施对数据的遍历,这将破坏AbstractObjectList类的封装性。

如何解决上述问题?解决方案之一就是将聚合类中负责遍历数据的方法提取出来,封装到专门的类中,实现数据存储和数据遍历分离,无须暴露聚合类的内部属性即可对其进行操作,而这正是迭代器模式的意图所在。

3.5.2 迭代器模式概述

迭代器模式(Iterator Pattern):提供一种方法来访问聚合对象,而不用暴露这个对象的内部表示,其别名为游标(Cursor)。迭代器模式是一种对象行为型模式。

在迭代器模式结构中包含聚合和迭代器两个层次结构,考虑到系统的灵活性和可扩展性,在迭代器模式中应用了工厂方法模式,其模式结构如下图所示:

迭代器中包含哪几个角色?

 ● Iterator(抽象迭代器):它定义了访问和遍历元素的接口,声明了用于遍历数据元素的方法,例如:用于获取第一个元素的first()方法,用于访问下一个元素的next()方法,用于判断是否还有下一个元素的hasNext()方法,用于获取当前元素的currentItem()方法等,在具体迭代器中将实现这些方法。

 ● ConcreteIterator(具体迭代器):它实现了抽象迭代器接口,完成对聚合对象的遍历,同时在具体迭代器中通过游标来记录在聚合对象中所处的当前位置,在具体实现时,游标通常是一个表示位置的非负整数。

 ● Aggregate(抽象聚合类):它用于存储和管理元素对象,声明一个createIterator()方法用于创建一个迭代器对象,充当抽象迭代器工厂角色。

 ● ConcreteAggregate(具体聚合类):它实现了在抽象聚合类中声明的createIterator()方法,该方法返回一个与该具体聚合类对应的具体迭代器ConcreteIterator实例。

3.5.3 上代码


AbstractObjectList充当抽象聚合类,ProductList充当具体聚合类,AbstractIterator充当抽象迭代器,ProductIterator充当具体迭代器。完整代码如下所示:

//在本实例中,为了详细说明自定义迭代器的实现过程,我们没有使用JDK中内置的迭代器,事实上,JDK内置迭代器已经实现了对一个List对象的正向遍历
import java.util.*;
 
//抽象聚合类
abstract class AbstractObjectList {
	protected List<Object> objects = new ArrayList<Object>();
 
	public AbstractObjectList(List objects) {
		this.objects = objects;
	}
	
	public void addObject(Object obj) {
		this.objects.add(obj);
	}
	
	public void removeObject(Object obj) {
		this.objects.remove(obj);
	}
	
	public List getObjects() {
		return this.objects;
	}
	
    //声明创建迭代器对象的抽象工厂方法
	public abstract AbstractIterator createIterator();
}
 
//商品数据类:具体聚合类
class ProductList extends AbstractObjectList {
	public ProductList(List products) {
		super(products);
	}
	
    //实现创建迭代器对象的具体工厂方法
	public AbstractIterator createIterator() {
		return new ProductIterator(this);
	}
} 
 
//抽象迭代器
interface AbstractIterator {
	public void next(); //移至下一个元素
	public boolean isLast(); //判断是否为最后一个元素
	public void previous(); //移至上一个元素
	public boolean isFirst(); //判断是否为第一个元素
	public Object getNextItem(); //获取下一个元素
	public Object getPreviousItem(); //获取上一个元素
}
 
//商品迭代器:具体迭代器
class ProductIterator implements AbstractIterator {
	private ProductList productList;
	private List products;
	private int cursor1; //定义一个游标,用于记录正向遍历的位置
	private int cursor2; //定义一个游标,用于记录逆向遍历的位置
	
	public ProductIterator(ProductList list) {
		this.productList = list;
		this.products = list.getObjects(); //获取集合对象
		cursor1 = 0; //设置正向遍历游标的初始值
		cursor2 = products.size() -1; //设置逆向遍历游标的初始值
	}
	
	public void next() {
		if(cursor1 < products.size()) {
			cursor1++;
		}
	}
	
	public boolean isLast() {
		return (cursor1 == products.size());
	}
	
	public void previous() {
		if (cursor2 > -1) {
			cursor2--;
		}
	}
	
	public boolean isFirst() {
		return (cursor2 == -1);
	}
	
	public Object getNextItem() {
		return products.get(cursor1);
	} 
		
	public Object getPreviousItem() {
		return products.get(cursor2);
	} 	
}

客户端代码:

class Client {
	public static void main(String args[]) {
		List products = new ArrayList();
		products.add("倚天剑");
		products.add("屠龙刀");
		products.add("断肠草");
		products.add("葵花宝典");
		products.add("四十二章经");
			
		AbstractObjectList list;
		AbstractIterator iterator;
		
		list = new ProductList(products); //创建聚合对象
		iterator = list.createIterator();	//创建迭代器对象
		
		System.out.println("正向遍历:");	
		while(!iterator.isLast()) {
			System.out.print(iterator.getNextItem() + ",");
			iterator.next();
		}
		System.out.println();
		System.out.println("-----------------------------");
		System.out.println("逆向遍历:");
		while(!iterator.isFirst()) {
			System.out.print(iterator.getPreviousItem() + ",");
			iterator.previous();
		}
	}
}

运行结果:

正向遍历:

倚天剑,屠龙刀,断肠草,葵花宝典,四十二章经,

-----------------------------

逆向遍历:

四十二章经,葵花宝典,断肠草,屠龙刀,倚天剑,

如果需要增加一个新的具体聚合类,如客户数据集合类,并且需要为客户数据集合类提供不同于商品数据集合类的正向遍历和逆向遍历操作,只需增加一个新的聚合子类和一个新的具体迭代器类即可,原有类库代码无须修改,符合“开闭原则”;如果需要为ProductList类更换一个迭代器,只需要增加一个新的具体迭代器类作为抽象迭代器类的子类,重新实现遍历方法,原有迭代器代码无须修改,也符合“开闭原则”;但是如果要在迭代器中增加新的方法,则需要修改抽象迭代器源代码,这将违背“开闭原则”。

3.5.4 使用内部类实现迭代器

可以看到具体迭代器类和具体聚合类之间存在双重关系,其中一个关系为关联关系,在具体迭代器中需要维持一个对具体聚合对象的引用,该关联关系的目的是访问存储在聚合对象中的数据,以便迭代器能够对这些数据进行遍历操作。

除了使用关联关系外,为了能够让迭代器可以访问到聚合对象中的数据,我们还可以将迭代器类设计为聚合类的内部类,JDK中的迭代器类就是通过这种方法来实现的。

可以将ProductIterator类作为ProductList类的内部类,代码如下所示:

//商品数据类:具体聚合类
class ProductList extends AbstractObjectList {
	public ProductList(List products) {
		super(products);
	}
	
	public AbstractIterator createIterator() {
		return new ProductIterator();
	}
	
	//商品迭代器:具体迭代器,内部类实现
	private class ProductIterator implements AbstractIterator {
		private int cursor1;
		private int cursor2;
		
		public ProductIterator() {
			cursor1 = 0;
			cursor2 = objects.size() -1;
		}
		
		public void next() {
			if(cursor1 < objects.size()) {
				cursor1++;
			}
		}
		
		public boolean isLast() {
			return (cursor1 == objects.size());
		}
		
		public void previous() {
			if(cursor2 > -1) {
				cursor2--;
			}
		}
		
		public boolean isFirst() {
			return (cursor2 == -1);
		}
		
		public Object getNextItem() {
			return objects.get(cursor1);
		} 
			
		public Object getPreviousItem() {
			return objects.get(cursor2);
		} 	
	}
}

无论使用哪种实现机制,客户端代码都是一样的,也就是说客户端无须关心具体迭代器对象的创建细节,只需通过调用工厂方法createIterator()即可得到一个可用的迭代器对象,这也是使用工厂方法模式的好处,通过工厂来封装对象的创建过程,简化了客户端的调用。

3.5.5 jdk内置迭代器

为了让开发人员能够更加方便地操作聚合对象,在Java、C#等编程语言中都提供了内置迭代器。在Java集合框架中,常用的List和Set等聚合类都继承(或实现)了java.util.Collection接口。

除了包含一些增加元素和删除元素的方法外,还提供了一个iterator()方法,用于返回一个Iterator迭代器对象,以便遍历聚合中的元素;具体的Java聚合类可以通过实现该iterator()方法返回一个具体的Iterator对象。

3.5.6 迭代器模式总结

1. 适用场景

(1) 访问一个聚合对象的内容而无须暴露它的内部表示。将聚合对象的访问与内部数据的存储分离,使得访问聚合对象时无须了解其内部实现细节。

(2) 需要为一个聚合对象提供多种遍历方式。

(3) 为遍历不同的聚合结构提供一个统一的接口,在该接口的实现类中为不同的聚合结构提供不同的遍历方式,而客户端可以一致性地操作该接口。

3.6 观察者模式

3.6.1 观察者模式概述

观察者模式是使用频率最高的设计模式之一,它用于建立一种对象与对象之间的依赖关系,一个对象发生改变时将自动通知其他对象,其他对象将相应作出反应。在观察者模式中,发生改变的对象称为观察目标,而被通知的对象称为观察者,一个观察目标可以对应多个观察者,而且这些观察者之间可以没有任何相互联系,可以根据需要增加和删除观察者,使得系统更易于扩展。

观察者模式定义如下:

观察者模式(Observer Pattern):定义对象之间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。观察者模式的别名包括发布-订阅(Publish/Subscribe)模式、模型-视图(Model/View)模式、源-监听器(Source/Listener)模式或从属者(Dependents)模式。观察者模式是一种对象行为型模式。

观察者模式中有哪些角色?

● Subject(目标):目标又称为主题,它是指被观察的对象。在目标中定义了一个观察者集合,一个观察目标可以接受任意数量的观察者来观察,它提供一系列方法来增加和删除观察者对象,同时它定义了通知方法notify()。目标类可以是接口,也可以是抽象类或具体类。
● ConcreteSubject(具体目标):具体目标是目标类的子类,通常它包含有经常发生改变的数据,当它的状态发生改变时,向它的各个观察者发出通知;同时它还实现了在目标类中定义的抽象业务逻辑方法(如果有的话)。如果无须扩展目标类,则具体目标类可以省略。

● Observer(观察者):观察者将对观察目标的改变做出反应,观察者一般定义为接口,该接口声明了更新数据的方法update(),因此又称为抽象观察者。

● ConcreteObserver(具体观察者):在具体观察者中维护一个指向具体目标对象的引用,它存储具体观察者的有关状态,这些状态需要和具体目标的状态保持一致;它实现了在抽象观察者Observer中定义的update()方法。通常在实现时,可以调用具体目标类的attach()方法将自己添加到目标类的集合中或通过detach()方法将自己从目标类的集合中删除。

观察者模式描述了如何建立对象与对象之间的依赖关系,以及如何构造满足这种需求的系统。观察者模式包含观察目标和观察者两类对象,一个目标可以有任意数目的与之相依赖的观察者,一旦观察目标的状态发生改变,所有的观察者都将得到通知。作为对这个通知的响应,每个观察者都将监视观察目标的状态以使其状态与目标状态同步,这种交互也称为发布-订阅(Publish-Subscribe)。观察目标是通知的发布者,它发出通知时并不需要知道谁是它的观察者,可以有任意数目的观察者订阅它并接收通知。

3.6.2 上代码

AllyControlCenter充当目标类,ConcreteAllyControlCenter充当具体目标类,Observer充当抽象观察者,Player充当具体观察者。完整代码如下所示:

import java.util.*;
 
//抽象观察类
interface Observer {
	public String getName();
	public void setName(String name);
	public void help(); //声明支援盟友方法
	public void beAttacked(AllyControlCenter acc); //声明遭受攻击方法
}
 
//战队成员类:具体观察者类
class Player implements Observer {
	private String name;
 
	public Player(String name) {
		this.name = name;
	}
	
	public void setName(String name) {
		this.name = name;
	}
	
	public String getName() {
		return this.name;
	}
	
    //支援盟友方法的实现
	public void help() {
		System.out.println("坚持住," + this.name + "来救你!");
	}
	
    //遭受攻击方法的实现,当遭受攻击时将调用战队控制中心类的通知方法notifyObserver()来通知盟友
	public void beAttacked(AllyControlCenter acc) {
        System.out.println(this.name + "被攻击!");
        acc.notifyObserver(name);		
	}
}
 
//战队控制中心类:目标类
abstract class AllyControlCenter {
	protected String allyName; //战队名称
	protected ArrayList<Observer> players = new ArrayList<Observer>(); //定义一个集合用于存储战队成员
	
	public void setAllyName(String allyName) {
		this.allyName = allyName;
	}
	
	public String getAllyName() {
		return this.allyName;
	}
	
    //注册方法
	public void join(Observer obs) {
		System.out.println(obs.getName() + "加入" + this.allyName + "战队!");
		players.add(obs);
	}
	
    //注销方法
	public void quit(Observer obs) {
		System.out.println(obs.getName() + "退出" + this.allyName + "战队!");
		players.remove(obs);
	}
	
    //声明抽象通知方法
	public abstract void notifyObserver(String name);
}
 
//具体战队控制中心类:具体目标类
class ConcreteAllyControlCenter extends AllyControlCenter {
	public ConcreteAllyControlCenter(String allyName) {
		System.out.println(allyName + "战队组建成功!");
		System.out.println("----------------------------");
		this.allyName = allyName;
	}
	
    //实现通知方法
	public void notifyObserver(String name) {
		System.out.println(this.allyName + "战队紧急通知,盟友" + name + "遭受敌人攻击!");
        //遍历观察者集合,调用每一个盟友(自己除外)的支援方法
        for(Object obs : players) {
            if (!((Observer)obs).getName().equalsIgnoreCase(name)) {
                ((Observer)obs).help();
            }
        }		
	}
}

客户端代码:

class Client {
	public static void main(String args[]) {
		//定义观察目标对象
AllyControlCenter acc;
		acc = new ConcreteAllyControlCenter("金庸群侠");
		
        //定义四个观察者对象
		Observer player1,player2,player3,player4;
		
		player1 = new Player("杨过");
		acc.join(player1);
		
		player2 = new Player("令狐冲");
		acc.join(player2);
		
		player3 = new Player("张无忌");
		acc.join(player3);
		
		player4 = new Player("段誉");
		acc.join(player4);
		
		//某成员遭受攻击
		Player1.beAttacked(acc);
	}
}

运行结果:

金庸群侠战队组建成功!

----------------------------

杨过加入金庸群侠战队!

令狐冲加入金庸群侠战队!

张无忌加入金庸群侠战队!

段誉加入金庸群侠战队!

杨过被攻击!

金庸群侠战队紧急通知,盟友杨过遭受敌人攻击!

坚持住,令狐冲来救你!

坚持住,张无忌来救你!

坚持住,段誉来救你!

3.6.3 观察者模式总结

1. 适用场景
在以下情况下可以考虑使用观察者模式:

(1) 一个抽象模型有两个方面,其中一个方面依赖于另一个方面,将这两个方面封装在独立的对象中使它们可以各自独立地改变和复用。

(2) 一个对象的改变将导致一个或多个其他对象也发生改变,而并不知道具体有多少对象将发生改变,也不知道这些对象是谁。

(3) 需要在系统中创建一个触发链,A对象的行为将影响B对象,B对象的行为将影响C对象……,可以使用观察者模式创建一种链式触发机制。

3.7 解释器模式

其实,解释器模式实际上就是:我们在向计算机输入一个句子或者一个文件之后,它就会按照预先定义的文法规则来对句子或文件进行解释,从而实现相应的功能。

我们可以根据需要,自定义几条文法规则,这几条文法规则就对应了相等个数的语言单位,这些语言单位可以分为两类:一类为终结符,另一类为非终结符。终结符是语言最小的组成单位,不可再进行拆分。

解释器中包含了如下结果角色:

   ● AbstractExpression(抽象表达式):在抽象表达式中声明了抽象的解释操作,它是所有终结符表达式和非终结符表达式的公共父类。

   ● TerminalExpression(终结符表达式):终结符表达式是抽象表达式的子类,它实现了与文法中的终结符相关联的解释操作,在句子中的每一个终结符都是该类的一个实例。通常在一个解释器模式中只有少数几个终结符表达式类,它们的实例可以通过非终结符表达式组成较为复杂的句子。

   ● NonterminalExpression(非终结符表达式):非终结符表达式也是抽象表达式的子类,它实现了文法中非终结符的解释操作,由于在非终结符表达式中可以包含终结符表达式,也可以继续包含非终结符表达式,因此其解释操作一般通过递归的方式来完成。

   ● Context(环境类):环境类又称为上下文类,它用于存储解释器之外的一些全局信息,通常它临时存储了需要解释的语句。

   在解释器模式中,每一种终结符和非终结符都有一个具体类与之对应,正因为使用类来表示每一条文法规则,所以系统将具有较好的灵活性和可扩展性。对于所有的终结符和非终结符,我们首先需要抽象出一个公共父类,即抽象表达式类。

3.7.1 解释器应用实例说明

某玩具公司开发一套机器人控制程序,在该机器人控制程序中包含一些简单的英文控制指令,每一个指令对应一个表达式(expression),该表达式可以是简单表达式也可以是复合表达式,每一个简单表达式由移动方向(direction),移动方式(action)和移动距离(distance)三部分组成,其中移动方向包括上(up)、下(down)、左(left)、右(right);移动方式包括移动(move)和快速移动(run);移动距离为一个正整数。两个表达式之间可以通过与(and)连接,形成复合(composite)表达式。

用户通过对图形化的设置界面进行操作可以创建一个机器人控制指令,机器人在收到指令后将按照指令的设置进行移动,例如输入控制指令:up move 5,则“向上移动5个单位”;输入控制指令:down run 10 and left move 20,则“向下快速移动10个单位再向左移动20个单位”。

根据上述需求描述,用形式化语言来表示该简单语言的文法规则如下:
expression ::= direction action distance | composite //表达式

composite ::= expression ‘and’ expression //复合表达式

direction ::= ‘up’ | ‘down’ | ‘left’ | ‘right’ //移动方向

action ::= ‘move’ | ‘run’ //移动方式

distance ::= an integer //移动距离

针对五条文法规则,分别提供五个类来实现,其中终结符表达式direction、action和distance对应DirectionNode类、ActionNode类和DistanceNode类,非终结符表达式expression和composite对应SentenceNode类和AndNode类。代码如下:

//注:本实例对机器人控制指令的输出结果进行模拟,将英文指令翻译为中文指令,实际情况是调用不同的控制程序进行机器人的控制,包括对移动方向、方式和距离的控制等
import java.util.*;
 
//抽象表达式
abstract class AbstractNode {
	public abstract String interpret();
}
 
//And解释:非终结符表达式
class AndNode extends AbstractNode {
	private AbstractNode left; //And的左表达式
	private AbstractNode right; //And的右表达式
 
	public AndNode(AbstractNode left, AbstractNode right) {
		this.left = left;
		this.right = right;
	}
    
    //And表达式解释操作
	public String interpret() {
		return left.interpret() + "再" + right.interpret();
	}
}
 
//简单句子解释:非终结符表达式
class SentenceNode extends AbstractNode {
	private AbstractNode direction;
	private AbstractNode action;
	private AbstractNode distance;
 
	public SentenceNode(AbstractNode direction,AbstractNode action,AbstractNode distance) {
		this.direction = direction;
		this.action = action;
		this.distance = distance;
	}
    
    //简单句子的解释操作
	public String interpret() {
		return direction.interpret() + action.interpret() + distance.interpret();
	}	
}
 
//方向解释:终结符表达式
class DirectionNode extends AbstractNode {
	private String direction;
	
	public DirectionNode(String direction) {
		this.direction = direction;
	}
	
    //方向表达式的解释操作
	public String interpret() {
		if (direction.equalsIgnoreCase("up")) {
			return "向上";
		}
		else if (direction.equalsIgnoreCase("down")) {
			return "向下";
		}
		else if (direction.equalsIgnoreCase("left")) {
			return "向左";
		}
		else if (direction.equalsIgnoreCase("right")) {
			return "向右";
		}
		else {
			return "无效指令";
		}
	}
}
 
//动作解释:终结符表达式
class ActionNode extends AbstractNode {
	private String action;
	
	public ActionNode(String action) {
		this.action = action;
	}
	
    //动作(移动方式)表达式的解释操作
	public String interpret() {
		if (action.equalsIgnoreCase("move")) {
			return "移动";
		}
		else if (action.equalsIgnoreCase("run")) {
			return "快速移动";
		}
		else {
			return "无效指令";
		}
	}
}
 
//距离解释:终结符表达式
class DistanceNode extends AbstractNode {
	private String distance;
	
	public DistanceNode(String distance) {
		this.distance = distance;
	}
	
//距离表达式的解释操作
	public String interpret() {
		return this.distance;
	}	
}
 
//指令处理类:工具类
class InstructionHandler {
	private String instruction;
	private AbstractNode node;
    
    public void handle(String instruction) {
    	AbstractNode left = null, right = null;
    	AbstractNode direction = null, action = null, distance = null;
    	Stack stack = new Stack(); //声明一个栈对象用于存储抽象语法树
    	String[] words = instruction.split(" "); //以空格分隔指令字符串
    	for (int i = 0; i < words.length; i++) {
//本实例采用栈的方式来处理指令,如果遇到“and”,则将其后的三个单词作为三个终结符表达式连成一个简单句子SentenceNode作为“and”的右表达式,而将从栈顶弹出的表达式作为“and”的左表达式,最后将新的“and”表达式压入栈中。    		        if (words[i].equalsIgnoreCase("and")) {
    			left = (AbstractNode)stack.pop(); //弹出栈顶表达式作为左表达式
    		    String word1= words[++i];
    		    direction = new DirectionNode(word1);
    		    String word2 = words[++i];
    		    action = new ActionNode(word2);
    		    String word3 = words[++i];
    		    distance = new DistanceNode(word3);
    		    right = new SentenceNode(direction,action,distance); //右表达式
    			stack.push(new AndNode(left,right)); //将新表达式压入栈中
    		}
            //如果是从头开始进行解释,则将前三个单词组成一个简单句子SentenceNode并将该句子压入栈中
    		else {
    		    String word1 = words[i];
    		    direction = new DirectionNode(word1);
    		    String word2 = words[++i];
    		    action = new ActionNode(word2);
    		    String word3 = words[++i];
    		    distance = new DistanceNode(word3);
    		    left = new SentenceNode(direction,action,distance);
    		    stack.push(left); //将新表达式压入栈中
    		}
    	}
    	this.node = (AbstractNode)stack.pop(); //将全部表达式从栈中弹出
    }
	
	public String output() {
		String result = node.interpret(); //解释表达式
		return result;
	}
}
/*
工具类InstructionHandler用于对输入指令进行处理,将输入指令分割为字符串数组,将第1个、第2个和第3个单词组合成一个句子,并存入栈中;如果发现有单词“and”,则将“and”后的第1个、第2个和第3个单词组合成一个新的句子作为“and”的右表达式,并从栈中取出原先所存句子作为左表达式,然后组合成一个And节点存入栈中。依此类推,直到整个指令解析结束。
*/

客户端代码如下:

class Client {
	public static void main(String args[]) {
		String instruction = "up move 5 and down run 10 and left move 5";
		InstructionHandler handler = new InstructionHandler();
		handler.handle(instruction);
		String outString;
		outString = handler.output();
		System.out.println(outString);
	}
}

运行结果如下:

向上移动5再向下快速移动10再向左移动5

context 的作用其实就是用于存储命令语句,以及获取命令中的终结符。

3.7.2 解释器模式总结

优点:
(1)每一条文法规则都可以表示为一个类,因此可以方便地实现一个简单的语言。
(2)增加新的解释表达式较为容易。如果用户需要增加新的解释表达式只需要对应增加一个新的终结符表达式或非终结符表达式类,原有表达式类无需修改,符合“开闭原则”。
(3)易于改变和扩展。由于在解释器模式中使用类来表示语言的文法规则,因此可以通过继承等机制来改变或扩展文法。

缺点:
(1)对于复杂文法难以维护。在解释器模式中,每一条规则至少需要定义一个类,因此如果一个语言包含太多文法规则,类的个数将会急剧增加,导致系统难以管理和维护。
(2)执行效率低。由于在解释器模式中使用了大量的循环和递归调用,因此在解释较为复杂的句子时其速度很慢,而且代码的调试过程也比较麻烦。

适用场景:
(1)可以将一个需要解释执行的语言中的句子表示为一个抽象语法树。
(2)一些重复出现的问题可以用一种简单的语言来进行表达。
(3)一个语言的文法较为简单。
(4)执行效率不是关键问题。

文中有些部分借鉴了https://blog.csdn.net/lovelion/article/details/17517213。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值