Java-设计模式-全-

Java 设计模式(全)

原文:Java Design Patterns

协议:CC BY-NC-SA 4.0

一、单例模式

这一章涵盖了单例模式。

GoF 定义

确保一个类只有一个实例,并提供对它的全局访问点。

概念

一个类不能有多个实例。创建后,下一次开始,您将只使用现有的实例。这种方法有助于在集中式系统中限制不必要的对象创建。该方法还促进了容易的维护。

真实世界的例子

让我们假设你是一个运动队的成员,你的球队正在参加锦标赛。您的团队需要在整个锦标赛中与多个对手比赛。在每场比赛之前,按照比赛规则,双方队长必须掷硬币。如果你的团队没有队长,你需要选举一个人作为队长。在每场比赛和每次抛硬币之前,如果你已经提名了一个人作为队长,你就不能重复选举队长的过程。基本上,从每个团队成员的角度来看,团队应该只有一个队长。

计算机世界的例子

在某些特定的软件系统中,您可能更喜欢只使用一个文件系统来集中管理资源。此外,这种模式可以实现缓存机制。

注意

当您考虑 java.lang.Runtime 类的 getRuntime()方法时,您会注意到类似的模式。它被实现为一个单例类的急切初始化。您将很快了解到急切初始化。

说明

这些是以下实现中的关键特征。

  • 构造函数是私有的,以防止使用“new”运算符。

  • 您将创建该类的一个实例,如果您之前没有创建任何这样的实例;否则,您将简单地重用现有的。

  • 为了保证线程安全,我使用了“synchronized”关键字。

类图

图 1-1 显示了单例模式的类图。

img/395506_2_En_1_Fig1_HTML.jpg

图 1-1

类图

包资源管理器视图

图 1-2 显示了程序的高层结构。

img/395506_2_En_1_Fig2_HTML.jpg

图 1-2

包资源管理器视图

讨论

我已经向您展示了一个简单的例子来说明单例模式的概念。

让我们用下面的方法来回顾一下显著的特征。

  • 构造函数是私有的,所以不能在外部实例化单例类(Captain)。它帮助我们引用系统中存在的唯一实例,同时,您限制了 Captain 类的额外对象创建。

  • 私有构造函数还确保不能扩展 Captain 类。所以,子类不能误用这个概念。

  • 我用了“同步”这个关键词。因此,多线程不能同时参与实例化过程。我强迫每个线程等待轮到它来获取这个方法,所以线程安全得到了保证。但是同步是一个代价很高的操作,一旦实例被创建,它就是一个额外的开销。(我将在接下来的章节中讨论一些替代方法,但每种方法都有其优缺点)。

履行

下面是实现。

package jdp2e.singleton.demo;

final class Captain
{
    private static Captain captain;
    //We make the constructor private to prevent the use of "new"
    private Captain() {    }

    public static synchronized Captain getCaptain()
    {

        // Lazy initialization
        if (captain == null)
        {
            captain = new Captain();
            System.out.println("New captain is elected for your team.");
        }
        else
        {
            System.out.print("You already have a captain for your team.");
            System.out.println("Send him for the toss.");
        }
        return captain;
    }
}
// We cannot extend Captain class.The constructor is private in this case.
//class B extends Captain{}// error
public class SingletonPatternExample {
    public static void main(String[] args) {
        System.out.println("***Singleton Pattern Demo***\n");
        System.out.println("Trying to make a captain for your team:");
        //Constructor is private.We cannot use "new" here.
        //Captain c3 = new Captain();//error
        Captain captain1 = Captain.getCaptain();
        System.out.println("Trying to make another captain for your team:");
        Captain captain2 = Captain.getCaptain();
        if (captain1 == captain2)
        {
            System.out.println("captain1 and captain2 are same instance.");
        }

    }

}

输出

这是输出。

***Singleton Pattern Demo***

Trying to make a captain for your team:
New captain is elected for your team.
Trying to make another captain for your team:
You already have a captain for your team.Send him for the toss.
captain1 and captain2 are same instance.

问答环节

1.你为什么要把程序复杂化?您可以简单地编写 Singleton 类,如下所示。

class Captain
{
   private static Captain captain;
    //We make the constructor private to prevent the use of "new"
   private Captain() { }

   public static Captain getCaptain()
   {

             // Lazy initialization
             if (captain == null)
             {
                captain = new Captain();
                System.out.println("New captain is elected for your team.");
             }
             else
             {
                 System.out.print("You already have a captain for your team.");
                 System.out.println("Send him for the toss.");
             }
             return captain;
      }
}

这种理解正确吗?

它只能在单线程环境中工作,但在多线程环境中不能被认为是线程安全的实现。考虑下面的情况。假设在多线程环境中,两个(或更多)线程试图对此进行评估:

if (captain == null)

如果他们发现实例还没有被创建,他们每个人都会尝试创建一个新的实例。因此,您可能会得到该类的多个实例。

2.为什么在代码中使用了 懒惰初始化 这个术语?

因为在这里调用getCaptain()方法之前不会创建 singleton 实例。

3.什么意思 懒惰初始化

简单地说,惰性初始化是一种延迟对象创建过程的技术。它说你应该只在需要的时候创建一个对象。当您处理创建对象的昂贵过程时,这种方法会很有帮助。

4.你为什么把这门课定为期末考试?您有一个私有构造函数,它可能会阻止继承。这是正确的吗?

子类化可以通过多种方式来避免。是的,在这个例子中,由于构造函数已经标记了“private”关键字,所以不需要它。但是如果您将 Captain 类设置为 final,如示例所示,这种方法被认为是更好的实践。当你考虑一个嵌套类时,它是有效的。例如,让我们修改私有构造函数体来检查创建的(Captain 类的)实例数量。让我们进一步假设在前面的例子中,您有一个非静态嵌套类(在 Java 中称为内部类),如下所示。(所有更改都以粗体显示。)

//final class Captain
class Captain
{
    private static Captain captain;
    //We make the constructor private to prevent the use of "new"
    static int numberOfInstance=0;
    private Captain()
    {
        numberOfInstance++;
        System.out.println("Number of instances at this moment="+ numberOfInstance);
    }

    public static synchronized Captain getCaptain()
    {

        // Lazy initialization
        if (captain == null)
        {
            captain = new Captain();
            System.out.println("New captain is elected for your team.");
        }
        else
        {
            System.out.print("You already have a captain for your team.");
            System.out.println("Send him for the toss.");
        }
        return captain;
    }
     //A non-static nested class (inner class)
   public class CaptainDerived extends Captain
   {
     //Some code
   }
}

现在在main()方法中添加另一行代码(以粗体显示),如下所示。

public class SingletonPatternExample {
    public static void main(String[] args) {
        System.out.println("***Singleton Pattern Demo***\n");
        System.out.println("Trying to make a captain for your team:");
        //Constructor is private.We cannot use "new" here.
        //Captain c3 = new Captain();//error
        Captain captain1 = Captain.getCaptain();
        System.out.println("Trying to make another captain for your team:");
        Captain captain2 = Captain.getCaptain();
        if (captain1 == captain2)
        {
            System.out.println("captain1 and captain2 are same instance.");
        }
        Captain.CaptainDerived derived=captain1.new CaptainDerived();
    }
}

现在请注意输出。

输出

现在,您可以看到程序违反了关键目标,因为我从未打算创建多个实例。

***Singleton Pattern Demo***

Trying to make a captain for your team:
Number of instances at this moment=1
New captain is elected for your team.
Trying to make another captain for your team:
You already have a captain for your team.Send him for the toss.
captain1 and captain2 are same instance.

Number of instances at this moment=2

5.有没有任何替代的方法来模拟单例设计模式?

有许多方法。各有利弊。你已经见过其中两个了。让我们讨论一些替代方法。

急切初始化

下面是急切初始化的一个示例实现。

class Captain
{
    //Early initialization
    private static final Captain captain = new Captain();
    //We make the constructor private to prevent the use of "new"
    private Captain()
    {
        System.out.println("A captain is elected for your team.");
    }
    /* Global point of access.The method getCaptain() is a public static method*/
    public static Captain getCaptain()
    {
        System.out.println("You have a captain for your team.");
        return captain;
    }

}

讨论

急切初始化方法有以下优点和缺点。

优点

  • 它简单明了,也更干净。

  • 它与惰性初始化相反,但仍然是线程安全的。

  • 当应用程序处于执行模式时,它有一个小的延迟时间,因为所有的东西都已经加载到内存中了。

cons

  • 应用程序需要更长的启动时间(与惰性初始化相比),因为所有东西都需要先加载。为了检查代价,让我们在 Singleton 类中添加一个虚拟方法(以粗体显示)。注意,在 main 方法中,我只调用了这个伪方法。现在检查输出。
package jdp2e.singleton.questions_answers;

class Captain
{
    //Early initialization
    private static final Captain captain = new Captain();
    //We make the constructor private to prevent the use of "new"
    private Captain()
    {
        System.out.println("A captain is elected for your team.");
    }
    /* Global point of access.The method getCaptain() is a public static method*/
    public static Captain getCaptain()
    {
        System.out.println("You have a captain for your team.");
        return captain;
    }
    public static void dummyMethod()
    {
        System.out.println("It is a dummy method");
    }
}
public class EagerInitializationExample {
    public static void main(String[] args) {
        System.out.println("***Singleton Pattern Demo With Eager Initialization***\n");
        Captain.dummyMethod();
        /*System.out.println("Trying to make a captain for your team:");
        Captain captain1 = Captain.getCaptain();
        System.out.println("Trying to make another captain for your team:");
        Captain captain2 = Captain.getCaptain();
            if (captain1 == captain2)
            {
                System.out.println("captain1 and captain2 are same instance.");
            }*/
    }
}

输出

***Singleton Pattern Demo With Eager Initialization***

A captain is elected for your team.

It is a dummy method

分析

注意为你的团队选出的队长仍然出现在输出中,尽管你可能不打算处理它。

因此,在前面的情况下,Singleton 类的对象总是被实例化。此外,在 Java 5 之前,有许多关于单例类的问题。

比尔·普格溶液

Bill Pugh 提出了一种不同的方法,使用静态嵌套 helper 类。

package jdp2e.singleton.questions_answers;

class Captain1
{
    private Captain1() {
        System.out.println("A captain is elected for your team.");
    }
    //Bill Pugh solution
    private static class SingletonHelper{
        /*Nested class is referenced after getCaptain() is called*/

        private static final Captain1 captain = new Captain1();
    }

    public static Captain1 getCaptain()
    {
        return SingletonHelper.captain;
    }
    /*public static void dummyMethod()
    {
        System.out.println("It is a dummy method");
    }  */
}

该方法不使用同步技术和急切初始化。注意,只有当有人调用getCaptain()方法时,SingletonHelper 类才会被考虑。如果您只是从main()调用任何dummyMethod(),这种方法不会产生任何不需要的输出,就像前面的例子一样(为了检查结果,您需要取消对dummyMethod()主体的注释)。它也被视为在 Java 中实现单件的一种常见的标准方法。

双重检查锁定

还有另一种流行的方法,叫做双重检查锁定。如果您注意到我们的 singleton 模式的同步实现,您可能会发现同步操作通常成本很高,并且该方法对于一些可能破坏 singleton 实现的初始线程很有用。但是在后面的阶段,同步操作可能会产生额外的开销。为了避免这个问题,您可以在一个if条件中使用一个 synchronized 块,如下面的代码所示,以确保不会创建不需要的实例。

package jdp2e.singleton.questions_answers;

final class Captain2
{
    private static Captain2 captain;
    //We make the constructor private to prevent the use of "new"
    static int numberOfInstance=0;
    private Captain2() {
        numberOfInstance++;
        System.out.println("Number of instances at this moment="+ numberOfInstance);
    }

    public static  Captain2 getCaptain(){
        if (captain == null) {
            synchronized (Captain2.class) {
                // Lazy initialization
                if (captain == null){
                    captain = new Captain2();
                    System.out.println("New captain is elected for your team.");
                }
                else
                {
                    System.out.print("You already have a captain for your team.");
                    System.out.println("Send him for the toss.");
                }
            }
        }
        return captain;
    }
}

如果你对单例模式更感兴趣,请阅读位于 www.journaldev.com/1377/java-singleton-design-pattern-best-practices-examples 的文章。

6.简言之,如果我需要创建同步代码,我可以在 Java 中使用 synchronized 关键字。这是正确的吗?

是的,JVM 确保了这一点。在内部,它使用类或对象上的锁来确保只有一个线程在访问数据。在 Java 中,您可以将该关键字应用于一个方法或语句(或代码块)。在这一章中,我从两个方面进行了练习。(在最初的实现中,您使用了 synchronized 方法,在双重检查锁定中,您看到了另一个版本的使用)。

7.为什么创建多个对象是一个大问题?

  • 在现实世界的场景中,对象创建被视为代价高昂的操作。

  • 有时你需要实现一个易于维护的集中式系统,因为它可以帮助你提供一个全局访问机制。

8.什么时候我应该考虑单例模式?

模式的使用取决于特定的用例。但是一般来说,您可以考虑使用单例模式来实现集中式管理系统、维护公共日志文件、在多线程环境中维护线程池、实现缓存机制或设备驱动程序等等。

9.我对急切初始化的例子有些担心。根据定义,它似乎不完全是急切初始化。这个类只有在应用程序执行期间被代码引用时才由 JVM 加载。这意味着这也是惰性初始化。这是正确的吗?

是的,在某种程度上你的观察是正确的。这个讨论有争论。简而言之,与以前的方法相比,它是热切的。当你只调用dummyMethod()时,你看到了这一点;尽管如此,你实例化了单例,尽管你并不需要它。因此,在这样的上下文中,它是渴望的,但它是懒惰的,因为在类被初始化之前,单体实例化不会发生。所以,懒惰的程度是这里的关键。

二、原型模式

本章涵盖了原型模式。

GoF 定义

使用原型实例指定要创建的对象种类,并通过复制该原型来创建新对象。

概念

一般来说,从头开始创建一个新实例是一项开销很大的操作。使用原型模式,您可以通过复制或克隆现有实例的实例来创建新的实例。这种方法节省了从头创建新实例的时间和金钱。

真实世界的例子

假设我们有一份有价值文件的主拷贝。我们需要对其进行一些更改,以查看更改的效果。在这种情况下,我们可以复印原始文档并编辑更改。

再考虑一个例子。假设一群人决定庆祝他们的朋友罗恩的生日。他们去面包店买了一个蛋糕。为了让蛋糕与众不同,他们要求卖家在蛋糕上写上“罗恩生日快乐”。从卖方的角度来看,他并没有制造任何新的模型。他已经定义了模型,每天按照相同的流程制作许多蛋糕(看起来都一样),最后通过一些小的变化使每个蛋糕都很特别。

计算机世界的例子

让我们假设您有一个非常稳定的应用程序。将来,您可能希望通过一些小的修改来更新应用程序。因此,您从原始应用程序的副本开始,进行更改,并进一步分析。当然,为了节省你的时间和金钱,你不希望从零开始。

注意

将 Object.clone()方法作为原型的一个例子。

说明

图 2-1 展示了一个简单的原型结构。

img/395506_2_En_2_Fig1_HTML.jpg

图 2-1

一个样本原型结构

在这里,BasicCar 是一个基本原型。Nano 和 Ford 是实现了 BasicCar 中定义的clone()方法的具体原型。在这个例子中,我们创建了一个带有默认价格(印度货币)的 BasicCar 类。后来,我们修改了每个型号的价格。PrototypePatternExample.java 是这个实现中的客户端。

类图

图 2-2 显示了原型模式的类图。

img/395506_2_En_2_Fig2_HTML.jpg

图 2-2

类图

包资源管理器视图

图 2-3 显示了程序的高层结构。

img/395506_2_En_2_Fig3_HTML.jpg

图 2-3

包资源管理器视图

履行

下面是实现。

//BasicCar.java
package jdp2e.prototype.demo;
import java.util.Random;

public abstract class BasicCar implements Cloneable
{
    public String modelName;
    public int basePrice,onRoadPrice;

    public String getModelname() {
        return modelName;
    }
    public void setModelname(String modelname) {
        this.modelName = modelname;
    }

    public static int setAdditionalPrice()
    {
        int price = 0;
        Random r = new Random();
        //We will get an integer value in the range 0 to 100000
        int p = r.nextInt(100000);
        price = p;
        return price;
    }
    public BasicCar clone() throws CloneNotSupportedException
    {
        return  (BasicCar)super.clone();
    }
}
//Nano.java
package jdp2e.prototype.demo;

class Nano extends BasicCar
{
    //A base price for Nano
    public int basePrice=100000;
    public Nano(String m)
    {
        modelName = m;
    }
    @Override
    public BasicCar clone() throws CloneNotSupportedException
    {
         return (Nano)super.clone();
         //return this.clone();
    }
}

//Ford.java

package jdp2e.prototype.demo;

class Ford extends BasicCar
{
    //A base price for Ford
    public int basePrice=100000;
    public Ford(String m)
    {
        modelName = m;
    }

    @Override
    public BasicCar clone() throws CloneNotSupportedException
    {
        return (Ford)super.clone();
    }
}
//Client
// PrototypePatternExample.java

package jdp2e.prototype.demo;

public class PrototypePatternExample
{
    public static void main(String[] args) throws CloneNotSupportedException
    {
        System.out.println("***Prototype Pattern Demo***\n");
        BasicCar nano = new Nano("Green Nano") ;
        nano.basePrice=100000;

        BasicCar ford = new Ford("Ford Yellow");
        ford.basePrice=500000;

        BasicCar bc1;
        //Nano
        bc1 =nano.clone();
        //Price will be more than 100000 for sure
        bc1.onRoadPrice = nano.basePrice+BasicCar.setAdditionalPrice();
        System.out.println("Car is: "+ bc1.modelName+" and it's price is Rs."+bc1.onRoadPrice);

        //Ford
        bc1 =ford.clone();
        //Price will be more than 500000 for sure
        bc1.onRoadPrice = ford.basePrice+BasicCar.setAdditionalPrice();
        System.out.println("Car is: "+ bc1.modelName+" and it's price is Rs."+bc1.onRoadPrice);
    }
}

输出

这是输出。

***Prototype Pattern Demo***

Car is: Green Nano and it's price is Rs.123806
Car is: Ford Yellow and it's price is Rs.595460

注意

您可以在您的系统中看到不同的价格,因为我们在 BasicCar 类中的 setAdditionalPrice()方法中生成了一个随机价格。但我保证福特的价格会比 Nano 高。

问答环节

  1. 使用原型设计模式的 优势 有哪些?

    • 当创建一个类的实例是一个复杂(或者无聊)的过程时,这是很有用的。相反,你可以专注于其他关键活动。

    • 您可以在运行时包含或丢弃产品。

    • 您可以以更低的成本创建新的实例。

  2. 与使用原型设计模式相关的 挑战 有哪些?

    • 每个子类都需要实现克隆或复制机制。

    • 有时,从现有实例创建副本并不简单。例如,如果所考虑的对象不支持复制/克隆,或者存在循环引用,那么实现克隆机制可能会很困难。比如在 Java 中,带有clone()方法的类需要实现可克隆的标记接口;否则,它将抛出 CloneNotSupportedException。

    • 在这个例子中,我使用了在 Java 中执行浅层复制的clone()方法。按照约定,我通过调用super.clone()获得了返回的对象。(如果您想了解更多这方面的内容,请将光标放在 eclipse 编辑器上,仔细阅读说明)。如果您的应用程序需要一个深层副本,那可能会很贵。

  3. 你能详细说明浅拷贝和深拷贝的区别吗?

    一个浅拷贝创建一个新对象,然后将各种字段值从原始对象拷贝到新对象。所以,它也被称为逐场复制。如果原始对象包含作为字段的对其他对象的任何引用,则这些对象的引用被复制到新对象中(即,您不创建这些对象的副本)。

    Let’s try to understand the mechanism with a simple diagram. Suppose we have an object, X1, and it has a reference to another object, Y1. Further assume that object Y1 has a reference to object Z1.

    img/395506_2_En_2_Fig4_HTML.jpg

    图 2-4

    在引用的浅拷贝之前

现在,使用X1的浅层副本,创建了一个新对象X2;它也提到了Y1

img/395506_2_En_2_Fig5_HTML.jpg

图 2-5

在引用的浅拷贝之后

您已经看到了在我们的实现中使用了clone()方法。它执行浅层复制。

对于X1深度副本,创建一个新对象X3X3引用了新对象Y3,实际上是Y1的副本。同样,Y3又引用了另一个新对象Z3,它是Z1的副本。

img/395506_2_En_2_Fig6_HTML.jpg

图 2-6

在引用的深层副本之后

在深层副本中,新对象与原始对象完全分离。在一个对象中进行的任何更改都不应反映在另一个对象上。要用 Java 创建深层副本,您可能需要覆盖clone()方法,然后继续。此外,深层拷贝的成本很高,因为您需要创建额外的对象。深度复制的完整实现在本书 Memento 模式的“Q & A Session ”(第十九章)中给出。

  1. 什么时候你会选择浅层拷贝而不是深层拷贝(反之亦然)?

    浅层拷贝速度更快,成本更低。如果您的目标对象只有基本字段,那总是更好。

    深层拷贝既昂贵又缓慢。但是,如果您的目标对象包含许多引用其他对象的字段,这是很有用的。

  2. 当我在 Java 中复制一个对象时,我需要使用 clone()方法。这种理解正确吗?

    不,还有其他选择,其中之一就是使用序列化机制。但是你总是可以定义你自己的复制构造函数并使用它。

  3. 你能给出一个简单的例子来演示用户定义的复制构造函数吗?

    Java 不支持默认的复制构造函数。你可能需要自己写。考虑下面的程序,它演示了这种用法。

示范

这是演示。

package jdp2e.prototype.questions_answers;

class Student
{
    int rollNo;
    String name;
    //Instance Constructor
    public Student(int rollNo, String name)
    {
        this.rollNo = rollNo;
        this.name = name;
    }
    //Copy Constructor
    public Student( Student student)
    {
        this.name = student.name;
        this.rollNo = student.rollNo;
    }
    public void displayDetails()
    {
        System.out.println(" Student name: " + name + ",Roll no: "+rollNo);
    }
}

class UserDefinedCopyConstructorExample {
    public static void main(String[] args) {
        System.out.println("***User defined copy constructor example in Java***\n");
        Student student1 = new Student(1, "John");
        System.out.println(" The details of Student1 is as follows:");
        student1.displayDetails();
        System.out.println("\n Copying student1 to student2 now");
        //Invoking the user-defined copy constructor
        Student student2 = new Student (student1);
        System.out.println(" The details of Student2 is as follows:");
        student2.displayDetails();
    }
}

输出

这是输出。

***User defined copy constructor example in Java***

 The details of Student1 is as follows:
 Student name: John,Roll no: 1

 Copying student1 to student2 now
 The details of Student2 is as follows:
 Student name: John,Roll no: 1

三、构建器模式

本章涵盖了构建器模式。

GoF 定义

将复杂对象的构造与其表示分离,以便相同的构造过程可以创建不同的表示。

概念

构建器模式对于创建包含多个部分的复杂对象非常有用。对象的创建机制应该独立于这些部分。构造过程并不关心这些部件是如何组装的。同样的构建过程必须允许我们创建对象的不同表示。

图 3-1 中的结构是构建器模式的一个例子。该结构采用了四人组的书,设计模式:可重用面向对象软件的要素 (Addison-Wesley,1995)。

img/395506_2_En_3_Fig1_HTML.jpg

图 3-1

构建器模式的一个例子

产品是您想要创建的复杂对象。ConcreteBuilder 通过实现抽象接口 Builder 来构造和组装产品的各个部分。ConcreteBuilder 对象构建产品的内部表示,并定义创建过程/组装机制。构建器还可以提供方法来获取一个已创建并可供使用的对象(注意下面实现中构建器接口中的getVehicle()方法)。Director 负责使用构建器界面创建最终对象。换句话说,Director 使用 Builder 并控制构建最终产品的步骤/顺序。构建者还可以保留他们构建的产品的引用,以便可以再次使用。

真实世界的例子

要完成计算机订单,需要根据客户偏好组装不同的部件(例如,一位客户可以选择采用英特尔处理器的 500 GB 硬盘,另一位客户可以选择采用 AMD 处理器的 250 GB 硬盘)。考虑另一个例子。假设你打算和一家旅游公司一起去旅游,这家旅游公司为同一次旅游提供各种套餐(例如,他们可以提供特殊安排、不同类型的观光交通工具等。).你可以根据自己的预算选择套餐。

计算机世界的例子

当我们想要将一种文本格式转换成另一种文本格式(例如,RTF 到 ASCII 文本)时,可以使用构建器模式。

注意

Java.util.Calendar.Builder 类就是这类的一个例子,但它只在 Java 8 及更高版本中可用。在这种情况下,java.lang.StringBuilder 类就是一个很好的例子。但是你需要记住,GoF 定义说这种模式允许你使用相同的构造过程来做出不同的表示。在这种情况下,这个例子并不完全符合这种模式。

说明

在这个例子中,我们有以下参与者:构建者、汽车、摩托车、产品和主管。前三个非常简单明了;汽车和摩托车是具体的类,它们实现了构建器接口。构建器用于创建产品对象的各个部分,其中产品表示正在构建的复杂对象。

因为汽车和摩托车是构建器接口的具体实现,所以它们需要实现在构建器接口中声明的方法。这就是为什么他们需要为 s tartUpOperations()buildBody()insertWheels()addHeadlights()endOperations()getVehicle()方法提供身体。前五种方法很简单;它们用于在开始(或结束)时执行一个操作,构建车辆的车身,插入车轮,以及添加前灯。getVehicle()返回最终产品。在这种情况下,Director负责使用Builder接口构建这些产品的最终表示。(参见 GoF 定义的结构)。注意Director正在调用相同的construct()方法来创建不同类型的车辆。

现在浏览代码,看看不同的部分是如何为这个模式组装的。

类图

图 3-2 显示了构建器模式的类图。

img/395506_2_En_3_Fig2_HTML.jpg

图 3-2

类图

包资源管理器视图

图 3-3 显示了程序的高层结构。

img/395506_2_En_3_Fig3_HTML.jpg

图 3-3

包资源管理器视图

履行

下面是实现。

package jdp2e.builder.demo;

import java.util.LinkedList;

//The common interface
interface Builder
{
     void startUpOperations();
     void buildBody();
     void insertWheels();
     void addHeadlights();
     void  endOperations();
     /*The following method is used to retrieve the object that is constructed.*/
     Product getVehicle();
}
//Car class
class Car implements Builder
{
    private String brandName;
    private Product product;
    public Car(String brand)
    {
        product = new Product();
        this.brandName = brand;
    }
    public void startUpOperations()
    {
      //Starting with brand name
        product.add(String.format("Car model is :%s",this.brandName));
    }
    public void buildBody()
    {
        product.add("This is a body of a Car");
    }
    public void insertWheels()
    {
        product.add("4 wheels are added");
    }

    public void addHeadlights()
    {
        product.add("2 Headlights are added");
    }
    public void endOperations()
    {   //Nothing in this case
    }

    public Product getVehicle()
    {
        return product;
    }
}
//Motorcycle class

class MotorCycle implements Builder
{
    private String brandName;
    private Product product;
    public MotorCycle(String brand)
    {
        product = new Product();
        this.brandName = brand;
    }
    public void startUpOperations()
    {   //Nothing in this case
    }

    public  void buildBody()
    {
        product.add("This is a body of a Motorcycle");
    }

    public void insertWheels()
    {
        product.add("2 wheels are added");
    }

    public void addHeadlights()
    {
        product.add("1 Headlights are added");
    }
    public void endOperations()
    {
        //Finishing up with brand name
        product.add(String.format("Motorcycle model is :%s", this.brandName));
    }
    public Product getVehicle()
    {
        return product;
    }
}

// Product class 

class Product
{
   /* You can use any data structure that you prefer.
       I have used LinkedList<String> in this case.*/
    private LinkedList<String> parts;
    public Product()
    {
        parts = new LinkedList<String>();
    }

    public void add(String part)
    {
       //Adding parts
        parts.addLast(part);
    }

    public void showProduct()
    {
      System.out.println("\nProduct completed as below :");
        for (String part: parts)
        System.out.println(part);
    }
}
// Director class 

class Director
{
    Builder builder;
    // Director knows how to use the builder and the sequence of steps.
    public void construct(Builder builder)
    {
        this.builder = builder;
        builder.startUpOperations();
        builder.buildBody();
        builder.insertWheels();
        builder.addHeadlights();
        builder.endOperations();
    }
}
public class BuilderPatternExample {

      public static void main(String[] args) {
             System.out.println("***Builder Pattern Demo***");
        Director director = new Director();

        Builder fordCar = new Car("Ford");
        Builder hondaMotorycle = new MotorCycle("Honda");

        // Making Car
        director.construct(fordCar);
        Product p1 = fordCar.getVehicle();
        p1.showProduct();

        //Making MotorCycle
        director.construct(hondaMotorycle );
        Product p2 = hondaMotorycle.getVehicle();
        p2.showProduct();
      }
}

输出

这是输出。

***Builder Pattern Demo***

Product completed as below :

Car model is :Ford

This is a body of a Car
4 wheels are added
2 Headlights are added

Product completed as below :
This is a body of a Motorcycle
2 wheels are added
1 Headlights are added

Motorcycle model is :Honda

问答环节

  1. 使用构建器模式的 优势 是什么?

    • 您可以一步一步地创建一个复杂的对象,并改变步骤。您通过隐藏复杂构造过程的细节来促进封装。整个施工结束后,主管可以从施工员那里取回最终产品。总的来说,在高层次上,你似乎只有一种方法来制造完整的产品。其他内部方法只涉及部分创建。因此,你可以更好地控制施工过程。

    • 使用这种模式,相同的构建过程可以产生不同的产品。

    • 因为你可以改变构造步骤,你可以改变产品的内部表现。

  2. 与构建者模式相关的 缺点/陷阱 有哪些?

    • 如果你想处理易变的对象(可以在以后修改),它是不合适的。

    • 您可能需要复制部分代码。这些重复可能会在某些上下文中产生重大影响,并变成反模式。

    • 混凝土建筑商致力于特定类型的产品。所以,为了创造不同类型的产品,你可能需要不同的混凝土建筑商。

    • 如果结构非常复杂,这种方法更有意义。

  3. 我可以用一个 抽象类 来代替这个模式的说明中的接口吗?

    是的。在这个例子中,你可以使用一个抽象类来代替一个接口。

  4. 如何决定在应用程序中使用抽象类还是接口?

    我相信,如果你想有一些集中或默认的行为,抽象类是一个更好的选择。在这些情况下,您可以提供一些默认的实现。另一方面,接口实现从零开始。它们在上指明了某种规则/契约,即要做什么(例如,你必须实现该方法),但它们不会强制执行如何的部分。此外,当您试图实现多重继承的概念时,最好使用接口。

    但与此同时,如果你需要在一个接口中添加一个新的方法,那么你需要追踪这个接口的所有实现,并且你需要把这个方法的具体实现放在所有这些地方。你可以在一个抽象类中添加一个默认实现的新方法,现有的代码可以顺利运行。

    Java 对这最后一点特别小心。Java 8 在接口中引入了‘default’关键字的使用。您可以在预期的方法签名前添加默认单词的前缀,并提供默认实现。默认情况下,接口方法是公共的,因此不需要用关键字 public 来标记它。

    这些概括的建议来自位于 https://docs.oracle.com/javase/tutorial/java/IandI/abstract.html 的 Oracle Java 文档。

    You should prefer the abstract class in the following scenarios:

    • 您希望在多个密切相关的类之间共享代码。

    • 扩展抽象类的类可以有许多公共方法或字段,或者它们需要内部的非公共访问修饰符。

    • 您希望使用非静态或/和非最终字段,这使我们能够定义可以访问和修改它们所属对象的状态的方法。

      On the other hand, you should prefer interfaces for these scenarios:

    • 您期望几个不相关的类将实现您的接口(例如,类似的接口可以由许多不相关的类实现)。

    • 您可以指定特定数据类型的行为,但实现者如何实现并不重要。

    • 您希望在应用程序中使用多重继承的概念。

注意

在我的书Java 中的交互式面向对象编程 (Apress,2016)中,我讨论了抽象类、接口,以及使用“default”关键字的各种示例和输出。有关详细的讨论和分析,请参考该书。

  1. 我看到在汽车中,型号名称是加在开头的,但在摩托车中,型号名称是加在结尾的。是故意的吗?

    是的。它被用来证明这样一个事实,即每一个混凝土建筑商可以决定如何生产他们想要的最终产品。他们有这种自由。

  2. 你为什么为导演单独上课?您可以使用客户端代码来扮演导演的角色。

    没有人限制你这样做。在前面的实现中,我想在实现中将这个角色与客户端代码分开。但是在即将到来的/修改后的实现中,我将客户端用作了导演。

  3. 什么叫 客户代码

    包含main()方法的类是客户端代码。在本书的大部分内容中,客户端代码的意思是一样的。

  4. 你几次提到不同的步骤。你能演示一个用不同的变化和步骤创建最终产品的实现吗?

    接得好。您要求展示构建器模式的真正威力。那么,让我们考虑另一个例子。

修改后的插图

下面是修改后的实现的主要特征。

  • 在这个修改后的实现中,我只考虑汽车作为最终产品。

  • 我创建具有以下属性的定制汽车:启动消息(startUpMessage)、流程完成消息(endOperationsMessage)、汽车的车身材料(bodyType)、汽车的车轮数量(noOfWheels)以及汽车的前灯数量(noOfHeadLights)。

  • 在这个实现中,客户端代码扮演着导演的角色。

  • 我已经将构建器接口重命名为 ModifiedBuilder。除了constructCar()getConstructedCar()方法,接口中的每个方法都有 ModifiedBuilder 返回类型,这有助于我们在客户端代码中应用方法链接机制。

已修改的包资源管理器视图

图 3-4 显示了修改后的包浏览器视图。

img/395506_2_En_3_Fig4_HTML.jpg

图 3-4

已修改的包资源管理器视图

修改的实现

下面是修改后的实现。

package jdp2e.builder.pattern;

//The common interface
interface ModifiedBuilder
{
       /*All these methods return type is ModifiedBuilder.
        * This will help us to apply method chaining*/
       ModifiedBuilder startUpOperations(String startUpMessage);
       ModifiedBuilder buildBody(String bodyType);
       ModifiedBuilder insertWheels(int noOfWheels);
       ModifiedBuilder addHeadlights(int noOfHeadLights);
       ModifiedBuilder endOperations(String endOperationsMessage);
       //Combine the parts and make the final product.
       ProductClass constructCar();
       //Optional method:To get the already constructed object
       ProductClass getConstructedCar();
}
//Car class
class CarBuilder implements ModifiedBuilder
{
       private String startUpMessage="Start building the product";//Default //start-up message
       private String  bodyType="Steel";//Default body
       private int noOfWheels=4;//Default number of wheels
       private int  noOfHeadLights=2;//Default number of head lights
       //Default finish up message private String  endOperationsMessage="Product creation completed";
       ProductClass product;
       @Override
       public ModifiedBuilder startUpOperations(String startUpMessage)
       {
             this.startUpMessage=startUpMessage;
             return this;
       }

       @Override
       public ModifiedBuilder buildBody(String bodyType)
       {
             this.bodyType=bodyType;
             return this;
       }

       @Override
       public ModifiedBuilder insertWheels(int noOfWheels)
       {
             this.noOfWheels=noOfWheels;
             return this;
       }

       @Override
       public ModifiedBuilder addHeadlights(int noOfHeadLights)
       {
             this.noOfHeadLights=noOfHeadLights;
             return this;
       }
       @Override
       public ModifiedBuilder endOperations(String endOperationsMessage)
       {       this.endOperationsMessage=endOperationsMessage;
       return this;
       }

       @Override
       public ProductClass constructCar() {

             product= new ProductClass(this.startUpMessage,this.bodyType,this.noOfWheels,this.noOfHeadLights,this.endOperationsMessage);
             return product;
       }

       @Override
       public ProductClass   getConstructedCar()
 {
       return product;
 }
}

//Product class
final class ProductClass
{
       private String startUpMessage;
       private String  bodyType;
       private int noOfWheels;
       private int  noOfHeadLights;
       private String  endOperationsMessage;
       public ProductClass(final String startUpMessage, String bodyType, int noOfWheels, int noOfHeadLights,
                   String endOperationsMessage) {
             this.startUpMessage = startUpMessage;
             this.bodyType = bodyType;
             this.noOfWheels = noOfWheels;
             this.noOfHeadLights = noOfHeadLights;
             this.endOperationsMessage = endOperationsMessage;
       }
       /*There is no setter methods used here to promote immutability.
       Since the attributes are private and there is no setter methods, the keyword "final" is not needed to attach with the attributes.
        */
       @Override
       public String toString() {
             return "Product Completed as:\n startUpMessage=" + startUpMessage + "\n bodyType=" + bodyType + "\n noOfWheels="
                          + noOfWheels + "\n noOfHeadLights=" + noOfHeadLights + "\n endOperationsMessage=" + endOperationsMessage
                          ;
       }

}
//Director class
public class BuilderPatternModifiedExample {

       public static void main(String[] args) {
             System.out.println("***Modified Builder Pattern Demo***");
             /*Making a custom car (through builder)
               Note the steps:
               Step1:Get a builder object with required parameters
               Step2:Setter like methods are used.They will set the optional fields also.
               Step3:Invoke the constructCar() method to get the final car.
              */
             final ProductClass customCar1 = new CarBuilder().addHeadlights(5)
                          .insertWheels(5)
                          .buildBody("Plastic")
                          .constructCar();
             System.out.println(customCar1);
             System.out.println("--------------");
             /* Making another custom car (through builder) with a different
              * sequence and steps.
              */
             ModifiedBuilder carBuilder2=new CarBuilder();
             final ProductClass customCar2 = carBuilder2.insertWheels(7)
                          .addHeadlights(6)
                          .startUpOperations("I am making my own car")
                          .constructCar();
             System.out.println(customCar2);
             System.out.println("--------------");

             //Verifying the getConstructedCar() method
             final ProductClass customCar3=carBuilder2.getConstructedCar();
             System.out.println(customCar3);

       }
}

修改输出

这是修改后的输出。(有些行是加粗的,以引起您注意输出中的差异)。

***Modified Builder Pattern Demo***
Product Completed as:
 startUpMessage=Start building the product
 bodyType=Plastic
 noOfWheels=5
 noOfHeadLights=5
 endOperationsMessage=Product creation completed
--------------
Product Completed as:
 startUpMessage=I am making my own car
 bodyType=Steel
 noOfWheels=7
 noOfHeadLights=6
 endOperationsMessage=Product creation completed
--------------
Product Completed as:
 startUpMessage=I am making my own car
 bodyType=Steel
 noOfWheels=7
 noOfHeadLights=6
 endOperationsMessage=Product creation completed

分析

请注意客户端代码中创建定制汽车的以下代码行(来自前面的实现)。

System.out.println("***Modified Builder Pattern Demo***");
             /*Making a custom car (through builder)
               Note the steps:
               Step1:Get a builder object with required parameters
               Step2:Setter like methods are used.They will set the optional fields also.
               Step3:Invoke the constructCar() method to get the final car.
              */
             final ProductClass customCar1 = new CarBuilder().addHeadlights(5)
                    .insertWheels(5)
                    .buildBody("Plastic")
                    .constructCar();
             System.out.println(customCar1);
       System.out.println("--------------");
             /* Making another custom car (through builder) with a different
              * sequence and steps.
              */
             ModifiedBuilder carBuilder2=new CarBuilder();
             final ProductClass customCar2 = carBuilder2.insertWheels(7)
              .addHeadlights(6)
              .startUpOperations("I am making my own car")
              .constructCar();
       System.out.println(customCar2);

通过在对“build”方法的调用之间改变生成器属性,您正在使用生成器创建多个对象;例如,在第一种情况下,您通过一个 builder 对象一个接一个地调用addHeadLights()insertWheels()buildBody()方法,然后您得到最终的汽车(customCar1)。但是在第二种情况下,当您创建另一个汽车对象(customCar2)时,您以不同的顺序调用这些方法。当您不调用任何方法时,会为您提供默认实现。

  1. 我在客户代码中看到了 final 关键字的使用。但是你还没有使用那些用于 的产品类属性 。这是什么原因呢?

    在客户端代码中,我使用了 final 关键字来提高不变性。但是在 ProductClass 类中,属性已经用私有关键字标记了,并且没有 setter 方法,所以这些已经是不可变的。

  2. 不可变对象的主要好处是什么?

    一旦构造好,就可以安全地共享它们,最重要的是,它们是线程安全的,因此在多线程环境中可以节省很多同步成本。

  3. 什么时候应该考虑使用构建器模式?

    如果您需要制作一个复杂的对象,它涉及到构建过程中的各个步骤,同时,产品需要是不可变的,那么 builder 模式是一个不错的选择。

四、工厂方法模式

本章涵盖了工厂方法模式。

GoF 定义

定义一个创建对象的接口,但是让子类决定实例化哪个类。工厂方法让一个类将实例化推迟到子类。

要记住的要点

要理解这种模式,我建议你去第二十四章,其中涵盖了简单工厂模式。简单工厂模式并不直接属于四人组设计模式,所以我将该模式的讨论放在本书的第二部分。如果从简单的工厂模式开始,工厂方法模式会更有意义。

概念

在这里,您从定义应用程序基本结构的抽象 creator 类(creator)开始开发。从这个抽象类派生的子类执行实际的实例化过程。当你开始用下面的例子思考这个模式时,这个概念对你来说就有意义了。

真实世界的例子

考虑一家汽车制造公司,该公司生产不同型号的汽车,业务运营良好。基于汽车的模型,不同的零件被制造和组装。

在不久的将来,客户可以选择更好的型号,公司应该为这种变化做好准备。如果公司需要为一个新型号做一个全新的设置,只需要几个新功能,这可能会极大地影响其利润率。因此,该公司应该以这样一种方式建立工厂,使其能够为即将到来的车型生产零件。

计算机世界的例子

假设您正在构建一个需要支持两个不同数据库的应用程序,比如 Oracle 和 SQL Server。因此,无论何时向数据库中插入数据,都要创建特定于 SQL Server 的连接(SqlServerConnection )或特定于 Oracle server 的连接(OracleConnection )),然后才能继续。如果你把这些代码放到if-else(或 switch)语句中,你可能需要重复很多代码。这种代码不容易维护,因为每当需要支持新类型的连接时,都需要重新打开代码并进行修改。工厂方法模式专注于解决应用程序开发中的类似问题。

注由于简单工厂模式是工厂方法模式的最简单形式,所以您可以考虑这里的相同示例。因此,java.text.NumberFormat 类的静态 getInstance()方法就是这种类型的一个例子。Java . net . urlstreamhandlerfactory 接口的 createURLStreamHandler(字符串协议)是此类别中的另一个示例。您可以将 ftp、http 等作为不同的协议进行传递,该方法将返回特定协议的 URLStreamHandler。

说明

我继续讨论第二十四章中提到的简单工厂模式。所以,我会努力改进实现。为了简单起见,我将这个实现中的所有类放在一个文件中。因此,您不需要为单独的类创建任何单独的文件夹。我建议你参考相关评论,以便更好地理解。

类图

图 4-1 显示了工厂方法模式的类图。

img/395506_2_En_4_Fig1_HTML.jpg

图 4-1

类图

包资源管理器视图

图 4-2 显示了程序的高层结构。

img/395506_2_En_4_Fig2_HTML.jpg

图 4-2

包资源管理器视图

履行

下面是实现。

package jdp2e.factorymethod.demo;
interface Animal
{
       void speak();
       void preferredAction();
}
class Dog implements Animal
{
       public void speak()
       {
              System.out.println("Dog says: Bow-Wow.");
       }
       public void preferredAction()
       {
              System.out.println("Dogs prefer barking...\n");
       }
}
class Tiger implements Animal
{
       public void speak()
       {
              System.out.println("Tiger says: Halum.");
       }
       public void preferredAction()
       {
              System.out.println("Tigers prefer hunting...\n");
       }
}

abstract class AnimalFactory
{
/*Remember that the GoF definition says "....Factory method lets a class defer instantiation to subclasses."
In our case, the following method will create a Tiger or Dog but at this point it does not know whether it will get a Dog or a Tiger. This decision will be taken by the subclasses i.e. DogFactory or TigerFactory. So,in this implementation, the following method is playing the role of a factory (of creation)*/
       public abstract Animal createAnimal();
}
class DogFactory extends AnimalFactory
{
       public Animal createAnimal()
       {
              //Creating a Dog
              return new Dog();
       }
}
class TigerFactory extends AnimalFactory
{
       public Animal createAnimal()
       {
              //Creating a Tiger
              return new Tiger();
       }
}

class FactoryMethodPatternExample {
       public static void main(String[] args) {
              System.out.println("***Factory Pattern Demo***\n");
              // Creating a Tiger Factory
              AnimalFactory tigerFactory =new TigerFactory();
              // Creating a tiger using the Factory Method
              Animal aTiger = tigerFactory.createAnimal();
              aTiger.speak();
              aTiger.preferredAction();

              // Creating a DogFactory
              AnimalFactory dogFactory = new DogFactory();
              // Creating a dog using the Factory Method
              Animal aDog = dogFactory.createAnimal();
              aDog.speak();
              aDog.preferredAction();
       }
}

输出

这是输出。

***Factory Pattern Demo***

Tiger says: Halum.
Tigers prefer hunting...

Dog says: Bow-Wow.
Dogs prefer barking...

修改的实现

在这个实现中,AnimalFactory 类是一个抽象类。因此,让我们利用使用抽象类的优势。假设您希望一个子类遵循一个可以从其父类(或基类)强加的规则。所以,我在下面的设计中测试这样一个场景。

以下是该设计的主要特征。

  • 只有 AnimalFactory 被修改如下(即,我引入了一个新的makeAnimal()方法)。

  • 客户端代码适应了这些变化:

//Modifying the AnimalFactory class.
abstract class AnimalFactory
{
    public Animal makeAnimal()
    {
      System.out.println("I am inside makeAnimal() of AnimalFactory.You cannot ignore my rules.");
        /*
        At this point, it doesn't know whether it will get a Dog or a Tiger. It will be decided by the subclasses i.e.DogFactory or TigerFactory.But it knows that it will Speak and it will have a preferred way of Action.
        */
        Animal animal = createAnimal();
        animal.speak();
        animal.preferredAction();
        return animal;
    }

/*Remember that the GoF definition says "....Factory method lets a class defer instantiation to subclasses."
In our case, the following method will create a Tiger or Dog but at this point it does not know whether it will get a Dog or a Tiger.
This decision will be taken by the subclasses i.e. DogFactory or TigerFactory. So,in this implementation, the following method is playing the role of a factory (of creation)*/
     public abstract Animal createAnimal();
}

class ModifiedFactoryMethodPatternExample {
       public static void main(String[] args) {
              System.out.println("***Modified Factory Pattern Demo***\n");
              // Creating a Tiger Factory
              AnimalFactory tigerFactory =new TigerFactory();
              // Creating a tiger using the Factory Method
              Animal aTiger = tigerFactory.makeAnimal();
              //aTiger.speak();
              //aTiger.preferredAction();

              // Creating a DogFactory
              AnimalFactory dogFactory = new DogFactory();
              // Creating a dog using the Factory Method
              Animal aDog = dogFactory.makeAnimal();
              //aDog.speak();
              //aDog.preferredAction();

       }
}

修改输出

这是修改后的输出。

***Modified Factory Pattern Demo***

I am inside makeAnimal() of AnimalFactory.You cannot ignore my rules.

Tiger says: Halum.
Tigers prefer hunting...

I am inside makeAnimal() of AnimalFactory.You cannot ignore my rules.

Dog says: Bow-Wow.
Dogs prefer barking...

分析

在每种情况下,您都会看到消息(或警告):“您不能忽略我的规则”

问答环节

img/395506_2_En_4_Fig3_HTML.jpg

图 4-3

我们例子中的两个类层次结构

  1. 为什么将 CreateAnimal()方法 从客户端代码中分离出来?

    这是我的真实意图。我希望子类创建专门化的对象。如果你仔细观察,你会发现只有这个“创造性的部分”在不同的产品中是不同的。我将在简单工厂模式的问答环节中详细讨论这一点(参见第二十四章)。

  2. 使用这样的工厂有什么好处?

    • 您将会变化的代码与不会变化的代码分离开来(也就是说,使用简单工厂模式的优势仍然存在)。这种技术有助于您轻松维护代码。

    • 您的代码不是紧密耦合的;因此,您可以随时在系统中添加新的类,如 Lion、Beer 等,而无需修改现有的体系结构。所以,你遵循了“修改时封闭,扩展时开放”的原则。

  3. 使用这样的工厂有哪些挑战?

    如果您需要处理大量的类,那么您可能会遇到维护开销。

  4. 我发现工厂模式支持两个并行的层次结构。这是正确的吗?

    接得好。是的,从类图(见图 4-3 )可以明显看出,这种模式支持并行的类层次结构。

因此,在本例中,AnimalFactory、DogFactory 和 TigerFactory 位于一个层次结构中,而 Animal、Dog 和 Tiger 位于另一个层次结构中。因此,创作者和他们的创作/产品是平行运行的两个层次。

  1. 我应该总是用抽象关键字来标记工厂方法,这样子类就可以完成它们。这是正确的吗?

    不。如果创建者没有子类,您可能会对默认的工厂方法感兴趣。在这种情况下,不能用 abstract 关键字标记工厂方法。

    为了展示工厂方法模式的真正威力,您可能需要遵循类似的设计,这里实现了类似的设计。

  2. 在我看来,工厂方法模式和简单工厂没有太大区别。这是正确的吗?

    如果你看看这两章的例子中的子类,你可能会发现一些相似之处。但是你不应该忘记工厂方法模式的关键目标是它提供了一个框架,通过这个框架不同的子类可以生产不同的产品。但是在一个简单的工厂中,您不能像使用工厂方法模式那样改变产品。把简单工厂想象成一次性交易但是最重要的是,你的创造部分不会因为修改而关闭。每当你想添加一个新东西时,你需要在你的简单工厂模式的工厂类中添加一个if/else块或者一个 switch 语句。

    在这种情况下,记住 GoF 的定义:工厂方法让一个类将实例化推迟到子类。因此,在我们简单的工厂模式演示中,您只使用了一个具体的类(SimpleFactory))。您不需要覆盖createAnimal()方法,并且没有子类参与最终的决策/产品制作过程。但是如果你试图编码到一个抽象类(或者接口),这总是被认为是一个好的实践,并且这个机制提供了你在抽象类中放置一些常见行为的灵活性。

*### 注意

在简单工厂模式中,您只需将实例化逻辑从客户端代码中分离出来。在这种情况下,它知道它可以创建其对象的所有类。另一方面,当使用工厂方法模式时,您将对象创建委托给子类。还有,工厂方法事先对产品子类没有绝对的把握。

  1. 在工厂方法模式中,我可以简单地使用一种子类化机制(即使用继承),然后实现工厂方法(在父类中定义)。这是正确的吗?

    如果您想严格遵循 GoF 定义,这个问题的答案是肯定的。但重要的是要注意,在许多应用程序/实现中,没有使用抽象类或接口;例如,在 Java 中,XML reader 对象是这样使用的:

//Some code before…
XMLReader xmlReader1 = XMLReaderFactory.createXMLReader();
//Some code after

XMLReaderFactory 是 Java 中的最终类。所以,你不能继承它。

但是当您使用 SAXParserFactory 时,如下所示,您使用的是一个抽象类 SAXParserFactory。

//some code before….
SAXParserFactory factory = SAXParserFactory.newInstance();
              SAXParser parser = factory.newSAXParser();
              XMLReader xmlReader2 = parser.getXMLReader();
//Some code after

```*

# 五、抽象工厂模式

本章涵盖了抽象工厂模式。

## GoF 定义

提供一个接口,用于创建相关或依赖对象的系列,而无需指定它们的具体类*。*

### 注意

为了更好地理解这种模式,我建议您从第二十四章(简单工厂模式)开始,然后学习第四章(工厂方法模式)。简单工厂模式并不直接属于四人组设计模式,所以关于该模式的讨论放在本书的第二部分。

## 概念

这基本上是一个工厂的工厂,提供了比工厂方法模式更高的抽象层次。这种模式帮助我们交换特定的实现,而不改变使用它们的代码,甚至在运行时。

这种模式提供了一种封装一组具有共同主题的单个工厂的方法。这里,类不直接创建对象;相反,它将任务委托给工厂对象。

简单工厂方法模式创建一组相关的对象。类似地,由于抽象工厂基本上是工厂的工厂,它返回创建一组相关对象的工厂。(我将在“问答”部分详细讨论这些差异。)

## 真实世界的例子

假设我们用两张不同的桌子装饰房间:一张是木制的,一张是钢制的。对于木桌,我们需要去找木匠,对于另一张桌子,我们需要去五金商店。两者都是桌子工厂,所以基于我们的需求,我们决定我们需要什么样的工厂。

在这种背景下,你可能会考虑两家不同的汽车制造公司:本田和福特。本田制造诸如 CR-V、Jazz、布里奥等车型。福特生产不同的车型,如野马、菲戈、Aspire 等等。所以,如果你想买一辆 Jazz,你必须去本田展厅,但是如果你喜欢 Figo,你就去福特展厅。

## 计算机世界的例子

为了理解这个模式,我将扩展工厂方法模式中的需求。在工厂方法模式中,我们有两个工厂:一个创建狗,另一个创建老虎。但是现在,你想把狗和老虎进一步分类。你可以通过这些工厂选择家养动物(狗或老虎)或野生动物(狗或老虎)。为了满足这个需求,我引进了两个混凝土工厂:野生动物工厂和动物工厂。野生动物工厂负责创造野生动物,动物工厂负责创造家养动物或宠物。

### 注意

javax . XML . parsers . documentbuilderfactory 的 newInstance()方法是 JDK 中抽象工厂模式的一个例子。javax . XML . transform . transformer factory 的 newInstance()方法是这种情况下的另一个例子。如果你熟悉 C#,你可能会注意到 ADO.NET 已经实现了类似的概念来建立到数据库的连接。

## 说明

维基百科描述了抽象工厂模式的典型结构( [`https://en.wikipedia.org/wiki/Abstract_factory_pattern`](https://en.wikipedia.org/wiki/Abstract_factory_pattern) ),类似于图 5-1 所示。

![img/395506_2_En_5_Fig1_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-java-zh/raw/master/docs/java-design-ptn/img/395506_2_En_5_Fig1_HTML.jpg)

图 5-1

抽象工厂模式的典型例子

在我们的实现中,我将遵循类似的结构。因此,在下面的实现中,我使用了两个具体的工厂:WildAnimalFactory 和 PetAnimalFactory。他们负责创造具体的产品,狗和老虎。WildAnimalFactory 制造野生动物(野狗和野生老虎),PetAnimalFactory 制造家养宠物(宠物狗和宠物老虎)。为了便于参考,参与者及其角色总结如下。

*   *AnimalFactory* :在下面的实现中被当作抽象工厂的接口。

*   *WildAnimalFactory* :实现 AnimalFactory 接口的具体工厂。它创造了野狗和野生老虎。

*   *PetAnimalFactory* :另一个实现 AnimalFactory 接口的具体工厂。它创造了宠物狗和宠物老虎。

*   *老虎*和*狗*:两个接口,在本例中被视为抽象产品。

*   *派蒂格*、*派蒂格*、*野虎*、*野狗*:以下实现中的具体产品。

这里客户端代码寻找动物(狗和老虎)。当我们使用抽象工厂的具体实例来构造类时,可以看到这种模式的一种常见用法。我也一样。注意,client 类包含 AnimalFactory 的组合实现。您可以在下面的实现中探索宠物和野生动物的构造过程。

### 类图

图 5-2 显示了类图。

![img/395506_2_En_5_Fig2_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-java-zh/raw/master/docs/java-design-ptn/img/395506_2_En_5_Fig2_HTML.jpg)

图 5-2

类图

### 包资源管理器视图

图 5-3 显示了程序的高层结构。

![img/395506_2_En_5_Fig3_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-java-zh/raw/master/docs/java-design-ptn/img/395506_2_En_5_Fig3_HTML.jpg)

图 5-3

包资源管理器视图

### 履行

下面是实现。

```java
package jdp2e.abstractfactory.demo;

interface Dog
{
      void speak();
      void preferredAction();
}

interface Tiger
{
      void speak();
      void preferredAction();
}

//Types of Dogs-wild dogs and pet dogs

class WildDog implements Dog
{
      @Override
      public void speak()
      {
            System.out.println("Wild Dog says loudly: Bow-Wow.");

      }
      @Override
      public void preferredAction()
      {
            System.out.println("Wild Dogs prefer to roam freely in jungles.\n");

      }
}
class PetDog implements Dog
{
      @Override
      public void speak()
      {
                System.out.println("Pet Dog says softly: Bow-Wow.");
      }
      @Override
      public void preferredAction()
      {
            System.out.println("Pet Dogs prefer to stay at home.\n");

      }
}
//Types of Tigers-wild tigers and pet tigers

class WildTiger implements Tiger
{
      @Override
      public void speak()
      {
            System.out.println("Wild Tiger says loudly: Halum.");

      }
      @Override
      public void preferredAction()
      {
            System.out.println("Wild Tigers prefer hunting in jungles.\n");

      }
}
class PetTiger implements Tiger
{
      @Override
      public void speak()
      {
            System.out.println("Pet Tiger says softly: Halum.");

      }
      @Override
      public void preferredAction()
      {
            System.out.println("Pet Tigers play in the animal circus.\n");
      }
}

//Abstract Factory
interface AnimalFactory
{
      Dog createDog();
      Tiger createTiger();
}
//Concrete Factory-Wild Animal Factory
class WildAnimalFactory implements AnimalFactory
{
      @Override
      public Dog createDog()
      {
            return new WildDog();
      }
      @Override
      public Tiger createTiger()
      {
            return new WildTiger();
      }

}
//Concrete Factory-Pet Animal Factory

class PetAnimalFactory implements AnimalFactory
{
      @Override
      public Dog createDog()
      {
            return new PetDog();
      }
      @Override
      public Tiger createTiger()
      {
            return new PetTiger();
      }
}
//Client
class AbstractFactoryPatternExample {

      public static void main(String[] args) {
            AnimalFactory myAnimalFactory;
            Dog myDog;
            Tiger myTiger;
            System.out.println("***Abstract Factory Pattern Demo***\n");
            //Making a wild dog through WildAnimalFactory
            myAnimalFactory = new WildAnimalFactory();
            myDog = myAnimalFactory.createDog();
            myDog.speak();
            myDog.preferredAction();
            //Making a wild tiger through WildAnimalFactory
            myTiger = myAnimalFactory.createTiger();
            myTiger.speak();
            myTiger.preferredAction();

            System.out.println("******************");

            //Making a pet dog through PetAnimalFactory
            myAnimalFactory = new PetAnimalFactory();
            myDog = myAnimalFactory.createDog();
            myDog.speak();
            myDog.preferredAction();
            //Making a pet tiger through PetAnimalFactory

            myTiger = myAnimalFactory.createTiger();
            myTiger.speak();
            myTiger.preferredAction();

      }

}

输出

这是输出。

***Abstract Factory Pattern Demo***

Wild Dog says loudly: Bow-Wow.
Wild Dogs prefer to roam freely in jungles.

Wild Tiger says loudly: Halum.
Wild Tigers prefer hunting in jungles.

******************
Pet Dog says softly: Bow-Wow.
Pet Dogs prefer to stay at home.

Pet Tiger says softly: Halum.
Pet Tigers play in the animal circus.

问答环节

  1. 我看到狗和老虎的接口都包含同名的方法(两个接口都包含 speak()和****preferredation()方法 )。它是强制性的吗?

    不,你可以为你的方法使用不同的名字。此外,这些接口中方法的数量可以不同。但是我在本书中介绍了一个简单的工厂模式和工厂方法模式。你可能对它们之间的相似或不同之处感兴趣。所以,我从一个例子开始,不断修改。这就是为什么我在这个例子中保留了 s peak()和 preferred Action()方法。注意这些方法在简单工厂模式(参见第二十四章)和工厂方法模式(参见第四章)中都有使用。

  2. 使用这样的抽象工厂有什么挑战?

    • 抽象工厂的任何变化都迫使我们传播对具体工厂的修改。如果你遵循的设计理念是将编程为接口,而不是实现,你需要为此做好准备。这是开发人员始终牢记的关键原则之一。在大多数情况下,开发人员不想改变他们的抽象工厂。

    • 整体架构可能看起来很复杂。此外,在某些情况下,调试变得很棘手。

  3. 如何区分简单工厂模式和工厂方法模式或抽象工厂模式?

    我在第四章的“问答”部分讨论了简单工厂模式和工厂方法模式的区别。

    让我们用下面的图表来修改所有三个工厂。

简单工厂模式代码片段

下面是代码片段。

Animal preferredType=null;
SimpleFactory simpleFactory = new SimpleFactory();
// The code that will vary based on users preference.
preferredType = simpleFactory.createAnimal();

图 5-4 显示了如何在简单工厂模式中获取动物对象。

img/395506_2_En_5_Fig4_HTML.jpg

图 5-4

简单工厂模式

工厂方法模式代码片段

下面是代码片段。

// Creating a Tiger Factory
AnimalFactory tigerFactory =new TigerFactory();
// Creating a tiger using the Factory Method
Animal aTiger = tigerFactory.createAnimal();

//...Some code in between...

// Creating a DogFactory
AnimalFactory dogFactory = new DogFactory();
// Creating a dog using the Factory Method
Animal aDog = dogFactory.createAnimal();

图 5-5 显示了如何在工厂方法模式中获取动物对象。

img/395506_2_En_5_Fig5_HTML.jpg

图 5-5

工厂方法模式

抽象工厂模式代码片段

下面是代码片段。

AnimalFactory myAnimalFactory;
Dog myDog;
Tiger myTiger;
System.out.println("***Abstract Factory Pattern Demo***\n");
//Making a wild dog through WildAnimalFactory
myAnimalFactory = new WildAnimalFactory();
myDog = myAnimalFactory.createDog();
//Making a wild tiger through WildAnimalFactory
myTiger = myAnimalFactory.createTiger();

//Making a pet dog through PetAnimalFactory
myAnimalFactory = new PetAnimalFactory();
myDog = myAnimalFactory.createDog();
//Making a pet tiger through PetAnimalFactory
myTiger = myAnimalFactory.createTiger();
myTiger.speak();
myTiger.preferredAction();

图 5-6 展示了如何在抽象工厂方法模式中获取动物对象。

img/395506_2_En_5_Fig6_HTML.jpg

图 5-6

抽象工厂方法模式

结论

使用简单工厂,您可以将不同于其他代码的代码分离出来(基本上,您可以将客户端代码解耦)。这种方法有助于您轻松管理代码。这种方法的另一个主要优点是客户端不知道对象是如何创建的。因此,它同时促进了安全性和抽象性。但是会违反开闭原则。

您可以使用工厂方法模式来克服这个缺点,该模式允许子类决定如何完成实例化过程。换句话说,您将对象创建委托给实现工厂方法来创建对象的子类。

抽象工厂基本上是工厂的工厂。它创建相关对象的系列,但是它不依赖于具体的类。

我试图保持彼此接近的简单例子。工厂方法促进继承;它们的子类需要实现工厂方法来创建对象。抽象工厂模式促进了对象组合,在这种模式下,您可以使用抽象工厂的具体实例来组合类。

这些工厂都通过减少对具体类的依赖来促进松散耦合。

  1. 在所有这些工厂例子中,你都避免使用参数化的构造函数。这是故意的吗?

    在许多应用程序中,您会看到参数化构造函数的使用;许多专家更喜欢这种方法。但是我的重点纯粹是在设计上,所以,我忽略了参数化构造函数的使用。但是如果您是参数化构造函数的爱好者,让我们稍微修改一下实现,以便您可以对其余部分做同样的事情。

修改后的插图

假设您希望您的工厂用指定的颜色初始化 tigers,客户端可以选择这些颜色。

让我们修改以下几段代码(更改以粗体显示)。

修改的实现

下面是修改后的实现。

package jdp2e.abstractfactory.questions_answers;

interface Dog
{
      void speak();
      void preferredAction();
}

interface Tiger
{
      void speak();
      void preferredAction();
}

//Types of Dogs-wild dogs and pet dogs
class WildDog implements Dog
{
      @Override
      public void speak()
      {
            System.out.println("Wild Dog says loudly: Bow-Wow.");

      }
      @Override
      public void preferredAction()
      {
            System.out.println("Wild Dogs prefer to roam freely in jungles.\n");

      }
}
class PetDog implements Dog
{
      @Override
      public void speak()
      {
            System.out.println("Pet Dog says softly: Bow-Wow.");
      }
      @Override
      public void preferredAction()
      {
            System.out.println("Pet Dogs prefer to stay at home.\n");

      }
}
//Types of Tigers-wild tigers and pet tigers
class WildTiger implements Tiger
{
      public WildTiger(String color)
      {
            System.out.println("A wild tiger with "+ color+ " is created.");
      }
      @Override
      public void speak()
      {
            System.out.println("Wild Tiger says loudly: Halum.");

      }
      @Override
      public void preferredAction()
      {
            System.out.println("Wild Tigers prefer hunting in jungles.\n");

      }
}
class PetTiger implements Tiger
{
      public PetTiger(String color)
      {
            System.out.println("A pet tiger with "+ color+ " is created.");
      }
      @Override
      public void speak()
      {
            System.out.println("Pet Tiger says softly: Halum.");

      }
      @Override
      public void preferredAction()
      {
            System.out.println("Pet Tigers play in the animal circus.\n");
      }
}

//Abstract Factory
interface AnimalFactory
{
      Dog createDog();
      Tiger createTiger(String color);
}
//Concrete Factory-Wild Animal Factory
class WildAnimalFactory implements AnimalFactory
{
      @Override
      public Dog createDog()
      {
            return new WildDog();
      }
      @Override
      public Tiger createTiger(String color)
      {
            return new WildTiger(color);
      }

}
//Concrete Factory-Pet Animal Factory
class PetAnimalFactory implements AnimalFactory
{
      @Override
      public Dog createDog()
      {
            return new PetDog();
      }
      @Override
      public Tiger createTiger(String color)
      {
            return new PetTiger(color);
      }
}
//Client

class AbstractFactoryPatternModifiedExample {
          public static void main(String[] args) {
            AnimalFactory myAnimalFactory;
            Dog myDog;
            Tiger myTiger;
            System.out.println("***Abstract Factory Pattern Demo***\n");
            //Making a wild dog through WildAnimalFactory
            myAnimalFactory = new WildAnimalFactory();
            myDog = myAnimalFactory.createDog();
            myDog.speak();
            myDog.preferredAction();
            //Making a wild tiger through WildAnimalFactory
            //myTiger = myAnimalFactory.createTiger();
            myTiger = myAnimalFactory.createTiger("white and black stripes");
            myTiger.speak();
            myTiger.preferredAction();

            System.out.println("******************");

            //Making a pet dog through PetAnimalFactory
            myAnimalFactory = new PetAnimalFactory();
            myDog = myAnimalFactory.createDog();
            myDog.speak();
            myDog.preferredAction();
            //Making a pet tiger through PetAnimalFactory
            myTiger = myAnimalFactory.createTiger("golden and cinnamon stripes");
            myTiger.speak();
            myTiger.preferredAction();

      }

}

修改输出

这是修改后的输出。

***Abstract Factory Pattern Demo***

Wild Dog says loudly: Bow-Wow.
Wild Dogs prefer to roam freely in jungles.

A wild tiger with white and black stripes is created.

Wild Tiger says loudly: Halum.
Wild Tigers prefer hunting in jungles.

******************
Pet Dog says softly: Bow-Wow.
Pet Dogs prefer to stay at home.

A pet tiger with golden and cinnamon stripes is created.

Pet Tiger says softly: Halum.
Pet Tigers play in the animal circus.

六、代理模式

本章介绍代理模式。

GoF 定义

为另一个对象提供代理或占位符,以控制对它的访问。

概念

代理基本上是预期对象的替代品。由于许多因素,访问原始对象并不总是可能的。例如,创建它的成本很高,它需要受到保护,它位于远程位置,等等。代理设计模式在类似的情况下可以帮助我们。当一个客户端处理一个代理对象时,它假设它正在与实际的对象进行对话。因此,在这种模式中,您可能希望使用一个可以作为其他东西的接口的类。

真实世界的例子

在教室里,当一个学生缺席时,他最好的朋友可能会在点名时试图模仿他的声音,以试图让他的朋友出席。

计算机世界的例子

在编程领域,创建一个复杂对象(重对象)的多个实例是很昂贵的。因此,只要有需要,就可以创建多个指向原始对象的代理对象。这种机制还可以帮助节省系统/应用程序内存。ATM 可以实现这种模式来保存可能存在于远程服务器上的银行信息的代理对象。

注意

在 java.lang.reflect 包中,可以有一个代理类和一个支持类似概念的 InvocationHandler 接口。 java.rmi.* 包还提供了一些方法,通过这些方法,一个 java 虚拟机上的对象可以调用驻留在不同 Java 虚拟机上的对象的方法。

说明

在下面的程序中,我调用了代理对象的doSomework()方法,而代理对象又调用了 ConcreteSubject 对象的doSomework()方法。当客户端看到输出时,他们不知道代理对象完成了任务。

类图

图 6-1 为类图。

img/395506_2_En_6_Fig1_HTML.jpg

图 6-1

类图

包资源管理器视图

图 6-2 显示了程序的高层结构。

img/395506_2_En_6_Fig2_HTML.jpg

图 6-2

包资源管理器视图

履行

下面是实现。

package jdp2e.proxy.demo;

// Abstract class Subject
abstract class Subject
{
      public abstract void doSomeWork();
}
// ConcreteSubject class
class ConcreteSubject extends Subject
{
      @Override
      public void doSomeWork()
      {
            System.out.println("doSomeWork() inside ConcreteSubject is invoked.");
      }
}

/**
 * Proxy Class: It will try to invoke the doSomeWork()
 * of a ConcreteSubject instance
 */
Class Proxy extends  Subject
{
      static Subject cs;
      @Override
      public void doSomeWork()
      {
            System.out.println("Proxy call happening now...");
            //Lazy initialization:We'll not instantiate until the method is //called

            if (cs == null)
            {
                   cs = new ConcreteSubject();
            }
            cs.doSomeWork();

      }
}

/**
 * The client is talking to a ConcreteSubject instance
 * through a proxy method.
 */
public class ProxyPatternExample {
      public static void main(String[] args) {
            System.out.println("***Proxy Pattern Demo***\n");
            Proxy px = new Proxy();
            px.doSomeWork();

      }
}

输出

这是输出。

***Proxy Pattern Demo***

Proxy call happening now...
doSomeWork() inside ConcreteSubject is invoked.

问答环节

  1. 有哪些不同类型的代理?

    These are the common types:

    • 远程代理。隐藏位于不同地址空间的实际对象。

    • 虚拟代理。执行优化技术,例如根据需求创建重物。

    • 保护代理。处理不同的访问权限。

    • 智能参考。当客户端访问对象时,执行额外的内务处理工作。典型的操作是计算在特定时刻对实际对象的引用次数。

  2. You could create the ConcreteSubject instance in the proxy class constructor, as follows.

    class Proxy extends  Subject
    {
          static Subject cs;
          public Proxy()
          {
                //Instantiating inside the constructor
                cs = new ConcreteSubject();
          }
    
          @Override
          public void doSomeWork()
          {
                System.out.println("Proxy call happening now...");
                cs.doSomeWork();
          }
    }
    
    

    这是正确的吗?

    是的,你可以这样做。但是如果您遵循这种设计,无论何时实例化一个代理对象,您都需要实例化一个 ConcreteSubject 类的对象。所以,这个过程最终可能会产生不必要的对象。您可以简单地用下面这段代码和相应的输出进行测试。

替代实现

下面是替代实现。

package jdp2e.proxy.questions_answers;

//Abstract class Subject
abstract class Subject
{
      public abstract void doSomeWork();
}
//ConcreteSubject class
class ConcreteSubject extends Subject
{
      @Override
      public void doSomeWork()
      {
            System.out.println("doSomeWork() inside ConcreteSubject is invoked");
      }
}

/**
 * Proxy Class
 * It will try to invoke the doSomeWork() of a ConcreteSubject instance *
 */
class Proxy extends  Subject
{
      static Subject cs;
      static int count=0;//A counter to track the number of instances
      public Proxy()
      {
            //Instantiating inside the constructor

            cs = new ConcreteSubject();
            count ++;
      }

      @Override
      public void doSomeWork()
      {
            System.out.println("Proxy call happening now...");
            //Lazy initialization:We'll not instantiate until the method is //called
            /*if (cs == null)
            {
                   cs = new ConcreteSubject();
                   count ++;
             }*/
             cs.doSomeWork();
      }
}

/**
  * The client is talking to a ConcreteSubject instance
 * through a proxy method.
 */
public class ProxyPatternQuestionsAndAnswers {
      public static void main(String[] args) {
             System.out.println("***Proxy Pattern Demo without lazy instantiation***\n");
             //System.out.println("***Proxy Pattern Demo with lazy instantiation***\n");
             Proxy px = new Proxy();
             px.doSomeWork();
             //2nd proxy instance
             Proxy px2 = new Proxy();
             px2.doSomeWork();

             System.out.println("Instance Count="+Proxy.count);
      }
}

没有惰性实例化的输出

这是输出。

***Proxy Pattern Demo without lazy instantiation***

Proxy call happening now...
doSomeWork() inside ConcreteSubject is invoked
Proxy call happening now...
doSomeWork() inside ConcreteSubject is invoked
Instance Count=2

分析

请注意,您已经创建了两个代理实例。

现在,用惰性实例化来尝试我们之前的方法。(删除代理构造函数,取消惰性实例化的注释)。

延迟实例化输出

这是输出。

***Proxy Pattern Demo with lazy instantiation***

Proxy call happening now...
doSomeWork() inside ConcreteSubject is invoked
Proxy call happening now...
doSomeWork() inside ConcreteSubject is invoked

Instance Count=1

分析

注意,这次您只创建了一个代理实例。

  1. 但是在这种 的懒惰实例化技术 中,你可能会在多线程应用中创建不必要的对象。这是正确的吗?

    是的。在这本书里,我只展示简单的插图,所以我忽略了那部分。在关于单例模式的讨论中,我分析了一些处理多线程环境的替代方法。在这种情况下,你可以参考这些讨论。(例如,在这个特定的场景中,您可以实现同步技术、锁定机制或智能代理等等,以确保在授予对特定对象的访问权限之前锁定该对象。)

  2. 你能举一个远程代理的例子吗?

    假设,你想调用一个对象的方法,但是该对象运行在不同的地址空间(例如,不同的位置或不同的计算机,等等。).你是如何进行的?在远程代理的帮助下,您可以调用代理对象上的方法,该方法又将调用转发给远程计算机上运行的实际对象。这种类型的需求可以通过众所周知的机制来实现,如 ASP.NET、CORBA、C# 的 WCF(3.0 版本以上)或 Java 的 RMI(远程方法调用)。

    Figure 6-3 demonstrates a simple remote proxy structure.

    img/395506_2_En_6_Fig3_HTML.jpg

    图 6-3

    一个简单的远程代理图

  3. 什么时候可以使用虚拟代理?

    它可以用来避免多次加载一个非常大的图像。

  4. 什么时候可以使用保护代理?

    组织中的安全团队可以实现保护代理来阻止对特定网站的互联网访问。

考虑下面的例子,它基本上是前面描述的代理模式实现的修改版本。为了简单起见,我们假设目前只有三个注册用户可以使用doSomeWork()代理方法。除此之外,如果任何其他用户(比如 Robin)试图调用该方法,系统将拒绝这些尝试。你必须同意,当系统将拒绝这种不必要的访问;制作代理对象毫无意义。因此,如果避免在代理类构造函数中实例化 ConcreteSubject 的对象,就可以很容易地避免创建这类额外的对象。

现在来看一下修改后的实现。

已修改的包资源管理器视图

图 6-4 显示了修改后的程序高层结构。

img/395506_2_En_6_Fig4_HTML.jpg

图 6-4

已修改的包资源管理器视图

修改的实现

下面是修改后的实现。

package jdp2e.proxy.modified.demo;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

//Abstract class Subject
abstract class Subject
{
       public abstract void doSomeWork();
}
//ConcreteSubject class
class ConcreteSubject extends Subject
{
       @Override
       public void doSomeWork()
       {
              System.out.println("doSomeWork() inside ConcreteSubject is invoked.");
       }
}

/**
 * Proxy Class:It will try to invoke the doSomeWork()
 * of a ConcreteSubject instance
 */
class ModifiedProxy extends  Subject
{
       static Subject cs;
       String currentUser;
       List<String> registeredUsers;
       //Or, simply create this mutable list in one step
       /*List<String> registeredUsers=new ArrayList<String>(Arrays.asList( "Admin","Rohit","Sam"));*/
       public ModifiedProxy(String currentUser)
       {
              //Registered users are Admin, Rohit and Sam only

.
              registeredUsers = new ArrayList<String>();
              registeredUsers.add("Admin");
              registeredUsers.add("Rohit");
              registeredUsers.add("Sam");
              this.currentUser = currentUser;
       }
       @Override
       public void doSomeWork()
       {
              System.out.println("\n Proxy call happening now...");
              System.out.println(currentUser+" wants to invoke a proxy method.");
              if (registeredUsers.contains(currentUser))
              {
                     //Lazy initialization:We'll not instantiate until the
                     //method is called
                     if (cs == null)
                     {
                            cs = new ConcreteSubject();
                     }
                     //Allow the registered user to invoke the method
                     cs.doSomeWork();
              }
              else
              {
                     System.out.println("Sorry "+ currentUser+ " , you do not have access rights.");
              }
       }
}

/**
 * The client is talking to a ConcreteSubject instance

 * through a proxy method.
 */
public class ModifiedProxyPatternExample {

       public static void main(String[] args) {
              System.out.println("***Modified Proxy Pattern Demo***\n");
              //Admin is an authorized user
              ModifiedProxy px1 = new ModifiedProxy("Admin");
              px1.doSomeWork();
              //Robin is an unauthorized user
              ModifiedProxy px2 = new ModifiedProxy("Robin");
              px2.doSomeWork();

       }

}

修改输出

这是修改后的输出。

  1. 代理就像装饰者一样。这是正确的吗?

    您可以实现一个类似于 decorators 的保护代理,但是您不应该忘记它的意图。装饰者专注于增加职责,但是代理专注于控制对对象的访问。代理的类型和实现各不相同。另外,一般来说,代理在相同的接口上工作,但是装饰者可以在扩展的接口上工作。所以,如果你能记住他们的用途,在大多数情况下,你就能清楚地把他们和装修工区分开来。

  2. 代理人有哪些缺点?

    If you are careful enough in your implementation, the pros are much greater than the cons, but

    • 您可以提出您对响应时间的担忧。因为您没有直接与实际对象对话,所以通过这些代理的响应时间可能会更长。

    • 您需要为代理维护额外的代码。

    • 代理可以隐藏对象的实际响应,这在特殊情况下可能会造成混乱。

***Modified Proxy Pattern Demo***

Proxy call happening now...
Admin wants to invoke a proxy method.
doSomeWork() inside ConcreteSubject is invoked.

Proxy call happening now...
Robin wants to invoke a proxy method.

Sorry Robin, you do not have access rights.

七、装饰模式

本章涵盖了装饰模式。

GoF 定义

动态地将附加责任附加到对象上。Decorators 为扩展功能提供了子类化的灵活替代方案。

概念

这种模式说,类必须为修改而关闭,但为扩展而打开;也就是说,可以在不干扰现有功能的情况下添加新功能。当我们想给一个特定的对象而不是整个类添加特殊的功能时,这个概念非常有用。在这个模式中,我们尝试使用对象组合的概念来代替继承。所以,当我们掌握了这种技术,我们就可以在不影响底层类的情况下向对象添加新的职责。

真实世界的例子

假设你已经拥有一栋房子。现在你决定在它的上面再建一层楼。您可能不希望更改底层(或现有楼层)的体系结构,但可能希望在不影响现有体系结构的情况下更改新添加楼层的体系结构设计。

图 7-1 ,图 7-2 ,图 7-3 说明了这个概念。

img/395506_2_En_7_Fig3_HTML.jpg

图 7-3

从现有的装饰者中创建一个额外的装饰者(并粉刷房子)

img/395506_2_En_7_Fig2_HTML.jpg

图 7-2

有装修工的原始房屋(新结构建立在原始结构之上)

img/395506_2_En_7_Fig1_HTML.jpg

图 7-1

原始房屋

注意

情况 3 是可选的。您可以使用已经修饰过的对象来以这种方式增强行为,或者您可以创建一个新的 decorator 对象并将所有新的行为放入其中。

计算机世界的例子

假设在一个基于 GUI 的工具包中,我们想要添加一些边框属性。我们可以通过继承来做到这一点。但它不能被视为最终的解决方案,因为用户不能从一开始就对这种创造拥有绝对的控制权。所以在这种情况下核心选择是静态的。

装饰者以灵活的方式进入画面。他们提倡动态选择的概念,例如,我们可以将组件包围在另一个对象中。封闭对象被称为装饰器。它必须符合它所修饰的组件的接口。它将请求转发给组件。它可以在转发之前或之后执行附加操作。这个概念可以增加无限的责任。

注意

您可以注意到装饰模式在两者的 I/O 流实现中的使用。NET 框架和 Java。例如,java.io.BufferedOutputStream 类可以修饰任何 java.io.OutputStream 对象。

说明

浏览下面的例子。这里我们从未试图修改核心的makeHouse()方法。我们创建了两个额外的装饰器:ConcreteDecoratorEx1 和 ConcreteDecoratorEx2 来满足我们的需求,但是我们保持了原来的结构不变。

类图

图 7-4 显示了装饰器模式的类图。

img/395506_2_En_7_Fig4_HTML.jpg

图 7-4

类图

包资源管理器视图

图 7-5 显示了程序的高层结构。

img/395506_2_En_7_Fig5_HTML.jpg

图 7-5

包资源管理器视图

履行

下面是实现。

package jdp2e.decorator.demo;
abstract class Component
{
    public abstract void makeHouse();

}
class ConcreteComponent extends Component
{
    public void makeHouse()
    {
        System.out.println("Original House is complete. It is closed for modification.");
    }
}

abstract class AbstractDecorator extends Component
{
    protected Component component ;
    public void setTheComponent(Component c)
    {
        component = c;
    }
    public void makeHouse()
    {
        if (component != null)
        {
            component.makeHouse();//Delegating the task

        }
    }
}
//A floor decorator
class FloorDecorator extends AbstractDecorator
{
    public  void makeHouse()
    {
        super.makeHouse();
        //Decorating now.
        System.out.println("***Floor decorator is in action***");
        addFloor();
        /*You can put additional stuffs as per your need*/
    }
    private void addFloor()
    {
        System.out.println("I am making an additional floor on top of it.");
    }
}
//A paint decorator
class PaintDecorator extends AbstractDecorator
{
    public void makeHouse()
    {
        super.makeHouse();
        //Decorating now.
        System.out.println("***Paint decorator is in action now***");
        paintTheHouse();
        //You can add additional stuffs as per your need

    }
    private void paintTheHouse()
    {
        System.out.println("Now I am painting the house.");
    }
}

public class DecoratorPatternExample {

    public static void main(String[] args) {
        System.out.println("***Decorator pattern Demo***\n");
        ConcreteComponent withoutDecorator = new ConcreteComponent();
        withoutDecorator.makeHouse();
        System.out.println("_________________");

        //Using a decorator to add floor
        System.out.println("Using a Floor decorator now.");
        FloorDecorator floorDecorator = new FloorDecorator();
        floorDecorator.setTheComponent(withoutDecorator);
        floorDecorator.makeHouse();
        System.out.println("_________________");

        //Using a decorator to add floor to original house and then
        //paint it.
        System.out.println("Using a Paint decorator now.");
        PaintDecorator paintDecorator = new PaintDecorator();
        //Adding results from floor decorator
        paintDecorator.setTheComponent(floorDecorator);
        paintDecorator.makeHouse();
        System.out.println("_________________");
    }
}

输出

这是输出。

***Decorator pattern Demo***

Original House is complete. It is closed for modification.
_________________
Using a Floor decorator now.
Original House is complete. It is closed for modification.
***Floor decorator is in action***
I am making an additional floor on top of it.
_________________
Using a Paint decorator now.
Original House is complete. It is closed for modification.
***Floor decorator is in action***
I am making an additional floor on top of it.
***Paint decorator is in action now***
Now I am painting the house.
_________________

问答环节

img/395506_2_En_7_Fig6_HTML.jpg

图 7-6

等级继承

  1. 你能解释一下组合是如何促进一种动态行为,而继承却不能吗?

    我们知道,当派生类从父类继承时,它只继承基类当时的行为。尽管不同的子类可以以不同的方式扩展基类/父类,但这种类型的绑定在编译时是已知的,所以选择本质上是静态的。但是您在示例中使用组合概念的方式允许您尝试动态行为。

    当我们设计一个父类时,我们可能对客户在后期阶段可能想要什么样的额外责任没有足够的了解。我们的约束是我们不应该频繁地修改现有的代码。在这种情况下,对象组合不仅远远超过了继承,还确保了我们不会将错误引入到现有的架构中。

    最后,在这种情况下,你必须记住一个关键的设计原则:类应该对扩展开放,但对修改关闭。

  2. 使用装潢师的关键 优势 是什么?

    • 现有的结构没有被改动,所以您不会在那里引入错误。

    • 新的功能可以很容易地添加到现有的对象。

    • 您不需要在初始设计阶段预测/实现所有支持的功能。您可以增量开发(例如,一个接一个地添加装饰对象来支持增量需求)。你必须承认这样一个事实,如果你首先创建一个复杂的类,然后你试图扩展它的功能,这将是一个乏味的过程。

  3. 整体设计模式与继承有何不同?

    您可以通过简单地附加或分离 decorators 来添加或删除职责。但是对于简单的继承机制,您需要为新的职责创建一个新的类。所以,有可能你最终会得到一个复杂的系统。

    再次考虑这个例子。假设你想加一层新地板,粉刷房子,做一些额外的工作。为了满足这一需求,您从 decorator2 开始,因为它已经提供了向现有架构添加地板的支持,然后您可以对其进行绘制。因此,您可以添加一个简单的包装器来完成这些额外的职责。

    但是如果你从继承开始,那么你可能有多个子类(例如,一个用于添加地板,一个用于粉刷房子)。图 7-6 显示了层次继承。

如果你需要一个具有额外功能的额外涂漆地板,你可能最终会得到如图 7-7 所示的设计。

img/395506_2_En_7_Fig7_HTML.jpg

图 7-7

一个类(额外特性)需要从多个基类继承

现在你感受到了钻石效应的热度,因为在包括 Java 在内的许多编程语言中,多个父类是不允许的。

在这种情况下,即使您考虑多级继承,您会发现总体上继承机制比装饰模式更具挑战性和耗时,并且它可能会在您的应用程序中产生重复的代码。最后,您必须记住,继承机制只提倡编译时绑定(而不是动态绑定)。

  1. 为什么多级 传承 不能在以前的背景下得分更高?

    让我们假设油漆类是从附加层派生出来的,而附加层又是从核心架构派生出来的。现在,如果您的客户想在不增加地板的情况下粉刷房子,decorator 模式肯定优于继承机制,因为您可以简单地在现有的只支持粉刷的系统中添加一个 decorator。

  2. 为什么要创建一个只有单一职责的类?你可以创建一个子类,简单地添加一个地板,然后进行绘画。在这种情况下,你会得到更少的子类。这种理解正确吗?

    如果你熟悉扎实的原理,你就知道有一个原理叫单责。这一原则背后的思想是,每个类都应该对软件中的一部分功能负责。当您使用单一责任原则时,装饰模式非常有效,因为您可以简单地动态添加/删除责任。

  3. 与此格局相关的 劣势 有哪些?

    我相信,如果你足够小心的话,并没有显著的劣势。但是你必须意识到这样一个事实,如果你在系统中创建了太多的装饰器,那么维护和调试将会很困难。因此,在这种情况下,它会造成不必要的混乱。

  4. 在示例中,AbstractDecorator 类中没有抽象方法。这怎么可能?

    在 Java 中,你可以拥有一个没有任何抽象方法的抽象类,但反之则不然;也就是说,如果一个类包含至少一个抽象方法,那么这个类本身就是不完整的,你必须用 abstract 关键字来标记它。

    Let’s revisit the AbstractDecorator class in the comment shown in bold.

    abstract class AbstractDecorator extends Component
    {
        protected Component component ;
        public void setTheComponent(Component c)
        {
            component = c;
        }
        public void makeHouse()
        {
          if (component != null)
            {
             component.makeHouse();//Delegating the task
            }
        }
    }
    
    

    您可以看到,我将任务委托给了一个具体的 decorator,因为我只想使用和实例化具体的 decorator。

    同样,在这个例子中,您不能简单地实例化一个AbstractDecorator实例,因为它用 abstract 关键字标记。

    The following line creates the Cannot instantiate the type AbstractDecorator compilation error.

    AbstractDecorator abstractDecorator = new AbstractDecorator();
    
    

img/395506_2_En_7_Figa_HTML.jpg

  1. In your example, instead of using concrete decorators, you could use the concept of polymorphism in the following way to generate the same output.

    System.out.println("Using a Floor decorator now."); //FloorDecorator floorDecorator = new FloorDecorator();
    
    AbstractDecorator floorDecorator = new FloorDecorator();
    
    floorDecorator.setTheComponent(withoutDecorator);
    floorDecorator.makeHouse();
    
    //Using a decorator to add floor to original house and then paint //it.
    System.out.println("Using a Paint decorator now.");
    //PaintDecorator paintDecorator = new PaintDecorator();
    
    AbstractDecorator paintDecorator = new PaintDecorator();
    
    //Adding results from decorator1
    paintDecorator.setTheComponent(floorDecorator);
    paintDecorator.makeHouse();
    
    System.out.println("_________________");
    
    

    这是正确的吗?

    是的。

  2. 只对动态绑定强制使用 decorators 吗?

    不可以。静态和动态绑定都可以使用。但是动态绑定是它的强项,所以我集中在这上面。您可能会注意到,GoF 定义也只关注动态绑定。

  3. 你使用装饰者来包装你的核心架构。这是正确的吗?

    是的。装饰器是扩展应用程序核心功能的包装器代码。但是当你使用它们的时候,核心架构是不变的。

八、适配器模式

本章介绍适配器模式。

GoF 定义

将类的接口转换成客户端期望的另一个接口。适配器允许类一起工作,否则由于不兼容的接口而无法工作。

概念

下面的例子最好地描述了核心概念。

真实世界的例子

这种模式在国际旅行中的电源插座适配器/交流电源适配器中非常常见。当接受美国电源的电子设备(比如笔记本电脑)可以插入欧洲电源插座时,这些适配器充当中间人。考虑另一个例子。假设你需要给手机充电,但是你看到总机和你的充电器不兼容。在这种情况下,您可能需要使用适配器。或者,在现实生活中,为某人翻译语言的译者可以被认为遵循了这种模式。

现在,您可以想象这样一种情况,您需要将一个应用程序插入一个适配器(在本例中是 X 形的)来使用预期的接口。如果不使用这个适配器,您就不能正确地连接应用程序和接口。

图 8-1 显示了使用适配器之前的情况。

img/395506_2_En_8_Fig1_HTML.jpg

图 8-1

在使用适配器之前

图 8-2 显示了使用适配器后的情况。

img/395506_2_En_8_Fig2_HTML.jpg

图 8-2

使用适配器后

计算机世界的例子

假设您有一个应用程序,可以大致分为两部分:用户界面(UI 或前端)和数据库(后端)。通过用户界面,客户端可以传递特定类型的数据或对象。您的数据库与那些对象兼容,可以顺利地存储它们。经过一段时间,你可能会觉得你需要升级你的软件来让你的客户满意。因此,您可能希望允许新类型的对象通过 UI。但是在这种情况下,第一个阻力来自你的数据库,因为它不能存储这些新类型的对象。在这种情况下,您可以使用一个适配器,负责将新对象转换为旧数据库可以接受的兼容形式。

注意

在 Java 中,可以将 java.io.InputStreamReader 类和 java.io.OutputStreamWriter 类视为对象适配器的示例。它们将现有的 InputStream/OutputStream 对象应用于读取器/写入器接口。您将很快了解到类适配器和对象适配器。

说明

下面的例子描述了这种模式的一个简单用法。

在这个例子中,你可以很容易地计算出一个矩形的面积。如果您注意到 Calculator 类和它的getArea()方法,您就会明白您需要在getArea()方法中提供一个 rectangle 对象来计算矩形的面积。现在假设你想计算一个三角形的面积,但是你的约束是你想通过计算器类的getArea()方法得到它的面积。那么,如何才能实现这一点呢?

为了处理这类问题,我为 Triangle 类制作了 CalculatorAdapter,并在它的getArea()方法中传递了一个三角形。反过来,该方法将三角形视为矩形,并从 Calculator 类的getArea()方法计算面积。

类图

图 8-3 显示了类图。

img/395506_2_En_8_Fig3_HTML.jpg

图 8-3

类图

包资源管理器视图

图 8-4 显示了程序的高层结构。

img/395506_2_En_8_Fig4_HTML.jpg

图 8-4

包资源管理器视图

履行

package jdp2e.adapter.demo;

class Rectangle
{
    public double length;
    public double width;
}
class Calculator
{
    public double getArea(Rectangle rect)
    {
        return rect.length * rect.width;
    }
}
class Triangle
{
    public double base;//base
    public double height;//height
    public Triangle(int b, int h)
    {
        this.base = b;
        this.height = h;
    }
}
class CalculatorAdapter
{
    public double getArea(Triangle triangle)
    {
        Calculator c = new Calculator();
        Rectangle rect = new Rectangle();
        //Area of Triangle=0.5*base*height
        rect.length = triangle.base;
        rect.width = 0.5 * triangle.height;
        return c.getArea(rect);
    }
}

class AdapterPatternExample {
    public static void main(String[] args) {
        System.out.println("***Adapter Pattern Demo***\n");
        CalculatorAdapter calculatorAdapter = new CalculatorAdapter();
        Triangle t = new Triangle(20,10);
        System.out.println("Area of Triangle is " + calculatorAdapter.getArea(t) + " Square unit");
    }
}

输出

这是输出。

***Adapter Pattern Demo***

Area of Triangle is 100.0 Square unit

修改后的插图

您已经看到了适配器设计模式的一个非常简单的例子。但是,如果你想严格遵循面向对象的设计原则,你可能想修改实现,因为你已经知道,而不是使用具体的类,你应该总是更喜欢使用接口。因此,记住这个关键原则,让我们修改实现。

修改的类图

img/395506_2_En_8_Figa_HTML.jpg

修改后的实现的主要特征

以下是修改后的实现的主要特征。

  • rectangle 类实现 RectInterface,c alculateAreaOfRectangle()方法帮助计算 Rectangle 对象的面积。

  • 三角形类实现了 TriInterface,c alculateAreaOfTriangle()方法帮助计算三角形对象的面积。

  • 但是约束是您需要使用 RectInterface 计算三角形的面积(或者,您可以简单地说您现有的系统需要修改三角形对象)。为了达到这个目的,我引入了一个adapter(TriangleAdapter),它与 RectInterface 接口交互。

  • 矩形和三角形代码都不需要改变。您只是简单地使用适配器,因为它实现了 RectInterface 接口,并且使用 RectInterface 方法,您可以很容易地计算三角形的面积。这是因为我正在重写接口方法,以委托给我所适应的类(三角形)的相应方法。

  • 注意getArea(RectInterface)方法并不知道通过 TriangleAdapter,它实际上是在处理一个三角形对象而不是矩形对象。

  • 注意另一个重要的事实和用法。假设在一个特定的情况下,您需要处理一些面积为 200 平方单位的矩形对象,但是您没有足够数量的这样的对象。但是你注意到你有面积为 100 平方单位的三角形物体。所以,使用这种模式,你可以调整一些三角形的物体。怎么做?好吧,如果你仔细观察,你会发现在使用适配器的calculateAreaOfRectangle()方法时,你实际上是在调用一个三角形对象的calculateAreaOfTriangle()(即你是在委托你所适配的类的相应方法)。因此,您可以根据需要修改(覆盖)方法体(例如,在这种情况下,您可以将三角形面积乘以 2.0,得到 200 个平方单位的面积(就像一个长 20 个单位、宽 10 个单位的矩形对象)。

在您可能需要处理不完全相同但非常相似的对象的情况下,这种技术可以帮助您。在客户端代码的最后一部分,我展示了这样一种用法,应用程序使用增强的for循环(在 Java 5.0 中引入)显示系统中的当前对象。

注意

在最后一点的背景下,你必须同意,你不应该试图将一个圆转换成一个矩形(或类似类型的转换)来获得一个面积,因为它们是完全不同的。但在这个例子中,我谈论的是三角形和矩形,因为它们有一些相似之处,并且面积可以通过微小的变化很容易地计算出来。

已修改的包资源管理器视图

图 8-5 显示了修改后程序的结构。

img/395506_2_En_8_Fig5_HTML.jpg

图 8-5

已修改的包资源管理器视图

修改的实现

这是修改后的实现。

package jdp2e.adapter.modified.demo;

import java.util.ArrayList;
import java.util.List;

interface RectInterface
{
    void aboutRectangle();
    double calculateAreaOfRectangle();
}
class Rectangle implements RectInterface
{
    public double length;
    public double width;
    public Rectangle(double length, double width)
    {
        this.length = length;
        this.width = width;
    }

    @Override
    public void aboutRectangle()
    {
        System.out.println("Rectangle object with length: "+ this.length +" unit and width :" +this.width+ " unit.");

    }

    @Override
    public double calculateAreaOfRectangle()
    {
        return length * width;
    }
}

interface TriInterface
{
    void aboutTriangle();
    double calculateAreaOfTriangle();
}
class Triangle implements TriInterface
{
    public double base;//base
    public double height;//height
    public Triangle(double base, double height)
    {
        this.base = base;
        this.height = height;
    }

    @Override
    public void aboutTriangle() {
        System.out.println("Triangle object with base: "+ this.base +" unit and height :" +this.height+ " unit.");

    }

    @Override
    public double calculateAreaOfTriangle() {
        return 0.5 * base * height;
    }
}

/*TriangleAdapter is implementing RectInterface.
 So, it needs to implement all the methods defined
in the target interface.*/
class TriangleAdapter implements RectInterface
{
    Triangle triangle;
    public TriangleAdapter(Triangle t)
    {
        this.triangle = t;
    }
    @Override
    public void aboutRectangle() {
        triangle.aboutTriangle();

    }
    @Override
    public double calculateAreaOfRectangle() {
        return triangle.calculateAreaOfTriangle();
    }

}

class ModifiedAdapterPatternExample {
    public static void main(String[] args) {
        System.out.println("***Adapter Pattern Modified Demo***\n");
        Rectangle rectangle = new Rectangle(20, 10);
        System.out.println("Area of Rectangle is :  "+ rectangle.calculateAreaOfRectangle()+" Square unit.");
        Triangle triangle = new Triangle(10,5);
        System.out.println("Area of Triangle is : "+triangle.calculateAreaOfTriangle()+ " Square unit.");
        RectInterface adapter = new TriangleAdapter(triangle);
        //Passing a Triangle instead of a Rectangle
        System.out.println("Area of Triangle using the triangle adapter is : "+getArea(adapter)+" Square unit.");

        //Some Additional code (Optional) to show the power of adapter
        //pattern
        List<RectInterface> rectangleObjects=new ArrayList<RectInterface>();
        rectangleObjects.add(rectangle);
        //rectangleObjects.add(triangle);//Error
        rectangleObjects.add(adapter);//Ok
        System.out.println("");
        System.out.println("*****Current objects in the system are:******");
        for(RectInterface rectObjects:rectangleObjects)
        {
            rectObjects.aboutRectangle();
        }
    }

    /*getArea(RectInterface r) method  does not know that through TriangleAdapter, it is getting a Triangle  object instead of a Rectangle object*/
    static double getArea(RectInterface r)
    {
        return r.calculateAreaOfRectangle();
    }
}

修改输出

这是修改后的输出。

***Adapter Pattern Modified Demo***

Area of Rectangle is :  200.0 Square unit.
Area of Triangle is : 25.0 Square unit.
Area of Triangle using the triangle adapter is : 25.0 Square unit.

*****Current objects in the system are:******
Rectangle object with length: 20.0 unit and width :10.0 unit.
Triangle object with base: 10.0 unit and height :5.0 unit.

适配器的类型

GoF 解释了两种适配器:类适配器和对象适配器。

对象适配器

对象适配器通过对象组合进行适配,如图 8-6 所示。到目前为止讨论的适配器是对象适配器的一个例子。

img/395506_2_En_8_Fig6_HTML.jpg

图 8-6

典型的对象适配器

在我们的示例中,TriangleAdapter 是实现 RectInterface(目标接口)的适配器。三角形是适配器接口。适配器保存被适配器实例。

注意

因此,如果您遵循 TriangleAdapter 类的主体,您可以得出结论,要创建对象适配器,您需要遵循以下一般准则:

(1)您的类需要实现目标接口( 适配 接口)。如果目标是一个抽象类,你需要扩展它。

(2)在构造函数中提到你从 改编的 类,并在实例变量中存储对它的引用。

(3)重写接口方法,以委托您正在改编的类的相应方法。

类别适配器

类适配器通过子类化来适应。他们是多重继承的推动者。但是你知道在 Java 中,不支持通过类的多重继承。(你需要接口来实现多重继承的概念。)

图 8-7 显示了支持多重继承的类适配器的典型类图。

img/395506_2_En_8_Fig7_HTML.jpg

图 8-7

典型的类适配器

问答环节

  1. 如何用 Java 实现类适配器设计模式?

    You can subclass an existing class and implement the desired interface. For example, if you want to use a class adapter instead of an object adapter in the modified implementation, then you can use the following code.

    class TriangleClassAdapter extends Triangle implements RectInterface
    {
        public TriangleClassAdapter(double base, double height) {
            super(base, height);
        }
    
        @Override
        public void aboutRectangle()
        {
            aboutTriangle();
        }
        @Override
        public double calculateAreaOfRectangle()
        {
            return calculateAreaOfTriangle();
        }
    
    }
    
    

    但是请注意,您不能总是应用这种方法。例如,考虑一下 Triangle 类何时是最终类(因此,您不能从它派生)。除了这种情况,当您注意到您需要修改一个没有在接口中指定的方法时,您将再次被阻止。因此,在这种情况下,对象适配器是有用的。

  2. “除了这种情况,当你注意到你需要修改一个没有在接口中指定的方法时,你将再次被阻止。”你这么说是什么意思?

    In the modified implementation, you have used the aboutRectangle() and aboutTriangle() methods .These methods are actually telling about the objects of the Rectangle and Triangle classes. Now, say, instead of aboutTriangle() , there is a method called aboutMe(), which is doing the same but there is no such method in the RectInterface interface. Then it will be a challenging task for you to adapt the aboutMe() method from the Triangle class and write code similar to this:

    for(RectInterface rectObjects:rectangleObjects)
    {
        rectObjects.aboutMe();
    }
    
    
  3. 你更喜欢哪一个——类适配器还是对象适配器?

    在大多数情况下,我更喜欢组合而不是继承。对象适配器使用组合,更加灵活。此外,在许多情况下,您可能没有实现真正的类适配器。(在这种情况下,您可能会再次浏览前面问题的答案。)

  4. 这种模式有什么缺点?

    我看不出有什么大的挑战。我相信您可以使适配器的工作简单明了,但是您可能需要编写一些额外的代码。但是回报是巨大的——特别是对于那些不能被改变但是你仍然需要使用它们来保持稳定性的遗留系统。

    同时,专家建议您不要使用不同类型的验证或向适配器添加新的行为。理想情况下,adaptar 的工作应该仅限于执行简单的接口翻译。

九、外观模式

本章涵盖了外观模式。

GoF 定义

为子系统中的一组接口提供统一的接口。Facade 定义了一个更高级的接口,使得子系统更容易使用。

概念

门面使客户的生活更容易。假设有一个复杂的系统,其中多个对象需要执行一系列任务,你需要与系统进行交互。在这种情况下,facade 可以为您提供一个简化的接口,处理所有事情(创建那些对象,提供正确的任务序列,等等)。).结果,你不是以复杂的方式与多个对象交互,而是与单个对象交互。

这是支持松耦合的模式之一。在这里,您强调抽象,并通过公开一个简单的接口来隐藏复杂的细节。因此,代码变得更清晰,更有吸引力。

真实世界的例子

假设你要组织一个生日聚会,你打算邀请 500 人。现在,你可以去任何一个聚会组织者那里,让他们知道关键信息——聚会类型、日期和时间、参加人数等等。组织者会为您完成剩下的工作。你不需要考虑大厅将如何装饰,与会者是否会从自助餐桌上获得食物或由餐饮服务商提供服务,等等。所以,你不需要从商店里买东西或者自己装饰派对大厅——你只需要付钱给组织者,让他们把工作做好。

计算机世界的例子

考虑一种情况,您使用库中的方法(在编程语言的上下文中)。您不关心该方法在库中是如何实现的。您只需调用方法来试验它的简单用法。

注意

您可以有效地使用外观设计模式的概念,使您的 JDBC 应用程序更具吸引力。您可以将 java.net.URL 类视为门面模式实现的一个示例。考虑这个类中的简写 openStream()或 getContent()方法。openStream()方法返回 openConnection()。getInputStream()和 getContent()方法返回 openConnection.getContent() 在 URLConnection 类中进一步定义了 getInputStream()和 getContent()方法。

说明

在下面的实现中,您创建了一些机器人,然后销毁了这些对象。(在本例中,单词“destroy”没有用在垃圾收集的上下文中)。在这里,您可以通过调用 RobotFacade 类的constructMilanoRobot()和destroyMilanoRobot()这样的简单方法来构造或破坏特定类型的机器人。

从客户的角度来看,他/她只需要与 FacadePatternExample.java 进行交互。RobotFacade 全权负责创造或摧毁特定种类的机器人。这个 facade 与每个子系统(RobotHands、RobotBody、RobotColor)进行对话,以完成客户端的请求。RobotBody 类包括两个简单的静态方法,在创建或销毁机器人之前提供指令。

因此,在这个实现中,客户端不需要担心单独的类的创建和方法的调用顺序。

类图

图 9-1 显示了类图。

img/395506_2_En_9_Fig1_HTML.jpg

图 9-1

类图

包资源管理器视图

图 9-2 显示了程序的高层结构。

img/395506_2_En_9_Fig2_HTML.jpg

图 9-2

包资源管理器视图

履行

下面是实现。

// RobotBody.java

package jdp2e.facade.demo;

public class RobotBody
{
    //Instruction manual -how to create a robot
    public static void createRobot()
    {
        System.out.println(" Refer the manual before creation of a robot.");
    }
    //Method to create hands of a robot
    public void createHands()
    {
        System.out.println(" Hands manufactured.");
    }
    //Method to create remaining parts (other than hands) of a robot
    public void createRemainingParts()
    {
        System.out.println(" Remaining parts (other than hands) are created.");
    }
    //Instruction manual -how to destroy a robot
    public static void destroyRobot()
    {
        System.out.println(" Refer the manual before destroying of a robot.");
    }
    //Method to destroy hands of a robot
    public void destroyHands()
    {
        System.out.println(" The robot's hands are destroyed.");
    }
    //Method to destroy remaining parts (other than hands) of a robot
    public void destroyRemainingParts()
    {
        System.out.println(" The robot's remaining parts are destroyed.");
    }

}
//RobotColor.java

package jdp2e.facade.demo;

public class RobotColor

{
    public void setDefaultColor()
    {
        System.out.println(" This is steel color robot.");
    }
    public void setGreenColor()
    {
        System.out.println(" This is a green color robot.");
    }
}

// RobotHands.java

package jdp2e.facade.demo;

public class RobotHands
{
    public void setMilanoHands()
    {
        System.out.println(" The robot will have EH1 Milano hands.");
    }
    public void setRobonautHands()
    {
        System.out.println(" The robot will have Robonaut hands.");
    }
    public void resetMilanoHands()
    {
        System.out.println(" EH1 Milano hands are about to be destroyed.");
    }
    public void resetRobonautHands()
    {
        System.out.println(" Robonaut hands are about to be destroyed.");
    }
}

// RobotFacade.java
package jdp2e.facade.demo;

public class RobotFacade
{
    RobotColor rColor;
    RobotHands rHands ;
    RobotBody rBody;
    public RobotFacade()
    {
        rColor = new RobotColor();
        rHands = new RobotHands();
        rBody = new RobotBody();

    }
    //Constructing a Milano Robot
    public void constructMilanoRobot()

    {
        RobotBody.createRobot();
        System.out.println("Creation of a Milano Robot Start.");
        rColor.setDefaultColor();
        rHands.setMilanoHands();
        rBody.createHands();
        rBody.createRemainingParts();
        System.out.println(" Milano Robot Creation End.");
        System.out.println();
    }
    //Constructing a Robonaut Robot
    public void constructRobonautRobot()
    {
        RobotBody.createRobot();
        System.out.println("Initiating the creational process of a Robonaut Robot.");
        rColor.setGreenColor();
        rHands.setRobonautHands();
        rBody.createHands();
        rBody.createRemainingParts();
        System.out.println("A Robonaut Robot is created.");
        System.out.println();
    }
    //Destroying a Milano Robot
    public void destroyMilanoRobot()
    {
        RobotBody.destroyRobot();
        System.out.println(" Milano Robot's destruction process is started.");
        rHands.resetMilanoHands();
        rBody.destroyHands();
        rBody.destroyRemainingParts();
        System.out.println(" Milano Robot's destruction process is over.");
        System.out.println();
    }
    //Destroying a Robonaut Robot
    public void destroyRobonautRobot()
    {
        RobotBody.destroyRobot();
        System.out.println(" Initiating a Robonaut Robot's destruction process.");
        rHands.resetRobonautHands();
        rBody.destroyHands();
        rBody.destroyRemainingParts();
        System.out.println(" A Robonaut Robot is destroyed.");
        System.out.println();
    }

}
//Client code
//FacadePatternExample.java

package jdp2e.facade.demo;

public class FacadePatternExample {
    public static void main(String[] args) {
        System.out.println("***Facade Pattern Demo***\n");
        //Creating Robots
        RobotFacade milanoRobotFacade = new RobotFacade();
        milanoRobotFacade.constructMilanoRobot();
        RobotFacade robonautRobotFacade = new RobotFacade();
        robonautRobotFacade.constructRobonautRobot();
        //Destroying robots
        milanoRobotFacade.destroyMilanoRobot();
        robonautRobotFacade.destroyRobonautRobot();

    }

}

输出

这是输出。

***Facade Pattern Demo***

 Refer the manual before creation of a robot.
Creation of a Milano Robot Start.
 This is steel color robot.
 The robot will have EH1 Milano hands.
 Hands manufactured.
 Remaining parts (other than hands) are created.
 Milano Robot Creation End.

 Refer the manual before creation of a robot.
Initiating the creational process of a Robonaut Robot.
 This is a green color robot.
 The robot will have Robonaut hands.
 Hands manufactured.
 Remaining parts (other than hands) are created.
A Robonaut Robot is created.

 Refer the manual before destroying of a robot.
 Milano Robot's destruction process is started.
 EH1 Milano hands are about to be destroyed.
 The robot's hands are destroyed.
 The robot's remaining parts are destroyed.
 Milano Robot's destruction process is over.

 Refer the manual before destroying of a robot.
 Initiating a Robonaut Robot's destruction process.
 Robonaut hands are about to be destroyed.
 The robot's hands are destroyed.
 The robot's remaining parts are destroyed.
 A Robonaut Robot is destroyed

.

问答环节

  1. 使用外观模式的主要优势是什么?

    • 如果一个系统由许多子系统组成,管理所有这些子系统就变得非常困难,客户可能会发现很难与这些子系统中的每一个单独通信。在这种情况下,外观模式非常方便。它为客户提供了一个简单的界面。简而言之,您向客户呈现了一个简化的界面,而不是呈现复杂的子系统。这种方法还通过将客户端与子系统分离来促进弱耦合。

    • 它还可以帮助您减少客户端需要处理的对象数量。

  2. 我看到 facade 类正在使用组合。这是故意的吗?

    是的。使用这种方法,您可以很容易地访问每个子系统中的方法。

  3. 在我看来,外观并没有限制我们直接与子系统连接。这种理解正确吗?

    是的。外观不封装子系统类或接口。它只是提供了一个简单的接口(或层)让你的生活更轻松。您可以自由地公开子系统的任何功能,但是在这些情况下,您的代码可能看起来很脏,同时,您会失去与该模式相关的所有好处。

  4. 它与适配器设计模式有何不同?

    在适配器模式中,您试图改变一个接口,以便客户机感觉不到接口之间的差异。facade 模式简化了界面。它们为客户端提供了一个简单的交互界面(而不是复杂的子系统)。

  5. 一个复杂的子系统应该只有一个外观。这是正确的吗?

    一点也不。您可以为特定子系统创建任意数量的外观。

  6. 我可以用一个门面添加更多的东西/逻辑吗?

    是的,你可以。

  7. 与门面模式相关的挑战是什么?

    • 子系统与外观层相连接。因此,您需要关注额外的编码层(即,您的代码库增加)。

    • 当子系统的内部结构发生变化时,您也需要将变化合并到外观层中。

    • 开发人员需要了解这个新层,而他们中的一些人可能已经知道如何有效地使用子系统/API。

  8. 它与 mediator 设计模式有什么不同?

    在中介模式实现中,子系统知道中介。他们互相交谈。但是在外观中,子系统不知道外观,并且从外观到子系统提供单向通信。(本书第二十一章讨论了中介模式)。

  9. 在我看来,要实现一个门面模式,我必须写很多代码。这种理解正确吗?

    一点也不。这取决于系统和相应的功能。例如,在前面的实现中,如果您只考虑一种类型的机器人(Milano 或 Robonaut),并且如果您不想提供机器人的销毁机制,并且如果您想忽略说明手册(在这个示例中是两个静态方法),您的代码大小将会显著下降。为了完整的说明,我保留了所有这些。

十、享元模式

这一章涵盖了 flyweight 模式。

GoF 定义

使用共享来有效地支持大量细粒度的对象。

概念

在他们的名著设计模式:可重用面向对象软件的元素 (Addison-Wesley,1995)中,四人组(g of)对享元做了如下描述:

flyweight 是一个可以同时在多个上下文中使用的共享对象。在每个上下文中,flyweight 充当一个独立的对象——它与未共享的对象实例没有区别。Flyweights 不能对它们运行的环境做出假设。

当你考虑享元模式时,你需要记住以下几点:

  • 当您需要大量相似的对象时,这种模式非常有用,这些对象只有几个参数是唯一的,而大部分内容是通用的。

  • 飞锤是一个物体。它试图通过与其他类似对象尽可能多地共享数据来最小化内存使用。共享对象可以允许以最小的成本在精细的粒度上使用它们。

  • 在这种情况下使用两个常用术语:外在的和内在的。一个内在状态被存储/共享在 flyweight 对象中,并且它独立于 flyweight 的上下文。另一方面,外在状态随着 flyweight 的上下文而变化,这就是为什么它们不能被共享。客户端对象维护外在状态,它们需要将这种状态传递给一个 flyweight。请注意,如果需要,客户端也可以在使用 flyweights 时动态计算外部状态。

  • 专家建议,在实现这种模式时,我们应该使固有状态不可变。

真实世界的例子

假设你有笔。可以更换不同的笔芯,用不同的颜色书写。因此,没有笔芯的笔被认为是具有内在数据的飞锤,而有笔芯的笔被认为是外在数据。

再考虑一个例子。假设一家公司需要为员工打印名片。那么,这个过程从哪里开始呢?该公司可以创建一个带有公司徽标、地址等的通用模板(内部),然后在卡片上添加每个员工的特定联系信息(外部)。

计算机世界的例子

假设您想创建一个网站,让不同的用户可以用他们喜欢的计算机语言(如 Java、C++、C# 等)编译和执行程序。如果你需要在短时间内为每个用户建立一个独特的环境,你的网站将会超负荷,服务器的响应时间将会变得非常慢,没有人会有兴趣使用你的网站。因此,您不必为每个用户创建一个新的编程环境,而是可以在他们之间创建一个公共的编程环境(支持不同的编程语言,有/没有微小的变化)。为了检查现有的/可用的编程环境,并决定是否需要创建一个新的环境,您可以维护一个工厂。

考虑另一个例子。假设在一个电脑游戏中,你有大量的参与者,他们的核心结构是相同的,但是他们的外表不同(例如,不同的状态、颜色、武器等等。)因此,假设您需要创建(或存储)所有这些具有所有这些变化/状态的对象,内存需求将是巨大的。因此,不用存储所有这些对象,您可以用这样的方式设计您的应用程序,即创建具有公共属性的这些实例(具有内在状态的 flyweights ),并且您的客户机对象维护所有这些变化(外在状态)。如果你能成功地实现这个概念,你就可以宣称你在应用程序中遵循了 flyweight 设计模式。

这种模式的另一个常见用途是在字处理器中用图形表示字符。

注意

在 Java 中,当您使用包装类(如 java.lang.Integer、java.lang.Short、java.lang.Byte 和 java.lang.Character)时,您可能会注意到这种模式的使用,其中静态方法 valueof()复制了一个工厂方法。(值得记住的是,一些包装器类,如 java.lang.Double 和 java.lang.Float,不遵循这种模式。)弦池是享元的另一个例子。

说明

在下面的例子中,我使用了三种不同类型的对象:小型、大型和固定大小的机器人。这些机器人有两种状态:“robotTypeCreated”和“color”。第一个可以在“相似”的对象之间共享,所以它是一个内在状态。第二个(颜色)由客户端提供,它随上下文而变化。因此,在这个例子中,它是一个非本征态。

对于固定尺寸的机器人,客户提供哪种颜色并不重要。对于这些机器人,我忽略了外在状态,所以你可以得出结论,这些固定大小的机器人代表了非共享享元

在这个实现中,robotFactory 类缓存这些 flyweights,并提供一个获取它们的方法。

最后,这些对象是相似的。因此,一旦一个特定的机器人被创建,你不希望从头开始重复这个过程。相反,下一次向前,你将尝试使用这些飞锤来满足你的需求。现在,仔细阅读带有注释的代码,以便随时参考。

类图

图 10-1 为类图。

img/395506_2_En_10_Fig1_HTML.jpg

图 10-1

类图

包资源管理器视图

图 10-2 显示了程序的高层结构。

img/395506_2_En_10_Fig2_HTML.jpg

图 10-2

包资源管理器视图

履行

下面是实现。

package jdp2e.flyweight.demo;

import java.util.Map;
import java.util.HashMap;
import java.util.Random;

interface Robot
{
    //Color comes from client.It is extrinsic.
    void showMe(String color);
}
//A shared flyweight implementation
class SmallRobot implements Robot
{
    /*
     * Intrinsic state.
     * It is not supplied by client.
     * So, it is independent of the flyweight’s context.
     * This can be shared across.
     * These data are often immutable.
     */
    private final String robotTypeCreated;
    public SmallRobot()
    {
           robotTypeCreated="A small robot created";
           System.out.print(robotTypeCreated);
    }
    @Override
    public void showMe(String color)
    {
        System.out.print(" with " +color + " color");
    }
}
//A shared flyweight implementation
class LargeRobot implements Robot
{
    /*
     * Intrinsic state.
     * It is not supplied by client

.
     * So, it is independent of the flyweight’s context.
     * This can be shared across.
     * These data are often immutable.
     */
    private final String robotTypeCreated;
    public LargeRobot()
    {
        robotTypeCreated="A large robot created";
        System.out.print(robotTypeCreated);
    }
    @Override
    public void showMe(String color)
    {
        System.out.print(" with " + color + " color");

    }
}
//An unshared flyweight implementation
class FixedSizeRobot implements Robot
{
    /*
     * Intrinsic state.
     * It is not supplied by client.
     * So, it is independent of the flyweight’s context.
     * This can be shared acorss.
     */
    private final String robotTypeCreated;
    public FixedSizeRobot()
    {
        robotTypeCreated="A robot with a fixed size created";
        System.out.print(robotTypeCreated);
    }
    @Override
    //Ingoring the extrinsic state argument
    //Since it is an unshared flyweight
    public void showMe(String color)
    {
        System.out.print(" with " + " blue (default) color");
    }
}

class RobotFactory
{
    static Map<String, Robot> robotFactory = new HashMap<String, Robot>();
    public int totalObjectsCreated()
    {
        return robotFactory.size();
    }

    public static synchronized Robot getRobotFromFactory(String robotType) throws Exception
    {
        Robot robotCategory = robotFactory.get(robotType);
        if(robotCategory==null)
        {
            switch (robotType)
            {
            case "small":
                System.out.println("We do not have Small Robot at present.So we are creating a small robot now.") 

;
                robotCategory = new SmallRobot();
                break;
            case "large":
                System.out.println("We do not have Large Robot at present.So we are creating a large robot now.");
                robotCategory = new LargeRobot();
                break;

            case "fixed":
                System.out.println("We do not have fixed size at present. So we are creating a fixed size robot now.");
                robotCategory = new FixedSizeRobot();
                break;
            default:
                throw new Exception(" Robot Factory can create only small ,large or fixed size robots");
            }
            robotFactory.put(robotType,robotCategory);
        }
        else
        {
            System.out.print("\n \t Using existing "+ robotType +" robot and coloring it" );
        }
        return robotCategory;
    }
}

public class FlyweightPatternExample {

    public static void main(String[] args) throws Exception {
        RobotFactory robotFactory = new RobotFactory();
        System.out.println("\n***Flyweight Pattern Example ***\n");
        Robot myRobot;
        //Here we are trying to get 3 Small type robots
        for (int i = 0; i < 3; i++)
        {
            myRobot = RobotFactory.getRobotFromFactory("small");
            /*
            Not required to add sleep().But it is included to
            increase the probability of getting a new random number
            to see the variations in the output

.
             */
            Thread.sleep(1000);
            //The extrinsic property color is supplied by the client code.
            myRobot.showMe(getRandomColor());
        }
        int numOfDistinctRobots = robotFactory.totalObjectsCreated();
        System.out.println("\n Till now, total no of distinct robot objects created: " + numOfDistinctRobots);

        //Here we are trying to get 5 Large type robots
        for (int i = 0; i < 5; i++)
        {
            myRobot = RobotFactory.getRobotFromFactory("large");
            /*
            Not required to add sleep().But it is included to
            increase the probability of getting a new random number
            to see the variations in the output.
             */
            Thread.sleep(1000);
            //The extrinsic property color is supplied by the client code.
            myRobot.showMe(getRandomColor());
        }
        numOfDistinctRobots = robotFactory.totalObjectsCreated();
        System.out.println("\n Till now, total no of distinct robot objects created: " + numOfDistinctRobots);

        //Here we are trying to get 4 fixed sizerobots
        for (int i = 0; i < 4; i++)
        {
            myRobot = RobotFactory.getRobotFromFactory("fixed");
            /*
            Not required to add sleep().But it is included to
            increase the probability of getting a new random number
            to see the variations in the output.
             */
            Thread.sleep(1000);
            //The extrinsic property color is supplied by the client code.
            myRobot.showMe(getRandomColor());
        }
        numOfDistinctRobots = robotFactory.totalObjectsCreated();
        System.out.println("\n Till now, total no of distinct robot objects created: " + numOfDistinctRobots);
    }

    static String getRandomColor()

    {
        Random r = new Random();
        /* I am simply checking the random number generated that can be either an even number or an odd number. And based on that we are choosing the color. For simplicity, I am using only two colors-red and green
         */
        int random = r.nextInt();
        if (random % 2 == 0)
        {
            return "red";
        }
        else
        {
            return "green";
        }
    }

}

输出

这是第一次运行输出。

***Flyweight Pattern Example ***

We do not have Small Robot at present.So we are creating a small robot now.
A small robot created with green color
      Using existing small robot and coloring it with green color
      Using existing small robot and coloring it with red color
 Till now, total no of distinct robot objects created: 1
We do not have Large Robot at present.So we are creating a large robot now.
A large robot created with green color
      Using existing large robot and coloring it with red color
      Using existing large robot and coloring it with green color
      Using existing large robot and coloring it with green color
      Using existing large robot and coloring it with green color
 Till now, total no of distinct robot objects created: 2
We do not have fixed size at present.So we are creating a fixed size robot now.
A robot with a fixed size created with  blue (default) color
      Using existing fixed robot and coloring it with  blue (default) color
      Using existing fixed robot and coloring it with  blue (default) color
      Using existing fixed robot and coloring it with  blue (default) color
 Till now, total no of distinct robot objects created: 3

这是第二次运行的输出。

***Flyweight Pattern Example ***

We do not have Small Robot at present.So we are creating a small robot now.
A small robot created with red color
      Using existing small robot and coloring it with green color
      Using existing small robot and coloring it with green color
 Till now, total no of distinct robot objects created: 1
We do not have Large Robot at present.So we are creating a large robot now.
A large robot created with red color
      Using existing large robot and coloring it with green color
      Using existing large robot and coloring it with green color
      Using existing large robot and coloring it with red color
      Using existing large robot and coloring it with green color
 Till now, total no of distinct robot objects created: 2
We do not have fixed size at present.So we are creating a fixed size robot now.
A robot with a fixed size created with  blue (default) color
      Using existing fixed robot and coloring it with  blue (default) color

      Using existing fixed robot and coloring it with  blue (default) color
      Using existing fixed robot and coloring it with  blue (default) color
 Till now, total no of distinct robot objects created: 3

分析

  • 输出会有变化,因为在这个实现中,我随机选择了颜色。

  • 固定大小机器人的颜色永远不会改变,因为外部状态(颜色)被忽略以表示非共享的享元。

  • 客户需要与 12 个机器人(3 个小机器人,5 个大机器人,4 个固定大小的机器人)一起玩,但是这些需求只由三个不同的模板对象(每个类别一个)来满足,并且这些是动态配置的。

问答环节

  1. 我注意到了单例模式和享元模式之间的一些相似之处。你能突出他们之间的主要区别吗?

    单例模式帮助您在系统中只维护一个必需的对象。换句话说,一旦创建了所需的对象,就不能再创建更多。您需要重用现有的对象。

    flyweight 模式通常关注大量相似(可能很重)的对象,因为它们可能会占用很大的内存块。因此,您尝试创建一个较小的模板对象集,可以动态配置它来完成重对象的创建。这些更小的可配置对象被称为享元对象。您可以在应用程序中重用它们,以显示您有许多大型对象。这种方法有助于减少大块内存的消耗。基本上,享元让一个人看起来像很多人。这就是为什么 GoF 告诉我们:flyweight 是一个共享对象,可以同时在多个上下文中使用。flyweight 在每个上下文中充当一个独立的对象——它与没有共享的对象实例没有什么区别。

    Figure 10-3 visualizes the core concepts of the flyweight pattern before using flyweights.

    img/395506_2_En_10_Fig3_HTML.jpg

    图 10-3

    在使用飞锤之前

    Figure 10-4 shows the design after using flyweights.

    img/395506_2_En_10_Fig4_HTML.jpg

    图 10-4

    使用飞锤后

    In Figure 10-4, you can see that

    • 重对象 1 = Flyweight 对象(共享)+配置 1(非固有和非共享)

    • 重对象 2 = Flyweight 对象(共享)+配置 2(非固有和非共享)

通过组合内部和外部状态,flyweight 对象提供了完整的功能。

  1. 您能观察到多线程带来的影响吗?

    如果您在多线程环境中使用新的操作符创建对象,您可能最终会得到多个不需要的对象(类似于单例模式)。补救措施类似于在单例模式中处理多线程环境的方式。

  2. 使用 flyweight 设计模式的 优势 有哪些?

    • 您可以减少可同等控制的重物的内存消耗。

    • 您可以减少系统中“完全但相似的对象”的总数。

    • 您可以提供一种集中的机制来控制许多“虚拟”对象的状态。

  3. 使用享元设计模式有哪些 挑战

    • 在这种模式中,您需要花时间来配置这些享元。配置时间会影响应用程序的整体性能。

    • 要创建 flyweights,需要从现有对象中提取一个公共模板类。这个额外的编程层可能很棘手,有时很难调试和维护。

    • 您可以看到,一个类的逻辑实例的行为不能与其他实例不同。

    • flyweight 模式通常与 singleton 工厂实现相结合,为了保护这种独特性,需要额外的成本(例如,您可以选择同步方法或双重检查锁定,但它们都是高成本的操作)。

  4. 我可以拥有不可共享的 flyweight 接口吗?

    是的。flyweight 接口不会强制要求它总是可共享的。在某些情况下,您可能有不可共享的 flyweight,具体的 flyweight 对象作为子对象。在我们的例子中,您看到了使用固定大小的机器人来使用不可共享的享元。

  5. 由于 flyweights 的内在数据是相同的,我可以分享它们。这是正确的吗?

    是的。

  6. 客户如何处理这些享元的外部数据?

    他们需要将信息(状态)传递给享元。客户端要么管理数据,要么实时计算数据。

  7. 外部数据不可共享。这是正确的吗?

    是的。

  8. 你说我应该努力使内在状态不变。我怎样才能做到这一点?

    是的,为了线程安全,专家建议您实现它。在这种情况下,它已经实现了。在 Java 中,你必须记住字符串对象本质上是不可变的。

    此外,您可能会注意到,在具体的 flyweights (SmallRobot、LargeRobot、FixedSizeRobot)中,没有 setter 方法来设置/修改 robotTypeCreated 的值。当您只通过构造函数提供数据并且没有 setter 方法时,您遵循的是一种促进不变性的方法。

  9. 您已经用固有状态 robotTypeCreated 标记了 final 关键字,以实现不变性。这是正确的吗?

    你需要记住最终不变性不是同义词。在设计模式的上下文中,单词不变性通常意味着一旦创建,就不能改变对象的状态。尽管关键字 final 可以应用于一个类、一个方法或一个字段,但目的是不同的。

    final 字段可以帮助您构造一个线程安全的不可变对象,而无需同步,并且它在多线程环境中提供了安全性。所以,我在这个例子中使用了它。这个概念在 https://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.5-110 一文中有详细描述。

  10. getRobotFromFactory()方法 在这里被同步以提供线程安全。这种理解正确吗?

    没错。在单线程环境中,这不是必需的。
    *** getRobotFromFactory()方法在这里是静态的。这是强制性的吗?

不,你也可以实现一个非静态的工厂方法。您可能经常会注意到 flyweight 模式实现的单例工厂的存在。

 *   **“机器人工厂”的** **在这个实现中的作用是什么?**

它缓存了 flyweights,并提供了获取它们的方法。在这个例子中,有许多可以共享的对象。因此,将它们存放在一个中心位置总是一个好主意。

 **

十一、组合模式

本章涵盖了组合模式。

GoF 定义

将对象组成树结构来表示部分-整体层次结构。Composite 允许客户端统一处理单个对象和对象的组合。

概念

为了帮助你理解这个概念,我将从一个例子开始。考虑一家出售不同种类的干果和坚果的商店;比如腰果、枣和核桃。这些项目中的每一个都与特定的价格相关联。让我们假设你可以购买这些单独的物品,或者你可以购买包含不同物品的“礼包”(或盒装物品)。在这种情况下,数据包的开销是其组成部分的总和。组合模式在类似的情况下很有用,在这种情况下,您以相同的方式处理单个部分和部分的组合,以便可以统一处理它们。

这种模式对于表示对象的部分-整体层次结构很有用。在面向对象编程中,组合对象是由一个或多个相似对象组成的对象,其中每个对象都具有相似的功能。(这也称为对象之间的“有-有”关系)。这种模式在树状数据结构中非常常见。如果你能恰当地应用它,你就不需要区分分支和叶节点。使用这种模式可以实现两个关键目标。

  • 您可以将对象组成一个树结构来表示部分-整体层次结构。

  • 您可以统一访问组合对象(分支)和单个对象(叶节点)。因此,您可以降低代码的复杂性,同时使您的应用程序不容易出错。

真实世界的例子

你也可以想象一个由许多部门组成的组织。一般来说,一个组织有很多员工。这些员工中的一些被组合在一起形成一个部门,这些部门可以进一步组合在一起以构建组织的最终结构。

计算机世界的例子

任何树数据结构都可以遵循这个概念。客户可以用同样的方式对待树的叶子非叶子(或树的分支)。

注意

这种模式常见于各种 UI 框架中。在 Java 中,通用抽象窗口工具包(AWT)容器对象是一个可以包含其他 AWT 组件的组件。例如,在 java.awt.Container 类(它扩展了 java.awt.Component)中,你可以看到 add(Component comp)方法的各种重载版本。在 JSF 中,UIViewRoot 类像一个组合节点,UIOutput 像一个叶节点。当你遍历一棵树时,你经常会用到迭代器设计模式,这将在第十八章中介绍。

说明

在这个例子中,我代表一个大学组织。让我们假设有一个校长和两个系主任——一个负责计算机科学与工程(CSE),一个负责数学(Maths)。数学系有两位老师(或教授/讲师),CSE 系有三位老师(或教授/讲师)。该组织的树形结构类似于图 11-1 。

img/395506_2_En_11_Fig1_HTML.jpg

图 11-1

一个样本大学组织

我们还假设在最后,CSE 部门的一个讲师退休了。您将在接下来的小节中研究所有这些情况。

类图

图 11-2 显示了类图。

img/395506_2_En_11_Fig2_HTML.jpg

图 11-2

类图

包资源管理器视图

图 11-3 显示了程序的高层结构。

img/395506_2_En_11_Fig3_HTML.jpg

图 11-3

包资源管理器视图

履行

下面是实现。

package jdp2e.composite.demo;

import java.util.ArrayList;
import java.util.List;

interface IEmployee
{
    void printStructures();
    int getEmployeeCount();
}
class CompositeEmployee implements IEmployee
{
    //private static int employeeCount=0;
    private int employeeCount=0;

    private String name;
    private String dept;
    //The container for child objects
    private List<IEmployee> controls;
    //Constructor
    public CompositeEmployee(String name, String dept) 

    {
        this.name = name;
        this.dept = dept;
        controls = new ArrayList<IEmployee>();
    }

    public void addEmployee(IEmployee e)
    {
        controls.add(e);
    }

    public void removeEmployee(IEmployee e)
    {
        controls.remove(e);
    }
    @Override
    public void printStructures()
    {
        System.out.println("\t" + this.name + " works in  " + this.dept);
        for(IEmployee e: controls)
        {
            e.printStructures();
        }
    }
    @Override
    public int getEmployeeCount()
    {
        employeeCount=controls.size();
        for(IEmployee e: controls)
        {
            employeeCount+=e.getEmployeeCount();
        }
        return employeeCount;
    }
}
class Employee implements IEmployee

{
    private String name;
    private String dept;
    private int employeeCount=0;
    //Constructor
    public Employee(String name, String dept)
    {
        this.name = name;
        this.dept = dept;
    }
    @Override
    public void printStructures()
    {
        System.out.println("\t\t"+this.name + " works in  " + this.dept);
    }
    @Override
    public int getEmployeeCount()
    {
        return employeeCount;//0
    }

}
class CompositePatternExample {

    /**Principal is on top of college.
     *HOD -Maths and Comp. Sc directly reports to him
     *Teachers of Computer Sc. directly reports to HOD-CSE
     *Teachers of Mathematics directly reports to HOD-Maths
     */
    public static void main(String[] args) {
        System.out.println("***Composite Pattern Demo ***");
        //2 teachers other than HOD works in Mathematics department
        Employee mathTeacher1 = new Employee("Math Teacher-1","Maths");
        Employee mathTeacher2 = new Employee("Math Teacher-2","Maths");

        //teachers other than HOD works in Computer Sc. Department

        Employee cseTeacher1 = new Employee("CSE Teacher-1", "Computer Sc.");
        Employee cseTeacher2 = new Employee("CSE Teacher-2", "Computer Sc.");
        Employee cseTeacher3 = new Employee("CSE Teacher-3", "Computer Sc.");

        //The College has 2 Head of Departments-One from Mathematics, One //from Computer Sc.
        CompositeEmployee hodMaths = new CompositeEmployee("Mrs.S.Das(HOD-Maths)","Maths");
        CompositeEmployee hodCompSc = new CompositeEmployee("Mr. V.Sarcar(HOD-CSE)", "Computer Sc.");

        //Principal of the college
        CompositeEmployee principal = new CompositeEmployee("Dr.S.Som(Principal)","Planning-Supervising-Managing");

        //Teachers of Mathematics directly reports to HOD-Maths
        hodMaths.addEmployee(mathTeacher1);
        hodMaths.addEmployee(mathTeacher2);

        //Teachers of Computer Sc. directly reports to HOD-CSE

        hodCompSc.addEmployee(cseTeacher1);
        hodCompSc.addEmployee(cseTeacher2);
        hodCompSc.addEmployee(cseTeacher3);

        /*Principal is on top of college.HOD -Maths and Comp. Sc directly reports to him*/
        principal.addEmployee(hodMaths);
        principal.addEmployee(hodCompSc);

        /*Printing the leaf-nodes and branches in the same way i.e.
         in each case, we are calling PrintStructures() method
         */
        System.out.println("\n Testing the structure of a Principal object");
        //Prints the complete structure
        principal.printStructures();
        System.out.println("At present Principal has control over "+ principal.getEmployeeCount()+ " number of employees.");

        System.out.println("\n Testing the structure of a HOD-CSE object:");
        //Prints the details of Computer Sc, department

        hodCompSc.printStructures();
        System.out.println("At present HOD-CSE has control over "+ hodCompSc.getEmployeeCount()+ " number of employees.");

        System.out.println("\n Testing the structure of a HOD-Maths object:");
        //Prints the details of Mathematics department
        hodMaths.printStructures();
        System.out.println("At present HOD-Maths has control over "+ hodMaths.getEmployeeCount()+ " number of employees.");

        //Leaf node
        System.out.println("\n Testing the structure of a leaf node:");
        mathTeacher1.printStructures();
        System.out.println("At present Math Teacher-1 has control over "+ mathTeacher1.getEmployeeCount()+ " number of employees.");

        /*Suppose, one computer teacher is leaving now
         from the organization*/
        hodCompSc.removeEmployee(cseTeacher2);
        System.out.println("\n After CSE Teacher-2 resigned, the organization has following members:");
        principal.printStructures();

        System.out.println("At present Principal has control over "+ principal.getEmployeeCount()+ " number of employees");
        System.out.println("At present HOD-CSE has control over "+ hodCompSc.getEmployeeCount()+ " number of employees");
        System.out.println("At present HOD-Maths has control over "+ hodMaths.getEmployeeCount()+ " number of employees");

    }

}

输出

这是输出。关键变化以粗体显示。

***Composite Pattern Demo ***

 Testing the structure of a Principal object
    Dr.S.Som(Principal) works in  Planning-Supervising-Managing
    Mrs.S.Das(HOD-Maths) works in  Maths
        Math Teacher-1 works in  Maths
        Math Teacher-2 works in  Maths
    Mr. V.Sarcar(HOD-CSE) works in  Computer Sc.
        CSE Teacher-1 works in  Computer Sc.
        CSE Teacher-2 works in  Computer Sc.
        CSE Teacher-3 works in  Computer Sc.

At present Principal has control over 7 number of employees.

 Testing the structure of a HOD-CSE object:
    Mr. V.Sarcar(HOD-CSE) works in  Computer Sc.
        CSE Teacher-1 works in  Computer Sc.
        CSE Teacher-2 works in  Computer Sc.
        CSE Teacher-3 works in  Computer Sc.

At present HOD-CSE has control over 3 number of employees.

 Testing the structure of a HOD-Maths object:
    Mrs.S.Das(HOD-Maths) works in  Maths
        Math Teacher-1 works in  Maths
        Math Teacher-2 works in  Maths
At present HOD-Maths has control over 2 number of employees.

 Testing the structure of a leaf node:
        Math Teacher-1 works in  Maths
At present Math Teacher-1 has control over 0 number of employees.

 After CSE Teacher-2 resigned, the organization has following members:
    Dr.S.Som(Principal) works in  Planning-Supervising-Managing
    Mrs.S.Das(HOD-Maths) works in  Maths
        Math Teacher-1 works in  Maths
        Math Teacher-2 works in  Maths
    Mr. V.Sarcar(HOD-CSE) works in  Computer Sc.
        CSE Teacher-1 works in  Computer Sc.
        CSE Teacher-3 works in  Computer Sc.

At present Principal has control over 6 number of employees

At present HOD-CSE has control over 2 number of employees

At present HOD-Maths has control over 2 number of employees

问答环节

  1. 使用组合设计模式的 优势 有哪些?

    • 在树状结构中,您可以统一处理组合对象(分支)和单个对象(叶节点)。注意,在这个例子中,我使用了两个常用的方法:pr intStructures()getEmployeeCount()来打印结构,并从组合对象结构(principal 或 hods)和单个对象结构(例如,像 Math Teacher 1 这样的叶节点)中获取雇员数。)

    • 使用这种设计模式实现部分-整体层次结构是非常常见的。

    • 您可以轻松地向现有架构添加新组件,或者从您的架构中删除现有组件。

  2. 与使用组合设计模式相关的 挑战 有哪些?

    • 如果您想要保持子节点的顺序(例如,如果解析树被表示为组件),您可能需要付出额外的努力。

    • 如果你正在处理不可变的对象,你不能简单地删除它们。

    • 您可以轻松地添加一个新组件,但是这种支持会导致将来的维护开销。有时,您想要处理具有特殊组件的组合对象。这种约束会导致额外的开发成本,因为您可能需要实现一个动态检查机制来支持这个概念。

  3. 在这个例子中,你使用了列表数据结构 。但是我更喜欢使用其他数据结构。这样可以吗?

    绝对的。没有放之四海而皆准的规则。您可以自由使用您喜欢的数据结构。GoF 确认没有必要使用任何通用数据结构

  4. 如何将迭代器设计模式连接到组合设计模式?

    再看一遍我们的例子。如果您想要检查组合对象架构,您可能需要迭代对象。换句话说,如果你想用分支做特殊的活动,你可能需要迭代它的叶节点和非叶节点。迭代器模式通常与组合模式一起使用。

  5. 在您的实现的接口中,您只定义了两个方法:printStructures() 和 getEmployeeCount() 。但是在组合类(CompositeEmployee)中使用其他方法添加和移除对象。为什么没有把这些方法放到接口里?

    不错的观察。GoF 对此进行了讨论。让我们看看如果在接口中放入addEmployee (…)removeEmployee (…)方法会发生什么。叶节点需要实现添加和删除操作。但是这种情况下会有意义吗?显而易见的答案是否定的。看起来你失去了透明性,但我相信你有更多的安全性,因为我已经阻止了叶节点中无意义的操作。这就是为什么 GoF 提到这种决定涉及到安全性和透明度之间的权衡。

  6. 我想用抽象类代替接口。这是允许的吗?

    In most of the cases, the simple answer is yes. But you need to understand the difference between an abstract class and an interface. In a typical scenario, you find one of them more useful than the other one. Since I am presenting only simple and easy to understand examples, you may not see much difference between the two. Particularly in this example, if I use the abstract class instead of the interface, I may put a default implementation of getEmployeeCount() in the abstract class definition. Although you can still argue that with Java’s default keyword, you could achieve the same, as in the following:

    interface IEmployee
    {
        void printStructures();
        //int getEmployeeCount();
        default public int getEmployeeCount()
        {
            return 0;
        }
    }
    
    

注意

在 builder 模式的问答环节(见第三章),我讨论了如何在抽象类和接口之间做出决定。

十二、桥接模式

本章涵盖了桥接模式。

GoF 定义

将抽象与其实现解耦,这样两者可以独立变化。

概念

这种模式也被称为句柄/主体模式,在这种模式中,您将实现从抽象中分离出来,并为它们构建独立的继承结构。最后,你通过一座桥把它们连接起来。

您必须注意,抽象和实现可以通过接口或抽象类来表示,但是抽象包含对其实现者的引用。通常,抽象的孩子被称为精炼抽象,实现的孩子被称为具体实现

这个桥接口使得具体类的功能独立于接口实现者类。您可以在结构上改变不同种类的类,而不会相互影响。

真实世界的例子

在软件产品开发公司中,开发团队和营销团队都扮演着至关重要的角色。营销团队进行市场调查并收集客户需求,这些需求可能会因客户的性质而异。开发团队在他们的产品中实现这些需求,以满足客户的需求。一个团队中的任何变化(如运营策略)都不应对另一个团队产生直接影响。此外,当新的需求来自客户端时,它不应该改变开发人员在他们的组织中工作的方式。在软件组织中,营销团队在客户和开发团队之间扮演着桥梁的角色。

计算机世界的例子

GUI 框架可以使用桥模式将抽象从特定于平台的实现中分离出来。例如,使用这种模式,它可以从 Linux 或 macOS 的窗口实现中分离出一个窗口抽象。

注意

在 Java 中,您可能会注意到 JDBC 的使用,它在您的应用程序和特定数据库之间提供了一座桥梁。例如,java.sql.DriverManager 类和 java.sql.Driver 接口可以形成一个桥接模式,其中第一个扮演抽象的角色,第二个扮演实现者的角色。具体的实现者是 com.mysql.jdbc.Driver 或者 Oracle . JDBC . driver . Oracle driver 等等。

说明

假设你是一个遥控器制造商,你需要为不同的电子产品制造遥控器。为简单起见,让我们假设您目前正在接受订单,生产电视机和 DVD 播放器的遥控器。我们还假设你的遥控器有两个主要功能:开和关。

您可能希望从图 12-1 所示的设计或图 12-2 所示的设计开始。

img/395506_2_En_12_Fig2_HTML.png

图 12-2

方法 2

img/395506_2_En_12_Fig1_HTML.png

图 12-1

方法 1

经过进一步分析,您会发现方法 1 确实很混乱,很难维护。

起初,方法 2 看起来更简洁,但是如果您想要包括新的状态,如睡眠、静音等,或者如果您想要包括新的电子项目,如 AC、DVD 等,您将面临新的挑战,因为这些元素在这种设计方法中紧密耦合。但是在真实的场景中,这种增强是经常需要的。

这就是为什么,为了将来的增强,您需要从一个松散耦合的系统开始,这样两个层次结构(电子产品及其状态)中的任何一个都可以独立增长。桥的模式完全符合这种情况。

先说最常见的桥模式类图(见图 12-3 )。

img/395506_2_En_12_Fig3_HTML.jpg

图 12-3

经典的桥梁模式

  • 抽象(一个抽象类)定义了抽象接口,它维护实现者引用。

  • RefinedAbstraction (具体类)扩展了抽象定义的接口。

  • 实现者(一个接口)定义实现类的接口。

  • 具体实现者(具体类)实现Implementor接口。

在下面的实现中,我遵循了类似的架构。为了便于您参考,我已经指出了以下实现中的所有参与者,并附上了注释。

类图

图 12-4 显示了类图。

img/395506_2_En_12_Fig4_HTML.jpg

图 12-4

类图

包资源管理器视图

图 12-5 显示了程序的高层结构。

img/395506_2_En_12_Fig5_HTML.jpg

图 12-5

包资源管理器视图

关键特征

以下是以下实现的主要特征。

  • ElectronicGoods 抽象类扮演了抽象的角色。状态接口扮演实现者的角色。

  • 具体的实现者是 OnState 类和 OffState 类。他们根据自己的需求实现了moveState()hardPressed()interface方法。

  • ElectronicGoods 抽象类持有状态实现者的引用。

  • 抽象方法将实现委托给实现者对象。例如,请注意,hardButtonPressed()实际上是state.hardPressed()的简写,其中 state 是实现者对象

  • 有两种精炼的抽象:电视和 DVD。该类对从其父类继承的方法感到满意。但是 DVD 类想要提供一个额外的特性,所以它实现了一个特定于 DVD 的方法:doublePress()。**double press()方法* 仅按照超类抽象进行编码。*

*### 履行

下面是实现。

package jdp2e.bridge.demo;

//Implementor
interface State
{
    void moveState();
    void hardPressed();
}
//A Concrete Implementor.
class OnState implements State
{
    @Override
    public void moveState()
    {
        System.out.print("On State\n");
    }

    @Override
    public void hardPressed()
    {
        System.out.print("\tThe device is already On.Do not press the button so hard.\n");

    }
}
//Another Concrete Implementor.
class OffState implements State
{
    @Override
    public void moveState()

    {
        System.out.print("Off State\n");
    }

    @Override
    public void hardPressed()
    {
        System.out.print("\tThe device is Offline now.Do not press the off button again.\n");

    }
}
//Abstraction
abstract class ElectronicGoods
{
    //Composition - implementor
    protected State state;
    /*Alternative approach:
      We can also pass an implementor (as input argument) inside a constructor.
     */
    /*public ElectronicGoods(State state)
    {
        this.state = state;
    }*/
    public State getState()
    {
        return state;
    }

    public void setState(State state)
    {
        this.state = state;
    }
    /*Implementation specific:
      We are delegating the implementation to the Implementor object

.
     */
    public void moveToCurrentState()
    {
        System.out.print("The electronic item is functioning at : ");
        state.moveState();
    }
    public void hardButtonPressed()
    {
        state.hardPressed();
    }

}
//Refined Abstraction
//Television does not want to modify any superclass method.
class Television extends ElectronicGoods
{

    /*public Television(State state)
    {
        super(state);
    }*/
}
/*DVD class also ok with the super class method.
In addition to this, it uses one additional method*/
class DVD extends ElectronicGoods
{

    /*public DVD(State state)
    {
        super(state);
    }*/
    /* Notice that following DVD specific method is coded with superclass methods but not with the implementor (State) method.So, this approach will allow to  vary the abstraction and implementation independently

.
     */
    public void doublePress() {
        hardButtonPressed();
        hardButtonPressed();
    }
}
public class BridgePatternDemo {

    public static void main(String[] args) {
        System.out.println("***Bridge Pattern Demo***");

        System.out.println("\n Dealing with a Television at present.");

        State presentState = new OnState();
        //ElectronicGoods eItem = new Television(presentState);
        ElectronicGoods eItem = new Television();
        eItem.setState(presentState);
        eItem.moveToCurrentState();
        //hard press
        eItem.hardButtonPressed();
        //Verifying Off state of the Television now
        presentState = new OffState();
        //eItem = new Television(presentState);
        eItem.setState(presentState);
        eItem.moveToCurrentState();

        System.out.println("\n Dealing with a DVD now.");
        presentState = new OnState();
        //eItem = new DVD(presentState);
        eItem = new DVD();
        eItem.setState(presentState);
        eItem.moveToCurrentState();

        presentState = new OffState();
        //eItem = new DVD(presentState);
        eItem = new DVD();
        eItem.setState(presentState);
        eItem.moveToCurrentState();

        //hard press-A DVD specific method
        //(new DVD(presentState)).doublePress();
        ((DVD)eItem).doublePress();

        /*The following line of code will cause error because a television object does not have this method.*/
        //(new Television(presentState)).doublePress();
    }
}

输出

这是输出。

***Bridge Pattern Demo***

 Dealing with a Television at present.
The electronic item is functioning at : On State
The device is already On.Do not press the button so hard.
The electronic item is functioning at : Off State

 Dealing with a DVD now.
The electronic item is functioning at : On State
The electronic item is functioning at : Off State
    The device is Offline now.Do not press the off button again.
    The device is Offline now.Do not press the off button again.

问答环节

  1. 这个模式看起来类似于一个状态模式。这是正确的吗?

    No. The state pattern falls into the behavioral pattern and its intent is different. In this chapter, you have seen an example where the electronic items can be in different states, but the key intent was to show that

    • 如何避免项目及其状态之间的紧密耦合。

    • 如何维护两个不同的层次结构,并且这两个层次结构都可以在不相互影响的情况下扩展。

除了这些要点之外,您正在处理多个对象,在这些对象中,实现在它们之间共享。

为了更好地理解,请仔细阅读该实现附带的注释。我还想让你注意 DVD 特有的doublePress()方法。注意,它是用超类方法构造的,超类方法又将实现委托给实现者对象(在本例中是一个状态对象)。这种方法允许您独立地改变抽象和实现,这是桥接模式的关键目标。

  1. 你可以用简单的子类化来代替这种设计。这是正确的吗?

    不。通过简单的子类化,你的实现不能动态变化。使用子类化技术时,实现的行为可能会有所不同,但实际上,这些变化在编译时就已经被绑定到抽象中了。

  2. 在这个例子中,我看到很多死代码。你为什么留着这些?

    与 Getter/Setter 方法相比,一些开发人员更喜欢构造函数。您可以看到不同实现中的差异。我把这些保存起来,供你随时参考。你可以自由使用其中任何一个。

  3. 使用桥设计模式的关键 优势 是什么?

    • 实现并不局限于抽象。

    • 抽象和实现都可以独立发展。

    • 具体类独立于接口实现类(即,其中一个类的变化不会影响另一个类)。您还可以用不同的方式改变接口和具体实现。

  4. 与此格局相关的 挑战 有哪些?

    • 整体结构可能变得复杂。

    • 有时它与适配器模式相混淆。(适配器模式的主要目的是只处理不兼容的接口。)

  5. 假设我只有一种状态;例如,在州内或州外。在这种情况下,我需要使用状态接口吗?

    不,这不是强制性的。GoF 将这种情况归类为桥模式的退化情况。

  6. 在这个例子中,抽象类用于表示抽象,接口用于实现。它是强制性的吗?

    不。你也可以用一个接口来抽象。基本上,您可以为任何抽象或实现使用抽象类或接口。我使用这种格式只是为了更好的可读性。*

十三、访问者模式

本章介绍访问者模式。

GoF 定义

表示要在对象结构的元素上执行的操作。Visitor 允许您定义一个新的操作,而不改变它所操作的元素的类。

概念

这种模式帮助您在对象上添加新的操作,而无需修改相应的类,尤其是当您的操作经常改变时。理想情况下,访问者定义特定于类的方法,这些方法与该类的对象一起工作以支持新功能。在这里,您将算法从对象结构中分离出来,并使用新的层次结构添加新的操作。因此,这种模式可以支持开/关原则(允许扩展,但不允许修改类、函数、模块等实体)。).接下来的实现会让你更清楚这个概念。

注意

当您将这种设计模式与组合模式相结合时,您可以体验到这种设计模式的真正威力,如本章后面修改后的实现所示。

真实世界的例子

想象一个出租车预订的场景。当税到了,你上了车,出租车司机就控制了交通。出租车可能会通过一条你不熟悉的新路线把你带到目的地。所以,你可以在出租车司机的帮助下探索新的路线。但是你应该小心使用访问者模式,否则,你可能会遇到一些问题。(例如,考虑这样一种情况,你的出租车司机不知不觉地改变了目的地,你面临着麻烦)。

计算机世界的例子

当公共 API 需要支持插入操作时,这种模式非常有用。然后,客户端可以在不修改源代码的情况下对一个类(使用访问类)执行它们想要的操作。

注意

在 Java 中,当您使用抽象类 org.dom4j.VisitorSupport 时,您可能会注意到这种模式的使用,它扩展了 Object 并实现了 org.dom4j.Visitor 接口。此外,当您使用 javax . lang . model . element . element 接口或 javax . lang . model . element . element Visitor(其中 R 是 visitor 方法的返回类型,P 是 visitor 方法的附加参数类型)时,您可能会注意到 visitor 设计模式的使用。

说明

这里我们的讨论将从访问者设计模式的一个简单例子开始。让我们假设您有一个继承层次结构,其中 MyClass 具体类实现了 OriginalInterface 接口。MyClass 有一个整数 myInt。当创建 MyClass 的实例时,它用值 5 初始化。现在假设,您想要更新这个初始化的值并显示它。你可以用两种不同的方式来做这件事:你可以在我的类中添加一个方法来完成你的工作,或者使用一个访问者模式,我将要解释这一点。

在下面的实现中,我将现有值乘以 2,并使用访问者设计模式显示 myInt 的双精度值。如果我不使用这种模式,我需要在我的类中添加一个操作(或方法),它做同样的事情。

但是后一种方法有一个问题。如果您想要进一步更新逻辑(例如,您想要三重化 myInt 并显示值),您需要修改 MyClass 中的操作。这种方法的一个缺点是,如果涉及到许多类,那么在所有类中实现这种更新的逻辑将会很乏味。

但是在访问者模式中,您可以只更新访问者的方法。这样做的好处是你不需要改变原来的类。当您的操作经常变化时,这种方法会对您有所帮助。

那么,我们先来举个例子。让我们假设在这个例子中,您想要将 MyClass 中的初始整数值加倍并操纵它,但是您的约束是您不能更改 OriginalInterface 层次结构中的原始代码。因此,在这种情况下,您使用的是访问者模式。

为了实现这个目标,在下面的例子中,我将功能实现(即算法)从原始的类层次结构中分离出来。

类图

图 13-1 为类图。

img/395506_2_En_13_Fig1_HTML.jpg

图 13-1

类图

包资源管理器视图

图 13-2 显示了程序的高层结构。

img/395506_2_En_13_Fig2_HTML.jpg

图 13-2

包资源管理器视图

履行

下面是实现。

package jdp2e.visitor.demo;
interface OriginalInterface
{
    //The following method has a Visitor argument.
    void acceptVisitor(Visitor visitor);
}
class MyClass implements OriginalInterface
{
    //Here "myInt" is final.So, once initialized, it should not be changed.
    private final int myInt;
    public MyClass()
    {
        myInt=5;//Initial or default value
    }
    public int getMyInt()
    {
        return myInt;
    }

    @Override
    public void acceptVisitor(Visitor visitor)
    {
        visitor.visit(this);
    }
}

interface Visitor
{
    //The method to visit MyClass

    void visit(MyClass myClassObject);
}
class ConcreteVisitor implements Visitor
{
    @Override
    public void visit(MyClass myClassObject)
    {
        System.out.println("Current value of myInt="+ myClassObject.getMyInt());
        System.out.println("Visitor will make it double and display it.");
        System.out.println("Current value of myInt="+ 2*myClassObject.getMyInt());
        System.out.println("Exiting from Visitor.");
    }

}

public class VisitorPatternExample {

    public static void main(String[] args) {
        System.out.println("***Visitor Pattern Demo***\n");
        Visitor visitor = new ConcreteVisitor();
        MyClass myClass = new MyClass();
        myClass.acceptVisitor(visitor);
    }
}

输出

这是输出。

***Visitor Pattern Demo***

Current value of myInt=5
Visitor will make it double and display it.
Current value of myInt=10
Exiting from Visitor.

修改后的插图

您已经看到了访问者设计模式的一个非常简单的例子。但是当你将这种设计模式与组合模式结合起来时,你就可以发挥这种设计模式的真正威力了(参见第十一章)。因此,让我们来研究一个场景,在这个场景中,您需要将组合模式和访问者模式结合起来。

修改示例的关键特征

让我们回顾一下第十一章中的复合设计模式的例子。在这个例子中,有一个学院有两个不同的系。每个系都有一个系主任和多名教师(或教授/讲师)。每个团长向学院的校长报告。图 13-3 显示了我在那一章中讨论的树形结构。

img/395506_2_En_13_Fig3_HTML.jpg

图 13-3

复合设计示例的树结构

现在假设学院的校长想提升几个员工。让我们考虑教学经验是提升某人的唯一标准。理想情况下,高级教师和初级教师的标准应该不同。所以,我们假设初级教师的最低晋升标准是 12 年,高级教师的最低晋升标准是 15 年

要做到这一点,你需要引入一个新的领域,多年的经验。因此,当访问者从大学收集必要的信息时,它会显示出符合晋升条件的候选人。

访问者从原始学院结构中收集数据,而不对其进行任何修改,一旦收集过程结束,它将分析数据以显示预期的结果。为了直观地理解这一点,您可以按照接下来的图中的箭头进行操作。负责人在组织的最高层,所以你可以假设那个人不需要升职。

第一步

图 13-4 为步骤 1。

img/395506_2_En_13_Fig4_HTML.jpg

图 13-4

第一步

第二步

图 13-5 为步骤 2。

img/395506_2_En_13_Fig5_HTML.jpg

图 13-5

第二步

第三步

图 13-6 为步骤 3。

img/395506_2_En_13_Fig6_HTML.jpg

图 13-6

第三步

第四步

图 13-7 为步骤 4。

img/395506_2_En_13_Fig7_HTML.jpg

图 13-7

第四步

第五步

图 13-8 为步骤 5。

img/395506_2_En_13_Fig8_HTML.jpg

图 13-8

第五步

诸如此类…

在下面的实现中,有如下代码块。

@Override
public void acceptVisitor(Visitor visitor)
{
    visitor.visitTheElement(this);
}

从这个结构中,可以看出两个重要的东西。

  • 每当访问者访问一个特定的对象时,该对象就调用访问者的一个方法,并将自己作为一个参数传递。访问者拥有特定于特定类的方法。

  • 具体 employee 类(CompositeEmployee,SimpleEmployee)的对象只实现 acceptVisitor(Visitor visitor)方法。这些对象知道它应该调用的访问者的特定方法(在这里作为参数传递)。

那么,我们开始吧。

修改的类图

图 13-9 显示了修改后的类图。

img/395506_2_En_13_Fig9_HTML.jpg

图 13-9

修改的类图

已修改的包资源管理器视图

图 13-10 显示了修改后程序的高层结构。

img/395506_2_En_13_Fig10_HTML.jpg

图 13-10

已修改的包资源管理器视图

修改的实现

下面是修改后的实现。

package jdp2e.visitor.modified.demo;
import java.util.ArrayList;
import java.util.List;

interface Employee
{
    void printStructures();
    //The following method has a Visitor argument.
    void acceptVisitor(Visitor visitor);
}
//Employees who have Subordinates
class CompositeEmployee implements Employee
{
    private String name;
    private String dept;
    //New field for this example.
    //It is tagged with "final", so visitor cannot modify it.
    private final int  yearsOfExperience;
    //The container for child objects
    private List<Employee> controls;
    // Constructor
    public CompositeEmployee(String name,String dept, int experience)
    {
        this.name = name;
        this.dept = dept;
        this.yearsOfExperience = experience;
        controls = new ArrayList<Employee>();
    }
    public void addEmployee(Employee e)
    {
        controls.add(e);
    }
    public void removeEmployee(Employee e)
    {
        controls.remove(e);
    }
    // Gets the name
    public String getName()
    {
        return name;
    }
    // Gets the department name

    public String getDept()
    {
        return dept;
    }

    // Gets the yrs. of experience

    public int getExperience()
    {
        return yearsOfExperience;

    }
    public List<Employee> getControls()
    {
        return this.controls;
    }
    @Override
    public void printStructures()
    {
        System.out.println("\t" + getName() + " works in  " + getDept() + " Experience :" + getExperience() + " years");
        for(Employee e: controls)
        {
            e.printStructures();
        }

    }
    @Override
    public void acceptVisitor(Visitor visitor)
    {
        visitor.visitTheElement(this);
    }
}
class SimpleEmployee implements Employee
{
    private String name;
    private String dept;
    //New field for this example
    private int yearsOfExperience;
    //Constructor
    public SimpleEmployee(String name, String dept, int experience)
    {
        this.name = name;
        this.dept = dept;
        this.yearsOfExperience = experience;
    }
    // Gets the name

    public String getName()
    {
        return name;
    }
    // Gets the department name
    public String getDept()
    {
        return this.dept;

    }
    // Gets the yrs. of experience
    public int getExperience()
    {
        return yearsOfExperience;
    }
    @Override
    public void printStructures()
    {
        System.out.println("\t\t" + getName() + " works in  " + getDept() + " Experience :" + getExperience() + " years");
    }
    @Override
    public void acceptVisitor(Visitor visitor)
    {
        visitor.visitTheElement(this);
    }
}

interface Visitor
{
    void visitTheElement(CompositeEmployee employees);
    void visitTheElement(SimpleEmployee employee);
}
class ConcreteVisitor implements Visitor
{
    @Override
    public void visitTheElement(CompositeEmployee employee)
    {
        //We'll promote them if experience is greater than 15 years

        boolean eligibleForPromotion = employee.getExperience() > 15 ? true : false;
        System.out.println("\t\t" + employee.getName() + " from  " + employee.getDept() + " is eligible for promotion? " + eligibleForPromotion);
    }
    @Override
    public void visitTheElement(SimpleEmployee employee)
    {
        //We'll promote them if experience is greater than 12 years
        boolean eligibleForPromotion = employee.getExperience() > 12 ? true : false;
        System.out.println("\t\t" + employee.getName() + " from  " + employee.getDept() + " is eligible for promotion? " + eligibleForPromotion);
    }

}

public class ModifiedVisitorPatternExample {

    public static void main(String[] args) {
        System.out.println("***Visitor Pattern combined with Composite Pattern Demo***\n");
        /*2 teachers other than HOD works in
         Mathematics department*/
        SimpleEmployee mathTeacher1 = new SimpleEmployee("Math Teacher-1","Maths",13);
        SimpleEmployee mathTeacher2 = new SimpleEmployee("Math Teacher-2","Maths",6);

        /* 3 teachers other than HOD works in
          Computer Sc. department*/
        SimpleEmployee cseTeacher1 = new SimpleEmployee("CSE Teacher-1","Computer Sc.",10);
        SimpleEmployee cseTeacher2 = new SimpleEmployee("CSE Teacher-2", "Computer Sc.",13);
        SimpleEmployee cseTeacher3 = new SimpleEmployee("CSE Teacher-3", "Computer Sc.",7);

        //The College has 2 Head of Departments-One from Mathematics, One from Computer Sc.
        CompositeEmployee hodMaths = new CompositeEmployee("Mrs.S.Das(HOD-Maths)","Maths",14);
        CompositeEmployee hodCompSc = new CompositeEmployee("Mr. V.Sarcar(HOD-CSE)", "Computer Sc.",16);

        //Principal of the college

        CompositeEmployee principal = new CompositeEmployee("Dr.S.Som(Principal)","Planning-Supervising-Managing",20);

        //Teachers of Mathematics directly reports to HOD-Maths
        hodMaths.addEmployee(mathTeacher1);
        hodMaths.addEmployee(mathTeacher2);

        //Teachers of Computer Sc. directly reports to HOD-CSE

        hodCompSc.addEmployee(cseTeacher1);
        hodCompSc.addEmployee(cseTeacher2);
        hodCompSc.addEmployee(cseTeacher3);

        /*Principal is on top of college.HOD -Maths and Comp. Sc directly reports to him */
        principal.addEmployee(hodMaths);
        principal.addEmployee(hodCompSc);

        System.out.println("\n Testing the overall structure");
        //Prints the complete structure
        principal.printStructures();

        System.out.println("\n***Visitor starts visiting our composite structure***\n");
        System.out.println("---The minimum criteria for promotion is as follows ---");
        System.out.println("--Junior Teachers-12 years and Senior teachers-15 years.--\n");
        Visitor myVisitor = new ConcreteVisitor();
        /*
         * At first, we are building a container for employees who will be considered for promotion

.
         Principal is holding the highest position.So, he is not considered for promotion.
         */
        List<Employee> employeeContainer= new ArrayList<Employee>();
        //For employees who directly reports to Principal
        for (Employee e : principal.getControls())
        {
            employeeContainer.add(e);
        }
        //For employees who directly reports to HOD-Maths
        for (Employee e : hodMaths.getControls())
        {
            employeeContainer.add(e);
        }
        //For employees who directly reports to HOD-Comp.Sc
        for (Employee e : hodCompSc.getControls())
        {
            employeeContainer.add(e);
        }
        //Now visitor can traverse through the container.
        for (Employee e :employeeContainer)
        {
            e.acceptVisitor(myVisitor);
        }
    }

}

修改输出

这是修改后的输出。

***Visitor Pattern combined with Composite Pattern Demo***

 Testing the overall structure
 Dr.S.Som(Principal) works in  Planning-Supervising-Managing Experience :20 years
   Mrs.S.Das(HOD-Maths) works in  Maths Experience :14 years
     Math Teacher-1 works in  Maths Experience :13 years
     Math Teacher-2 works in  Maths Experience :6 years
   Mr. V.Sarcar(HOD-CSE) works in  Computer Sc. Experience :16 years
     CSE Teacher-1 works in  Computer Sc. Experience :10 years
     CSE Teacher-2 works in  Computer Sc. Experience :13 years
     CSE Teacher-3 works in  Computer Sc. Experience :7 years

***Visitor starts visiting our composite structure***

---The minimum criteria for promotion is as follows ---

--Junior Teachers-12 years and Senior teachers-15 years.--

   Mrs.S.Das(HOD-Maths) from  Maths is eligible for promotion? false
  Mr. V.Sarcar(HOD-CSE) from  Computer Sc. is eligible for promotion? true
   Math Teacher-1 from  Maths is eligible for promotion? true
   Math Teacher-2 from  Maths is eligible for promotion? false
  CSE Teacher-1 from  Computer Sc. is eligible for promotion? false
  CSE Teacher-2 from  Computer Sc. is eligible for promotion? true
  CSE Teacher-3 from  Computer Sc. is eligible for promotion? false

问答环节

  1. 什么时候应该考虑实现访问者设计模式?

    您需要向一组对象添加新的操作,而不改变它们对应的类。这是实现访问者模式的主要目标之一。当运营经常变化时,这种方法可以成为你的救星。在这种模式中,封装不是主要考虑的问题。

    如果需要改变各种操作的逻辑,只需通过 visitor 实现即可。

  2. 这种模式有什么缺点吗?

    • 封装不是它的主要关注点。所以,你可以通过使用访问者来打破封装的力量。

    • 如果您需要频繁地向现有架构添加新的具体类,那么访问者层次结构将变得难以维护。例如,假设您现在想在原来的层次结构中添加另一个具体的类。在这种情况下,您需要相应地修改 visitor 类的层次结构来实现这个目的。

  3. 你为什么说访问者类会违反封装?

    在我们的示例中,我测试了一个非常简单的 visitor 设计模式,在这个模式中,我通过 visitor 类显示了 myInt 的一个更新的整数值。此外,在许多情况下,您可能会看到访问者需要在一个复合结构周围移动,以从其中收集信息,然后它可以修改这些信息。所以,当你提供这种支持时,你违背了封装的核心目标。

  4. 为什么这种模式会损害封装?

    在这里,您对一组不同种类的对象执行一些操作。但是您的约束是您不能改变它们对应的类。因此,您的访问者需要一种方法来访问这些对象的成员。因此,您需要向访问者公开这些信息。

  5. 在修改后的实现的访问者接口中,您使用了方法重载的概念(即方法名相同)。这是强制性的吗?

    不。在我的《C# 中的设计模式》一书中,我在类似的上下文中使用了VisitCompositeElement()VisitLeafNode()这样的方法名。请记住,这些接口方法应该针对特定的类,比如 SimpleEmployee 或 CompositeEmployee。

  6. 假设在修改后的实现中,我添加了 Employee 的一个具体子类 UndefinedEmployee。我应该如何进行?我应该在访问者接口中有另一个特定的方法吗?

    Exactly. You need to define a new method that is specific to this new class. So, your interface may look like the following.

    interface Visitor
    {
        void visitTheElement(CompositeEmployee employees);
        void visitTheElement(SimpleEmployee employee);
        void visitTheElement(UndefinedEmployee employee);
    }
    
    

    稍后,您需要在具体的 visitor 类中实现这个新方法。

  7. 假设我需要在现有架构中支持新的操作。我应该如何处理访问者模式?

    对于每个新操作,创建一个新的 visitor 子类,并在其中实现操作。然后,按照前面示例中所示的方式访问您现有的结构。

  8. 在客户端代码中,你首先创建了一个雇员容器,然后它开始访问。创建这样的结构是强制性的吗?

    不。它只是帮助客户一次顺利访问。如果您没有创建任何这样的结构,您总是可以单独调用它。

十四、观察者模式

本章涵盖了观察者模式。

GoF 定义

定义对象之间的一对多依赖关系,这样当一个对象改变状态时,它的所有依赖对象都会得到通知并自动更新。

概念

在这个模式中,有许多观察者(对象)在观察一个特定的主体(也是一个对象)。观察者将自己注册到一个主题,以便在该主题内部发生变化时获得通知。当他们对该主题失去兴趣时,他们只是从该主题中注销。它也被称为发布-订阅模式。整个想法可以总结如下:使用这个模式,一个对象(subject)可以同时向多个观察者(一组对象)发送通知。

您可以在下面的图表中可视化这些场景。

第一步。观察者正在请求一个主题获得通知(见图 14-1 )。

img/395506_2_En_14_Fig1_HTML.jpg

图 14-1

第一步

第二步。主体同意请求,连接建立(见图 14-2 )。

img/395506_2_En_14_Fig2_HTML.jpg

图 14-2

第二步

第三步。主体向注册用户发送通知(在主体发生典型事件并希望通知其他人的情况下)(见图 14-3 )。

img/395506_2_En_14_Fig3_HTML.jpg

图 14-3

第三步

第四步(可选)。观察器 2 不想得到进一步的通知,所以它注销自己(见图 14-4 )。

img/395506_2_En_14_Fig4_HTML.jpg

图 14-4

第四步

第五步。从现在开始,只有观察者 1 和观察者 3 会收到受试者的通知(见图 14-5 )。

img/395506_2_En_14_Fig5_HTML.jpg

图 14-5

第五步

真实世界的例子

想想一个在社交媒体上有很多粉丝的名人。这些追随者中的每一个都想要他们最喜爱的名人的所有最新消息。所以,他们追随名人直到兴趣减退。当他们失去兴趣时,他们就不再关注那个名人了。你可以把这些粉丝或追随者看作观察者,把名人看作主体。

计算机世界的例子

在计算机科学领域,考虑一个简单的基于 UI 的例子。让我们假设这个 UI 连接到一个数据库。用户可以通过该 UI 执行查询,在搜索数据库后,结果将在 UI 中返回。在这里,您将 UI 与数据库隔离开来,如果数据库发生变化,UI 会得到通知,并根据变化更新其显示。

为了简化这个场景,假设您是组织中负责维护特定数据库的人。每当数据库内部发生变化时,您都需要一个通知,以便在必要时采取措施。

注意

通常,您会在事件驱动软件中看到这种模式的存在。像 C#、Java 等现代语言都内置了对遵循这种模式处理事件的支持。这些结构让你的生活变得简单。

在 Java 中,你可以看到事件监听器的使用。这些听众只是观察者。在 Java 中,你有一个现成的类叫做 Observable,它可以有多个观察者。这些观察者需要实现观察者接口。观察者接口有一个“更新”方法:void update(Observable o,Object arg)。每当被观察对象发生变化时,就会调用此方法。您的应用程序需要调用可观察对象的 notifyObservers 方法来通知观察者的变化。addObserver(Observer o)和 deleteObserver(Observer o)方法添加或删除观察器,类似于前面讨论的 register 和 unregister 方法。可以从 docs 了解更多。甲骨文。com/javase/8/docs/API/Java/util/Observer。html docs。甲骨文。com/ javase/ 8/ docs/ api/ index。html?java/ util/ Observable。html

如果你熟悉。NET 框架,你可以看到在 C# 中,你有通用系统。可观察的和系统。IObserver 接口,其中泛型类型参数提供通知。

说明

让我们考虑下面的例子,并对输出进行事后分析。我创建了三个观察者和一个主题。该主题为其所有注册用户维护一个列表。我们的观察者希望在主题中的标志值发生变化时收到通知。在输出中,您发现当标志值分别更改为 5、50 和 100 时,观察器会收到通知。但是其中一个人在标志值更改为 50 时没有收到任何通知,因为此时他不是 subject 中的注册用户。但最终,他会收到通知,因为他再次注册了自己。

在这个实现中,register()unregister()notifyRegisteredUsers()方法有它们典型的含义。register()方法在主题的通知列表中注册一个观察者,unregister()方法从主题的通知列表中删除一个观察者,notifyRegisteredUsers()在主题中发生典型事件时通知所有注册的用户。

类图

图 14-6 显示了类图。

img/395506_2_En_14_Fig6_HTML.jpg

图 14-6

类图

包资源管理器视图

图 14-7 显示了程序的高层结构。

img/395506_2_En_14_Fig7_HTML.jpg

图 14-7

包资源管理器视图

履行

下面是实现。

package jdp2e.observer.demo;

import java.util.*;

interface Observer
{
    void update(int updatedValue);
}
class ObserverType1 implements Observer
{
    String nameOfObserver;
    public ObserverType1(String name)
    {
        this.nameOfObserver = name;
    }
    @Override
    public void update(int updatedValue)
    {
        System.out.println( nameOfObserver+" has received an alert: Updated myValue in Subject is: "+ updatedValue);
    }
}
class ObserverType2 implements Observer
{
    String nameOfObserver;
    public ObserverType2(String name)
    {
        this.nameOfObserver = name;
    }
    @Override
    public void update(int updatedValue)
    {
        System.out.println( nameOfObserver+" has received an alert: The current value of myValue in Subject is: "+ updatedValue);
    }
}

interface SubjectInterface
{
    void register(Observer anObserver);
    void unregister(Observer anObserver);
    void notifyRegisteredUsers(int notifiedValue);
}
class Subject implements SubjectInterface
{
    private int flag;
    public int getFlag()
    {
        return flag;
    }
    public void setFlag(int flag)
    {
        this.flag = flag;
        //Flag value changed. So notify registered users/observers.
        notifyRegisteredUsers(flag);
    }
    List<Observer> observerList = new ArrayList<Observer>();
    @Override
    public void register(Observer anObserver) {
        observerList.add(anObserver);

    }
    @Override
    public void unregister(Observer anObserver) {
        observerList.remove(anObserver);
    }
    @Override
    public void notifyRegisteredUsers(int updatedValue)
    {
        for (Observer observer : observerList)
            observer.update(updatedValue); 

    }
}
public class ObserverPatternExample {

    public static void main(String[] args) {
        System.out.println(" ***Observer Pattern Demo***\n");
        //We have 3 observers- 2 of them are ObserverType1, 1 of them is of //ObserverType2
        Observer myObserver1 = new ObserverType1("Roy");
        Observer myObserver2 = new ObserverType1("Kevin");
        Observer myObserver3 = new ObserverType2("Bose");
        Subject subject = new Subject();
        //Registering the observers-Roy,Kevin,Bose
        subject.register(myObserver1);
        subject.register(myObserver2);
        subject.register(myObserver3);
        System.out.println(" Setting Flag = 5 ");
        subject.setFlag(5);
        //Unregistering an observer(Roy))
        subject.unregister(myObserver1);
        //No notification this time Roy. Since it is unregistered.
        System.out.println("\n Setting Flag = 50 ");
        subject.setFlag(50);
        //Roy is registering himself again
        subject.register(myObserver1);
        System.out.println("\n Setting Flag = 100 ");
        subject.setFlag(100);
    }
}

输出

这是输出。

***Observer Pattern Demo***

 Setting Flag = 5
Roy has received an alert: Updated myValue in Subject is: 5
Kevin has received an alert: Updated myValue in Subject is: 5
Bose has received an alert: The current value of myValue in Subject is: 5

 Setting Flag = 50
Kevin has received an alert: Updated myValue in Subject is: 50
Bose has received an alert: The current value of myValue in Subject is: 50

 Setting Flag = 100
Kevin has received an alert: Updated myValue in Subject is: 100
Bose has received an alert: The current value of myValue in Subject is: 100

Roy has received an alert: Updated myValue in Subject is: 100

分析

最初,所有三名观察者——罗伊、凯文和博斯——都注册了来自该对象的通知。所以,在最初阶段,他们都收到了通知。在某种程度上,罗伊变得对通知不感兴趣,所以他取消了自己的注册。因此,从这个时候开始,只有 Kevin 和 Bose 收到通知(注意,当我将标志值设置为 50 时)。但罗伊改变了主意,他重新注册了自己,以获得来自该主题的通知。所以,在最后一个案例中,所有人都收到了受试者的通知。

问答环节

  1. 如果我只有一个观察者,那么我可能不需要设置界面。这是正确的吗?

    是的。但是如果你想遵循纯面向对象的编程准则,编程到一个接口/抽象类总是被认为是一个更好的实践。所以,比起具体的类,你应该更喜欢接口(或者抽象类)。此外,通常,您有多个观察者,并且您希望他们按照约定以系统的方式实现方法。你从这种设计中获益。

  2. 同一个应用程序中可以有不同类型的观察者吗?

    是的。这就是为什么我和来自两个不同班级的三个观察者一起玩。但是你不应该对每个观察者都有这种感觉;您需要创建一个不同的类。

    考虑一个真实的场景。当公司发布或更新新软件时,公司业务合作伙伴和购买该软件的客户会收到通知。在这种情况下,业务伙伴和客户是两种不同类型的观察者。

  3. 我可以在运行时添加或删除观察者吗?

    是的。在我们的程序开始时,罗伊注册获得通知;然后他注销了,后来又重新注册了。

  4. 观察者模式和责任链模式之间似乎有相似之处。这是正确的吗?

    In an observer pattern, all registered users get notifications at the same time, but in a chain of responsibility pattern, objects in the chain are notified one by one, and this process continues until the object fully handles the notification. Figure 14-8 and Figure 14-9 summarize the differences.

    img/395506_2_En_14_Fig9_HTML.jpg

    图 14-9

    责任链模式的基本工作流程

    img/395506_2_En_14_Fig8_HTML.jpg

    图 14-8

    观察者模式的基本工作流程

  5. 此模型支持一对多关系。这是正确的吗?

    是的。由于一个主题可以向多个观察者发送通知,这种依赖关系显然是一对多的关系。

  6. 如果你已经有了这些现成的结构,为什么还要编写自己的代码呢?

    根据您的偏好更改现成的构造并不总是容易的。在许多情况下,您根本无法更改内置功能。当您尝试自己实现这个概念时,您可能会更好地理解如何使用那些现成的构造。

    Consider some typical scenarios.

    • 在 Java 中,Observable 是一个具体的类。它不实现接口。因此,您不能创建自己的实现来使用 Java 的内置 Observer API。

    • Java 不允许多重继承。所以,当你必须扩展 Observable 类时,你必须记住这个限制。这可能会限制重用潜力。

    • 可观察对象中setChanged方法的签名如下:protected void setChanged()。这意味着要使用它,你需要子类化 Observable 类。这违反了一个关键的设计原则,基本上就是说要优先组合而不是继承。

  7. 观察者模式的主要优势 是什么?

    • 主体和它的注册用户(观察者)正在构建一个松散耦合的系统。他们不需要明确地相互了解。

    • 在通知列表中添加或删除观察者时,不需要修改主题。

    • 此外,您可以随时单独添加或删除观察者。

  8. 与观察者模式相关的主要挑战是什么?

    • 毫无疑问,当您处理任何基于事件的机制时,内存泄漏是最大的问题。在这种情况下,自动垃圾收集器可能并不总是对您有所帮助。您可以考虑这样一种情况,即取消注册/取消注册操作没有正确执行。

    • 通知的顺序不可靠。

    • Java 对观察者模式的内置支持有一些关键的限制,我在前面已经讨论过了。(重温问题 6 的答案。)其中一个强迫你更喜欢继承而不是组合,所以它显然违反了一个更喜欢相反的关键设计原则。

十五、策略模式

本章涵盖了策略模式。

GoF 定义

定义一系列算法,封装每一个算法,并使它们可以互换。策略让算法独立于使用它的客户端而变化。

概念

假设有一个应用程序,其中有多个算法,每个算法都可以执行特定的任务。客户端可以动态地选择这些算法中的任何一个来满足其当前的需求。

策略模式建议您在单独的类中实现这些算法。当你将一个算法封装在一个单独的类中时,你称之为策略。使用策略对象的对象通常被称为上下文对象。这些“算法”在一些应用中也被称为行为

真实世界的例子

通常在足球比赛结束时,如果 A 队以 1 比 0 领先 B 队,他们不会进攻,而是采取防守来保持领先。另一方面,B 队全力以赴去扳平比分。

计算机世界示例

假设你有一个整数列表,你想对它们进行排序。你通过使用各种算法来做到这一点;例如,冒泡排序、合并排序、快速排序、插入排序等等。所以,你可以有很多不同的排序算法。现在,您可以在单独的类中实现这些变体(算法),并在客户端代码中传递这些类的对象来对整数列表进行排序。

注意

在这种情况下,可以考虑 java.util.Comparator 接口。您可以实现这个接口,并提供具有不同算法的比较器的多个实现,以便使用 compare() 方法进行各种比较。该比较结果可以进一步用于各种分类技术。在这种情况下,比较器接口扮演策略接口的角色。

说明

在您继续之前,让我们记住以下几点。

  • 策略模式鼓励你使用对象组合而不是子类化。所以,它建议你不要在不同的子类中覆盖父类行为。相反,您将这些行为放在共享一个公共接口的单独的类中(称为策略)。

  • 客户端类只决定使用哪种算法;上下文类并不决定这一点。

  • 上下文对象包含策略对象接口类型的引用变量。所以,你可以通过改变情境中的策略来获得不同的行为。

在下面的实现中,Vehicle 类是一个扮演上下文角色的抽象类。船和飞机是 Vehicle 类的两个具体实现。你知道它们与不同的行为相关联:一个通过水传播,另一个通过空气传播。

这些行为被分为两个具体的类:空运和水运。这些类共享一个公共接口 TransportMedium。因此,这些具体的类扮演着策略类的角色,不同的行为通过transport()方法实现来反映。

在 Vehicle 类中,有一个方法叫做showTransportMedium()。使用这种方法,我将任务委托给相应的行为类。因此,一旦选择了策略,就可以调用相应的行为。请注意,在 Vehicle 类中,有一个名为commonJob()的方法,它不应该在将来发生变化,因此它的行为不会被视为易变行为。

类图

图 15-1 为类图。

img/395506_2_En_15_Fig1_HTML.jpg

图 15-1

类图

包资源管理器视图

图 15-2 显示了程序的高层结构。

img/395506_2_En_15_Fig2_HTML.jpg

图 15-2

包资源管理器视图

履行

下面是实现。

// Vehicle.java

package jdp2e.strategy.demo;

//Context class
public abstract class Vehicle
{
    /*A context object contains reference variable/s for the strategy object/s interface type.*/
    TransportMedium transportMedium;
    public Vehicle()
    {

    }
    public void showTransportMedium()
    {
        //Delegate the task to the //corresponding behavior class.
        transportMedium.transport();
    }
    //The code that does not vary.
    public void commonJob()
    {
        System.out.println("We all can be used to transport");
    }
    public abstract void showMe();

}

// Boat.java

package jdp2e.strategy.demo;

public class Boat extends Vehicle
{
    public Boat()
    {
        transportMedium= new WaterTransport();
    }
    @Override
    public void showMe() {
        System.out.println("I am a boat.");

    }
}
// Aeroplane.java

package jdp2e.strategy.demo;

public class Aeroplane extends Vehicle
{
    public Aeroplane()
    {
        transportMedium= new AirTransport();
    }

    @Override
    public void showMe() {
        System.out.println("I am an aeroplane.");

    }
}
// TransportMedium.java
package jdp2e.strategy.demo;

public interface TransportMedium
{
    public void transport();
}
//WaterTransport.java
package jdp2e.strategy.demo;
//This class represents an algorithm/behavior

.
public class WaterTransport implements TransportMedium
{
    @Override
    public void transport()
    {
        System.out.println("I am transporting in water.");
    }
}
//AirTransport.java
package jdp2e.strategy.demo;
//This class represents an algorithm/behavior.
public class AirTransport implements TransportMedium
{
    @Override
    public void transport()
    {
        System.out.println("I am transporting in air.");
    }
}

// StrategyPatternExample.java

package jdp2e.strategy.demo;

//Client code

public class StrategyPatternExample {

    public static void main(String[] args) {
        System.out.println("***Strategy Pattern Demo***");
        Vehicle vehicleContext=new Boat();
        vehicleContext.showMe();
        vehicleContext.showTransportMedium();
        System.out.println("________");

        vehicleContext=new Aeroplane();
        vehicleContext.showMe();
        vehicleContext.showTransportMedium();

    }

}

输出

这是输出。

***Strategy Pattern Demo***
I am a boat.
I am transporting in water.
________
I am an aeroplane.
I am transporting in air.

问答环节

  1. 你为什么要通过避免这些行为的简单子类化来使这个例子复杂化呢?

    在面向对象的编程中,您可能更喜欢使用多态性的概念,这样您的代码就可以在运行时挑选预期的对象(在不同的对象类型中),而不改变您的代码。

    当你熟悉设计模式时,大多数情况下,你更喜欢组合而不是继承。

    策略模式帮助您将组合与多态性结合起来。我们来考察一下这背后的原因。

    It is assumed that you try to use the following guidelines in any application you write:

    • 将变化很大的代码与没有变化的代码部分分开。

    • 尽量保持不同部件独立(便于维护)。

    • 尽可能地重复使用它们。

遵循这些指导方针,我使用了组合来提取和封装代码的易变/可变部分,以便整个任务可以轻松处理,并且您可以重用它们。

但是当你使用继承时,你的父类可以提供一个默认的实现,然后派生类修改它(Java 称之为覆盖它)。下一个派生类可以进一步修改实现,所以您基本上是将任务分散到不同的级别,这可能会在将来导致严重的维护和可扩展性问题。我们来考察这样一个案例。

让我们假设您的 vehicle 类具有以下结构。

abstract class Vehicle

{
    //Default implementation
    public void showTransportMedium()
    {
        System.out.println("I am transporting in air.");
    }
    //The code that does not vary.
    public void commonJob()
    {
        System.out.println("We all can be used to transport");
    }
    public abstract void showMe();
}

所以,做一个具体的车辆实现,像这样:

class Aeroplane extends Vehicle
{
    @Override
    public void showMe() {
        System.out.println("I am an aeroplane.");

    }
}

并在客户端类中使用以下代码行。

Aeroplane aeroplane=new Aeroplane();
aeroplane.showMe();
aeroplane.showTransportMedium();

您将收到以下输出:

I am an aeroplane.
I am transporting in air.

到目前为止,看起来不错。现在假设您已经引入了另一个类 Boat,如下所示。

class Boat extends Vehicle

{
    @Override
    public void showMe() {
        System.out.println("I am a boat.");

    }
}

在客户端类中使用以下代码行(新行以粗体显示)。

Aeroplane aeroplane=new Aeroplane();
aeroplane.showMe();
aeroplane.showTransportMedium();

Boat boat=new Boat();

boat.showMe();

boat.showTransportMedium();

您会收到以下输出。

I am an aeroplane.
I am transporting in air.

I am a boat.

I am transporting in air.

你可以看到你的船正在向空中移动。为了防止这种糟糕的情况,您需要适当地覆盖它。

现在进一步假设你需要引入另一个类,快艇,它也可以在水中高速运输。你需要警惕这样的情况:

class Boat extends Vehicle

{
    @Override
    public void showMe()
    {
        System.out.println("I am a boat.");

    }
    @Override
    public void showTransportMedium() {
        System.out.println("I am transporting in water.");

    }
}
class SpeedBoat extends Vehicle
{
    @Override
    public void showMe() {
        System.out.println("I am a speedboat.");

    }
    @Override
    public void showTransportMedium() {
        System.out.println("I am transporting in water with high speed.");
    }
}

您可以看到,如果您将任务分散到不同的类(及其子类)中,从长远来看,维护会变得非常昂贵。如果您想经常适应类似的变化,您会经历很多痛苦,因为您需要在每种情况下不断更新showTransportMedium()方法。

  1. 如果是这种情况,您可以创建一个单独的接口 TransportInterface,并将 showTransportMedium()方法放在该接口中。现在,任何想要获取该方法的类也可以实现该接口。这种理解正确吗?

    Yes, you can do that. But this is what the code looks like:

    abstract class Vehicle
    {
        //The code that does not vary.
        public void commonJob()
        {
            System.out.println("We all can be used to transport");
        }
        public abstract void showMe();
    }
    interface TransportInterface
    {
        void showTransportMedium();
    }
    class Aeroplane extends Vehicle implements TransportInterface
    {
        @Override
        public void showMe() {
            System.out.println("I am an aeroplane.");
    
        }
        @Override
        public void showTransportMedium() {
            System.out.println("I am transporting in air.");
        }
    }
    class Boat extends Vehicle implements TransportInterface
    {
        @Override
        public void showMe()
        {
            System.out.println("I am a boat.");
    
        }
    
        @Override
        public void showTransportMedium() {
            System.out.println("I am transporting in water.");
    
        }
    }
    
    

    您可以看到每个类及其子类可能需要为showTransportMedium()方法提供自己的实现。所以,你不能重用你的代码,这和继承一样糟糕。

  2. 你能在你的实现中修改运行时的默认行为吗?

    Yes, you can. Let’s introduce a special vehicle that can transport in both water and air, as follows.

    public class SpecialVehicle extends Vehicle
    {
        public SpecialVehicle()
        {
            //Initialized with AirTransport
            transportMedium= new AirTransport();
        }
    
        @Override
        public void showMe()
        {
            System.out.println("I am a special vehicle who can transport both in air and water.");
        }
    }
    
    

    And add a setter method in the Vehicle class(changes are shown in bold).

    //Context class
    public abstract class Vehicle
    {
        //A context object contains reference variable/s
        //for the strategy object/s interface type
        TransportMedium transportMedium;
        public Vehicle()
        {
    
        }
        public void showTransportMedium()
        {
            //Delegate the task to the corresponding behavior class.
            transportMedium.transport();
        }
        //The code that does not vary.
        public void commonJob()
        {
            System.out.println("We all can be used to transport");
        }
        public abstract void showMe();
    
        //Additional code to explain the answer of question no 3 in
        //the "Q&A session"
    
        public void setTransportMedium(TransportMedium transportMedium)
        {
            this.transportMedium=transportMedium;
        }
    
    }
    
    

    To test this, add a few lines of code in the client class, as well.

    //Client code
    public class StrategyPatternExample {
    
        public static void main(String[] args) {
            System.out.println("***Strategy Pattern Demo***");
            Vehicle vehicleContext=new Boat();
            vehicleContext.showMe();
            vehicleContext.showTransportMedium();
            System.out.println("________");
    
            vehicleContext=new Aeroplane();
            vehicleContext.showMe();
            vehicleContext.showTransportMedium();
            System.out.println("________");
    
            //Additional code to explain the answer of question no
            //3 in the "Q&A session"
            vehicleContext=new SpecialVehicle();
            vehicleContext.showMe();
            vehicleContext.showTransportMedium();
            System.out.println("- - - - -");
            //Changing the behavior of Special vehicle
            vehicleContext.setTransportMedium(new WaterTransport());
            vehicleContext.showTransportMedium();
    
        }
    }
    
    

    Now if you execute this modified program, you get the following output.

    ***Strategy Pattern Demo***
    ***Strategy Pattern Demo***
    I am a boat.
    I am transporting in water.
    ________
    I am an aeroplane.
    I am transporting in air.
    ________
    
    I am a special vehicle who can transport both in air and water.
    
    I am transporting in air.
    
    - - - - -
    
    I am transporting in water.
    
    

    初始行为在稍后阶段被动态修改。

  3. 可以用抽象类代替接口吗?

    是的。在某些情况下,您可能希望将常见行为放在抽象类中,这是合适的。我在关于构建器模式的“问答环节”中详细讨论了它。

  4. 使用策略设计模式的关键 优势 是什么?

    • 这种模式使你的类独立于算法。这里,一个类在运行时动态地将算法委托给策略对象(封装了算法)。所以,你可以简单地说,算法的选择在编译时是不受约束的。

    • 更容易维护您的代码库。

    • 它很容易扩展。(在这种情况下,请参考对问题 2 和 3 的答复。)

  5. 与策略设计模式相关的主要挑战是什么?

    • 上下文类的添加导致我们的应用程序中有更多的对象。

    • 应用程序的用户必须了解不同的策略;否则,输出可能会让他们大吃一惊。因此,在客户端代码和不同策略的实现之间存在着紧密的耦合。

    • 当您引入一个新的行为/算法时,您可能还需要更改客户端代码。

十六、模板方法模式

本章涵盖了模板方法模式。

GoF 定义

在操作中定义算法的框架,将一些步骤推迟到子类。模板方法允许子类在不改变算法结构的情况下重新定义算法的某些步骤。

概念

在模板方法中,定义算法的最小或基本结构。然后你将一些责任委托给子类。关键的意图是你可以重新定义算法的某些步骤,但是这些改变不应该影响算法的基本流程。

因此,当您实现一个多步算法并且希望允许通过子类进行定制时,这种设计模式非常有用。

真实世界的例子

假设你正在一家餐馆订购比萨饼。对于厨师来说,披萨的基本准备是一样的;他根据顾客的选择加入了一些最终的配料。例如,你可以选择素食比萨饼或非素食比萨饼。你也可以选择培根、洋葱、额外的奶酪、蘑菇等配料。厨师根据你的喜好准备最后的成品。

计算机世界的例子

假设你正在做一个设计工程课程的程序。让我们假设第一学期对所有流都是通用的。在随后的学期中,你需要根据课程在申请中添加新的论文/科目。在接下来的插图中,您会看到类似的情况。请记住,当您希望在应用程序中避免重复代码时,这种模式是有意义的。同时,您可能希望允许子类更改基类工作流程的某些特定细节,以在应用程序中提供不同的行为。

注意

java.util.AbstractSet 的 removeAll()方法是模板方法模式的一个示例。除此之外,java.util.AbstractMap 和 java.util.AbstractSet 类中还有许多非抽象方法,也可以看作是模板方法模式的例子。

说明

在下面的实现中,我假设每个工科学生需要完成数学课程,然后是软技能(该科目可能涉及沟通技能、性格特征、人员管理技能等)。)来获得学位。稍后你将在这些课程(计算机科学或电子学)中加入特殊的论文。

为此,在抽象类 BasicEngineering 中定义了 completeCourse()方法。我还将方法标记为 final,这样 BasicEngineering 的子类就不能覆盖 completeCourse()方法来改变课程完成的顺序。

另外两个具体的类——计算机科学和电子学是基础工程类的子类,它们根据自己的需要完成抽象方法 completeSpecialPaper()。

类图

图 16-1 为类图。

img/395506_2_En_16_Fig1_HTML.jpg

图 16-1

类图

包资源管理器视图

图 16-2 显示了程序的高层结构。

img/395506_2_En_16_Fig2_HTML.jpg

图 16-2

包资源管理器视图

履行

实现如下:

package jdp2e.templatemethod.demo;

abstract class BasicEngineering
{
    //Making the method final to prevent overriding.
    public final void completeCourse()
    {
        //The course needs to be completed in the following sequence
        //1.Math-2.SoftSkills-3.Special Paper
        //Common Papers:
        completeMath();
        completeSoftSkills();
        //Specialization Paper:
        completeSpecialPaper();
    }
    private void completeMath()
    {
        System.out.println("1.Mathematics");
    }
    private void completeSoftSkills()
    {
        System.out.println("2.SoftSkills");
    }
    public abstract void completeSpecialPaper();
}
class ComputerScience extends BasicEngineering

{
    @Override
    public void completeSpecialPaper() {
        System.out.println("3.Object-Oriented Programming");
    }
}
class Electronics extends BasicEngineering
{
    @Override
    public void completeSpecialPaper()
    {
        System.out.println("3.Digital Logic and Circuit Theory");
    }
}
public class TemplateMethodPatternExample {

    public static void main(String[] args) {
        System.out.println("***Template Method Pattern Demo***\n");
        BasicEngineering preferrredCourse = new ComputerScience();
        System.out.println("Computer Sc. course will be completed in following order:");
        preferrredCourse.completeCourse();
        System.out.println();
        preferrredCourse = new Electronics();
        System.out.println("Electronics course will be completed in following order:");
        preferrredCourse.completeCourse();
    }
}

输出

以下是输出结果:

***Template Method Pattern Demo***

Computer Sc. course will be completed in following order:
1.Mathematics
2.SoftSkills
3.Object-Oriented Programming

Electronics course will be completed in following order:
1.Mathematics
2.SoftSkills
3.Digital Logic and Circuit Theory

问答环节

  1. 在这个模式中,我看到子类可以根据他们的需要简单地重新定义方法。理解正确吗?

    是的。

  2. 在抽象类 BasicEngineering 中,只有一个方法是抽象的,其他两个方法是具体的方法。背后的原因是什么?

    这是一个简单的例子,只有 3 个方法,我希望子类只覆盖completeSpecialPaper()方法。其他方法是两个流共有的,它们不需要被子类覆盖。

  3. 考虑这样一种情况:假设你想在 BasicEngineering 类中添加更多的方法,但是当且仅当子类需要这些方法时,你才想使用它们,否则你将忽略它们。这种情况在一些博士课程中很常见,有些课程并不是对所有考生都是必修的。例如,如果学生具有某些资格,他/她可能不需要参加那些科目的讲座。可以用模板方法模式设计这种情况吗?

    是的,我们可以。基本上,你可能需要放一个钩子,它只是一个方法,可以帮助我们控制算法中的流程。

    为了展示这种设计的一个例子,我在BasicEngineering中增加了一个叫做AdditionalPapersNeeded()的方法。让我们假设计算机科学的学生需要完成这门课程,但是电子学的学生可以选择不要这篇论文。让我们检查程序和输出。

修改的实现

下面是修改后的实现。关键变化以粗体显示。

package jdp2e.templatemethod.questions_answers;

abstract class BasicEngineering
{
    //Making the method final to prevent overriding.
    public final void completeCourse()
    {
        //The course needs to be completed in the following sequence
        //1.Math-2.SoftSkills-3.Special Paper-4.Additional Papers(if any)
        //Common Papers:
        completeMath();
        completeSoftSkills();
        //Specialization Paper:
        completeSpecialPaper();
        if (isAdditionalPapersNeeded())
        {
            completeAdditionalPapers();
        }
    }

    private void completeMath()
    {
        System.out.println("1.Mathematics");
    }
    private void completeSoftSkills()
    {
        System.out.println("2.SoftSkills");
    }
    public abstract void completeSpecialPaper();
    private void completeAdditionalPapers()
    {
        System.out.println("4.Additional Papers are needed for this course.");
    }
    //By default, AdditionalPapers are needed for a course

.
    boolean isAdditionalPapersNeeded()
    {
        return true;
    }
}
class ComputerScience extends BasicEngineering
{
    @Override
    public void completeSpecialPaper()
    {
        System.out.println("3.Object-Oriented Programming");
    }
    //Additional papers are needed for ComputerScience
    //So, there is no change for the hook method.
}
class Electronics extends BasicEngineering
{
    @Override
    public void completeSpecialPaper()
    {
        System.out.println("3.Digital Logic and Circuit Theory");
    }
    //Overriding the hook method:
    //Indicating that AdditionalPapers are not needed for Electronics.
    @Override
    public  boolean isAdditionalPapersNeeded()
    {
        return false;
    }
}

public class TemplateMethodPatternModifiedExample {

    public static void main(String[] args) {
        System.out.println("***Template Method Pattern Modified Demo***\n");
        BasicEngineering preferrredCourse = new ComputerScience();
        System.out.println("Computer Sc. course will be completed in following order:");
        preferrredCourse.completeCourse();
        System.out.println();
        preferrredCourse = new Electronics();

        System.out.println("Electronics course will be completed in following order:");
        preferrredCourse.completeCourse();
    }
}

修改输出

下面是修改后的输出:

***Template Method Pattern Modified Demo***

Computer Sc. course will be completed in following order:
1.Mathematics
2.SoftSkills
3.Object-Oriented Programming

4.Additional Papers are needed for this course.

Electronics course will be completed in following order:
1.Mathematics
2.SoftSkills
3.Digital Logic and Circuit Theory

注意

你可能更喜欢另一种方法。例如,您可以在 BasicEngineering 中创建一个默认方法 isAdditionalPapersNeeded()。然后你可以覆盖电子类中的方法,然后你可以让方法体为空。但是,如果与前面的方法相比,这种方法看起来并没有更好。

  1. 看起来这个模式类似于 Builder 模式。理解正确吗?

    不。你不应该忘记核心意图;模板方法是一种行为设计模式,而构建器是一种创造设计模式。在构建模式中,客户/顾客是老板——他们可以控制算法的顺序。另一方面,在模板方法模式中,您是老板——您将代码放在一个中心位置,并且您只提供相应的行为(例如,注意 BasicEngineering 中的 completeCourse()方法,并查看课程完成顺序是如何在那里定义的)。所以,你对执行的流程有绝对的控制权。你也可以根据自己的需要修改你的模板,然后其他参与者需要跟随你。

  2. 使用模板设计模式的关键 优势 是什么?

    • 你可以控制算法的流程。客户端无法更改它们。(注意 completeCourse()在抽象类 BasicEngineering 中用 final 关键字标记)

    • 公共操作被放在一个集中的位置,例如,在一个抽象类中。子类可以只重新定义变化的部分,这样,你可以避免多余的代码。

  3. 模板设计模式的主要挑战是什么?

    • 客户端代码不能指导步骤的顺序(如果您需要这种方法,您可以遵循构建器模式)。

    • 子类可以覆盖父类中定义的方法(即在父类中隐藏原始定义),这可能违反 Liskov 替换原则,即:如果 S 是 T 的子类型,则 T 类型的对象可以被 S 类型的对象替换。您可以从以下链接了解详细信息: https://en.wikipedia.org/wiki/Liskov_substitution_principle

    • 子类越多,意味着代码越分散,维护越困难。

  4. 看起来子类可以覆盖基础工程中的其他父方法。理解正确吗?

    你可以这样做,但理想情况下,这不应该是你的意图。在这种模式中,您可能不希望完全覆盖所有的父方法来对子类进行彻底的修改。这样,它不同于简单的多态性。

十七、命令模式

本章介绍了命令模式。

GoF 定义

将请求封装为一个对象,从而允许您用不同的请求、队列或日志请求来参数化客户端,并支持可撤销的操作。

概念

这里您封装了一个方法调用过程。一般来说,有四个术语是相关联的:调用方客户端命令方接收方。命令对象可以以特定于接收方的类的方式调用接收方的方法。然后,接收方开始处理作业。命令对象被单独传递给调用程序对象来调用命令。客户端对象持有调用者对象和命令对象。客户端只决定执行哪些命令,然后将命令传递给调用程序对象(用于执行)。

真实世界的例子

当你用铅笔画东西的时候,你可能需要撤销(擦除和重画)一些部分来使它变得更好。

计算机世界的例子

真实世界的绘画场景适用于 Microsoft Paint。您可以使用菜单或快捷键在这些上下文中执行撤消/重做操作。

通常,您可以在编辑器或 IDE(集成开发环境)的菜单系统中观察到这种模式。所以,如果你想做一个需要支持撤销、多次撤销或者类似操作的应用,那么 command 模式可以成为你的救星。

微软在 Windows 演示基础(WPF)中使用了这种模式。位于 https://visualstudiomagazine.com/articles/2012/04/10/command-pattern-in-net.aspx 的在线资源详细描述道:“命令模式非常适合处理 GUI 交互。它工作得非常好,微软已经将其紧密集成到 Windows 演示基础(WPF)堆栈中。最重要的部分是系统的 ICommand 接口。窗户。输入命名空间。任何实现 ICommand 接口的类都可以用于通过通用 WPF 控件处理键盘或鼠标事件。这种链接可以在 XAML 中完成,也可以在代码隐藏中完成。

注意

当您实现 java.lang.Runnable 接口的 run()方法时,您基本上是在使用命令设计模式。另一个接口 java.swing.Action 也代表了命令设计模式。重要的是要注意,undos 的实现各不相同,可能很复杂。memento 设计模式也支持撤销操作。您可能需要在应用程序中使用这两种设计模式来实现复杂的撤消操作。

说明

考虑下面的例子。为了便于理解,我使用了与前面描述的概念相似的类名。为了更好的理解,你可以参考相关的评论。

类图

图 17-1 为类图。

img/395506_2_En_17_Fig1_HTML.jpg

图 17-1

类图

包资源管理器视图

图 17-2 显示了程序的高层结构。

img/395506_2_En_17_Fig2_HTML.jpg

图 17-2

包资源管理器视图

履行

下面是实现。

package jdp2e.command.demo;

interface Command
{
    //Typically this method does not take any argument.
    //Some of the reasons are:
    //1.We supply all the information when it is created.
    //2.Invoker may reside in different address space.etc.
    void executeCommand();
}

class MyUndoCommand implements Command
{
    private Receiver receiver;
    public MyUndoCommand(Receiver receiver)
    {
        this.receiver=receiver;
    }
    @Override
    public void executeCommand()
    {
        //Perform any optional task prior to UnDo
        receiver.doOptionalTaskPriorToUndo();
        //Call UnDo in receiver now
        receiver.performUndo();
    }
}
class MyRedoCommand implements Command
{
    private Receiver receiver;
    public MyRedoCommand(Receiver receiver)
    {
        this.receiver=receiver;
    }
    @Override
    public void executeCommand()
    {
        //Perform any optional task prior to ReDo

        receiver.doOptionalTaskPriorToRedo();
        //Call ReDo in receiver now
        receiver.performRedo();
    }
}
//Receiver Class
class Receiver
{
    public void performUndo()
    {
        System.out.println("Performing an undo command in Receiver.");
    }
    public void performRedo()
    {
        System.out.println("Performing an redo command in Receiver.");
    }
    /*Optional method-If you want to perform

     any prior tasks before undo operations.*/
    public void doOptionalTaskPriorToUndo()
    {
        System.out.println("Executing -Optional Task/s prior to    execute undo command.");
    }
    /*Optional method-If you want to perform
     any prior tasks before redo operations*/
    public void doOptionalTaskPriorToRedo()
    {
        System.out.println("Executing -Optional Task/s prior to    execute redo command.");
    }

}
//Invoker class
class Invoker
{
    Command commandToBePerformed;
    //Alternative approach:
    //You can also initialize the invoker with a command object
    /*public Invoker(Command command)
    {
        this.commandToBePerformed = command;
    }*/

    //Set the command
    public void setCommand(Command command)
    {
        this.commandToBePerformed = command;
    }
    //Invoke the command
    public void invokeCommand()
    {
        commandToBePerformed.executeCommand();
    }
}

//Client
public class CommandPatternExample {

    public static void main(String[] args) {
        System.out.println("***Command Pattern Demo***\n");
        /*Client holds both the Invoker and Command Objects*/
        Receiver intendedReceiver = new Receiver();
        MyUndoCommand undoCmd = new MyUndoCommand(intendedReceiver);
        //If you use parameterized constructor of Invoker

        //use the following line of code.
        //Invoker invoker = new Invoker(undoCmd);
        Invoker invoker = new Invoker();
        invoker.setCommand(undoCmd);
        invoker.invokeCommand();

        MyRedoCommand redoCmd = new MyRedoCommand(intendedReceiver);
        invoker.setCommand(redoCmd);
        invoker.invokeCommand();
    }
}

输出

这是输出。

***Command Pattern Demo***

Executing -Optional Task/s prior to    execute undo command.
Performing an undo command in Receiver.
Executing -Optional Task/s prior to    execute redo command.
Performing an redo command in Receiver.

问答环节

  1. 我有两个问题。在本例中,您只处理一个接收者。你如何处理多个接收者?GoF 定义说这种模式支持可撤销的操作。您能展示一个使用这种模式的真正撤销操作的例子吗?

考虑下面的程序。该计划的主要特点如下:

  • 这里有两个不同的接收器(接收器 1 和接收器 2)。它们中的每一个都实现了接收器接口方法。因为我要处理多个接收器,所以我引入了一个公共接口 Receiver。

  • 在撤消操作中,您通常希望撤销上一个操作。典型的撤消操作可能涉及复杂的逻辑。但是在即将到来的实现中,我将给出一个简单的例子,它支持撤销操作,假设如下。

    • Receiver1 对象用值 10 初始化(myNumber 实例变量用于此目的),Receiver2 对象用“关机”状态初始化(status 实例变量用于此目的)。任何 Receiver1 对象都可以在现有的整数上加 2。

    • 我在值 10 上打了一个勾号,这样当您处理一个撤销操作时,如果您注意到一个 Receiver1 对象的 myNumber 是 10,您将不会超出(因为您从 10 开始)。

    • Receiver2 对象做不同的事情。它可以打开或关闭机器。如果机器已经通电,则通过请求撤销操作,您可以关闭机器,反之亦然。但是,如果您的机器已经处于开机模式,那么进一步的“开机”请求将被忽略。

修改的类图

图 17-3 所示修改后的类图中有很多参与者和依赖关系。为了说明主要设计并保持图表整洁,我没有显示客户端代码依赖关系。

img/395506_2_En_17_Fig3_HTML.jpg

图 17-3

修改的类图

已修改的包资源管理器视图

图 17-4 显示了修改后的包浏览器视图。

img/395506_2_En_17_Fig4_HTML.jpg

图 17-4

已修改的包资源管理器视图

修改的实现

下面是修改后的实现

package jdp2e.command.modified.demo;

/**
 *In general, an undo operation involves complex logic.
 But for simplicity, in this example,I assume that executeDo() can either add 2 with a given integer or it can switch on a machine.
 Similarly, executeUnDo() can either subtract 2 from a given number() or,
 it will switch off a machine.But you cannot go beyond the initialized value(i.e.10 in this case)*/

interface Command
{
    void executeDo();
    void executeUnDo();
}
class AdditionCommand implements Command
{
    private Receiver receiver;
    public AdditionCommand(Receiver receiver)
    {
        this.receiver = receiver;
    }
    @Override
    public void executeDo()
    {
        receiver.performDo();
    }
    @Override
    public void executeUnDo()
    {
        receiver.performUnDo();
    }
}
class PowerCommand implements Command
{
    private Receiver receiver;
    public PowerCommand(Receiver receiver)
    {
        this.receiver = receiver;
    }
    @Override
    public void executeDo()
    {
        receiver.performDo();
    }
    @Override
    public void executeUnDo()
    {
        receiver.performUnDo();
    }
}

//To deal with multiple receivers , we are using interfaces here
interface Receiver
{
    //It will add 2 with a number or switch on the m/c
    void performDo();
    //It will subtract 2 from a number or switch off the m/c
    void performUnDo();
}
//Receiver Class
class Receiver1 implements Receiver
{
    private int myNumber;

    public int getMyNumber()
    {
        return myNumber;
    }
    public void setMyNumber(int myNumber)
    {
        this.myNumber = myNumber;
    }
    public Receiver1()
    {
        myNumber = 10;
        System.out.println("Receiver1 initialized with " + myNumber);
        System.out.println("The objects of receiver1 cannot set beyond "+ myNumber);
    }
    @Override
    public void performDo()
    {
        System.out.println("Received an addition request.");
        int presentNumber = getMyNumber();
        setMyNumber(presentNumber + 2);
        System.out.println(presentNumber +" + 2 ="+ this.myNumber);
    }
    @Override
    public void performUnDo()
    {
        System.out.println("Received an undo addition request.");
        int presentNumber = this.myNumber;
        //We started with number 10.We'll not decrease further.
        if (presentNumber > 10)
        {
            setMyNumber(this.myNumber - 2);
            System.out.println(presentNumber +" - 2 ="+ this.myNumber);
            System.out.println("\t Undo request processed.");
        }
        else
        {
            System.out.println("Nothing more to undo...");
        }
    }
}
//Receiver2 Class

class Receiver2 implements Receiver
{
    boolean status;

    public Receiver2()
    {
        System.out.println("Receiver2 initialized ");
        status=false;
    }
    @Override
    public void performDo()
    {
        System.out.println("Received a system power on request.");
        if( status==false)
        {
            System.out.println("System is starting up.");
            status=true;
        }
        else
        {
            System.out.println("System is already running.So, power on request is ignored.");

        }

    }
    @Override
    public void performUnDo()
    {
        System.out.println("Received a undo request.");
        if( status==true)
        {
            System.out.println("System is currently powered on.");
            status=false;
            System.out.println("\t Undo request processed.System is switched off now.");
        }
        else
        {
            System.out.println("System is switched off at present.");
            status=true;
            System.out.println("\t Undo request processed.System is powered on now.");

        }
    }
}

//Invoker class

class Invoker
{
    Command commandToBePerformed;
    public void setCommand(Command command)
    {
        this.commandToBePerformed = command;
    }
    public void executeCommand()
    {
        commandToBePerformed.executeDo();
    }
    public void undoCommand()
    {
        commandToBePerformed.executeUnDo();
    }
}

//Client
public class ModifiedCommandPatternExample {
    public static void main(String[] args) {

        System.out.println("***Command Pattern Q&As***");
        System.out.println("***A simple demo with undo supported operations***\n");
        //Client holds  both the Invoker and Command Objects

        //Testing receiver -Receiver1
        System.out.println("-----Testing operations in Receiver1-----");
        Receiver intendedreceiver = new Receiver1();
        Command currentCmd = new AdditionCommand(intendedreceiver);

        Invoker invoker = new Invoker();
        invoker.setCommand(currentCmd);
        System.out.println("*Testing single do/undo operation*");
        invoker.executeCommand();
        invoker.undoCommand();
        System.out.println("_______");
        System.out.println("**Testing a series of do/undo operations**");
        //Executed the command 2 times
        invoker.executeCommand();
        //invoker.undoCommand();
        invoker.executeCommand();
        //Trying to undo 3 times
        invoker.undoCommand();
        invoker.undoCommand();
        invoker.undoCommand();

        System.out.println("\n-----Testing operations in Receiver2-----");
        intendedreceiver = new Receiver2();
        currentCmd = new PowerCommand(intendedreceiver);
        invoker.setCommand(currentCmd);

        System.out.println("*Testing single do/undo operation*");
        invoker.executeCommand();
        invoker.undoCommand();
        System.out.println("_______");
        System.out.println("**Testing a series of do/undo operations**");
        //Executing the command 2 times
        invoker.executeCommand();
        invoker.executeCommand();
        //Trying to undo 3 times
        invoker.undoCommand();
        invoker.undoCommand();
        invoker.undoCommand();

    }

}

修改输出

这是修改后的输出。

  1. 在这个修改过的程序中,两个接收者在做不同的事情。这是故意的吗?

    是的。它展示了命令设计模式提供的强大功能和灵活性。你可以看到这些接收器中的performDo()实际上执行不同的动作。对于 Receiver1,它将 2 与现有整数相加,对于 Receiver2,它将打开一台机器。所以,你可能会认为其他一些名字像addNumber()powerOn()会更适合他们。

    但是在这种情况下,我需要同时使用接收器和它们对应的方法。因此,我需要使用一个通用的接口和通用的名称,以便两个接收者都可以使用。

    因此,如果您需要使用具有不同方法名称的两个不同的接收器,您可以用一个公共名称替换它们,使用一个公共接口,并且通过多态性,您可以轻松地调用这些方法。

  2. 你为什么需要发票员?

    很多时候,程序员在面向对象的编程中尝试封装数据和相应的方法。但是如果你仔细观察,你会发现在这个模式中,你是在试图封装命令对象。换句话说,您正在从不同的角度实现封装。

    当您处理一组复杂的命令时,这种方法是有意义的。

    现在让我们再看一遍这些条款。您创建命令对象将它们发送给接收者并调用一些方法。但是您通过调用程序来执行这些命令,调用程序调用命令对象的方法(例如,executeCommand)。但是对于一个简单的例子,这个 invoker 类不是强制的;例如,考虑这样一种情况,其中一个命令对象只有一个方法要执行,而您正试图免除调用程序来调用该方法。但是,当您希望跟踪日志文件(或队列)中的多个命令时,调用程序可能会发挥重要作用。

  3. 你为什么对跟踪这些日志感兴趣?

    如果您想要执行撤消或重做操作,它们会很有用。

  4. 指挥模式的主要优势是什么?

    • 创建请求和最终执行是分离的。客户端可能不知道调用者如何执行操作。

    • 您可以创建宏(命令序列)。

    • 可以在不影响现有系统的情况下添加新命令。

    • 最重要的是,您可以支持撤销/重做操作。

  5. 与指挥模式相关的挑战是什么?

    • 为了支持更多的命令,您需要创建更多的类。因此,随着时间的推移,维护可能会很困难。

    • 当出现错误情况时,如何处理错误或决定如何处理返回值变得很棘手。客户可能想知道这些。但是这里您将命令与客户端代码解耦,所以这些情况很难处理。在多线程环境中,调用者也在不同的线程中运行,这一挑战变得很大。

***Command Pattern Q&As***
***A simple demo with undo supported operations***

-----Testing operations in Receiver1-----
Receiver1 initialized with 10
The objects of receiver1 cannot set beyond 10
*Testing single do/undo operation*
Received an addition request.
10 + 2 =12
Received an undo addition request.
12 - 2 =10
     Undo request processed.
_______
**Testing a series of do/undo operations**
Received an addition request.
10 + 2 =12
Received an addition request.
12 + 2 =14
Received an undo addition request.
14 - 2 =12
     Undo request processed.
Received an undo addition request.
12 - 2 =10
     Undo request processed.
Received an undo addition request.
Nothing more to undo...

-----Testing operations in Receiver2-----
Receiver2 initialized
*Testing single do/undo operation*
Received a system power on request.
System is starting up.
Received a undo request.
System is currently powered on.
     Undo request processed.System is switched off now.
_______
**Testing a series of do/undo operations**
Received a system power on request.
System is starting up.
Received a system power on request.
System is already running.So, power on request is ignored.
Received a undo request.
System is currently powered on.
     Undo request processed.System is switched off now.
Received a undo request.
System is switched off at present.
     Undo request processed.System is powered on now.
Received a undo request.
System is currently powered on.
     Undo request processed.System is switched off now

.

十八、迭代器模式

本章涵盖了迭代器模式。

GoF 定义

提供一种方法来顺序访问聚合对象的元素,而不暴露其底层表示。

概念

使用迭代器,客户机对象可以遍历容器(或对象集合)来访问其元素,而不需要知道这些数据是如何在内部存储的。当您需要以标准和统一的方式遍历不同种类的集合对象时,这个概念非常有用。以下是关于这种模式的一些要点。

img/395506_2_En_18_Fig1_HTML.jpg

图 18-1

迭代器模式的示例图

  • 它通常用于遍历树状结构的节点。因此,在许多场景中,您可能会注意到组合模式中迭代器模式的使用。

  • 迭代器的作用不仅限于遍历。这个角色可以改变以支持各种需求。

  • 客户端看不到实际的遍历机制。客户端程序只使用本质上是公共的迭代器方法。

  • 图 18-1 显示了一个迭代器模式的示例图。

与会者如下:

  • 迭代器:访问或遍历元素的接口。

  • 具体迭代器:实现迭代器接口方法。它还可以跟踪聚合遍历中的当前位置。

  • Aggregate :定义一个可以创建迭代器对象的接口。

  • ConcreteAggregate :实现聚合接口。它返回 ConcreteIterator 的一个实例。

真实世界的例子

假设有两家公司:A 公司和 b 公司。A 公司存储其员工记录(即姓名、地址、工资明细等。)在链表数据结构中。B 公司将其员工数据存储在一个数组数据结构中。一天,这两家公司决定合并成一个大组织。迭代器模式在这种情况下非常方便,因为开发人员不想从头开始编写代码。他们可以创建一个公共接口,这样他们就可以访问两家公司的数据,并以统一的方式调用这些方法。

考虑另一个例子。假设你的公司决定根据员工的表现提升他们。所以,所有的经理聚在一起,为晋升制定一个共同的标准。然后,他们遍历每个员工的过去记录,以标记潜在的晋升候选人。

最后,当您将歌曲存储在您喜欢的音频设备中时,例如 MP3 播放器或移动设备,您可以通过各种按钮按压或滑动动作来迭代它们。基本思想是为您提供一些机制来平滑地迭代您的列表。

计算机世界的例子

同样,让我们假设,一个大学艺术系正在使用数组数据结构来维护其学生的记录。科学系使用链表数据结构来保存学生的记录。行政部门不关心不同的数据结构,他们只是对从每个部门获取数据感兴趣,并且他们希望以通用的方式访问数据。

注意

Java 的集合框架中的迭代器类就是迭代器的例子。当您使用像 java.util.Iterator 或 java.util.Enumeration 这样的接口时,您基本上使用了这种模式。java.util.Scanner 类也遵循这种模式。如果你熟悉 C#,你可以使用 Visual Studio 2005 中引入的 C# 自己的迭代器。foreach 语句经常在此上下文中使用。

说明

在这一章中,迭代器模式有三种不同的实现。我将从一个遵循该模式核心理论的例子开始。在下一个例子中,我将使用 Java 对迭代器模式的内置支持来修改这个例子。在第三个也是最后一个例子中,您将这个模式用于不同的数据结构。在前两个例子中,我将简单地使用“字符串”数据类型,但在最后一个例子中,我将使用复杂的数据类型。

在开始之前,我建议您注意一下 Package Explorer 视图中的结构,以便立即参考。

在第一个实现中,让我们假设在一个特定的学院中,一个艺术系的学生需要学习四篇论文(或科目)——英语、历史、地理和心理学。这些论文的细节存储在一个数组数据结构中。你的工作是用迭代器打印课程表。

假设您的迭代器目前支持四种基本方法:first()next()currentItem()hasNext()

  • 在开始遍历数据结构之前,first()方法将指针重置为指向第一个元素。

  • next()方法返回容器中的下一个元素。

  • currentItem()方法返回迭代器在特定时间点指向的容器的当前元素。

  • hasNext()验证下一个元素是否可用于进一步处理。因此,它可以帮助您确定是否已经到达容器的末尾。

类图

图 18-2 显示了类图。

img/395506_2_En_18_Fig2_HTML.jpg

图 18-2

类图

注意

像本书中前面的许多例子一样,为了展示一个清晰的类图,我只展示了客户端代码依赖。对于 Eclipse 编辑器中显示的任何 ObjectAid 类图,您总是可以通过选择图中的一个元素,右键单击它,然后选择 Add ➤依赖项来查看其他依赖项。

包资源管理器视图

图 18-3 显示了程序的高层结构。

img/395506_2_En_18_Fig3_HTML.jpg

图 18-3

包资源管理器视图

首次实现

这是第一个实现。

package jdp2e.iterator.demo;

interface Subjects
{
    Iterator createIterator();
}
class Arts implements Subjects
{
    private String[] papers;

    public Arts()
    {
        papers = new String[] { "English","History", "Geography","Psychology" };
    }

    public Iterator createIterator()
    {
        return new ArtsIterator(papers);
    }
}
interface Iterator
{
    void first();//Reset to first element
    String next();//To get the next element
    String currentItem();//To retrieve the current element
    boolean hasNext();//To check whether there is any next element or not.
}
class ArtsIterator implements Iterator
{
    private String[] papers;
    private int position;
    public ArtsIterator(String[] papers)
    {
        this.papers = papers;
        position = 0;
    }
    @Override
    public void first()
    {
        position = 0;
    }
    @Override
    public String next()
    {
        //System.out.println("Currently pointing to: "+ this.currentItem())

;
        return papers[position++];
    }
    @Override
    public String currentItem()
    {
        return papers[position];
    }
    @Override
    public boolean hasNext()
    {
        if(position >= papers.length)
            return false;
        return true;
    }
}

public class IteratorPatternExample {

    public static void main(String[] args) {
        System.out.println("***Iterator Pattern Demo***");
        Subjects artsSubjects = new Arts();

        Iterator iteratorForArts = artsSubjects.createIterator();
        System.out.println("\n Arts subjects are as follows:");
        while (iteratorForArts.hasNext())
        {
            System.out.println(iteratorForArts.next());
        }
        //Moving back to first element
        iteratorForArts.first();
        System.out.println(" Currently pointing back to: "+ iteratorForArts.currentItem());
    }

}

输出

这是输出。

***Iterator Pattern Demo***

 Arts subjects are as follows:
English
History
Geography
Psychology
 Currently pointing back to: English

注意

如果想看到迭代器所指向的当前元素,可以在 next()方法:// System.out.println("当前指向:"+ this.currentItem())中取消对该行的注释;

现在让我们使用 Java 的内置迭代器接口来修改前面的实现。

第二个实现方案的主要特征

我使用了 Java 对迭代器模式的内置支持。请注意,在程序的开头包含了下面一行。

                        import java.util.Iterator;

如果你打开源代码,你会看到这个接口有三个方法:hasNext()next()remove()。但是remove()方法已经有了默认的实现。因此,在下面的例子中,我只需要重写hasNext()next()方法。

这里使用的是 Java 的迭代器接口,所以不需要定义自己的迭代器接口。

在这个修改的实现中,关键的变化以粗体显示。

第二次实现

下面是第二个实现。

package jdp2e.iterator.modified.demo;

import java.util.Iterator;

interface Subjects
{
    //Iterator CreateIterator();
    ArtsIterator createIterator();
}
class Arts implements Subjects
{
    private String[] papers;

    public Arts()
    {
        papers = new String[] { "English","History", "Geography","Psychology" };
    }

    //public Iterator CreateIterator()
    public ArtsIterator createIterator()
    {
        return new ArtsIterator(papers);
    }
}

class ArtsIterator implements Iterator<String>

{
    private String[] papers;
    private int position;
    public ArtsIterator(String[] papers)
    {
        this.papers = papers;
        position = 0;
    }
    public void first()
    {
        position = 0;
    }
    public String currentItem()
    {
        return papers[position];
    }
    @Override
    public boolean hasNext()
    {
        if(position >= papers.length)
            return false;
        return true;
    }
    @Override
    public String next()
    {
        return papers[position++];
    }
}

public class ModifiedIteratorPatternExample {

    public static void main(String[] args) {
        System.out.println("***Modified Iterator Pattern Demo.***");
        Subjects artsSubjects = new Arts();

        //Iterator IteratorForArts = artsSubjects.createIterator();
        ArtsIterator iteratorForArts = artsSubjects.createIterator();
        System.out.println("\nArts subjects are as follows:");
        while (iteratorForArts.hasNext())
        {
            System.out.println(iteratorForArts.next());
        }
        //Moving back to first element
        iteratorForArts.first();
        System.out.println("Currently pointing to: "+ iteratorForArts.currentItem());
    }
}

输出

这是修改后的输出。

***Modified Iterator Pattern Demo.***

Arts subjects are as follows:
English
History
Geography
Psychology
Currently pointing to: English

问答环节

  1. 迭代器模式有什么用?

    • 你可以遍历一个对象结构而不知道它的内部细节。因此,如果您有一个不同的子集合的集合(例如,您的容器与数组、列表或链表等混合在一起。),您仍然可以遍历整个集合,并以通用的方式处理元素,而无需了解内部细节或它们之间的差异。

    • 您可以用不同的方式遍历集合。您还可以提供同时支持多个遍历的实现。

  2. 与此模式相关的主要挑战是什么?

    理想情况下,在遍历/迭代过程中,您不应该对核心架构进行任何意外的修改。

  3. 但是为了应对前面提到的挑战,你可以做一个备份,然后继续。这是正确的吗?

    制作备份并在以后重新检查是一项成本高昂的操作。

  4. 在整个讨论过程中,你一直在谈论收藏。什么是收藏?

    它是在一个单元中呈现的一组单独的对象。您可能经常会在 java 程序中看到 java.util.Collection、java.util.Map 等接口的使用。这些是 Java 集合类的一些通用接口,它们是在 JDK 1.2 中引入的。

    在集合之前,您可以选择数组、向量等来存储或操作一组对象。但是这些类没有一个公共接口;访问数组元素的方式与访问向量元素的方式截然不同。这就是为什么很难编写一个通用算法来访问这些不同实现中的不同元素。此外,这些方法中有许多是最终的,所以您不能扩展它们。

    收集框架的引入就是为了解决这些困难。同时,他们提供了高性能的实现,使程序员的生活更加轻松。

  5. 在修改后的实现中,为什么我看不到 first()和 currentItem()方法的@Override 注释?

    java.util.Iterator 接口中没有这两个方法。内置的迭代器接口有hasNext()next()方法。因此,我对这些方法使用了@Override 注释。在这个接口中还有另一个方法remove()。它有一个默认的实现。因为我没有使用过它,所以我不需要修改这个方法。

  6. 在这些实现中,我发现你只使用数组字符串来存储和操作数据。能否展示一个使用相对复杂的数据类型和不同数据结构的迭代器模式实现?

    To make these examples simple and straightforward, I only used strings and an array data structure. You can always choose your preferred data structure and apply the same process when you consider a complex data type. For example, consider the following illustration (third implementation) with these key characteristics.

    • 这里我使用了相对复杂的数据类型 Employee。每个 employee 对象有三样东西:一个名字、一个标识号(id)和薪水。

    • 在下面的实现中,我没有使用数组,而是使用了不同的数据结构 LinkedList。因此,我需要在这个实现中包含下面一行。

      import java.util.LinkedList;

    • 我采用了与前一个例子中相同的方法。

第三次实现

下面是第三个实现。

package jdp2e.iterator.questions_answers;
import java.util.Iterator;
import java.util.LinkedList;

class Employee
{
    private String name;
    private int id;
    private double salary;
    public Employee(String name, int id, double salary )
    {
        this.name=name;
        this.id=id;
        this.salary=salary;
    }

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public double getSalary() {
        return salary;
    }

    public void setSalary(double salary) {
        this.salary = salary;
    }

    @Override
    public String toString(){
        return "Employee Name: "+this.getName()+", ID: "+this.getId()+ " and salary: "+this.getSalary()+"$";
    }
}
interface DataBase
{
    EmployeeIterator createIterator();
}
class EmployeeDatabase implements DataBase
{
    private LinkedList<Employee> employeeList;

    public EmployeeDatabase()
    {
        employeeList = new LinkedList<Employee>();
        employeeList.add(new Employee("Ron",1, 1000.25));
        employeeList.add(new Employee("Jack",2, 2000.5));
        employeeList.add(new Employee("Ambrose",3, 3000.75));
        employeeList.add(new Employee("Jian",4, 2550.0));
        employeeList.add(new Employee("Alex",5, 753.83));
    }
    public EmployeeIterator createIterator()
    {
        return new EmployeeIterator(employeeList);
    }
}

class EmployeeIterator implements Iterator<Employee>
{
    private LinkedList<Employee> employeeList;
    private int position;
    public EmployeeIterator(LinkedList<Employee> employeeList)
    {
        this.employeeList= employeeList;
        position = 0;
    }
    //@Override
    public void first()
    {
        position = 0;
    }

    //@Override
    public Employee currentItem()
    {
        return employeeList.get(position);
    }

    @Override
    public Employee next()
    {
        return employeeList.get(position++);
    }
    @Override
    public boolean hasNext() {
        if(position >= employeeList.size())
            return false;
        return true;
    }
}

public class ModifiedIteratorPatternExample2 {

    public static void main(String[] args) {
        System.out.println("***Modified Iterator Pattern Demo.Example-2.***");
        DataBase employeesList = new EmployeeDatabase();

        EmployeeIterator iteratorForEmployee = employeesList.createIterator();
        System.out.println("\n -----Employee details are as follows-----\n");

        while (iteratorForEmployee.hasNext())
        {
            System.out.println(iteratorForEmployee.next());
        }

    }
}

输出

这是第三个实现的输出。

***Modified Iterator Pattern Demo.Example-2.***

 -----Employee details are as follows-----

Employee Name: Ron, ID: 1 and salary: 1000.25$
Employee Name: Jack, ID: 2 and salary: 2000.5$
Employee Name: Ambrose, ID: 3 and salary: 3000.75$
Employee Name: Jian, ID: 4 and salary: 2550.0$
Employee Name: Alex, ID: 5 and salary: 753.83$

注意

您可以在一个实现中使用两种或多种不同的数据结构来展示这种模式的强大功能。您已经看到,在这些不同的实现中,我使用了 first()、next()、hasNext()和 currentItem()方法,它们的不同实现因其内部数据结构而有所不同。

十九、备忘录模式

这一章涵盖了备忘录模式。

GoF 定义

在不违反封装的情况下,捕获并具体化一个对象的内部状态,以便该对象可以在以后恢复到这个状态。

概念

在您的应用程序中,您可能需要支持“撤销”操作。为了实现这一点,你需要记录一个对象的内部状态。因此,您必须将此状态信息保存在一个可以再次引用的位置,以恢复对象的旧状态。但是一般来说,对象封装了它们的状态,这些状态对于外部世界是不可访问的。因此,如果您公开状态信息,那么您就违反了封装。

memento 的字典意思是(对过去事件的)提醒。因此,您可以猜测使用这种模式,您可以将对象恢复到它以前的状态,但是它确保您在不违反封装的情况下实现您的目标。

真实世界的例子

这方面的一个经典例子是有限状态机。这是一个数学模型,但它的一个简单应用可以在十字转门中找到。它有旋转臂,最初是锁定的。如果你被允许通过它(例如,当你插入硬币或当安检人员允许你通过安检时),锁就会被打开。一旦你通过,十字转门又回到锁定状态。

计算机世界的例子

在绘图应用程序中,您可能需要恢复到以前的状态。

注意

当您考虑 JTextField 类时,您会注意到类似的模式,该类扩展了 javax . swing . text . jtextcomponent 抽象类并提供了撤销支持机制。这里 javax.swing.undo.UndoManager 可以充当看管人,javax.swing.undo. UndoableEdit 的实现可以充当备忘录,javax.swing.text.Document 的实现可以充当发起人。你将很快了解到发起者、看管者和备忘录这些术语。此外,java.io.Serializable 通常被称为 memento 的一个例子,但是尽管您可以序列化 memento 对象,但它并不是 memento 设计模式的强制要求。

说明

仔细阅读代码,并按照注释进行操作,以便随时参考。在这个例子中,涉及三个对象:备忘录、发起者和看护者。(这些名称非常常见,所以我在我们的实现中保留了相同的命名约定。)

发起者对象有一个内部状态。客户端可以在其中设置状态。一个备忘录对象可以存储或多或少的发起者状态,由发起者决定。当看管人想要记录发起者的状态时,它向其请求当前状态。因此,它首先向发起者请求备忘录对象。

在下面的示例中,看守对象通过显示控制台消息来确认保存操作。假设客户端进行了一些更改,然后想要恢复到以前的状态。因为发起者对象的状态已经改变,所以回滚到以前的状态需要管理员对象的帮助,管理员对象先前保存了状态。看管人对象将备忘录对象(具有先前状态)返回给发起者。备忘录对象本身是不透明的对象(不允许管理员对其进行任何改变,并且理想地,只有创建该备忘录的发起者可以访问该备忘录的内部状态)。

因此,您可以得出结论,看守者对备忘录的视图/界面很窄,因为它只能将其传递给其他对象。相反,发起者看到的是宽接口,因为它可以访问返回到先前状态所需的数据。

类图

图 19-1 为类图。

img/395506_2_En_19_Fig1_HTML.jpg

图 19-1

类图

包资源管理器视图

图 19-2 显示了程序各部分的高层结构。

img/395506_2_En_19_Fig2_HTML.jpg

图 19-2

包资源管理器视图

履行

下面是实现。

package jdp2e.memento.demo;

class Memento
{
    private int stateId;
    public Memento(int stateId)
    {
        this.stateId = stateId;
    }
    public int getStateId() {
        return stateId;
    }
    /*This class does not have the
    setter method.We need to use this class
    to get the state of the object only.*/

    /*public void setState(String state) {
        this.state = state;
    }*/

}

/*
The 'Originator' class
WikiPedia notes( for your reference):
Make an object (originator) itself responsible for:
1.Saving its internal state to a(memento) object and
2.Restoring to a previous state from a(memento) object.
3.Only the originator that created a memento is allowed to access it

.
 */
class Originator
{
    private int stateId;
    public Originator()
    {
        this.stateId = 0;
        System.out.println(" Originator is created with state id : "+ stateId);
    }

    public int getStateId()
    {
        return stateId;
    }

    public void setStateId(int stateId)
    {
        System.out.println(" Setting the state id of the originator to : "+ stateId);
        this.stateId= stateId;
    }
    //Saving its internal state to a(memento) object
    public Memento saveMemento(int stateId)
    {
        System.out.println(" Saving originator's current state id. ");
        //Create memento with the current state and return it.
        return new Memento(stateId);
    }

    //Restoring to a previous state from a(memento) object

.
    public void revertMemento(Memento previousMemento)
    {
        System.out.println(" Restoring to state id..."+ previousMemento.getStateId());
        this.stateId = previousMemento.getStateId();
        System.out.println(" Current state id of originator : "+ stateId);
    }
}
/*
The 'Caretaker' class.
WikiPedia notes( for your reference):
1.A client (caretaker) can request a memento from the originator  to save the internal state of the originator and
2.Pass a memento back to the originator (to restore to a previous state)
This enables to save and restore the internal state of an originator without violating its encapsulation.
 */
public class MementoPatternExample {

    public static void main(String[] args) {
        System.out.println("***Memento Pattern Demo***\n");
        //Originator is initialized with a state
        Originator originatorObject = new Originator();
        Memento mementoObject;
        originatorObject.setStateId(1);
        // A client (caretaker) can request a memento from the originator
        //to save the internal state of the originator
        mementoObject=originatorObject.saveMemento(originatorObject.getStateId());
        System.out.println(" Snapshot #1: Originator's current state id is saved in caretaker.");
        //A client (or caretaker) cannot set/modify the memento's state
        //mementoObject.setState("arbitratyState");//error

        //Changing the state id of Originator
        originatorObject.setStateId(2);
        mementoObject=originatorObject.saveMemento(originatorObject.getStateId());
        System.out.println(" Snapshot #2: Originator's current state id is saved in caretaker.");

        //Changing the state id of Originator again

.
        originatorObject.setStateId(3);
        //Reverting back to previous state id.
        originatorObject.revertMemento(mementoObject);
    }

}

输出

这是输出。

***Memento Pattern Demo***

 Originator is created with state id : 0
 Setting the state id of the originator to : 1
 Saving originator's current state id.
 Snapshot #1: Originator's current state id is saved in caretaker.
 Setting the state id of the originator to : 2
 Saving originator's current state id.
 Snapshot #2: Originator's current state id is saved in caretaker.
 Setting the state id of the originator to : 3
 Restoring to state id...2
 Current state id of originator : 2

注意

如果您处理一个可变引用类型的状态,您可能需要做一个深度复制来将状态存储在 Memento 对象中。

问答环节

  1. 我可以恢复以前的快照/还原点。但是在现实生活中,我可能有多个恢复点。如何使用这种设计模式来实现呢?

    在这种情况下,可以使用数组列表。考虑下面的程序。

    发起者职业和备忘录职业和以前一样,所以我只展示修改后的看守者职业。我在即将到来的实现中使用了下面的代码行。

List<Memento> savedStateIds = new ArrayList<Memento>();

所以,你需要在开头包含这两行代码。

import java.util.ArrayList;
import java.util.List;

改良看守级

这是修改后的看守者类。

   /*
The modified 'Caretaker' class.
WikiPedia notes( for your reference):
1.A client (caretaker) can request a memento from the originator to save the internal state of the originator and
2.Pass a memento back to the originator (to restore to a previous state)
This enables to save and restore the internal state of an originator
without violating its encapsulation.
 */

public class ModifiedMementoPatternExample {

    public static void main(String[] args) {
        System.out.println("***Modified Memento Pattern Demo***\n");
        List<Memento> savedStateIds = new ArrayList<Memento>();
        //Originator is initialized with a state
        Originator originatorObject = new Originator();
        Memento mementoObject;
        originatorObject.setStateId(1);
        mementoObject=originatorObject.saveMemento(originatorObject.getStateId());
        savedStateIds.add( mementoObject);
        System.out.println(" Snapshot #1: Originator's current state id is saved in caretaker.");
        //A client or caretaker cannot set/modify the memento's state
        //mementoObject.setState("arbitratyState");//error

        //Changing the state id of Originator
        originatorObject.setStateId(2);
        mementoObject=originatorObject.saveMemento(originatorObject.getStateId());
        savedStateIds.add( mementoObject);
        System.out.println(" Snapshot #2: Originator's current state id is saved in caretaker.");

        //Changing the state id of Originator
        originatorObject.setStateId(3);
        mementoObject=originatorObject.saveMemento(originatorObject.getStateId());
        savedStateIds.add( mementoObject);
        System.out.println(" Snapshot #3: Originator's current state id is saved in caretaker (client).");

        //Reverting back to previous state id.
        //originatorObject.revertMemento(mementoObject);
        //Reverting back to specific id -say, Snapshot #1)
        //originatorObject.revertMemento(savedStateIds.get(0));

        //Roll back everything...
        System.out.println("Started restoring process...");
        for (int i = savedStateIds.size(); i > 0; i--)
        {
            originatorObject.revertMemento(savedStateIds.get(i-1));
        }
    }
}

修改输出

一旦您运行这个修改后的程序,您将获得以下输出。

***Modified Memento Pattern Demo***

 Originator is created with state id : 0
 Setting the state id of the originator to : 1
 Saving originator's current state id.
 Snapshot #1: Originator's current state id is saved in caretaker.
 Setting the state id of the originator to : 2
 Saving originator's current state id.
 Snapshot #2: Originator's current state id is saved in caretaker.
 Setting the state id of the originator to : 3
 Saving originator's current state id.
 Snapshot #3: Originator's current state id is saved in caretaker (client).
Started restoring process...
 Restoring to state id...3
 Current state id of originator : 3
 Restoring to state id...2
 Current state id of originator : 2
 Restoring to state id...1
 Current state id of originator : 1

分析

在这个修改过的程序中,你可以看到三种不同的“撤销”操作。

  • 您可以返回到上一个还原点。

  • 您可以返回到指定的还原点。

  • 您可以恢复到所有还原点。

要查看案例 1 和案例 2,请取消前面实现中的注释行。

  1. 在许多应用程序中,我注意到 memento 类是作为 Originator 的内部类出现的。你为什么不采用这种方法呢?

    memento 设计模式可以用许多不同的方式实现(例如,使用包私有可见性或使用对象序列化技术)。但是在每种情况下,如果您分析关键目标,您会发现一旦 memento 实例由发起者创建,除了它的创建者之外,其他任何人都不允许访问内部状态(这包括管理员/客户)。管理员的工作是存储备忘录实例(在我们的例子中是还原点),并在您需要时提供它们。所以,如果你的 memento 类是公开的,也没有什么坏处。您可以阻止 memento 的公共 setter 方法。我认为这已经足够了。

  2. 但是你还在使用 getter 方法 getStateId()。不违反封装吗?

    There is a lot of discussion and debate around this area—whether you should use getter/setter or not, particularly when you consider encapsulation. I believe that it depends on the amount of strictness that you want to impose. For example, if you just provide getter/setters for all fields without any reason, that is surely a bad design. The same thing applies when you use all the public fields inside the objects. But sometimes the accessor methods are required and useful. In this book, my aim is to encourage you learn design patterns with simple examples. If I need to consider each and every minute detail such as this, you may lose interest. So, in these examples, I show a simple way to promote encapsulation using the memento pattern. But, if you want to be stricter, you may prefer to implement the memento class as an inner class of the originator and modify the initial implementation, like in the following.

    package jdp2e.memento.questions_answers;
    
    /*
    The 'Originator' class
    WikiPedia notes( for your reference):
    Make an object (originator) itself responsible for:
    1.Saving its internal state to a(memento) object and
    2.Restoring to a previous state from a(memento) object.
    3.Only the originator that created a memento is allowed to access it.
     */
    class Originator
    {
        private int stateId;
        Memento myMemento;
        public Originator()
        {
            this.stateId = 0;
            System.out.println(" Originator is created with state id : "+ stateId);
        }
    
        public int getStateId()
        {
            return stateId;
        }
    
        public void setStateId(int stateId)
        {
            System.out.println(" Setting the state id of the originator to : "+ stateId);
            this.stateId= stateId;
        }
        //Saving its internal state to a(memento) object
        public Memento saveMemento()
        {
            System.out.println(" Saving originator's current state id. ");
            //Create memento with the current state and return it.
            return new Memento(this.stateId);
        }
    
        //Restoring to a previous state from a(memento) object.
        public void revertMemento(Memento previousMemento)
        {
            System.out.println(" Restoring to state id..."+ previousMemento.getStateId());
            this.stateId = previousMemento.getStateId();
            System.out.println(" Current state id of originator : "+ stateId);
        }
        //A memento class implemented as an inner class of Originator
        static class Memento
        {
            private int stateId;
            public Memento(int stateId)
            {
                this.stateId = stateId;
            }
            //Only outer class can access now
            public int getStateId() {
                return stateId;
            }
            /*This class does not have the
            setter method.We need to use this class
            to get the state of the object only.*/
    
            /*public void setState(String state) {
                this.state = state;
            }*/
    
        }
    }
    /*
    The 'Caretaker' class.
    WikiPedia notes( for your reference):
    1.A client (caretaker) can request a memento from the originator
    to save the internal state of the originator and
    2.Pass a memento back to the originator (to restore to a previous state)
    This enables to save and restore the internal state of an originator without violating its encapsulation.
     */
    public class MementoAsInnerClassExample {
    
        public static void main(String[] args) {
            System.out.println("***Memento Pattern Demo***\n");
            //Originator is initialized with a state
            Originator originatorObject = new Originator();
            Originator.Memento mementoObject;
            originatorObject.setStateId(1);
            // A client (caretaker) can request a memento from the originator
            //to save the internal state of the originator
            mementoObject=originatorObject.saveMemento();
            System.out.println(" Snapshot #1: Originator's current state id is saved in caretaker.");
            //A client (or caretaker) cannot set/modify the memento's state
    
            //Changing the state id of Originator
            originatorObject.setStateId(2);
            mementoObject=originatorObject.saveMemento();
            System.out.println(" Snapshot #2: Originator's current state id is saved in caretaker.");
    
            //Changing the state id of Originator again.
            originatorObject.setStateId(3);
            //Reverting back to previous state id.
            originatorObject.revertMemento(mementoObject);
        }
    
    }
    
    
  3. 使用备忘录设计模式的主要优势是什么?

    • 最大的优点是您可以随时丢弃不需要的更改,并将其恢复到预期的或稳定的状态。

    • 您不会损害与参与此模型的关键对象相关联的封装。

    • 保持高度凝聚力。

    • 提供了一种简单的恢复技术。

  4. 有哪些关键的 挑战 与备忘录设计模式相关联?

    • 大量的备忘录需要更多的存储空间。同时,它们给看护者增加了额外的负担。

    • 前面一点同时增加了维护成本。

    • 你不能忽略保存这些状态的时间。保存状态的额外时间降低了系统的整体性能。

注意在 C# 或 Java 这样的语言中,开发人员可能更喜欢序列化/反序列化技术,而不是直接实现 memento 设计模式。这两种技术各有利弊。但是您也可以在应用程序中结合使用这两种技术。

  1. 在这些实现中,如果你公开发起者的状态,那么我们的客户也可以直接访问这些状态。这是正确的吗?

    是的。但你不应该试图打破封装。注意 GoF 定义的开头是“不违反封装 …”

  2. 在这些实现中,memento 类没有公共的 setter 方法。这背后的原因是什么?

    再过一遍问题 2 的答案。并阅读代码中的注释,“只有创建备忘录的发起人才能访问它。”因此,如果您没有为 memento 类提供一个公共 setter 方法,那么管理员或客户就不能修改由发起者创建的 memento 实例。

  3. In these implementations, you could ignore the getter method of the memento by using package-private visibility for stateId. For example, you could code memento class like the following.

    class Memento
    {
        //private int stateId;
        int stateId;//←-Change is here
        public Memento(int stateId)
        {
            this.stateId = stateId;
        }
        public int getStateId() {
            return stateId;
        }
        /*This class does not have the
        setter method.We need to use this class
        to get the state of the object only.*/
    
        /*public void setState(String state) {
            this.state = state;
        }*/
    
    }
    
    

    And then you can use the following line.

            //System.out.println(" Restoring to state id..."+ previousMemento.getStateId());
            System.out.println(" Restoring to state id..."+ previousMemento.stateId);//←The change is shown in bold
    
    

    这是正确的吗?

    是的。在许多应用程序中,其他类(除了发起者)甚至不能读取备忘录的状态。当您使用包私有可见性时,您不需要任何访问器方法。换句话说,在这种情况下,您只是使用默认修饰符。

    因此,这种可见性比私有可见性稍微开放一些,同一个包中的其他类可以访问类成员。因此,在这种情况下,预期的类需要放在同一个包中。同时,您需要接受同一包中的所有其他类都可以直接访问这个状态。因此,当您将类放入您的特殊包中时,您需要足够小心。

  4. 我很困惑。为了支持撤销操作,我应该选择哪种模式——memento 还是 command?

    GoF 告诉我们这些是相关的模式。这主要取决于你想如何处理这种情况。例如,假设您正在将 10 加到一个整数上。添加之后,您希望通过执行相反的操作来撤消操作(例如,50 + 10 = 60,所以要返回,您需要执行 60–10 = 50)。在这种类型的操作中,我们不需要存储以前的状态。

    但是考虑一种情况,您需要在操作之前存储对象的状态。在这种情况下,备忘录是你的救星。因此,在绘画应用程序中,您可以避免撤销绘画操作的成本。您可以在执行命令之前存储对象列表。在这种情况下,这个存储的列表可以被视为备忘录。您可以将此列表与相关命令一起保存。我建议你去 www.developer.com/design/article.php/3720566/Working-With-Design-Patterns-Memento.htm 看看网上的好文章。

    因此,应用程序可以使用这两种模式来支持撤销操作。

    最后,您必须记住,在 memento 模式中存储 memento 对象是强制性的,这样您就可以回滚到以前的状态;但是在命令模式中,没有必要存储命令。一旦你执行一个命令,它的工作就完成了。如果您不支持“撤销”操作,您可能根本不会对存储这些命令感兴趣。

  5. 您谈到了首次实现后的深度复制。我为什么需要那个?

    在第二章(原型模式),我讨论了浅层复制和深层复制。可以参考这个讨论,供大家参考。为了回答你的问题,我们用一个简单的例子来分析一下 deep copy 有什么特别之处。考虑下面的例子。

Java 中的浅层拷贝与深层拷贝

你用 Java 中的clone()方法进行克隆,但同时你需要实现 Cloneable 接口(这是一个标记接口),因为实现这个 Cloneable 接口的 Java 对象只有资格进行克隆。默认版本的clone()创建一个浅层拷贝。要创建深层副本,您需要覆盖clone()方法。

以下计划的主要特征

在下面的示例中,您有两个类:Employee 和 EmpAddress。

Employee 类有三个字段:id、name 和 EmpAddress。因此,您可能会注意到,要形成 Employee 对象,您还需要传递一个 EmpAddress 对象。因此,在下面的示例中,您会注意到这一行代码:

Employee emp=new Employee(1,"John",initialAddress);

EmpAddress 只有一个名为 Address 的字段,它是一个字符串数据类型。

在客户机代码中,创建一个 Employee 对象 emp,然后通过克隆创建另一个对象 empClone。因此,您会注意到如下代码行:

Employee empClone=(Employee)emp.clone();

然后,更改 emp 对象的字段值。但是作为这种改变的副作用,empClone 对象的地址也改变了,但是这不是所希望的。

履行

下面是实现。

package jdp2e.memento.questions_answers;
class EmpAddress implements Cloneable
{
    String address;
    public EmpAddress(String address)
    {
        this.address=address;
    }
    public String getAddress()
    {
        return address;
    }
    public void setAddress(String address)
    {
        this.address = address;
    }
    @Override
    public String toString()
    {
        return  this.address

;
    }
    @Override
    public Object clone() throws CloneNotSupportedException
    {
        //Shallow Copy
        return super.clone();
    }
}
class Employee implements Cloneable
{
    int id;
    String name;
    EmpAddress empAddress;
    public Employee(int id,String name,EmpAddress empAddress)
    {
        this.id=id;
        this.name=name;
        this.empAddress=empAddress;
    }
    public int getId()
    {
        return id;
    }
    public void setId(int id)
    {
        this.id = id;
    }
    public String getName()
    {
        return name;
    }
    public void setName(String name)
    {
        this.name = name;
    }
    public EmpAddress getAddress()
    {
        return this.empAddress;
    }
    public void setAddress(EmpAddress newAddress) 

    {
        this.empAddress=newAddress;
    }
    @Override
    public String toString()
    {
        return "EmpId=" +this.id+ " EmpName="+ this.name+ " EmpAddressName="+ this.empAddress;
    }
    @Override
    public Object clone() throws CloneNotSupportedException
    {
        //Shallow Copy
        return super.clone();
    }
}

public class ShallowVsDeepCopy {

    public static void main(String[] args) throws CloneNotSupportedException  {
        System.out.println("***Shallow vs Deep Copy Demo***\n");
        EmpAddress initialAddress=new EmpAddress("21, abc Road, USA");
        Employee emp=new Employee(1,"John",initialAddress);
        System.out.println("emp1 object is as follows:");
        System.out.println(emp);
        Employee empClone=(Employee)emp.clone();
        System.out.println("empClone object is as follows:");
        System.out.println(empClone);
        System.out.println("\n Now changing the name, id and address of the emp object ");
        emp.setId(10);
        emp.setName("Sam");
        emp.empAddress.setAddress("221, xyz Road, Canada");
        System.out.println("Now emp1 object is as follows:");
        System.out.println(emp);
        System.out.println("And emp1Clone object is as follows:");
        System.out.println(empClone);
    }

}

输出

这是输出。

***Shallow vs Deep Copy Demo***

emp1 object is as follows:
EmpId=1 EmpName=John EmpAddressName=21, abc Road, USA
empClone object is as follows:
EmpId=1 EmpName=John EmpAddressName=21, abc Road, USA

 Now changing the name and id of emp object
Now emp1 object is as follows:
EmpId=10 EmpName=Sam EmpAddressName=221, xyz Road, Canada
And emp1Clone object is as follows:
EmpId=1 EmpName=John EmpAddressName=221, xyz Road, Canada

分析

注意输出的最后一行。你会看到不必要的副作用。由于对 emp 对象的修改,克隆对象的地址被修改。这是因为原始对象和克隆对象都指向同一个地址,并且它们不是 100%分离的。图 19-3 描述了该场景。

img/395506_2_En_19_Fig3_HTML.jpg

图 19-3

浅拷贝

所以,现在让我们来试验一个深度拷贝实现。让我们修改 Employee 类的克隆方法,如下所示。

@Override
    public Object clone() throws CloneNotSupportedException
    {
        //Shallow Copy
        //return super.clone();

        //For deep copy
        Employee employee = (Employee)  super.clone();
        employee.empAddress = (EmpAddress) empAddress.clone();
        return employee;
    }

修改输出

下面是修改后的输出。

***Shallow vs Deep Copy Demo***

emp1 object is as follows:
EmpId=1 EmpName=John EmpAddressName=21, abc Road, USA
empClone object is as follows:
EmpId=1 EmpName=John EmpAddressName=21, abc Road, USA

 Now changing the name, id and address of the emp object
Now emp1 object is as follows:

EmpId=10 EmpName=Sam EmpAddressName=221, xyz Road, Canada

And emp1Clone object is as follows:

EmpId=1 EmpName=John EmpAddressName=21, abc Road, USA

分析

注意输出的最后一行。现在,您不会看到由于修改 emp 对象而产生的不必要的副作用。这是因为原始对象和克隆对象完全不同,彼此独立。图 19-4 描述了该场景。

img/395506_2_En_19_Fig4_HTML.jpg

图 19-4

深层拷贝

注意

你在第二章的“问答环节”看到了浅抄和深抄的理论部分。

二十、状态模式

本章介绍了状态模式。

GoF 定义

允许对象在其内部状态改变时改变其行为。该对象看起来会改变它的类。

概念

假设您正在处理一个代码库快速增长的大规模应用程序。结果,情况变得复杂,您可能需要引入大量的if-else blocks/switch 语句来保护各种情况。状态模式适合这样的环境。它允许您的对象基于当前状态有不同的行为,并且您可以用不同的类定义特定于状态的行为。

因此,在这种模式中,您开始考虑应用程序的可能状态,并相应地分离代码。理想情况下,每个状态都独立于其他状态。您跟踪这些状态,并且您的代码根据当前状态的行为做出响应。例如,假设你正在观看一个电视节目。如果您按下电视遥控器上的静音按钮,您会发现电视的状态发生了变化。但是如果电视已经关闭,你就不会注意到任何变化。

因此,基本思想是,如果您的代码可以跟踪应用程序的当前状态,您就可以集中任务,分离您的代码,并相应地做出响应。

真实世界的例子

考虑网络连接的场景 TCP 连接。一个对象可以处于各种状态;例如,连接可能已经建立,连接可能已经关闭,或者对象已经开始通过连接进行侦听。当此连接收到来自其他对象的请求时,它会根据其当前状态做出响应。

交通信号灯或电视(TV)的功能也可以被认为属于这一类。例如,如果电视已经处于打开模式,您可以更换频道。如果它处于关闭模式,它将不响应频道改变请求。

计算机世界的例子

假设您有一个作业处理系统,可以一次处理一定数量的作业。当一个新的作业出现时,要么系统处理该作业,要么它发出信号表明系统正忙于它一次可以处理的最大数量的作业。换句话说,当达到作业处理能力的总数时,系统发送一个繁忙信号。

注意

在 javax.faces.lifecycle 包中,有一个名为 lifecycle 的类。这个类有一个名为 execute(FacesContext context),的方法,在这个方法中,您可能会注意到状态设计模式的一个实现。FacesServlet 可以调用生命周期的 execute 方法,生命周期对象与不同的阶段(状态)进行通信。

说明

下面的实现模拟了电视及其遥控器的功能。假设您有一个支持电视操作的遥控器。您可以简单地假设,在任何给定的时间,电视处于这三种状态之一:开、关或静音。最初,电视处于关闭状态。当您按下遥控器上的 on 按钮时,电视将进入 On 状态。如果您按下静音按钮,它将进入静音状态。

您可以假设,如果您在电视已经处于关闭状态时按下关闭按钮,或者如果您在电视已经处于打开状态时按下打开按钮,或者如果您在电视已经处于静音模式时按下静音按钮,电视的状态不会改变。

如果您按下关闭按钮,电视可以从打开状态进入关闭状态,如果您按下静音按钮,电视将进入静音状态。图 20-1 显示了反映所有这些可能情况的状态图。

img/395506_2_En_20_Fig1_HTML.jpg

图 20-1

电视的不同状态

注意

在此图中,我没有将任何状态标记为最终状态,尽管在此图中,在最后,我关闭了电视。为了使设计简单,我假设如果您在电视已经处于关闭状态时按下关闭按钮,或者如果您在电视已经处于打开状态时按下打开按钮,或者如果您在电视已经处于静音模式时按下静音按钮,则电视的状态不会改变。但在现实世界中,遥控器的工作方式可能会有所不同。例如,如果电视当前处于打开状态,并且您按下了静音按钮,电视可以进入静音模式,然后如果再次按下静音按钮,电视可能会再次回到打开状态。因此,您可能需要添加额外的逻辑来支持这种行为。

关键特征

以下实现的主要特征如下。

  • 对于特定于状态的行为,有单独的类。例如,这里有像开、关和静音这样的类。

  • TV 类是这里的主类(单词 main 并不意味着它包括main()方法),客户端代码只与它对话。用设计模式的术语来说,电视是这里的上下文类。

  • TV 类中定义的操作将行为委托给当前状态的对象实现。

  • PossibleState 是定义当您拥有一个对象时调用的方法/操作的接口。开、关和静音是实现该接口的具体状态。

  • 状态本身触发状态转换(从一个状态到另一个状态)。

类图

图 20-2 为类图。

img/395506_2_En_20_Fig2_HTML.jpg

图 20-2

类图

包资源管理器视图

图 20-3 显示了程序的高层结构。

img/395506_2_En_20_Fig3_HTML.jpg

图 20-3

包资源管理器视图

履行

下面是实现。

package jdp2e.state.demo;
interface PossibleState
{
    void pressOnButton(TV context);
    void pressOffButton(TV context);
    void pressMuteButton(TV context);
}
//Off state
class Off implements  PossibleState
{
    //User is pressing Off button when the TV is in Off state
    @Override
    public void pressOnButton(TV context)
    {
        System.out.println("You pressed On button. Going from Off to On state.");
        context.setCurrentState(new On());
        System.out.println(context.getCurrentState().toString());
    }
    //TV is Off already, user is pressing Off button again
    @Override
    public void pressOffButton(TV context)
    {
        System.out.println("You pressed Off button. TV is already in Off state.");
    }
    //User is pressing Mute button when the TV is in Off state
    @Override
    public void pressMuteButton(TV context)
    {
        System.out.println("You pressed Mute button.TV is already in Off state, so Mute operation will not work.");
    }
    public String toString()
    {
        return "\t**TV is switched off now.**";
    }
}
//On state
class On implements PossibleState
{
    //TV is On already, user is pressing On button again
    @Override
    public void pressOnButton(TV context)
    {
        System.out.println("You pressed On button. TV is already in On state.");
    }
    //User is pressing Off button when the TV is in On state
    @Override
    public void pressOffButton(TV context)
    {
        System.out.println("You pressed Off button.Going from On to Off state.");
        context.setCurrentState(new Off());
        System.out.println(context.getCurrentState().toString());
    }
    //User is pressing Mute button when the TV is in On state

    @Override
    public void pressMuteButton(TV context)
    {
        System.out.println("You pressed Mute button.Going from On to Mute mode.");
        context.setCurrentState(new Mute());
        System.out.println(context.getCurrentState().toString());
    }
    public String toString()
    {
        return "\t**TV is switched on now.**";
    }
}
//Mute state
class Mute implements PossibleState
{
    //User is pressing On button when the TV is in Mute mode
    @Override
    public void pressOnButton(TV context)
    {
        System.out.println("You pressed On button.Going from Mute mode to On state.");
        context.setCurrentState(new On());
        System.out.println(context.getCurrentState().toString());
    }
    //User is pressing Off button when the TV is in Mute mode
    @Override
    public void pressOffButton(TV context)
    {
        System.out.println("You pressed Off button. Going from Mute mode to Off state.");
        context.setCurrentState(new Off());
        System.out.println(context.getCurrentState().toString());
    }
    //TV is in mute mode already, user is pressing mute button again

    @Override
    public void pressMuteButton(TV context)
    {
        System.out.println("You pressed Mute button.TV is already in Mute mode.");
    }
    public String toString()
    {
        return "\t**TV is silent(mute) now**";
    }
}
class TV
{
    private PossibleState currentState;
    public TV()
    {
        //Initially TV is initialized with Off state
        this.setCurrentState(new Off());
    }
    public PossibleState getCurrentState()
    {
        return currentState;
    }
    public void setCurrentState(PossibleState currentState)
    {
        this.currentState = currentState;
    }
    public void pressOffButton()
    {
        currentState.pressOffButton(this);//Delegating the state
    }
    public void pressOnButton()
    {
        currentState.pressOnButton(this);//Delegating the state

    }
    public void pressMuteButton()
    {
        currentState.pressMuteButton(this);//Delegating the state
    }
}

//Client
public class StatePatternExample {

    public static void main(String[] args) {
        System.out.println("***State Pattern Demo***\n");
        //Initially TV is Off.
        TV tv = new TV();
        System.out.println("User is pressing buttons in the following sequence:");
        System.out.println("Off->Mute->On->On->Mute->Mute->Off\n");
        //TV is already in Off state.Again Off button is pressed.
        tv.pressOffButton();
        //TV is already in Off state.Again Mute button is pressed.
        tv.pressMuteButton();
        //Making the TV on
        tv.pressOnButton();
        //TV is already in On state.Again On button is pressed.
        tv.pressOnButton();
        //Putting the TV in Mute mode
        tv.pressMuteButton();
        //TV is already in Mute mode.Again Mute button is pressed.
        tv.pressMuteButton();
        //Making the TV off
        tv.pressOffButton();
    }

}

输出

这是输出。

***State Pattern Demo***

User is pressing buttons in the following sequence:

Off->Mute->On->On->Mute->Mute->Off

You pressed Off button. TV is already in Off state.
You pressed Mute button.TV is already in Off state, so Mute operation will not work.
You pressed On button. Going from Off to On state.
    **TV is switched on now.**
You pressed On button. TV is already in On state.
You pressed Mute button.Going from On to Mute mode.
    **TV is silent(mute) now**
You pressed Mute button.TV is already in Mute mode.
You pressed Off button. Going from Mute mode to Off state.
    **TV is switched off now.**

问答环节

  1. 你能详细说明这种模式在另一个真实场景中是如何有用的吗?

    心理学家反复证明了这样一个事实:当人们处于放松状态并且没有紧张感时,他们可以表现得最好,但是在相反的情况下,当他们的头脑充满紧张感时,他们就不能产生好的结果。这就是为什么心理学家总是建议你应该在放松的心情下工作。你可以把这个简单的哲学与电视插图联系起来。如果电视开着,它可以娱乐你;如果关闭,就不能。正确所以,如果你想设计一个对象内部状态变化时类似的行为变化,这种模式是有用的。

    除了这个例子之外,您还可以考虑一个场景,客户购买了一张在线机票,然后在某个阶段取消了它。退款金额可能因不同情况而异;例如,您可以取消机票的天数。

  2. 在本例中,您只考虑了电视的三种状态:开、关或静音。还有许多其他状态,例如,可能有一个处理连接问题或显示条件的状态。为什么你忽略了这些?

    直截了当的回答是代表简单。如果系统中状态的数量显著增加,那么维护系统就变得很困难(这也是与这种设计模式相关的关键挑战之一)。但是如果你理解这个实现,你可以很容易地添加任何你想要的状态。

  3. 我注意到在他们的名著中,GoF 代表了国家模式和策略模式的相似结构。我对此感到困惑。

    是的,结构是相似的,但是你需要注意意图是不同的。除了这个关键的区别,你可以简单地这样想:用一个策略模式提供了一个子类化的更好的选择。另一方面,在状态设计模式中,不同类型的行为可以封装在一个状态对象中,并且上下文被委托给这些状态中的任何一个。当上下文的内部状态改变时,它的行为也会改变。

    State patterns can also help us avoid lots of if conditions in some contexts. (Consider our example once again. If the TV is in the Off state, it cannot go to the Mute state. From this state, it can move to the On state only.) So, if you do not like state design pattern, you may need to code like this for a On button press.

    class TV
    {
    //Some code before
    public void pressOnButton()
    {
    if(currentState==Off )
    {
    System.out.println (" You pressed Onbutton. Going from Off to OnState");
    //Do some operations
    }
    if(currentState==On )
     {
      System.out.println (" You pressed On button. TV is already  in On state");
     }
    //TV presently is in mute mode
    else
     {
      System.out.println (" You pressed On button . Going from Mute mode to On State");
     }
    //Do some operations
    }
    
    

    请注意,您需要对不同种类的按钮按压重复这些检查。(例如,对于pressOffButton()pressMuteButton()方法,您需要重复这些检查并相应地执行。)

    如果你不考虑状态,如果你的代码库增长,维护会变得困难。

  4. 在我们的实现中,您是如何支持开闭原则的?

    这些 TV 状态中的每一个都被关闭进行修改,但是您可以向 TV 类添加全新的状态。

  5. 策略模式和状态模式有什么共同特征?

    两者都可以促进构图和授权。

  6. 在我看来,这些状态对象就像单态对象一样。这是正确的吗?

    是的。大多数时候他们都是这样做的。

  7. Can you avoid the use of “contexts” in the method parameters. For example, can you avoid them in the following statements?

    void pressOnButton(TV context);
    
    ....
    
    

    如果您不想这样使用上下文参数,您可能需要修改实现。为了给出一个快速的概述,我将展示修改后的 Package Explorer 视图,其中只包含修改后的实现。

    下面实现中的一个关键变化可以在 TV 类中看到。用所有可能的状态对象初始化TV()构造函数,这些对象用于后面阶段的状态改变。为此调用 getter 方法。考虑下面的实现。

已修改的包资源管理器视图

在这种情况下,所有三种可能的状态都有相似的组成部分。因此,为了保持图表简短,我在下面的 Package Explorer 视图中只显示了其中的一个。

图 20-4 显示了修改后的程序高层结构。

img/395506_2_En_20_Fig4_HTML.jpg

图 20-4

已修改的包资源管理器视图

修改的实现

下面是修改后的实现。

package jdp2e.state.modified.demo;

interface PossibleStates
{
    void pressOnButton();
    void pressOffButton();
    void pressMuteButton();
}

class Off implements  PossibleStates
{
    TV tvContext;
    //Initially we'll start from Off state
    public Off(TV context)
    {
        //System.out.println(" TV is Off now.");
        this.tvContext = context;
    }
    //Users can press any of these buttons at this state-On, Off or Mute
    //TV is Off now, user is pressing On button
    @Override
    public void pressOnButton()
    {
        System.out.println(" You pressed On button. Going from Off to On state");
        tvContext.setCurrentState(tvContext.getOnState());
        System.out.println(tvContext.getCurrentState().toString());
    }
    //TV is Off already, user is pressing Off button again
    @Override
    public void pressOffButton()
    {
        System.out.println(" You pressed Off button. TV is already in Off state");
    }
    //TV is Off now, user is pressing Mute button
    @Override
    public void pressMuteButton()
    {
        System.out.println(" You pressed Mute button.TV is already in Off state, so Mute operation will not work.");
    }
    public String toString()
    {
        return "\t**TV is switched off now.**";
    }

}
class On implements PossibleStates
{
    TV tvContext;
    public On(TV context)
    {
        //System.out.println(" TV is On now.");
        this.tvContext = context;
    }
    //Users can press any of these buttons at this state-On, Off or Mute
    //TV is On already, user is pressing On button again
    @Override
    public void pressOnButton()
    {
        System.out.println("You pressed On button. TV is already in On state.");
    }
    //TV is On now, user is pressing Off button
    @Override
    public void pressOffButton()
    {
        System.out.println(" You pressed Off button.Going from On to Off state.");
        tvContext.setCurrentState(tvContext.getOffState());
        System.out.println(tvContext.getCurrentState().toString());
    }
    //TV is On now, user is pressing Mute button
    @Override
    public void pressMuteButton()
    {
        System.out.println("You pressed Mute button.Going from On to Mute mode.");
        tvContext.setCurrentState(tvContext.getMuteState());
        System.out.println(tvContext.getCurrentState().toString());
    }
    public String toString()
    {
        return "\t**TV is switched on now.**";
    }
}
class Mute implements PossibleStates
{
    TV tvContext;
    public Mute(TV context)
    {
        this.tvContext = context;
    }
    //Users can press any of these buttons at this state-On, Off or Mute
    //TV is in mute, user is pressing On button
    @Override
    public void pressOnButton()
    {
        System.out.println("You pressed On button.Going from Mute mode to On state.");
        tvContext.setCurrentState(tvContext.getOnState());
        System.out.println(tvContext.getCurrentState().toString());
    }
    //TV is in mute, user is pressing Off button
    @Override
    public void pressOffButton()
    {
        System.out.println("You pressed Off button. Going from Mute mode to Off state.");
        tvContext.setCurrentState(tvContext.getOffState());
        System.out.println(tvContext.getCurrentState().toString());
    }
    //TV is in mute already, user is pressing mute button again
    @Override
    public void pressMuteButton()
    {
        System.out.println(" You pressed Mute button.TV is already in Mute mode.");
    }
    public String toString()
    {
        return "\t**TV is silent(mute) now**";
    }
}
class TV
{
    private PossibleStates currentState;
    private PossibleStates onState;
    private PossibleStates offState;
    private PossibleStates muteState;
    public TV()
    {
        onState=new On(this);
        offState=new Off(this);
        muteState=new Mute(this);
        setCurrentState(offState);
    }
    public PossibleStates getCurrentState()
    {
        return currentState;
    }
    public void setCurrentState(PossibleStates currentState)
    {
        this.currentState = currentState;
    }
    public void pressOffButton()
    {
        currentState.pressOffButton();
    }
    public void pressOnButton()
    {
        currentState.pressOnButton();
    }
    public void pressMuteButton()
    {
        currentState.pressMuteButton();
    }
    public PossibleStates getOnState()
    {
        return onState;
    }
    public PossibleStates getOffState()
    {
        return offState;
    }
    public PossibleStates getMuteState()
    {
        return muteState;
    }
}

//Client
public class StatePatternAlternativeImplementation {

    public static void main(String[] args) {
        System.out.println("***State Pattern Alternative Implementation Demo***\n");
        //Initially TV is Off.
        TV tv = new TV();
        System.out.println("User is pressing buttons in the following sequence:");
        System.out.println("Off->Mute->On->On->Mute->Mute->Off\n");
        //TV is already in Off state.Again Off button is pressed.
        tv.pressOffButton();
        //TV is already in Off state.Again Mute button is pressed.
        tv.pressMuteButton();
        //Making the TV on
        tv.pressOnButton();
        //TV is already in On state.Again On button is pressed.
        tv.pressOnButton();
        //Putting the TV in Mute mode
        tv.pressMuteButton();
        //TV is already in Mute mode.Again Mute button is pressed.
        tv.pressMuteButton();
        //Making the TV off
        tv.pressOffButton();

    }
}

修改输出

下面是修改后的实现的输出。

  1. 在这些实现中,TV 是一个具体的类。在这种情况下,你为什么不编程接口?

    我假设 TV 类不会改变,所以我忽略了这一部分以减少程序的代码量。但是是的,你总是可以从一个你可以定义契约的界面开始。

  2. 状态设计模式有哪些优点和缺点?

    Pros

    • 您已经看到,遵循打开/关闭原则,您可以轻松地添加新状态和新行为。此外,状态行为可以毫无争议地扩展。例如,在这个实现中,您可以为 TV 类添加新的状态和新的行为,而无需更改 TV 类本身。

    • 减少了if-else语句的使用(即,条件复杂度降低。(参考问题 3 的回答)。

      Cons

    • 状态模式也被称为状态的对象。所以,你可以假设更多的状态需要更多的代码,而明显的副作用是你很难维护。

  3. 在 TV 类构造函数中,你初始化了一个关闭状态的 TV。那么,状态和上下文类都能触发状态转换吗?

    是的。

 ***State Pattern Alternative Implementation Demo***

User is pressing buttons in the following sequence:
Off->Mute->On->On->Mute->Mute->Off

 You pressed Off button. TV is already in Off state
 You pressed Mute button.TV is already in Off state, so Mute operation will not work.
 You pressed On button. Going from Off to On state
    **TV is switched on now.**
You pressed On button. TV is already in On state.
You pressed Mute button.Going from On to Mute mode.
    **TV is silent(mute) now**
 You pressed Mute button.TV is already in Mute mode.
You pressed Off button. Going from Mute mode to Off state.
    **TV is switched off now.**

二十一、中介模式

本章涵盖了中介模式。

GoF 定义

定义一个封装一组对象如何交互的对象。Mediator 通过防止对象显式地相互引用来促进松散耦合,并允许您独立地改变它们的交互。

概念

中介负责控制和协调无法显式引用彼此的特定对象组的交互。所以,你可以把一个中介者想象成一个媒介,通过它这些物体彼此交流。这种实现有助于减少不同对象之间的互连数量。因此,您可以促进系统中的松散耦合。

因此,在这种设计中,对象通信是用一个中介对象封装的,这样它们就不能直接相互通信,从而减少了它们之间的依赖性。

真实世界的例子

当航班需要起飞时,会进行一系列验证。这些类型的验证确认所有组件/零件(相互依赖)都处于完美状态。

还要考虑飞机飞行员(接近或离开航站区)何时与塔台通信。他们不明确地与来自不同航空公司的其他飞行员交流。他们只向塔台发送他们的状态。这些塔也发送信号来确认哪架飞机可以起飞或降落。你必须注意,这些塔并不控制整个飞行。它们仅在端子区域中实现约束。

计算机世界的例子

当客户端处理业务应用程序时,开发人员可能需要对其施加一些约束。例如,在一个表单中,客户需要提供用户 ID 和密码来访问他们的帐户。在同一个表单上,客户必须提供其他信息,如电子邮件、地址、年龄等等。让我们假设开发人员应用了如下约束。

最初,应用程序检查用户提供的 ID 是否有效。如果是有效的用户 id,则仅启用密码字段。在提供这两个字段之后,应用程序表单需要检查电子邮件地址是否是用户提供的。让我们进一步假设在提供了所有这些信息(有效的用户 id、密码、格式正确的电子邮件等)之后。),提交按钮被启用。因此,基本上,如果客户端以正确的顺序提供了有效的用户 id、密码、电子邮件和其他必需的详细信息,那么 Submit 按钮就会被启用。开发人员可能还会强制要求用户 ID 必须是一个整数,因此如果用户错误地在该字段中放置了任何字符,提交按钮将保持禁用状态。在这种情况下,中介模式变得非常方便。

因此,当一个程序由许多类组成,并且逻辑分布在它们之间时,代码变得更难阅读和维护。在这些场景中,如果您想在系统行为中引入新的变化,这可能会很困难,除非您使用中介模式。

注意

java.util.concurrent.Executor 接口中的 execute()方法遵循这种模式。

javax.swing.ButtonGroup 类是支持这种模式的另一个例子。这个类有一个方法 setSelected(),确保用户提供一个新的选择。

java.util.Timer 类的各种 schedule()方法的不同重载版本也可以视为遵循这种模式。

说明

mediator 模式(基本上采用了 GoF 的设计模式:可重用面向对象软件的元素)的一种常见结构,通常用图 21-1 所示的图表来描述。

img/395506_2_En_21_Fig1_HTML.jpg

图 21-1

中介模式示例

参与者描述如下。

  • Mediator :定义接口,提供同事对象之间的通信。

  • ConcreteMediator :维护同事对象列表。它实现中介接口并协调同事对象之间的通信。

  • 同事:定义与其他同事交流的接口。

  • ConcreteColleague1 和 ConcreteColleague2 :实现同事接口。这些对象通过中介相互通信。

在这一章中,我提供了这种模式的两个实现。在第一个实现中,我将单词同事替换为员工。此外,ConcreteColleague1 和 ConcreteColleague2 分别替换为 JuniorEmployee 和 SeniorEmployee。假设您有三名员工:Amit、Sohel 和 Raghu,其中 Amit 和 Sohel 是初级员工,向他们的老板 Raghu 报告,rag Hu 是高级员工。拉古想顺利协调事情。让我们进一步假设他们可以通过聊天服务器相互通信。

在下面的实现中,Mediator 是一个有两个方法的接口:register()sendMessage()register()方法向中介注册一个雇员,sendMessage()向服务器发送消息。ConcreteMediator 类是 Mediator 接口的具体实现。

Employee 是一个抽象类,JuniorEmployee 和 SeniorEmployee 类是它的具体实现。Employee 类的sendMessage()方法描述如下。

public void sendMessage(String msg) throws InterruptedException
{
    mediator.sendMessage(this, msg);
}

您可以看到,当一个雇员调用sendMessage()方法时,它正在调用 mediator 的sendMessage()方法。因此,实际的沟通过程是通过中介进行的。

在客户端代码中,我引入了另一个人,Jack。但是他没有向中介对象注册自己。因此,中介不允许他向该服务器发布任何消息。

现在检查代码和相应的输出。

类图

图 21-2 为类图。

img/395506_2_En_21_Fig2_HTML.jpg

图 21-2

类图

包资源管理器视图

图 21-3 显示了程序的高层结构。

img/395506_2_En_21_Fig3_HTML.jpg

图 21-3

包资源管理器视图

履行

这是第一个实现。

package jdp2e.mediator.demo;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

interface Mediator
{
    void register(Employee employee);
    void sendMessage(Employee employee, String msg) throws InterruptedException;
}
// ConcreteMediator
class ConcreteMediator implements Mediator
{
    List<Employee> participants = new ArrayList<Employee>();
    @Override
    public void register(Employee employee)
    {
        participants.add(employee);
    }
    public void displayRegisteredEmployees()
    {
        System.out.println("At present,registered employees are:");
        for (Employee employee: participants)
        {

            System.out.println(employee.getName());
        }
    }
    @Override
    public void sendMessage(Employee employee, String msg) throws InterruptedException
    {
        if (participants.contains(employee))
        {
            System.out.println(employee.getName() +" posts:"+ msg+"Last message posted at "+LocalDateTime.now());
            Thread.sleep(1000);
        }
        else
        {
            System.out.println("An outsider named "+ employee.getName()+" is trying to send some messages.");
        }
    }
}

// The abstract class-Employee
abstract class Employee
{
    protected Mediator mediator;
    protected String name;
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
    // Constructor
    public Employee(Mediator mediator)
    {
        this.mediator = mediator;
    }
    public void sendMessage(String msg) throws InterruptedException
    {
        mediator.sendMessage(this, msg);
    }
    public abstract String employeeType();
}
// Junior Employee
class JuniorEmployee extends Employee
{
    public JuniorEmployee(Mediator mediator, String name)
    {
        super(mediator);
        this.name = name;
    }

    @Override
    public String employeeType()
    {
        return "JuniorEmployee";
    }
}

//Senior Employee
class SeniorEmployee extends Employee
{
    // Constructor
    public SeniorEmployee(Mediator mediator, String name)
    {
        super(mediator);
        this.name = name;
    }
    @Override
    public String employeeType()
    {
        return "SeniorEmployee";
    }
}
// Unknown participant.
class Unknown extends Employee
{
    // Constructor
    public Unknown(Mediator mediator, String name)
    {
        super(mediator);
        this.name = name;
    }
    @Override
    public String employeeType()
    {
        return "Outsider";
    }
}

public class MediatorPatternExample {

    public static void main(String[] args) throws InterruptedException {
        System.out.println("***Mediator Pattern Demo***\n");

        ConcreteMediator mediator = new ConcreteMediator();

        JuniorEmployee amit = new JuniorEmployee(mediator, "Amit");
        JuniorEmployee sohel = new JuniorEmployee(mediator, "Sohel");
        SeniorEmployee raghu = new SeniorEmployee(mediator, "Raghu");

        //Registering participants
        mediator.register(amit);
        mediator.register(sohel);
        mediator.register(raghu);
        //Displaying the participant's list
        mediator.displayRegisteredEmployees();

        System.out.println("Communication starts among participants...");
        amit.sendMessage("Hi Sohel,can we discuss the mediator pattern?");
        sohel.sendMessage("Hi Amit,yup, we can discuss now.");
        raghu.sendMessage("Please get back to work quickly.");

        //An outsider/unknown person tries to participate
        Unknown unknown = new Unknown(mediator, "Jack");
        unknown.sendMessage("Hello Guys..");
    }
}

输出

这是输出。

***Mediator Pattern Demo***

At present,registered employees are:
Amit
Sohel
Raghu
Communication starts among participants...
Amit posts:Hi Sohel,can we discuss the mediator pattern?Last message posted at 2018-09-09T17:41:21.868
Sohel posts:Hi Amit,yup, we can discuss now.Last message posted at 2018-09-09T17:41:23.369
Raghu posts:Please get back to work quickly.Last message posted at 2018-09-09T17:41:24.870

An outsider named Jack is trying to send some messages

.

分析

请注意,只有注册用户才能相互通信,并在聊天服务器上成功发布消息。调解人不允许任何外人进入系统。(注意输出的最后一行。)

修改后的插图

您已经看到了中介模式的一个简单例子。但是你可以做得更好。你确定了以下几点。

  • 这些信息只是单向传递的。

  • 当一个参与者发布消息时,每个人都可以看到该消息。所以,没有隐私可言。

  • 如果员工忘记自己注册,则不允许他发送消息。这很好,但他不应该被当作外人。在正常情况下,组织外部人员应该与忘记在服务器上注册的组织员工区别对待。

  • 向中介注册参与者所需的客户机代码。尽管你可能认为这不是缺点,但你可以选择更好的方法。例如,当您在客户端代码中创建一个 Employee 对象时,您可以将参与者自动注册到一个中介。

  • 您没有在客户端代码中使用employeeType()方法。

因此,记住这几点,让我们修改前面的例子。下面是修改后的实现的一些关键特征。

  • JuniorEmployee 和 SeniorEmployee 类被替换为一个 ConcreteEmployee 类。它帮助我们容易地识别谁属于组织,谁不属于组织(换句话说,是局外人)。

  • 在修改后的实现中,这些参与者中的每一个都可以看到谁在发布消息,但是不会公开消息的目标是谁或者实际的消息是什么。因此,两个参与者之间有隐私,但这种方法可以帮助像 Raghu 这样的人协调事情,因为如果他看到员工聊得太多,他可能会干涉。

  • 在客户端代码中,您可以像下面这样创建参与者。

Employee Amit = new ConcreteEmployee(mediator, "Amit", true);

第三个参数(对/错)用于确定参与者是否希望向中介注册自己。当他试图发布消息时,他会得到相应的处理。

  • employeeType()方法确定参与者是来自组织内部还是来自外部。在这种情况下,您可能还会注意到,没有使用下面的代码行
if( fromEmployee.employeeType()=="UnauthorizedUser")

您可以直接使用这行代码:

if( fromEmployee.getClass().getSimpleName().equals("UnauthorizedUser"))

为了更好的可读性,我使用了前者。

修改的类图

图 21-4 显示了修改后的类图。为了显示关键的变化和简洁的图表,我没有在下面的图表中显示客户端代码依赖关系。

img/395506_2_En_21_Fig4_HTML.jpg

图 21-4

类图

已修改的包资源管理器视图

图 21-5 显示了修改后的包浏览器视图。

img/395506_2_En_21_Fig5_HTML.jpg

图 21-5

已修改的包资源管理器视图

修改的实现

下面是修改后的实现。

package jdp2e.mediator.modified.demo;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

interface Mediator
{
    void register(Employee employee);
    void sendMessage(Employee fromEmployee, Employee toEmployee,String msg) throws InterruptedException;
}
// ConcreteMediator
class ConcreteMediator implements Mediator
{
    List<Employee> participants = new ArrayList<Employee>();
    @Override
    public void register(Employee employee)
    {
        participants.add(employee);
    }
    public void displayRegisteredEmployees()
    {
        System.out.println("At present ,registered participants are:");
        for (Employee employee: participants)
        {

            System.out.println(employee.getName());
        }
    }
    @Override
    public void sendMessage(Employee fromEmployee,Employee toEmployee,String msg) throws InterruptedException
    {
        /*if( fromEmployee.getClass().getSimpleName().equals("UnauthorizedUser"))*/
        if( fromEmployee.employeeType()=="UnauthorizedUser")
        {
            System.out.println("[ALERT Everyone] An outsider named "+ fromEmployee.getName()+" trying to send some messages to "+ toEmployee.getName());
            fromEmployee.receive(fromEmployee, ",you are not allowed to enter here.");
        }
        else if (participants.contains(fromEmployee))
        {
            System.out.println("-----"+fromEmployee.getName() +" posts some message at: "+LocalDateTime.now()+"-----");
            Thread.sleep(1000);
            //No need to inform everyone or himself
            //Only let the target receiver know
            if(participants.contains(toEmployee))
            {
                toEmployee.receive(fromEmployee,msg);
            }
            //If target receipient does not exist
            else
            {
                System.out.println(fromEmployee.getName() +" , your target recipient does not exist");
            }
        }
        //An outsider tries to send message.
        else
        {
            System.out.println("[ALERT] An unregistered employee named "+ fromEmployee.getName()+" trying to send some messages to "+ toEmployee.getName());
            System.out.println(fromEmployee.getName()+", you need to register yourself first.");
        }

    }
}
// Employee
abstract class Employee
{
    private Mediator mediator;
    protected String name;
    private boolean authorizedUser;
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
    // Constructor
    public Employee(Mediator mediator, String name, boolean authorizedUser)
    {
        this.mediator = mediator;
        this.name=name;
        this.authorizedUser=authorizedUser;
        if(authorizedUser)
        {
            mediator.register(this);
        }

    }
    //The following method name need not be same as the Mediator's method name
    public void send(Employee toFriend,String msg) throws InterruptedException
    {
        mediator.sendMessage(this,toFriend, msg);
    }
    //public abstract void receive(Friend fromFriend,String message);

    public void receive(Employee fromFriend,String message)
    {
        System.out.println(this.name+" received a message : " + message +" from an employee "+ fromFriend.getName() +".");

    }
    public abstract String employeeType();
}
//A concrete friend
class ConcreteEmployee extends Employee
{

    public ConcreteEmployee(Mediator mediator, String name,boolean authorizedUser)
    {
        super(mediator,name, authorizedUser);
    }
    @Override
    public String employeeType()
    {
        return "ConcreteEmployee";
    }
}
//Unauthorized user
class UnauthorizedUser extends Employee
{
    public UnauthorizedUser(Mediator mediator, String name)
    {
        //The user is always treated an unauthorized user.So, the flag is
        //false always.
        super(mediator,name, false);
    }

    @Override
    public void receive(Employee fromEmployee,String message)
    {
        System.out.println(this.name + message);

    }

    @Override
    public String employeeType()
    {
        return "UnauthorizedUser";
    }
}

public class ModifiedMediatorPatternExample {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("***Mediator Pattern Demo***\n");

        ConcreteMediator mediator = new ConcreteMediator();

        Employee Amit = new ConcreteEmployee(mediator, "Amit", true);
        Employee Sohel = new ConcreteEmployee(mediator, "Sohel",true);
        Employee Raghu = new ConcreteEmployee(mediator, "Raghu",true);
        //Unauthorized user
        Employee Jack = new ConcreteEmployee(mediator, "Jack",false);
        //Only two parameter needed to pass in the following case.
        Employee Divya = new UnauthorizedUser(mediator, "Divya");

        //Displaying the participant's list
        mediator.displayRegisteredEmployees();

        System.out.println("Communication starts among participants...");
        Amit.send(Sohel,"Hi Sohel,can we discuss the mediator pattern?");
        Sohel.send(Amit,"Hi Amit,Yup, we can discuss now.");
        //Boss is sending messages to each of them individually
        Raghu.send(Amit,"Please get back to work quickly.");
        Raghu.send(Sohel,"Please get back to work quickly.");

        //An unregistered employee(Jack) and an outsider(Divya) are also
        //trying to participate.
        Jack.send(Amit,"Hello Guys..");
        Divya.send(Raghu, "Hi Raghu");

    }
}

修改输出

下面是修改后的输出。

***Mediator Pattern Demo***

At present ,registered participants are:
Amit
Sohel
Raghu
Communication starts among participants...
-----Amit posts some message at: 2018-09-04T20:37:00.999-----
Sohel received a message : Hi Sohel,can we discuss the mediator pattern? from an employee Amit.
-----Sohel posts some message at: 2018-09-04T20:37:01.999-----
Amit received a message : Hi Amit,Yup, we can discuss now. from an employee Sohel.
-----Raghu posts some message at: 2018-09-04T20:37:03.002-----
Amit received a message : Please get back to work quickly. from an employee Raghu.
-----Raghu posts some message at: 2018-09-04T20:37:04.016-----
Sohel received a message : Please get back to work quickly. from an employee Raghu.
[ALERT] An unregistered employee named Jack trying to send some messages to Amit
Jack, you need to register yourself first.
[ALERT Everyone] An outsider named Divya trying to send some messages to Raghu
Divya,you are not allowed to enter here

.

分析

请注意,当名为 Jack 的员工(属于该组织)在没有注册的情况下发送消息时,系统会阻止他发布消息,但会给他一个建议。但 Divya,谁是一个组织的局外人,被告知,她是不允许进入该系统。它也警告其他人。

问答环节

img/395506_2_En_21_Fig7_HTML.jpg

图 21-7

使用调解人的沟通

img/395506_2_En_21_Fig6_HTML.jpg

图 21-6

不使用调解人的沟通

  1. 你为什么要把事情复杂化?在第一个例子中,每个参与者都可以彼此直接对话,您可以绕过中介的使用。这是正确的吗?

    在这个例子中,您只有三个注册的参与者,所以看起来他们可以直接相互交流。但是你可能需要考虑一个相对复杂的场景。例如,只有当目标参与者处于在线模式(这是聊天服务器的常见情况)时,参与者才能向目标参与者发送消息。因此,根据您提出的体系结构,如果它们试图相互通信,那么在发送消息之前,它们中的每一个都需要维护所有其他参与者的状态。而如果参与人数不断增长,你能想象系统的复杂程度吗?

    因此,调解人肯定可以帮助你处理这种情况。图 21-6 和图 21-7 描绘了该场景。

    案例一。没有中间人的交流。

案例二。与调解人沟通。

此外,您可以在这个上下文中考虑修改后的实现。在修改后的实现中,您可以看到中介在维护逻辑——应该允许谁在服务器上发布消息以及应该如何对待他/她。

  1. 使用中介模式的 优势 是什么?

    • 您可以降低系统中对象通信的复杂性。

    • 该模式促进了松散耦合。

    • 它减少了系统中子类的数量。

    • 您可以将“多对多”关系替换为“一对多”关系,这样更容易阅读和理解。(考虑我们在这方面的第一个例子)。作为一个明显的效果,维护变得容易。

    • 您可以使用这种模式通过中介提供集中控制。

    • 简而言之,从我们的代码中移除紧密耦合(对象之间)一直是我们的目标,这种模式在这种情况下得分很高。

  2. 使用中介模式的缺点是什么?

    • 在某些情况下,实现适当的封装是很棘手的。

    • 如果在中介对象中放入过多的逻辑,那么它的架构可能会变得复杂。不恰当地使用中介模式可能会导致“上帝级”反模式。(你将在第二十八章中学习反模式)。

    • 有时维护中介会成为一个大问题。

  3. 如果您需要添加一个新的规则或逻辑,您可以直接将其添加到中介器中。这是正确的吗?

    是的。

  4. 我在门面模式和中介模式中发现了一些相似之处。这是正确的吗?

    是的。Steve Holzner 在他的书中提到了相似性,他把中介模式描述为一个复用的门面模式。在 mediator 中,您不是使用单个对象的接口,而是在多个对象之间创建一个多路复用的接口,以提供平滑的过渡。

  5. 在这个模式中,你减少了不同对象之间的相互联系。通过这一缩减,您获得了哪些关键优势?

    对象之间更多的互连可以形成一个整体系统,其中系统的行为很难改变(系统的行为分布在许多对象中)。作为副作用,您可能需要创建许多子类来在系统中带来这些变化。

  6. 在修改后的实现中,您使用的是线程。睡眠(1000)。这是什么原因呢?

    你可以忽略它。我用它来模拟现实生活。我假设参与者在正确阅读消息后发布消息,此活动至少需要 1 秒钟。

  7. 在一些应用中,我只看到了具体中介物的使用。这种做法可以吗?

    中介模式并不限制您只能使用一个具体的中介。但是我喜欢遵循专家的建议,“对超类型(抽象类/接口)编程是一种更好的方法”,从长远来看,它可以提供更多的灵活性。

  8. 我是否可以简单地说,如果一个类简单地从多个对象调用方法,它就是一个中介体?

    一点也不。中介的主要目的是简化系统中对象之间的复杂通信。我建议您始终牢记 GoF 定义和相应的概念。

  9. 在第一个实现中,两个发送方法(中介和雇员)都被命名为 sendMessage(),但是在修改后的实现中,它们是不同的——一个是 send(),另一个是 sendMessage()。我需要遵循任何特定的命名约定吗?

    不。两个都很好。这是你的选择。

二十二、责任链模式

本章涵盖了责任链模式。

GoF 定义

通过给多个对象一个处理请求的机会,避免将请求的发送方耦合到接收方。链接接收对象,并沿着链传递请求,直到有对象处理它。

概念

在这个模式中,您形成了一个对象链,链中的每个对象都处理一种特定的请求。如果一个对象不能完全处理请求,它会将请求传递给链中的下一个对象。同样的过程可以继续,直到到达链的末端。这种请求处理机制为您提供了在链的末端添加新处理对象(处理程序)的灵活性。图 22-1 描绘了这样一个有 N 个处理器的链。

img/395506_2_En_22_Fig1_HTML.jpg

图 22-1

责任链模式的概念

真实世界的例子

  • 每个组织都雇佣了客户服务主管,他们直接接收来自客户的反馈或投诉。如果员工不能正确回答客户的问题,他们会将这些问题/上报转发给组织中的相应部门。这些部门不会试图同时解决一个问题。在调查的第一阶段,看似负责的部门会对案件进行分析,如果他们认为问题应该移交给另一个部门,他们就会这样做。

  • 当病人去医院时,也会发生类似的情况。一个部门的医生可以将病人转到另一个部门做进一步诊断。

计算机世界的例子

考虑一个可以发送电子邮件和传真的软件应用程序(例如打印机)。因此,客户可以报告传真问题或电子邮件问题。让我们假设这些问题是由处理程序处理的。因此,您引入了两种不同类型的错误处理程序:EmailErrorHandlerFaxErrorHandler。你可以假设EmailErrorHandler只处理邮件错误;它不能修复传真错误。以类似的方式,FaxErrorHandler处理传真错误,而不关心电子邮件错误。

因此,您可能会形成这样的一个链:每当应用程序发现一个错误时,它都会发出一个标签并转发该错误,希望其中一个处理程序会处理它。让我们假设请求首先到达FaxErrorhandler。如果这个处理程序同意错误是传真问题,它就处理它;否则,处理程序将问题转发给EmailErrorHandler

注意,链以EmailErrorHandler结束。但是如果您需要处理不同类型的问题——例如,身份验证问题(可能由于安全漏洞而发生),您可以创建一个名为AuthenticationErrorHandler的处理程序,并将其放在EmailErrorHandler之后。现在,如果一个EmailErrorHandler不能完全解决问题,它会将问题转发给AuthenticationErrorHandler,这个链就此结束。

注意

您可以在您的应用程序中自由地以任何顺序放置这些处理程序。

因此,底线是处理链可能会在以下任何场景中结束。

  • 这些处理程序中的任何一个都可以完全处理请求,然后控制权就回来了。

  • 一个处理程序不能完全处理请求,所以它将请求传递给下一个处理程序。这样,你就到达了链条的末端。因此,请求在那里处理。但是如果请求不能在那里得到处理,就不能进一步传递。(对于这种情况,您可能需要特别小心。)

当您在 Java 应用程序中使用多个 catch 块实现异常处理机制时,您会注意到类似的机制。如果一个异常发生在一个try块中,第一个 catch 块试图处理它。如果它不能处理那种类型的异常,下一个catch块会尝试处理它,并且遵循相同的机制,直到异常被处理程序(catch块)正确处理。如果应用程序中的最后一个 catch 块无法处理它,就会在这个链之外引发一个异常。

注意

在 java.util.logging.Logger 中,您可以看到支持类似概念的 log()方法的不同重载版本。

另一个内置支持可以在 javax.Servlet.Filter 中的 doFilter (ServletRequest 请求、ServletResponse 响应、FilterChain 链)接口方法中看到。

说明

让我们考虑一下在计算机世界的例子中讨论的场景。让我们进一步假设,在下面的例子中,您可以处理来自电子邮件或传真支柱的普通和高优先级问题。

类图

图 22-2 为类图。

img/395506_2_En_22_Fig2_HTML.jpg

图 22-2

类图

包资源管理器视图

图 22-3 显示了程序的高层结构。

img/395506_2_En_22_Fig3_HTML.jpg

图 22-3

包资源管理器视图

履行

下面是实现。

package jdp2e.chainofresponsibility.demo;

enum MessagePriority
{
    NORMAL,
    HIGH
}
class Message
{
    public String text;
    public MessagePriority priority;
    public Message(String msg, MessagePriority p)
    {
        text = msg;
        this.priority = p;
    }
}

interface Receiver
{
    boolean handleMessage(Message message);
    void nextErrorHandler(Receiver nextReceiver);
}
class IssueRaiser
{
    public Receiver setFirstReceiver;
    public void setFirstErrorHandler(Receiver firstErrorHandler)
    {
        this.setFirstReceiver = firstErrorHandler;
    }
    public void raiseMessage(Message message)
    {
        if (setFirstReceiver != null)
            setFirstReceiver.handleMessage(message);
    }
}
// FaxErrorHandler class
class FaxErrorHandler implements Receiver
{
    private Receiver nextReceiver;
    @Override
    public void nextErrorHandler(Receiver nextReceiver)
    {
        this.nextReceiver = nextReceiver;
    }
    @Override
    public boolean handleMessage(Message message)
    {
        if (message.text.contains("Fax"))
        {
            System.out.println(" FaxErrorHandler processed " +message.priority +" priority issue :"+ message.text);
            return true;
        }
        else
        {
            if (nextReceiver != null)
                nextReceiver.handleMessage(message);
        }
        return false;
    }
}
// EmailErrorHandler class
class EmailErrorHandler implements Receiver

{
    private Receiver nextReceiver;
    @Override
    public void nextErrorHandler(Receiver nextReceiver)
    {
        this.nextReceiver = nextReceiver;
    }
    @Override
    public boolean handleMessage(Message message)
    {
        if (message.text.contains("Email"))
        {
            System.out.println(" EmailErrorHandler processed "+message.priority+ " priority issue: "+message.text);
            return true;
        }
        else
        {
            if (nextReceiver != null)
                nextReceiver.handleMessage(message);
        }
        return false;
    }
}
//Client code
public class ChainofResponsibilityPattern {

    public static void main(String[] args) {
        System.out.println("\n ***Chain of Responsibility Pattern Demo***\n");
        /* Forming the chain as IssueRaiser->FaxErrorhandler->EmailErrorHandler

*/
        Receiver faxHandler, emailHandler;
        //Objects of the chains
        IssueRaiser issueRaiser = new IssueRaiser();
        faxHandler = new FaxErrorHandler();
        emailHandler = new EmailErrorHandler();
        //Making the chain
        //Starting point:IssueRaiser will raise issues and set the first
        //handler
        issueRaiser.setFirstErrorHandler(faxHandler);
        //FaxErrorHandler will pass the error to EmailHandler if needed.
        faxHandler.nextErrorHandler(emailHandler);
        //EmailErrorHandler will be placed at the last position in the chain
        emailHandler.nextErrorHandler(null);

        Message m1 = new Message("Fax is going slow.", MessagePriority.NORMAL);
        Message m2 = new Message("Emails are not reaching.", MessagePriority.HIGH);
        Message m3 = new Message("In Email, CC field is disabled always.", MessagePriority.NORMAL);
        Message m4 = new Message("Fax is not reaching destinations.", MessagePriority.HIGH);

        issueRaiser.raiseMessage(m1);
        issueRaiser.raiseMessage(m2);
        issueRaiser.raiseMessage(m3);
        issueRaiser.raiseMessage(m4);
    }

}

输出

这是输出。

***Chain of Responsibility Pattern Demo***

 FaxErrorHandler processed NORMAL priority issue :Fax is going slow.
 EmailErrorHandler processed HIGH priority issue: Emails are not reaching.
 EmailErrorHandler processed NORMAL priority issue: In Email, CC field is disabled always.
 FaxErrorHandler processed HIGH priority issue :Fax is not reaching destinations.

问答环节

  1. 在示例中,消息优先级的目的是什么?

    接得好。实际上,您可以忽略它们,因为为了处理程序的简单,您只需搜索单词 emailfax 。添加这些优先级是为了美化代码。但是,您可以创建一个不同类型的链,根据优先级来处理消息,而不是对电子邮件和传真使用单独的处理程序。在这种情况下,可以更有效地利用这些优先级。

  2. 使用责任链设计模式的优势是什么?

    • 您可以有多个对象来处理一个请求。(请注意,如果一个处理程序不能处理整个请求,它可能会将责任转发给链中的下一个处理程序)。

    • 链的节点可以动态添加或删除。另外,你可以打乱顺序。例如,如果您注意到大多数问题都与电子邮件处理有关,那么您可以将 EmailErrorHandler 作为链中的第一个处理程序,以节省应用程序的平均处理时间。

    • 处理程序不需要知道链中的下一个处理程序将如何处理请求。它只关注自己的处理机制。

    • 在这种模式中,您提倡松耦合,因为它将(请求的)发送者与接收者分离。

  3. 使用责任链设计模式的相关挑战是什么?

    • 不能保证请求会被处理(全部或部分),因为您可能会到达链的末端;但是有可能您还没有找到任何显式的接收者来处理请求。

    • 对于这种设计,调试可能会变得棘手。

  4. 你如何处理已经到达链末端,但是请求根本没有被处理的情况?

    一个简单的解决办法就是使用try / catch(或者try / finally或者try / catch / finally)积木。您可以将处理程序放在这些结构中。您可能会注意到一个try模块也可以与多个catch模块相关联。

    最后,如果没有人能够处理这个请求,您可以用适当的消息引发一个异常,并在您想要的catch块中捕获这个异常以引起您的注意(或者用一些不同的方式处理它)。

    GoF 在类似的背景下谈到了 Smalltalk 的自动转发机制doesNotUnderstand。如果消息找不到合适的处理程序,它会在doesNotUnderstand实现中被捕获,该实现可以被覆盖以在对象的后继中转发消息,将其记录在文件中,并将其存储在队列中以供以后处理,或者您可以简单地执行任何其他预期的操作。但是您必须注意,在默认情况下,该方法会引发一个需要以适当方式处理的异常。

  5. 简而言之,如果一个处理程序不能完全处理请求,它将把它传递给下一个处理程序。这是正确的吗?

    是的。

  6. 观察者模式和责任链模式之间似乎有相似之处。这是正确的吗?

    在观察者模式中,所有注册用户并行获得通知;但是在责任链模式中,链中的对象以连续的方式被一个接一个地通知。这个过程一直持续到一个对象完全处理完通知为止(或者到达链的末尾)。我在第十四章的“问答环节”中用图表展示了这些对比。

二十三、解释器模式

本章涵盖了解释器模式。

GoF 定义

给定一种语言,为它的语法定义一个表示,以及一个使用该表示来解释该语言中的句子的解释器。

概念

为了理解这个模式,你需要熟悉一些关键术语,比如句子语法、语言等等。所以,如果你对自动机中的形式语言不熟悉,你可能需要访问它们。

通常,这种模式处理的是如何评估语言中的句子。所以,你首先需要定义一个语法来表示这种语言。然后解释器处理语法。如果语法简单,这种模式是最好的。

这个模式中的每个类可能代表该语言中的一个规则,并且它应该有一个解释表达式的方法。因此,为了处理更多的规则,您需要创建更多的类。这就是解释器模式不应该用于处理复杂语法的原因。

让我们考虑计算器程序中不同的算术表达式。虽然这些表达式是不同的,但它们都是使用一些基本规则构造的,这些规则是在语言的语法中定义的(这些算术表达式)。因此,如果您能够解释这些规则的一般组合,而不是将每个规则组合视为单独的情况,这是最好的。在这种情况下,可以使用解释器模式。

这种模式的典型结构通常用类似于图 23-1 的图表来描述。

img/395506_2_En_23_Fig1_HTML.jpg

图 23-1

典型解释器模式的结构

术语描述如下。

  • 抽象表达式(abstract expression):通常是一个带有解释方法的接口。您需要向该方法传递一个上下文对象。

  • 终端表达式:用于终端表达式。终结表达式不需要其他表达式来解释。这些基本上是数据结构中的叶节点(即,它们没有子节点)。

  • 非终结符:用于非终结符表达式。也称为交替表达式重复表达式顺序表达式。这就像可以包含终结和非终结表达式的组合。当你在这上面调用interpret()方法时,你基本上是在它的所有子节点上调用它。

  • Context :保存解释器需要的全局信息。

  • 客户端:调用interpret()方法。它可以根据语言的规则有选择地建立一个语法树。

注意

解释器用于处理具有简单规则或语法的语言。理想情况下,开发人员不想创建他们自己的语言。这就是他们很少使用这种模式的原因。

真实世界的例子

  • 翻译外语的翻译者。

  • 把音符看作语法,音乐家在其中扮演解释者的角色。

计算机世界的例子

  • Java 编译器将 Java 源代码解释成 JVM 可以理解的字节码。

  • 在 C# 中,源代码被转换成由 CLR 解释的 MSIL 代码。在执行时,这个 MSIL(中间代码)被 JIT 编译器转换成本机代码(二进制可执行代码)。

注意

在 Java 中,您可能还会注意到充当解释器的 java.util.regex.Pattern 类。您可以通过调用 compile()方法创建这个类的一个实例,然后您可以使用 Matcher 实例根据语法来评估一个句子。

说明

这些是实现这种模式的一些重要步骤。

  • 第一步。定义您想要为其构建解释器的语言的规则。

  • 第二步。定义抽象类或接口来表示表达式。它应该包含一个解释表达式的方法。

    • 迈步 2A。识别终结和非终结表达式。例如,在接下来的示例中,IndividualEmployee 类是一个终端表达式类。

    • 步骤 2B。创建非终结符表达式类。他们每个人都在他们的孩子身上调用解释方法。例如,在接下来的示例中,OrExpression 和 AndExpression 类是非终结表达式类。

  • 第三步。使用这些类构建抽象语法树。您可以在客户端代码中完成这项工作,也可以创建一个单独的类来完成任务

  • 第四步。一个客户现在用这个树来解释一个句子。

  • 第五步。将上下文传递给解释器。它通常有需要解释的句子。解释器可以使用这个上下文执行额外的任务。

在接下来的程序中,我使用解释器模式作为规则验证器。我用不同员工的“经验年数”和当前等级来举例说明他们。请注意下面几行。

       Employee emp1 = new IndividualEmployee(5,"G1");
       Employee emp2 = new IndividualEmployee(10,"G2");
       Employee emp3 = new IndividualEmployee(15,"G3");
       Employee emp4 = new IndividualEmployee(20,"G4");

为简单起见,这里考虑了四个不同级别的四名员工——G1、G2、G3 和 G4。

还要注意上下文,如下所示。

       //Minimum Criteria for promoton is:
       //The year of experience is minimum 10 yrs. and
       //Employee grade should be either G2 or G3
       Context context=new Context(10,"G2","G3");

因此,您可以假设我想要根据上下文验证一些条件,这基本上是告诉您,要获得晋升,员工应该至少有 10 年的工作经验,并且他/她应该来自 G2 级或 G3 级。解释完这些表达式后,您会看到布尔值形式的输出。

需要注意的重要一点是,这种设计模式并没有指导你如何构建语法树或者如何解析句子。它给了你继续前进的自由。因此,为了给出一个简单的场景,我使用了一个 EmployeeBuilder 类和一个名为buildExpression()的方法来完成我的任务。

类图

图 23-2 为类图。

img/395506_2_En_23_Fig2_HTML.jpg

图 23-2

类图

包资源管理器视图

图 23-3 显示了程序的高层结构。

img/395506_2_En_23_Fig3_HTML.jpg

图 23-3

包资源管理器视图

履行

下面是实现。

package jdp2e.interpreter.demo;

import java.util.ArrayList;
import java.util.List;

interface Employee
{
    public boolean interpret(Context context);
}
class IndividualEmployee implements Employee
{
    private int yearOfExperience;

    private String currentGrade;

    public IndividualEmployee(int experience, String grade){
        this.yearOfExperience=experience;
        this.currentGrade=grade;
    }
    @Override
    public boolean interpret(Context context)
    {

        if(this.yearOfExperience>=context.getYearofExperience() && context.getPermissibleGrades().contains(this.currentGrade))
        {
            return true;
        }
        return false;
    }
}
class OrExpression implements Employee
{

    private Employee  emp1;
    private Employee  emp2;

    public OrExpression(Employee emp1, Employee emp2)
    {
        this.emp1 = emp1;
        this.emp2 = emp2;
    }

    @Override
    public boolean interpret(Context context)
    {
        return emp1.interpret(context) || emp2.interpret(context);
    }
}
class AndExpression implements Employee
{

    private Employee  emp1;
    private Employee  emp2;

    public AndExpression(Employee emp1, Employee emp2)
    {
        this.emp1 = emp1;
        this.emp2 = emp2;
    }

    @Override
    public boolean interpret(Context context)
    {
        return emp1.interpret(context) && emp2.interpret(context);
    }
}
class NotExpression implements Employee
{
    private Employee  emp;

    public NotExpression(Employee  expr)
    {
        this.emp = expr;
    }

    @Override
    public boolean interpret(Context context)
    {
        return !emp.interpret(context);
    }
}
class Context
{
    private int yearofExperience;
    private List<String> permissibleGrades;
    public Context(int experience,String... allowedGrades)
    {
        this.yearofExperience=experience;
        this.permissibleGrades=new ArrayList<>();
        for( String grade:allowedGrades)
        {
            permissibleGrades.add(grade);
        }
    }
    public int getYearofExperience()
    {
        return yearofExperience;
    }
    public List<String> getPermissibleGrades()
    {
        return permissibleGrades;
    }
}
class EmployeeBuilder
{
    public Employee buildExpression(Employee emp1,  String operator, Employee emp2)
    {

        //Whatever the input,converting it to lowarcase
        switch(operator.toLowerCase())
        {
        case "or":
            return new OrExpression(emp1,emp2);
        case "and":
            return new AndExpression(emp1,emp2);
        case "not":
            return new NotExpression(emp1);
        default:
            System.out.println("Only AND,OR and NOT operators are allowed at present");
            return null;
        }

    }
}
public class InterpreterPatternExample {

    public static void main(String[] args) {
        System.out.println("***Interpreter Pattern Demo***\n");

        //Minimum Criteria for promoton is:
        //The year of experience is minimum 10 yrs. and
        //Employee grade should be either G2 or G3
        Context context=new Context(10,"G2","G3");

        //Different employees with grades
        Employee emp1 = new IndividualEmployee(5,"G1");
        Employee emp2 = new IndividualEmployee(10,"G2");
        Employee emp3 = new IndividualEmployee(15,"G3");
        Employee emp4 = new IndividualEmployee(20,"G4");

        EmployeeBuilder builder=new EmployeeBuilder();

        System.out.println("emp1 is eligible for promotion. " + emp1.interpret(context));
        System.out.println("emp2 is eligible for promotion. " + emp2.interpret(context));
        System.out.println("emp3 is eligible for promotion. " + emp3.interpret(context));
        System.out.println("emp4 is eligible for promotion. " + emp4.interpret(context));

        System.out.println("Is either emp1 or emp3 is eligible for promotion?" +builder.buildExpression(emp1,"Or",emp3).interpret(context));
        System.out.println("Is both emp2 and emp4 are eligible for promotion? ?" + builder.buildExpression(emp2,"And",emp4).interpret(context));
        System.out.println("The statement 'emp3 is NOT eligible for promotion' is true? " + builder.buildExpression(emp3, "Not",null).interpret(context));
        //Invalid input expression
        //System.out.println("Is either emp1 or emp3 is eligible for promotion?" +builder.buildExpression(emp1,"Wrong",emp3).interpret(context));
    }
}

输出

这是输出。

***Interpreter Pattern Demo***

emp1 is eligible for promotion. false
emp2 is eligible for promotion. true
emp3 is eligible for promotion. true
emp4 is eligible for promotion. false
Is either emp1 or emp3 is eligible for promotion?true
Is both emp2 and emp4 are eligible for promotion? ?false
The statement 'emp3 is NOT eligible for promotion' is true? false

分析

您可以看到每个复合表达式都在调用其所有子表达式的interpret()方法。

修改后的插图

您已经看到了解释器模式的一个简单例子。从这个实现来看,您似乎已经处理了一些简单明了的表达式。因此,让我们在修改后的实现中处理一些复杂的规则或表达式。

修改的类图

在修改后的实现中,主要更改仅在 EmployeeBuilder 类中进行。所以,让我们快速浏览一下这个类的类图(见图 23-4 )。

img/395506_2_En_23_Fig4_HTML.jpg

图 23-4

修改了 EmployeeBuilder 类的类图

已修改的包资源管理器视图

在修改后的实现中,主要更改仅反映在 EmployeeBuilder 类中。因此,在本节中,我只扩展了这个类。图 23-5 显示了修改后的包浏览器视图。

img/395506_2_En_23_Fig5_HTML.jpg

图 23-5

已修改的包资源管理器视图

修改的实现

下面是修改后的实现。关键变化以粗体显示。

package jdp2e.interpreter.modified.demo;

import java.util.ArrayList;
import java.util.List;

interface Employee
{
    public boolean interpret(Context context);
}
class IndividualEmployee implements Employee
{
    private int yearOfExperience;

    private String currentGrade;

    public IndividualEmployee(int experience, String grade){
        this.yearOfExperience=experience;
        this.currentGrade=grade;
    }
    @Override
    public boolean interpret(Context context)
    {

        if(this.yearOfExperience>=context.getYearofExperience() && context.getPermissibleGrades().contains(this.currentGrade))
        {
            return true;
        }
        return false;
    }
}
class OrExpression implements Employee
{

    private Employee  emp1;
    private Employee  emp2;

    public OrExpression(Employee emp1, Employee emp2)
    {
        this.emp1 = emp1;
        this.emp2 = emp2;
    }

    @Override
    public boolean interpret(Context context)
    {
        return emp1.interpret(context) || emp2.interpret(context);
    }
}
class AndExpression implements Employee
{

    private Employee  emp1;
    private Employee  emp2;

    public AndExpression(Employee emp1, Employee emp2)
    {
        this.emp1 = emp1;
        this.emp2 = emp2;
    }

    @Override
    public boolean interpret(Context context)
    {
        return emp1.interpret(context) && emp2.interpret(context);
    }
}
class NotExpression implements Employee
{
    private Employee  emp;

    public NotExpression(Employee  expr)
    {
        this.emp = expr;
    }

    @Override
    public boolean interpret(Context context)
    {
        return !emp.interpret(context);
    }
}
class Context
{
    private int yearofExperience;
    private List<String> permissibleGrades;
    public Context(int experience,String... allowedGrades)
    {
        this.yearofExperience=experience;
        this.permissibleGrades=new ArrayList<>();
        for( String grade:allowedGrades)
        {
            permissibleGrades.add(grade);
        }
    }
    public int getYearofExperience()
    {
        return yearofExperience;
    }
    public List<String> getPermissibleGrades()
    {
        return permissibleGrades;
    }
}

class EmployeeBuilder

{

    // Building the tree
    //Complex Rule-1: emp1 and (emp2 or (emp3 or emp4))

    public Employee buildTree(Employee emp1, Employee emp2,Employee emp3,Employee emp4)
    {
        //emp3 or emp4
        Employee firstPhase=new OrExpression(emp3,emp4);
        //emp2 or (emp3 or emp4)
        Employee secondPhase=new OrExpression(emp2,firstPhase);
        //emp1 and (emp2 or (emp3 or emp4))
        Employee finalPhase=new AndExpression(emp1,secondPhase);
        return finalPhase;

    }
    //Complex Rule-2: emp1 or (emp2 and (not emp3 ))
    public Employee buildTreeBasedOnRule2(Employee emp1, Employee emp2,Employee emp3)
    {
        //Not emp3
        Employee firstPhase=new NotExpression(emp3);
        //emp2 or (not emp3)
        Employee secondPhase=new AndExpression(emp2,firstPhase);
        //emp1 and (emp2 or (not emp3 ))
        Employee finalPhase=new OrExpression(emp1,secondPhase);
        return finalPhase;

    }

}

public class ModifiedInterpreterPatternExample {

    public static void main(String[] args) {
        System.out.println("***Modified Interpreter Pattern Demo***\n");

        //Minimum Criteria for promoton is:
        //The year of experience is minimum 10 yrs. and
        //Employee grade should be either G2 or G3
        Context context=new Context(10,"G2","G3");
        //Different Employees with grades
        Employee emp1 = new IndividualEmployee(5,"G1");
        Employee emp2 = new IndividualEmployee(10,"G2");
        Employee emp3 = new IndividualEmployee(15,"G3");
        Employee emp4 = new IndividualEmployee(20,"G4");

        EmployeeBuilder builder=new EmployeeBuilder();

        //Validating the 1st complex rule
        System.out.println("Is emp1 and any of emp2,emp3, emp4 is eligible for promotion?" +builder.buildTree(emp1,emp2, emp3,emp4).interpret(context));
        System.out.println("Is emp2 and any of emp1,emp3, emp4 is eligible for promotion?" +builder.buildTree(emp2,emp1, emp3,emp4).interpret(context));
        System.out.println("Is emp3 and any of emp1,emp2, emp3 is eligible for promotion?" +builder.buildTree(emp3,emp1, emp2,emp4).interpret(context));
        System.out.println("Is emp4 and any of emp1,emp2, emp3 is eligible for promotion?" +builder.buildTree(emp4,emp1, emp2,emp3).interpret(context));

        System.out.println("");
        //Validating the 2nd complex rule
        System.out.println("Is emp1 or (emp2 but not emp3) is eligible for promotion?" +builder.buildTreeBasedOnRule2(emp1, emp2, emp3).interpret(context));
        System.out.println("Is emp2 or (emp3 but not emp4) is eligible for promotion?" +builder.buildTreeBasedOnRule2(emp2, emp3, emp4).interpret(context));
    }
}

修改输出

下面是修改后的输出。

***Modified Interpreter Pattern Demo***

Is emp1 and any of emp2,emp3, emp4 is eligible for promotion?false
Is emp2 and any of emp1,emp3, emp4 is eligible for promotion?true
Is emp3 and any of emp1,emp2, emp4 is eligible for promotion?true
Is emp4 and any of emp1,emp2, emp3 is eligible for promotion?false

Is emp1 or (emp2 but not emp3) is eligible for promotion?false
Is emp2 or (emp3 but not emp4) is eligible for promotion?true

分析

现在,您已经了解了如何使用解释器模式来处理遵循所示方法的复杂规则。

问答环节

  1. 什么时候应该使用这种模式?

    在日常编程中,不是很需要。尽管在一些罕见的情况下,您可能需要使用自己的编程语言来定义特定的协议。在这种情况下,这种模式可能会变得很方便。但是在你继续之前,你必须问问你自己关于投资回报(ROI)的问题。

  2. 使用解释器设计模式有什么好处?

    • 你很大程度上参与了如何为你的语言定义语法,以及如何表达和解释这些句子的过程。你也可以改变和扩展你的语法。

    • 你有充分的自由去解释这些表达。

  3. 使用解释器设计模式的相关挑战是什么?

    我相信工作量是最大的问题。维护复杂的语法也变得棘手,因为您可能需要创建(和维护)单独的类来处理不同的规则。

二十四、简单工厂模式

本章介绍简单工厂模式。

目的

在不向客户端公开实例化逻辑的情况下创建对象。

概念

在面向对象编程中,工厂是一种可以创建其他对象的特殊对象。可以通过多种方式调用工厂,但最常见的是,它使用一种可以返回具有不同原型的对象的方法。任何可以帮助创建这些新对象的子程序都被认为是一个工厂。使用工厂方法的最终目的是从应用程序的消费者那里抽象出对象创建机制(或过程)。

真实世界的例子

考虑一家制造不同型号汽车的汽车制造公司。他们必须有一个不同生产单位的工厂。其中一些单位可以生产所有型号通用的零件,而其他单位则专用于生产特定型号的零件。当他们制造最终产品时,他们将特定型号的零件与通用零件组装在一起。从客户的角度来看,汽车是由汽车厂制造的;客户不知道汽车是如何制造的。但是如果你进一步调查,你会发现基于汽车的模型,工厂的一个生产单元改变零件。例如,一个特定的汽车型号可以只支持手动变速箱,而另一个型号可以同时支持自动和手动变速箱。因此,基于汽车的模型,汽车工厂为汽车制造特殊的变速箱。

考虑一个更简单的例子。当孩子向他/她的父母要玩具时,孩子不知道父母将如何满足他/她的需求。在这种情况下,父母被认为是他们小孩的工厂。现在站在家长的角度思考。父母可以自己制作玩具或从商店购买玩具来逗孩子开心。

计算机世界的例子

简单工厂模式在软件应用程序中非常常见,但是在我们进一步讨论之前,您必须记住以下几点。

  • 在 GoF 的著名著作中,简单工厂并不被视为标准设计模式,但是这种方法对于您编写的任何应用程序来说都是常见的,您需要将变化很大的代码与没有变化的代码部分分开。假设您在编写的任何应用程序中都尝试遵循这种方法。

  • 简单工厂被认为是工厂方法模式(或抽象工厂模式)的最简单形式。因此,您可以假设任何遵循工厂方法模式或抽象工厂模式的应用程序也支持简单工厂模式的设计目标的概念。

注意

java.text.NumberFormat 类的静态 getInstance()方法就是这种类型的一个例子。

让我们按照我在一个常见用例中讨论的这个模式的实现来看。

说明

以下是以下实现的重要特征。

  • 在这个例子中,有两种动物:狗和老虎。对象创建过程取决于用户的输入。

  • 我假设他们每个人都会说话,他们更喜欢做一些动作。

  • SimpleFactory 是工厂类,simpleFactory(注意“s”不是大写的)是该类的对象。在客户端代码(SimpleFactoryPatternExample 类)中,您会看到下面一行。

preferredType = simpleFactory.createAnimal();

这意味着要获得 preferredType 对象,需要调用 simpleFactory 对象的createAnimal()方法。因此,使用这种方法,您不需要在客户端代码中直接使用“new”操作符来获取对象。

  • 我将变化的代码与最不可能变化的代码分开。这种方法有助于消除系统中的紧密耦合。(如何?遵循“问答环节”部分。)

注意

在某些应用程序中,您可能会注意到,在建议使用参数化构造函数的地方,这种模式有一点变化。因此,在那些应用程序中,要获得 preferredType 对象,您可能需要使用类似于这行的代码:preferred type = simple factory . create animal(" Tiger ")。

类图

图 24-1 显示了简单工厂模式的类图。

img/395506_2_En_24_Fig1_HTML.jpg

图 24-1

类图

包资源管理器视图

图 24-2 显示了程序的高层结构。

img/395506_2_En_24_Fig2_HTML.jpg

图 24-2

包资源管理器视图

履行

下面是实现。

package jdp2e.simplefactory.demo;
import java.util.Scanner;//Available Java5 onwards

interface Animal
{
    void speak();
    void preferredAction();
}
class Dog implements Animal
{
    public void speak()
    {
        System.out.println("Dog says: Bow-Wow.");
    }
    public void preferredAction()
    {
        System.out.println ("Dogs prefer barking...");
    }
}
class Tiger implements Animal
{
    public void speak()

    {
        System.out.println("Tiger says: Halum.");
    }
    public void preferredAction()
    {
        System.out.println("Tigers prefer hunting...");
    }
}
class SimpleFactory
{
    public Animal createAnimal()
    {
        Animal intendedAnimal=null;
        System.out.println("Enter your choice( 0 for Dog, 1 for Tiger)");
        /* To suppress the warning message:Resource leak:'input' is never closed. So,the following line is optional in this case*/
        @SuppressWarnings("resource")
        Scanner input=new Scanner(System.in);
        int choice=Integer.parseInt(input.nextLine());
        System.out.println("You have entered :"+ choice);
        switch (choice)
        {
        case 0:
            intendedAnimal = new Dog();
            break;
        case 1:
            intendedAnimal = new Tiger();
            break;
        default:
            System.out.println("You must enter either 0 or 1");
            //We'll throw a runtime exception for any other choices.
            throw new IllegalArgumentException(" Your choice tries to create an unknown Animal");
        }

        return intendedAnimal;
    }
}
//A client is interested to get an animal who can speak and perform an

//action.
class SimpleFactoryPatternExample
{
    public static void main(String[] args)     {
        System.out.println("*** Simple Factory Pattern Demo***\n");
        Animal preferredType=null;
        SimpleFactory simpleFactory = new SimpleFactory();
        // The code that will vary based on users preference.
        preferredType = simpleFactory.createAnimal();
        //The codes that do not change frequently.
        //These animals can speak and prefer to do some specific actions.
        preferredType.speak();
        preferredType.preferredAction();

    }
}

输出

这是输出。

案例 1。用户输入:0

*** Simple Factory Pattern Demo***

Enter your choice( 0 for Dog, 1 for Tiger)
0
You have entered :0
Dog says: Bow-Wow.
Dogs prefer barking...

案例 2。用户输入:1

*** Simple Factory Pattern Demo***

Enter your choice( 0 for Dog, 1 for Tiger) 

1
You have entered :1
Tiger says: Halum.
Tigers prefer hunting...

案例 3。不需要的用户输入:2

*** Simple Factory Pattern Demo***

Enter your choice( 0 for Dog, 1 for Tiger)
2
You have entered :2
You must enter either 0 or 1Exception in thread "main"
java.lang.IllegalArgumentException:  Your choice tries to create an unknown Animal
    at jdp2e.simplefactory.demo.SimpleFactory.createAnimal(SimpleFactoryPatternExample.java:54)
    at jdp2e.simplefactory.demo.SimpleFactoryPatternExample.main(SimpleFactoryPatternExample.java:68)

问答环节

  1. 在这个例子中,客户通过简单工厂委托对象的创建。但是相反,他们可以用“new”操作符直接创建对象。这是正确的吗?

    No. These are the key reasons behind the preceding design.

    • 一个重要的面向对象设计原则是将代码中最有可能发生变化的部分与其余部分分开。

    • 在这种情况下,只有“对象创建部分”不同。我假设这些动物必须说话和执行动作,并且我不需要改变客户端内部的那部分代码。因此,在将来,如果您需要修改创建过程,您只需要更改 SimpleFactory 类的createAnimal()方法。这些修改不会影响客户端代码。

    • “你是如何创造物体的?”隐藏在客户端代码中。这种抽象提高了安全性。

    • 这种方法可以帮助您避免在客户端代码中使用大量的if/else块(或 switch 语句),因为它们会让您的代码看起来很笨拙。

  2. 与此模式相关的挑战是什么?

    • 随着时间的推移,决定实例化哪个对象变得复杂。在这些情况下,您应该更喜欢工厂方法模式。

    • 如果你想添加一个新的动物或者删除一个已有的动物,你需要修改工厂类的createAnimal()方法。这种方法显然违反了坚实原则的开闭原则(基本上是说你的代码应该对扩展开放,但对修改关闭)。

注意

罗伯特·c·马丁提倡坚实的原则。你可以在 https://en.wikipedia.org/wiki/SOLID 了解他们。

  1. I learned that programming with an abstract class or interface is always a better practice. So, to make a better implementation, you could write something like this:

    abstract class ISimpleFactory
    {
        public abstract IAnimal createAnimal() throws IOException;
    }
    class SimpleFactory extends ISimpleFactory
    {
    
    
        //rest of the code
    }
    
    

    这是正确的吗?

    是的。用抽象类或接口编程总是更好的做法。这种方法可以防止您将来进行更改,因为任何新添加的类都可以简单地实现接口,并通过多态在体系结构中安顿下来。但是如果你仅仅依赖于具体的类,当你想在架构中集成一个新的类时,你需要改变你的代码,在这种情况下,你违反了规则,即你的代码应该被关闭以进行修改

    所以,你的理解是正确的。您可以使用这样的结构使它成为一个更好的程序。但最终,你会学到工厂方法模式(见第四章第四章),在这里你需要将实例化过程推迟到子类。因此,在这种情况下,建议你用抽象类或接口来编写程序。

  2. 能否让工厂类(SimpleFactory)成为静态的?

    不可以。在 Java 中,不允许用顶级类来标记单词 static 。换句话说,按照设计,编译器总是抱怨 Java 中的顶级静态类。

二十五、空对象模式

维基百科上说,“在面向对象的计算机编程中,空对象是没有引用值或定义了中性(空)行为的对象。空对象设计模式描述了此类对象的用途及其行为(或缺乏行为)。它最初发表在程序设计的模式语言系列丛书中。Hillside Group 发起了程序模式语言(PLoP)年会。

该模式可以实现“什么都不做”关系,或者当应用程序遇到空对象而不是真实对象时,它可以提供默认行为。简而言之,核心目标是通过if块避免“空对象检查”或“空协作检查”,从而制定一个更好的解决方案。使用这种模式,您试图通过提供一个默认的什么都不做的行为来封装对象的缺失。

概念

这种模式的显著特点是,当您在空对象上调用操作时,您不需要做任何事情(或者不存储任何东西)。考虑下面的程序和相应的输出。让我们试着理解与下面的程序段相关的问题,分析可能的解决方案,在本章的结尾,你会看到一个使用这种设计模式的更好的实现。

在下面的实现中,让我们假设您有两种类型的交通工具:公共汽车和火车。客户可以通过不同的输入选择公共汽车或火车对象,如“a”或“b”。让我们进一步假设应用程序认为这两个只是有效的输入。

错误的程序

这是一个错误的程序。

package jdp2e.nullobject.context.demo;

import java.util.Scanner;

interface Vehicle
{
    void travel();
}
class Bus implements Vehicle
{
    public static int busCount = 0;
    public Bus()
    {
        busCount++;
    }
    @Override
    public void travel()
    {
        System.out.println("Let us travel with a bus");
    }
}
class Train implements Vehicle
{
    public static int trainCount = 0;
    public Train()
    {
        trainCount++;
    }
    @Override
    public void travel()
    {
        System.out.println("Let us travel with a train");
    }

}

public class NeedForNullObjectPattern {

    public static void main(String[] args) {
        System.out.println("***Need for Null Object Pattern Demo***\n");
        String input = null;
        int totalObjects = 0;

        while (true)
        {
            System.out.println("Enter your choice( Type 'a' for Bus, 'b' for Train ) ");
            Scanner scanner=new Scanner(System.in);
            input = scanner.nextLine();
            Vehicle vehicle = null;
            switch (input.toLowerCase())
            {
            case "a":
                vehicle = new Bus();
                break;
            case "b":
                vehicle = new Train();
                break;
            }
            totalObjects = Bus.busCount + Train.trainCount;

            vehicle.travel();
            System.out.println("Total number of objects created in the system is : "+ totalObjects);
        }
    }
}

具有有效输入的输出

***Need for Null Object Pattern Demo***

Enter your choice( Type 'a' for Bus, 'b' for Train )
a
Let us travel with a bus
Total number of objects created in the system is : 1
Enter your choice( Type 'a' for Bus, 'b' for Train )
b
Let us travel with a train
Total number of objects created in the system is : 2
Enter your choice( Type 'a' for Bus, 'b' for Train )
b
Let us travel with a train
Total number of objects created in the system is : 3
Enter your choice( Type 'a' for Bus, 'b' for Train )

不需要输入的分析

假设用户错误地提供了一个不同的字符“d ”,如下所示:

***Need for Null Object Pattern Demo***

Enter your choice( Type 'a' for Bus, 'b' for Train )
a
Let us travel with a bus
Total number of objects created in the system is : 1
Enter your choice( Type 'a' for Bus, 'b' for Train )
b
Let us travel with a train
Total number of objects created in the system is : 2
Enter your choice( Type 'a' for Bus, 'b' for Train )
b
Let us travel with a train
Total number of objects created in the system is : 3
Enter your choice( Type 'a' for Bus, 'b' for Train )

d

遇到异常

这一次,您会收到System.NullPointerException运行时异常。

Enter your choice( Type 'a' for Bus, 'b' for Train )
d

Exception in thread "main"
java.lang.NullPointerException

    at jdp2e.nullobject.context.demo.NeedForNullObjectPattern.main(NeedForNullObjectPattern.java:61)

即时补救措施

您可能想到的直接补救方法是在调用操作之前进行空检查,如下所示:

//A immediate remedy
if(vehicle !=null)
{
    vehicle.travel();
}

分析

先前的解决方案在这种情况下有效。但是请考虑一个企业应用程序。如果您需要对每个可能的场景进行空检查,那么您可能需要在每次执行时评估大量的if条件,这种方法会使您的代码变脏。与此同时,你可能会注意到难以维护的副作用。空对象模式的概念在类似的情况下很有用。

真实世界的例子

让我们考虑一个现实生活中的洗衣机场景。如果门是关着的,并且供水顺畅,没有任何内部泄漏,洗衣机就可以正常洗涤。但是假设,有一次,你忘记关门或者中途停止供水。在这些情况下,洗衣机不应损坏自身。它可以发出一些警报来引起你的注意,并指示目前没有水或门仍然开着。

计算机世界的例子

假设在客户机服务器体系结构中,服务器根据客户机输入进行某种处理。服务器应该足够智能,以便它不会启动任何不必要的计算。在处理输入之前,它可能希望进行交叉验证,以确保是否需要启动该流程,或者是否应该忽略无效的输入。在这种情况下,您可能会注意到命令模式和空对象模式的使用。

基本上,在企业应用程序中,使用这种设计模式可以避免大量的空检查和if/else块。下面的实现可以让您很好地了解这种模式。

注意

在 Java 中,你可能见过 java.awt.event 包中各种适配器类的使用。可以认为这些类更接近于空对象模式。例如,考虑 MouseMotionAdapter 类。它是一个抽象类,但包含具有空体的方法,如 mouseDragged(MouseEvent e){ },mouseMoved(MouseEvent e){ }。但是由于适配器类是用 abstract 关键字标记的,所以不能直接创建该类的对象。

说明

和以前一样,在下面的实现中,让我们假设您有两种类型的交通工具:公共汽车和火车。客户可以通过不同的输入选择公共汽车或火车:“a”或“b”。如果用户错误地提供了任何无效数据(即,在这种情况下除了“a”或“b”之外的任何输入),他根本不能旅行。应用程序通过使用 NullVehicle 对象 什么都不做来忽略无效输入。在下面的例子中,我不会重复创建这些 NullVehicle 对象。一旦它被创建,我将简单地重用该对象。**

类图

图 25-1 为类图。(这个概念是用单例模式实现的,因此,您可以避免不必要的对象创建)。

img/395506_2_En_25_Fig1_HTML.jpg

图 25-1

类图

包资源管理器视图

图 25-2 显示了程序的高层结构。

img/395506_2_En_25_Fig2_HTML.jpg

图 25-2

包资源管理器视图

履行

下面是实现。

package jdp2e.nullobject.demo;
import java.util.Scanner;

interface Vehicle
{
    void travel();
}
class Bus implements Vehicle
{
    public static int busCount = 0;
    public Bus()
    {
        busCount++;
    }
    @Override
    public void travel()
    {
        System.out.println("Let us travel with a bus");
    }
}

class Train implements Vehicle
{
    public static int trainCount = 0;
    public Train()
    {
        trainCount++;
    }
    @Override
    public void travel()
    {
        System.out.println("Let us travel with a train");
    }
}
class NullVehicle implements Vehicle
{
    //Early initialization
    private static  NullVehicle instance = new NullVehicle();
    public static int nullVehicleCount;
    //Making constructor private to prevent the use of "new"
    private NullVehicle()
    {
        nullVehicleCount++;
        System.out.println(" A null vehicle object created.Currently null vehicle count is :  "+nullVehicleCount);
    }
    // Global point of access.
    public static NullVehicle getInstance()
    {
        //System.out.println("We already have an instance now. Use it.");
        return instance;
    }
    @Override
    public void travel()
    {
        //Do Nothing
    }
}

public class NullObjectPatternExample  {

    public static void main(String[] args) {
        System.out.println("***Null Object Pattern Demo***\n");
        String input = "dummyInput";
        int totalObjects = 0;
        Scanner scanner;
        while(!input.toLowerCase().contains("exit"))
        {
            System.out.println("Enter your choice( Type 'a' for Bus, 'b' for Train.Type 'exit' to close the application. ) ");
            scanner=new Scanner(System.in);
            if(scanner.hasNextLine())
            {
                input = scanner.nextLine();
            }
            Vehicle vehicle = null;
            switch (input.toLowerCase())
            {
            case "a":
                vehicle = new Bus();
                break;
            case "b":
                vehicle = new Train();
                break;
            case "exit":
                System.out.println("Closing the application");
                vehicle = NullVehicle.getInstance();
                break;
            default:
                System.out.println("Invalid input");
                vehicle =  NullVehicle.getInstance();
            }
            totalObjects = Bus.busCount + Train.trainCount+NullVehicle.nullVehicleCount;
            //A immediate remedy
            //if(vehicle !=null)
            //{
            vehicle.travel();
            //}
            System.out.println("Total number of objects created in the system is : "+ totalObjects);
        }

    }
}

输出

这是输出。

***Null Object Pattern Demo***

Enter your choice( Type 'a' for Bus, 'b' for Train.Type 'exit' to close the application. )
a
 A null vehicle object created.Currently null vehicle count is :  1
Let us travel with a bus
Total number of objects created in the system is : 2
Enter your choice( Type 'a' for Bus, 'b' for Train.Type 'exit' to close the application. )
b
Let us travel with a train
Total number of objects created in the system is : 3
Enter your choice( Type 'a' for Bus, 'b' for Train.Type 'exit' to close the application. )

c

Invalid input

Total number of objects created in the system is : 3
Enter your choice( Type 'a' for Bus, 'b' for Train.Type 'exit' to close the application. )

dfh

Invalid input

Total number of objects created in the system is : 3
Enter your choice( Type 'a' for Bus, 'b' for Train.Type 'exit' to close the application. )
exit

Closing the application
Total number of objects created in the system is : 3

分析

  • 无效输入及其影响以粗体显示。

  • 除了最初的情况,请注意,对象计数并没有因为空的车辆对象或无效的输入而增加。

  • 这次我没有执行任何空检查(注意下面代码段中的注释行)。

    //A immediate remedy
      //if(vehicle !=null)
      //{
       vehicle.travel();
      //}
    
    
  • 这次程序执行不会因为无效的用户输入而中断。

问答环节

  1. 开始时,我看到创建了一个额外的对象。是故意的吗?

    为了节省内存,我遵循了一种单例设计模式机制,该机制支持 NullVehicle 类结构中的早期初始化。我不想为每个无效输入创建一个 NullVehicle 对象。应用程序很可能需要处理大量的无效输入。如果您不注意这种情况,大量的 NullVehicle 对象驻留在系统中(基本上是无用的),这些对象会占用更多的内存。因此,您可能会注意到一些典型的副作用(例如,系统变慢等。).

  2. 为了实现一个简单的空对象模式,我可以忽略不同的对象计数器(在前一个例子中使用的)并减少大量代码。这是正确的吗?

    Yes. Ideally, consider the following code segment.

    //Another context
    List<Vehicle> vehicleList=new ArrayList<Vehicle>();
    vehicleList.add(new Bus());
    vehicleList.add(new Train());
    vehicleList.add(null);
    for( Vehicle vehicle : vehicleList)
      {
          vehicle.travel();
      }
    
    

    您不能循环通过这段代码,因为您遇到了java.lang.NullPointerException

    Note a class like the following.

    class NullVehicle implements Vehicle
    {
        @Override
        public void travel()
        {
            //Do nothing
        }
    }
    
    

    And you code like this:

    //Another context discussed in Q&A session
    List<Vehicle> vehicleList=new ArrayList<Vehicle>();
    vehicleList.add(new Bus());
    vehicleList.add(new Train());
    //vehicleList.add(null);
    
    vehicleList.add(new NullVehicle());
    
    for( Vehicle vehicle : vehicleList)
    {
     vehicle.travel();
    }
    
    

    This time you can loop through smoothly. So, remember that the following structure prior to implementing a null object pattern (see Figure 25-3).

    img/395506_2_En_25_Fig3_HTML.jpg

    图 25-3

    空对象模式的基本结构

  3. 什么时候应该使用这种模式?

    • 如果您不想在某些典型场景中遇到 Java 中的 NullPointerException,那么该模式非常有用。(例如,如果您错误地试图调用一个空对象的方法。)

    • 您可以忽略代码中的大量“空检查”。

    • 没有这些空检查会使您的代码更整洁,更易于维护。

  4. 与空对象模式相关的挑战是什么?

    • 在某些情况下,您可能希望找到失败的根本原因。因此,如果抛出对您更有意义的 NullPointerException,您总是可以在try / catchtry / catch / finally块中处理这些异常,并相应地更新日志信息。

    • 空对象模式基本上帮助我们实现一个默认行为,当你无意识地想要处理一个根本不存在的对象时。但是这种方法可能不适合系统中每个可能的对象。

    • 空对象模式的不正确实现会抑制在程序执行中正常出现的真包。

    • 在所有可能的场景中创建一个合适的空对象可能并不容易。在某些类中,这可能会导致影响类方法的变化。

  5. 空对象像代理一样工作。这是正确的吗?

    不。一般来说,代理在某个时间点作用于真实对象,它们也可以提供一些行为。但是空对象不应该做这样的事情。

  6. 空对象模式总是与 NullPointerException 相关联。这是正确的吗?

    概念是相同的,但是异常名可以不同或者是特定于语言的。例如,在 Java 中,您使用它来保护 java.lang.NullPointerException,但是在像 C# 这样的语言中,您可以使用此模式来保护 System.NullReferenceException。

二十六、MVC 模式

模型-视图-控制器(MVC)是一种架构模式。

这种模式的使用在 web 应用程序中或我们开发强大的用户界面时很常见。但值得注意的是,Trygve Reenskaug 在 1979 年的一篇题为“Smalltalk-80TM 中的应用程序编程:如何使用模型-视图-控制器”的论文中首次描述了 MVC,这是在万维网时代之前。那时候还没有 web 应用的概念。但是现代的应用程序可以被看作是最初概念的一种适应。值得注意的是,一些开发人员认为这不是真正的设计模式,相反,他们更愿意称之为“MVC 架构”

在这里,您将用户界面逻辑从业务逻辑中分离出来,并以一种可以有效重用的方式分离主要组件。这种方法也促进了并行开发。MVC 最好的标题之一是“我们需要智能模型、瘦控制器和愚蠢的视图。”( http://wiki.c2.com/?ModelViewController

概念

从这个介绍中,很明显模式由三个主要组件组成:模型、视图和控制器。控制器放置在视图和模型之间,模型和视图只能通过控制器相互通信。在这里,您将数据显示的机制与数据操作的机制分开。图 26-1 显示了一个典型的 MVC 架构。

img/395506_2_En_26_Fig1_HTML.jpg

图 26-1

典型的 MVC 架构

需要记住的要点

以下是对该模式中关键组件的简要描述。

  • 视图表示输出。它是表示层。把它想象成一个用户界面/图形用户界面。你可以用各种技术来设计它。例如,在. NET 应用程序中,您可以使用 HTML、CSS、WPF 等等,而对于 Java 应用程序,您可以使用 AWT、Swing、JSF、JavaFX 等等。

  • 模型是应用程序的大脑。它管理数据和业务逻辑。它知道如何存储和管理(或操作)数据,以及如何处理来自控制器的请求。但是这个组件与视图组件是分离的。一个典型的例子是数据库、文件系统或类似的存储。它可以用 JavaBeans、Oracle、SQL Server、DB2、Hadoop、MySQL 等等来设计。

  • 控制器是接受来自视图组件的用户输入并将请求传递给模型的中介。当它从模型得到响应时,它将数据传递给视图。可以用 C# 设计。NET、ASP.NET、VB.NET、核心 Java、JSP、servlets、PHP、Ruby、Python 等等。

这种架构在不同的应用中有不同的实现方式。其中一些如下:

  • 您可以有多个视图。

  • 视图可以将运行时值(例如,使用 JavaScript)传递给控制器。

  • 您的控制器可以验证用户的输入。

  • 您的控制器可以通过多种方式接收输入。例如,它可以通过 URL 从 web 请求中获取输入,或者您可以通过按下表单上的 Submit 按钮来传递输入。

  • 在某些应用程序中,模型组件可以更新视图组件。

基本上,你需要使用这个模式来支持你自己的需求。图 26-2 、图 26-3 和图 26-4 展示了 MVC 架构的一些已知变体。

变体 1

img/395506_2_En_26_Fig2_HTML.jpg

图 26-2

典型的 MVC 框架

变体 2

img/395506_2_En_26_Fig3_HTML.jpg

图 26-3

一个多视图的 MVC 框架

变体 3

img/395506_2_En_26_Fig4_HTML.jpg

图 26-4

用观察者模式/基于事件的机制实现的 MVC 模式

我最喜欢的关于 MVC 的描述来自 Connelly Barnes,他说:“理解 MVC 的一个简单方法是:模型是数据,视图是屏幕上的窗口,控制器是两者之间的粘合剂。”( http://wiki.c2.com/?ModelViewController

真实世界的例子

让我们重温一下模板方法模式的真实例子。但是这一次你有不同的解释。我说过,在餐馆里,根据顾客的输入,厨师可以改变口味,做出最终的产品。顾客不直接向厨师点餐。顾客看到菜单卡(视图),可能会咨询服务员,并下订单。服务员将订单交给厨师,厨师从餐厅的厨房收集所需的材料(类似于仓库/计算机数据库)。准备好后,服务员会把盘子端到顾客的桌子上。所以,你可以考虑把服务员/女招待的角色作为控制者,把厨师和他们的厨房作为模型(把准备食物的材料作为数据)。

计算机世界的例子

许多 web 编程框架使用 MVC 框架的概念。一些典型的例子包括 Django、Ruby on Rails、ASP.NET 等等。例如,一个典型的 ASP.NET MVC 项目的结构如图 26-5 所示。

img/395506_2_En_26_Fig5_HTML.jpg

图 26-5

ASP.NET 项目中典型的 MVC 结构

但是应该注意的是,不同的技术可以遵循不同的结构,因此,没有必要像这样获得具有严格命名约定的文件夹结构。在 Java 世界中,在 MVC 架构中,您可能会注意到使用 Java servlets 作为控制器,使用 JavaBeans 作为模型,而 JSP 创建不同的视图。

说明

大多数情况下,您希望将 MVC 的概念与能够为您提供内置支持并为您做大量基础工作的技术结合使用。在这种情况下,你可能需要学习新的术语。在 Java 应用程序中,您可能希望使用 Swing 或 JavaFX 等来获得更好的 GUI。

在本书中,我使用了一个控制台窗口来显示不同设计模式实现的输出。因此,让我们在即将到来的实现中继续使用控制台窗口作为视图,因为这里的重点是 MVC 结构,而不是新技术。

为了简单和符合我们的理论,我将即将到来的实现分成三个基本部分:模型、视图和控制器。当您查看 Package Explorer 视图时,您会看到创建了单独的包来完成这项任务。以下是一些要点。

  • 在这个应用中,要求非常简单。有些员工需要在应用程序/系统中注册自己。最初,应用程序从三个不同的注册员工开始:Amit、Jon 和 Sam。在任何时候,您都应该能够在系统中看到注册的员工。

  • 您可以在注册员工列表中添加新员工或删除员工。

  • 在 Employee 类中添加了一个简单的检查,以确保不会在应用程序中重复添加雇员。

  • 要从注册列表中删除一个雇员,您需要在客户机代码中传递雇员 ID,但是如果在注册列表中没有找到雇员 ID,应用程序将什么也不做。

现在检查一下实现,并考虑一下您的即时参考意见。

类图

图 26-6 为类图。为了强调核心架构,我省略了客户端代码依赖性。

img/395506_2_En_26_Fig6_HTML.png

图 26-6

类图

包资源管理器视图

图 26-7 显示了程序的高层结构。

img/395506_2_En_26_Fig7_HTML.jpg

图 26-7

包资源管理器视图

履行

下面是实现。

//Employee.java

package jdp2e.mvc.model;

//The key "data" in this application
public class Employee
{
    private String empName;
    private String empId;
    public String getEmpName() {
        return empName;
    }
    public String getEmpId() {
        return empId;
    }
    public Employee(String empName, String empId)
    {
        this.empName=empName;
        this.empId=empId;
    }
    @Override
    public String toString()
    {
        return empName + "'s employee id is: "+ empId ;
    }
    @Override
    //To check uniqueness.
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Employee)) return false;

        Employee empObject = (Employee) o;

        if (!empName.equals(empObject.empName)) return false;
        //cannot use the following for an int
        if (!empId.equals(empObject.empId)) return false;
        return true;
    }
}

//Model.java

package jdp2e.mvc.model;

import java.util.List;

//Model interface
public interface Model
{

    List<Employee> getEnrolledEmployeeDetailsFromModel();
    void addEmployeeToModel(Employee employeee);
    void removeEmployeeFromModel(String employeeId);
}

//EmployeeModel.java

package jdp2e.mvc.model;

import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;

//EmployeeModel class

public class EmployeeModel implements Model
{
    List<Employee> enrolledEmployees;

    public EmployeeModel()
    {
        //Adding 3 employees at the beginning.
        enrolledEmployees = new ArrayList<Employee>();
        enrolledEmployees.add(new Employee("Amit","E1"));
        enrolledEmployees.add(new Employee("John","E2"));
        enrolledEmployees.add(new Employee("Sam","E3"));
    }

    public List<Employee> getEnrolledEmployeeDetailsFromModel()
    {
        return enrolledEmployees;
    }

    //Adding an employee to the model(student list)
    @Override
    public void addEmployeeToModel(Employee employee)
    {
        System.out.println("\nTrying to add an employee to the registered list.");
        if( !enrolledEmployees.contains(employee))
        {
            enrolledEmployees.add(employee);
            System.out.println(employee+" [added recently.]");
        }
        else
        {
            System.out.println(employee+" is already added in the system.");
        }
    }
    //Removing an employee from model(student list)
    @Override
    public void removeEmployeeFromModel(String employeeId)
    {
        boolean flag=false;
        ListIterator<Employee> employeeIterator=enrolledEmployees.listIterator();
        System.out.println("\nTrying to remove an employee from the registered list.");
        while(employeeIterator.hasNext())
        {
            Employee removableEmployee=((Employee)employeeIterator.next());
            if(removableEmployee.getEmpId().equals(employeeId))
            {
                //To avoid ConcurrentModificationException,try to
                //remember to invoke remove() on the iterator but not on
                //the list.
                employeeIterator.remove();
                System.out.println("Employee " + removableEmployee.getEmpName()+ " with id "+ employeeId+" is removed now.");
                flag=true;
            }

        }
        if(flag==false)
        {
            System.out.println("###Employee Id " + employeeId +" Not found.###");
        }

    }

}

//View.java

package jdp2e.mvc.view;
import java.util.List;
import jdp2e.mvc.model.Employee;

public interface View
{
    void showEnrolledEmployees(List<Employee> enrolledEmployees);
}

//ConsoleView.java

package jdp2e.mvc.view;

import java.util.List;
import jdp2e.mvc.model.Employee;

//ConsoleView class

public class ConsoleView implements View
{
    @Override
    public void showEnrolledEmployees(List<Employee> enrolledEmployees)
    {
        System.out.println("\n ***This is a console view of currently enrolled employees.*** ");
        for( Employee employee : enrolledEmployees)
        {
            System.out.println(employee);
        }
        System.out.println("---------------------");
    }
}

//Controller.java

package jdp2e.mvc.controller;

import jdp2e.mvc.model.Employee;

//Controller
public interface Controller
{
    void displayEnrolledEmployees();
    void addEmployee(Employee employee);
    void removeEmployee(String employeeId);
}

//EmployeeController.java

package jdp2e.mvc.controller;

import java.util.List;

import jdp2e.mvc.model.*;
import jdp2e.mvc.view.*;

public class EmployeeController implements Controller
{
    private Model model;
    private View  view;

    public EmployeeController(Model model, View view)
    {
        this.model = model;
        this.view = view;
    }
    @Override
    public void displayEnrolledEmployees()
    {
        //Get data from Model
        List<Employee> enrolledEmployees = model.getEnrolledEmployeeDetailsFromModel();
        //Connect to View
        view.showEnrolledEmployees(enrolledEmployees);
    }

    //Sending a request to model to add an employee to the list.
    @Override
    public void addEmployee(Employee employee)
    {
        model.addEmployeeToModel(employee);
    }
    //Sending a request to model to remove an employee from the list.
    @Override
    public void removeEmployee(String employeeId)
    {
        model.removeEmployeeFromModel(employeeId);
    }
}

//客户端代码

//mvcarchitectureexample . Java

package jdp2e.mvc.demo;
import jdp2e.mvc.model.*;
import jdp2e.mvc.view.*;
import jdp2e.mvc.controller.*;

public class MVCArchitectureExample {

    public static void main(String[] args) {
        System.out.println("***MVC architecture Demo***\n");
        //Model
        Model model  = new EmployeeModel();

        //View
        View view = new ConsoleView();

        //Controller
        Controller controller = new EmployeeController(model, view);
        controller.displayEnrolledEmployees();

        //Add an employee
        controller.addEmployee(new Employee("Kevin","E4"));
        controller.displayEnrolledEmployees();

        //Remove an existing employee using the employee id.
        controller.removeEmployee("E2");
        controller.displayEnrolledEmployees();

        //Cannot remove an  employee who does not belong to the list.
        controller.removeEmployee("E5");
        controller.displayEnrolledEmployees();

        //Avoiding duplicate entry
        controller.addEmployee(new Employee("Kevin","E4"));
    }
}

输出

这是输出。

***MVC architecture Demo***

 ***This is a console

view of currently enrolled employees.***
Amit's employee id is: E1
John's employee id is: E2
Sam's employee id is: E3
---------------------

Trying to add an employee to the registered list.
Kevin's employee id is: E4 [added recently.]

 ***This is a console view of currently enrolled employees.***
Amit's employee id is: E1
John's employee id is: E2
Sam's employee id is: E3
Kevin's employee id is: E4
---------------------

Trying to remove an employee from the registered list.
Employee John with id E2 is removed now.

 ***This is a console view of currently enrolled employees.***
Amit's employee id is: E1
Sam's employee id is: E3
Kevin's employee id is: E4
---------------------

Trying to remove an employee from the registered list.
###Employee Id E5 Not found.###

 ***This is a console view of currently enrolled employees.***
Amit's employee id is: E1
Sam's employee id is: E3
Kevin's employee id is: E4
---------------------

Trying to add an employee to the registered list.
Kevin's employee id is: E4 is already added in the system.

问答环节

  1. 假设你有一名程序员、一名数据库管理员和一名图形设计师。你能猜出他们在 MVC 架构中的角色吗?

    图形设计师设计视图层。数据库管理员制作模型,程序员制作智能控制器。

  2. 使用 MVC 设计模式的关键 优势 是什么?

    • “高内聚、低耦合”是 MVC 的口号。在这种模式中,模型和视图之间的紧密耦合很容易消除。因此,它可以很容易地扩展和重用。

    • 它支持并行开发。

    • 您还可以提供多个运行时视图。

  3. 与 MVC 模式相关的 挑战 有哪些?

    • 需要高技能人员。

    • 它可能不适合微小的应用。

    • 开发者需要熟悉多种语言/平台/技术。

    • 多部分一致性是一个大问题,因为您将整个项目分成三个不同的部分。

  4. 在这个实现中,你能提供多个视图吗?

    Sure. Let’s add a new view called “Mobile view” in the application. Let’s add this class inside the jdp2e.mvc.view package as follows.

    package jdp2e.mvc.view;
    import java.util.List;
    import jdp2e.mvc.model.Employee;
    
    //This class is added to discuss a question in "Q&A Session"
    
    //MobileView class
    
    public class MobileView implements View
    {
        @Override
        public void showEnrolledEmployees(List<Employee> enrolledEmployees)
        {
            System.out.println("\n ***This is a mobile view of currently enrolled employees.*** ");
            System.out.println("Employee Id"+ "\t"+ " Employee Name");
            System.out.println("______________________");
            for( Employee employee : enrolledEmployees)
            {
                System.out.println(employee.getEmpId() + "\t"+ employee.getEmpName());
            }
            System.out.println("---------------------");
        }
    }
    
    

修改后的包浏览器视图类似于图 26-8 。

img/395506_2_En_26_Fig8_HTML.jpg

图 26-8

已修改的包资源管理器视图

将以下代码段添加到客户端代码的末尾。

//This segment is addeed to discuss a question in "Q&A Session"
view = new MobileView();
controller = new EmployeeController(model, view);
controller.displayEnrolledEmployees();

现在,如果您运行应用程序,您会看到修改后的输出。

修改输出

下面是修改后的输出。输出的最后一部分显示了新更改的效果。这些变化用粗体显示。

***MVC architecture Demo***

 ***This is a console view of currently enrolled employees.***
Amit's employee id is: E1
John's employee id is: E2
Sam's employee id is: E3
---------------------

Trying to add an employee to the registered list.
Kevin's employee id is: E4 [added recently.]

 ***This is a console view of currently enrolled employees.***
Amit's employee id is: E1
John's employee id is: E2
Sam's employee id is: E3
Kevin's employee id is: E4
---------------------

Trying to remove an employee from the registered list.
Employee John with id E2 is removed now.

 ***This is a console view of currently enrolled employees.***
Amit's employee id is: E1
Sam's employee id is: E3
Kevin's employee id is: E4
---------------------

Trying to remove an employee from the registered list.
###Employee Id E5 Not found.###

 ***This is a console view of currently enrolled employees.***
Amit's employee id is: E1
Sam's employee id is: E3
Kevin's employee id is: E4
---------------------

Trying to add an employee to the registered list.
Kevin's employee id is: E4 is already added in the system.

 ***This is a mobile view of currently enrolled employees.***

Employee Id   Employee Name

______________________

E1    Amit

E3    Sam

E4    Kevin

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

二十七、对设计模式的批评

在这一章中,我提出了一些对设计模式的批评。阅读批评能提供真正的价值。如果你在设计软件之前批判性地思考模式,你可以在某种程度上预测你的“投资回报”。设计模式基本上帮助你从他人的经验中获益。这就是通常所说的经验复用。你学习他们如何解决挑战,他们如何试图在他们的系统中适应新的行为,等等。一个模式可能并不完全适合你的工作,但是如果你在开始的时候专注于最佳实践以及模式的问题,你就更有可能做出更好的应用。

以下是对模式的一些批评。

  • Christopher Alexander 考虑了多年来变化不大的领域(与软件行业相比)。相反,软件行业总是在变化,软件开发的变化比任何其他领域都要快。所以,你不能从克里斯托弗·亚历山大考虑的领域(建筑和城镇)出发。

  • 在今天的世界里,你写程序的方式是不同的,你现在拥有的设备比过去的编程时代要多得多。因此,当您基于一些旧的实践提取模式时,您基本上对它们表现出了额外的尊重。

  • 许多模式彼此接近。每种模式都有利弊(我在每章末尾的“问答环节”中讨论过它们。)一种情况下的缺陷在另一种情况下可能是真正的优点。

  • 今天给你带来满意结果的模式,在不久的将来会成为你的一个大负担,因为软件行业的“持续变化”。

  • 用有限数量的设计模式很难设计出无限数量的需求。

  • 设计一个软件基本上是一门艺术。没有最好的艺术的定义或标准。

  • 设计模式给你的是想法,而不是实现(比如库或框架)。每个人的思想都是独特的。因此,每个工程师可能都有自己的偏好来实现类似的概念,这可能会在团队中造成混乱。

  • 考虑一个简单的例子。模式鼓励人们编写超类型(抽象类/接口)的代码。但是对于一个简单的应用程序,您知道没有即将到来的变化,或者应用程序只是为了演示的目的而创建的,这个规则可能没有多大意义。

  • 同样,在一些小型应用程序中,您可能会发现强制执行设计模式的规则会增加您的代码大小和维护成本。

  • 抹去旧的适应新的并不总是容易的。例如,当你第一次了解到遗传时,你很兴奋。你可能想以多种方式使用它,并且只看到这个概念的好处。但是后来当你开始尝试设计模式时,你开始了解到在许多情况下,组合比继承更受青睐。这种换挡并不容易。

  • 设计模式基于一些关键原则,其中之一是识别可能变化的代码,然后将其从代码的其余部分中分离出来。从理论角度听起来很不错。但是在现实世界中,谁能保证你的判断是完美的呢?软件行业总是在变化,它需要适应新的需求。

  • 许多模式已经与现代语言集成在一起。您可以使用语言构造中的内置支持,而不是从头开始实现模式。例如,您可能会注意到,在某些上下文中,每个模式都有 JDK 实现。

  • 不恰当地使用模式会导致反模式(例如,不恰当地使用中介模式会导致“上帝级”反模式)。我在第二十八章中给出了反模式的简要概述。

  • 许多人认为设计模式的概念只是表明一种编程语言可能需要额外的特性。因此,随着现代编程语言能力的增强,模式变得不那么重要了。维基百科说,计算机科学家 Peter Norvig 认为,通过 Lisp 或 Dylan 的直接语言支持,GoF 设计模式中的 23 种模式中有 16 种被简化或消除。在 https://en.wikipedia.org/wiki/Software_design_pattern 可以看到一些类似的想法。

  • 最后,设计模式基本上帮助你从别人的经验中获益。你正在了解他们的想法,你开始知道他们是如何遇到挑战的,他们是如何试图在他们的系统中适应新的行为,等等。但是你首先假设一个初学者或相对缺乏经验的人不能比他/她的前辈更好地解决问题。在一些特定的场合,一个相对来说经验不足的人可以比他的前辈有更好的眼光,他可以在未来更有效的证明自己。

问答环节

  1. 有这些模式的目录吗?

    我从 GoF 的 23 种设计模式开始,然后在本书中讨论了另外三种模式。GoF 的目录被认为是最基本的模式目录。

    但是肯定有许多其他目录专注于特定的领域。

    波特兰模式库和希尔赛德集团的网站在这方面是众所周知的。你可以在http://wiki.c2.com/?WelcomeVisitorshttps://hillside.net/patterns/patterns-catalog从这些资源中获得宝贵的见解和思想。

    希尔赛德集团的网站还提到了它的各种会议和研讨会。

注意

在撰写本文时,书中的 URL 运行良好,但其中一些链接和访问这些链接的策略可能会在未来发生变化。

  1. 你为什么不涵盖其他模式?

    These are my personal beliefs:

    • 计算机科学不断发展,你不断得到新的模式。

    • 如果您不熟悉基本模式,您就不能评估剩余的或即将到来的模式的真正需求。比如,如果你很了解 MVC,你就能看出它和模型-视图-展示者(MVP)有什么不同,明白为什么需要 MVP。

    • 这本书已经很厚了。对每个模式的详细讨论需要更多的页面,这将使这本书太大而难以消化。

因此,在这本书里,我把重点放在在今天的编程世界中仍然相关的基本模式上。

  1. 我经常在描述设计模式时看到“力”这个词。这是什么意思?

    这是开发人员证明其开发合理性的标准。概括地说,你的目标和当前的约束是你的力量的两个重要部分。因此,当您开发您的应用程序时,您可以用这些部分来证明您的开发。

  2. 在各种论坛上,我看到人们为模式的定义争论不休,并且说“模式是在一个环境中对一个问题的一个被证实的解决方案。”这是什么意思?

    这是一个简单易记的模式定义。但是简单地把它分成三个部分(问题、背景和解决方案)是不够的。

    举个例子,假设你要去机场,你很匆忙。突然,你发现你把登机牌落在家里了。我们来分析一下情况:

    问题 :你需要准时到达机场。

    语境 :把登机牌落在家里了。

    可能想到的解决办法 :折返,高速,冲回家拿登机牌。

    这个解决方案可能只工作一次,但是你能重复地应用同样的程序吗?你知道答案。这不是一个明智的解决方案,因为它取决于你有多少时间从家里领取通行证并返回机场。还要看目前路上的交通等很多因素。所以,即使你能获得一次成功,你也要为将来类似的情况准备一个更好的解决方案。

    所以,试着理解意思、意图、上下文等等,以便清楚地理解一个模式。

  3. 有时我会对两种不同模式的相似 UML 图感到困惑。此外,在许多情况下,我还对模式的分类感到困惑。我该如何克服这一点?

    这是非常自然的。您越是阅读和分析这些实现,越是试图理解设计背后的意图,它们之间的区别就会越清晰。

  4. 什么时候我应该考虑写一个新的模式?

    写一个新的模式并不容易。在投入精力之前,您需要大量研究并评估可用的模式。但是如果您找不到任何现有的模式来满足您特定领域的需求,您可能需要编写自己的模式。如果您的解决方案通过了“三原则”(基本上是说,要获得标签“模式”,一个解决方案需要在现实世界的解决方案中成功应用至少三次),那就非常好了。一旦你做到了这一点,你就可以让其他人知道它,参与讨论论坛,并从其他人那里得到反馈。这项活动对您和开发社区都有帮助。

二十八、反模式:避免常见错误

没有反模式,设计模式的讨论就不完整。本章简要概述了反模式。我们开始吧。

什么是反模式?

在现实世界的应用程序开发中,您可能会遵循最初非常吸引人的方法,但是从长远来看,它们会带来问题。例如,你试图快速解决问题,以满足交货期限,但如果你没有意识到潜在的陷阱,你可能会付出很大的代价。

反模式提醒您会导致糟糕解决方案的常见错误。了解它们有助于你采取预防措施。“预防胜于治疗”这句谚语非常适合这种情况。

注意

反模式通过描述有吸引力的方法如何使您未来的生活变得困难,提醒您常见的错误。与此同时,他们提出了一些替代解决方案,这些方案在开始时可能看起来很难,但最终会帮助你建立一个更好的解决方案。简而言之,反模式识别既定实践中的问题,它们可以将一般情况映射到特定类别的高效解决方案。他们还可以提供更好的计划来扭转一些不良做法,并制定健康的解决方案。

反模式简史

设计模式的最初想法来自建筑建筑师 Christopher Alexander。他分享了他在规划良好的城镇中建造建筑物的想法。渐渐地,这些概念进入了软件开发,并通过像沃德·坎宁安和肯特·贝克这样的前沿软件开发人员而流行起来。1994 年,通过一个关于设计模式的行业会议,设计模式的思想进入了主流的面向对象软件开发,被称为程序设计的模式语言(PLoP)。它是由希尔赛德集团主办的。吉姆·科普林的论文《生成模式语言的发展过程》就是在这种背景下发表的著名论文。随着四人帮推出经典文本设计模式:可重用面向对象软件的元素,设计模式变得非常流行。

毫无疑问,这些伟大的设计模式帮助了(并且仍然在帮助)程序员开发高质量的软件。但是人们也开始注意到负面影响。一个常见的例子是,许多开发人员想要展示他们的专业知识,而没有对这些模式在其特定领域中的结果进行真正的评估。作为一个明显的副作用,模式被植入了错误的上下文中,这产生了低质量的软件,并最终对开发者和他们的公司造成了巨大的损失。

因此,软件行业需要关注这些错误的负面后果,最终,反模式的思想得到了发展。许多专家开始在这一领域做出贡献,但是第一个结构良好的模型来自 Michael Akroyd 的演讲“反模式:针对对象误用的疫苗”这是 GoF 设计模式的对外观。

术语反模式在他们的书反模式:重构软件、架构和危机中的项目 (Wiley,1998)中变得流行起来。后来,斯科特·托马斯加入了他们的小组。他们说,

由于反模式有如此多的贡献者,将反模式的最初想法分配给单一来源是不公平的。相反,反模式是补充设计模式运动和扩展设计模式模型的自然步骤。

反模式的例子

下面是一些反模式及其背后的概念/思维模式的例子。

  • 过度使用模式:开发人员可能会不惜任何代价尝试使用模式,不管它是否合适。

  • 神类:试图用许多不相关的方法控制几乎一切的大物体。不恰当地使用中介模式可能会导致这种反模式。

  • 不是在这里发明的:我是一家大公司,我希望一切从零开始。虽然已经有一个由小公司开发的库,但我不会使用它。我会自己做所有的东西,一旦开发出来,我会用我的品牌价值宣布,“嘿,伙计们。终极图书馆为你而生。

** 零表示空:一个常见的例子包括开发者认为没有人想在纬度零,经度零。另一种常见的变化是程序员使用:1,999 或类似的数字来表示不合适的整数值。当用户在应用程序中将“09/09/9999”视为空日期时,会观察到另一个错误的用例。因此,在这些情况下,如果用户需要数字:1,999 或日期“09/09/9999”,他将无法获得这些数字。

*   *金锤*:X 先生相信技术 T 永远是最好的。所以,如果他需要开发一个新系统(这需要新的学习),他仍然更喜欢 T,即使它不合适。他认为,“如果我能用 t 来管理它,我就不需要学习更多的技术了。”

*   *数字管理*:提交次数越多,代码行数越多,或者修复的缺陷越多,都是伟大开发者的标志。比尔·盖茨说过,“用代码行数来衡量编程进度,就像用重量来衡量飞机制造进度一样。”

*   *拍信使*:你已经有压力了,节目截止日期快到了。有一个聪明的测试人员,他总能找到难以修复的典型缺陷。所以,在这个阶段,你不想让他参与进来,因为他会发现更多的缺陷,可能会错过最后期限。

*   *瑞士军刀*:要求产品能够满足顾客的各种需求。或者制造一种可以治愈所有疾病的药物。或者设计服务于各种不同需求的客户的软件。界面有多复杂并不重要。

*   *复制粘贴编程*:我需要解决一个问题,但是我已经有了一段代码来处理类似的情况。因此,我可以复制当前正在运行的旧代码,并在必要时开始修改它。但是当你从一个现有的拷贝开始时,你基本上继承了所有与之相关的潜在缺陷。此外,如果将来需要修改原始代码,您需要在多个地方实现修改。这种做法也违反了*不重复自己*的原则。

*   *架构师不编码*:我是一个架构师。我的时间很宝贵。我将只展示路径或给出一个关于编码的伟大演讲。有足够多的实现者应该实现我的想法。*架构师打高尔夫*是这个反模式的姐妹。

*   *隐藏并悬停*:不要暴露所有编辑或删除链接,直到他/她悬停在元素上。

*   *伪装链接和广告*:当用户点击一个链接或一个广告时获得收入,但他们不能得到他们想要的。* 

*### 注意

现在,您可以从不同的网站/来源了解各种反模式。例如,一个维基百科页面谈到了各种反模式(参见 https://en.wikipedia.org/wiki/Antipattern ) 您还可以在 http://wiki.c2.com/?AntiPatternsCatalog 获得反模式目录的详细列表,以了解更多信息。您可能还会注意到,反模式的概念并不局限于面向对象的编程。

反模式的类型

反模式可以属于多个类别。甚至一个典型的反模式也可以属于多个类别。

以下是一些常见的分类。

  • 架构反模式:瑞士军刀反模式就是这一类的例子。

  • 开发反模式:God 类和过度使用模式就是这一类的例子。

  • 管理反模式:拍摄信使反模式就属于这一类。

  • 组织反模式:架构师不编码,架构师打高尔夫就是这一类的例子。

  • 用户界面反模式:例子包括伪装的链接和广告。

注意

伪装链接和广告也被称为黑暗模式

问答环节

  1. 反模式和设计模式有什么关系?

    使用设计模式,您可以重用前人的经验。当你开始仅仅为了使用而盲目地使用这些概念时,你就陷入了重复使用循环解决方案的陷阱。这会让你陷入糟糕的境地。然后你会发现你的投资回报在不断减少,但维护成本却在不断增加。表面上简单和标准的解决方案(或模式)可能会在将来给你带来更多的问题。

  2. 设计模式可能会变成反模式。这是正确的吗?

    是的。如果你在错误的环境中应用了一个设计模式,那会带来比它所解决的问题更多的麻烦。最终它会变成一个反模式。所以,理解问题的本质和背景是非常重要的。

  3. 反模式只与软件开发人员相关。这是正确的吗?

    不。反模式的用处不仅限于开发人员;它可能也适用于其他人;例如,反模式对经理和技术架构师也很有用。

  4. 即使您现在没有从反模式中获得太多好处,它们也可以帮助您在将来以更少的维护成本轻松适应新的特性。这是正确的吗?

    是的。

  5. 反模式的 原因 可能有哪些?

    It can come from various sources/mindsets. The following are a few common examples.

    • “我们需要尽快交付产品。”

    • “我们现在不需要分析影响。”

    • “我是重用专家。我非常了解设计模式。”

    • “我们将使用最新的技术和功能来打动我们的客户。我们不需要关心传统系统。”

    • "更复杂的代码将反映我在这方面的专业知识."

  6. 讨论一些 症状 的反模式。

    在面向对象编程中,最常见的症状是您的系统不能轻易地适应新的特性。此外,维护成本也在不断增加。您可能还会注意到,您已经失去了关键的面向对象特性的能力,比如继承、多态等等。

    Apart from these, you may notice some/all of the following symptoms.

    • 全局变量的使用

    • 代码复制

    • 有限/没有代码重用

    • 一大类(神类)

    • 大量的无参数方法等。

  7. 如果检测到反模式,有什么 补救措施

    您可能需要遵循重构的更好的解决方案。例如,下面是一些避免反模式的解决方案。

    金锤:你可以尝试通过适当的训练来教育 X 先生。

    零表示空值:你可以使用一个额外的布尔变量来恰当地表示空值。

    数字管理:如果你明智地使用数字,数字就是好的。你不能通过一个程序员每周修复的缺陷数量来判断他/她的能力。质量也很重要。一个典型的例子包括修复一个简单的 UI 布局比修复系统中的一个严重的内存泄漏要容易得多。考虑另一个例子。“更多数量的测试通过”并不意味着您的系统更加稳定,除非测试使用不同的代码路径/分支。

    拍摄信使:欢迎测试人员并立即让他参与进来。他可以在早期发现典型的缺陷,你可以避免最后一刻的意外。

    复制粘贴编程:你可以重构你的代码,而不是寻找一个快速的解决方案。您还可以创建一个公共场所来维护经常使用的方法,以避免重复并提供更容易的维护。

    架构师不编码:让架构师参与部分实现阶段。这对组织和架构师都有帮助。这项活动可以让他们更清楚地了解产品的真正功能。

  8. 你说的重构是什么意思?

    在编码领域,术语重构意味着改进现有代码的设计,而不改变系统/应用程序的外部行为。这个过程有助于您获得更具可读性的代码。同时,代码应该更能适应新的需求(或者变更请求),并且更易于维护。*

二十九、常见问题

本章是本书所有章节中“问答”部分的子集。这些问题中有许多没有在某些章节中讨论,因为相关的模式还没有涉及到。因此,除了下面的问答,强烈建议您通读本书的所有“问答”部分,以便更好地理解所有模式。

  1. 你最喜欢哪种设计模式?

    这取决于许多因素,如背景、情况、需求、制约因素等等。如果你知道所有的模式,你有更多的选择。

  2. 开发人员为什么要使用设计模式?

    它们是对现实软件开发中反复出现的软件设计问题的可重用解决方案。

  3. 命令模式和纪念模式有什么区别?

    命令模式存储所有的动作,但是记忆模式只在请求时保存状态。此外,命令模式对每个动作都有撤销和重做操作,但是 memento 模式不需要这样。

  4. facade 模式和 builder 模式有什么区别?

    facade 模式的目的是使代码的特定部分更容易使用。它从开发者那里抽象出细节。

    构建器模式将对象的构造与其表示分离开来。在第三章中,导演调用相同的construct()方法来创造不同类型的车辆。换句话说,您可以使用相同的构造过程来创建多种类型。

  5. 构建者模式和策略模式有什么区别?它们有相似的 UML 表示。

    你首先需要理解意图。构建者模式属于创造模式的范畴,而策略模式属于行为模式的范畴。他们关注的领域不同。使用构建器模式,您可以使用相同的构造过程来创建多种类型,而使用策略模式,您可以在运行时自由选择算法。

  6. 命令模式和解释器模式有什么区别?

    在命令模式中,命令基本上是对象。在解释器模式中,命令是句子。有时候解释器模式可能看起来很方便,但是您必须记住构建一个解释器的成本。

  7. 责任链模式和观察者模式有什么区别?

    在 observer 模式中,所有注册用户都被并行通知/获取请求(主题的更改),但是在 chain-of-responsibility 模式中,您可能没有到达 chain 的末端,所以所有用户不需要处理相同的场景。位于链开始的用户可以更早地处理请求。

  8. 责任链模式和装饰者模式有什么区别?

    它们一点也不相同,但你可能会觉得它们在结构上很相似。与前面的区别类似,在责任链模式中,只有一个类处理请求,但是在装饰器模式中,所有的类都处理请求。您必须记住,装饰器只在添加和删除责任的上下文中有效,如果您可以将装饰器模式与单一责任原则结合起来,您就可以在运行时添加/删除单一责任。

  9. 调解人模式和观察者模式有什么区别?

    GoF 说,“这些是竞争模式。它们之间的区别在于,观察者通过引入观察者和主体对象来分发通信,而中介对象封装了其他对象之间的通信。”我建议你考虑一下第二十一章中的中介模式例子。在这个例子中,两个工人总是从他们的老板那里得到信息。他们是否喜欢这些信息并不重要。但是如果他们只是简单的观察者,那么他们应该可以取消他们老板对他们的控制,有效地说“我不想看到来自老板/Raghu 的消息。”

    GoF 还发现,与制作可重用的中介相比,当你制作可重用的观察者和主体时,你可能会面临更少的挑战。但是关于交流的流程,中介模式比观察者模式得分更高。

  10. 单例类和静态类,你更喜欢哪一个?

首先要记住的是,Java 不支持顶级静态类。您可以创建单例类的对象,这在静态类中是不可能的。因此,继承和多态的概念可以用单例类来实现。现在让我们考虑一种支持全阶段静态类的语言(比如 C#)。在这种情况下,一些开发人员认为在现实世界的应用程序中模仿静态类(例如,考虑单元测试场景)是具有挑战性的。
  1. 如何区分代理和适配器?
代理在与它们的主体相似的接口上工作,但是适配器在不同的接口上工作(对于它们所适应的对象)。
  1. 代理和装饰者有什么不同?
有不同类型的代理,它们因实现而异。因此,看起来有些实现接近装饰者。例如,保护代理可以像装饰器一样实现。但是你必须记住,装饰者专注于增加责任,而代理者专注于控制对对象的访问。
  1. 中介和门面有什么不同?
总的来说,两者都简化了复杂的系统。在中介模式中,中介和内部子系统之间存在双向连接,而在外观模式中,您提供单向连接(子系统不知道外观)。
  1. 享元模式和状态模式之间有什么联系吗?
GoF 说 flyweight 模式可以帮助你决定*何时以及如何*共享状态对象。
  1. 简单工厂、工厂方法和抽象工厂设计模式之间有什么相似之处?
它们都封装了对象创建。他们建议你编写抽象(接口)代码,而不是具体的类。这些工厂中的每一个都通过减少对具体类的依赖来促进松散耦合。
  1. 简单工厂、工厂方法和抽象工厂设计模式有什么区别?
这是你在各种工作面试中可能会面临的一个重要问题。建议你看清楚。所以,参考第五章“问答环节”第 3 题的回答。
  1. 如何区分单例模式和工厂方法模式?
单例模式确保您每次都能获得一个唯一的实例。它还限制创建其他实例。

但是工厂方法模式并没有说你只能得到一个唯一的实例。大多数情况下,这种模式用于创建任意多的实例,并且这些实例不一定是唯一的。这些新类型化的实例可以实现一个公共基类。(还记得*工厂方法让一个类将实例化推迟到 GoF 定义中的子类*吗?)
  1. 如何区分构建者模式和原型模式?
在原型模式中,您使用的是克隆/复制机制。因此,在最后,您可能想要覆盖最初的实现(注意在我们的 Ford 类和 Nano 类的实现中的单词 *@override* )。但是改变遗留(或原始)代码并不总是容易的。

除了这一点,当您使用克隆机制时,您不需要考虑具有不同参数的构造函数。

但是在构建器模式实现中,使用带有不同参数的构造器是很常见的。
  1. 如何区分访问者模式和策略模式?
在策略模式中,每个子类使用不同的算法来解决一个共同的问题。但是在访问者设计模式中,每个访问者子类可能提供不同的功能。
  1. 空对象和代理有什么不同?
一般来说,代理在某些时候作用于真实对象,它们也可以提供行为。但是空对象不做任何这样的操作。
  1. 如何区分解释者模式和访问者模式?
在解释器模式中,您将简单的语法表示为对象结构,但是在访问者模式中,您定义了想要在对象结构上使用的特定操作。除此之外,解释器可以直接访问所需的属性,但是在访问者模式中,您需要特殊的功能(类似于观察者)来访问它们。
  1. 如何区分 flyweight 模式和对象池模式?
我没有在本书中讨论对象池模式。但是如果你已经了解了对象池模式,你会注意到在 flyweight 模式中,flyweight 有内部和外部状态。因此,如果一个 flyweight 有两种状态,那么这两种状态是分开的,客户端需要将状态的一部分传递给它。此外,一般而言,客户端不会更改固有状态,因为它是共享的。

对象池模式不存储外部状态的任何部分;所有状态信息都存储/封装在池化对象中。此外,客户端可以更改池化对象的状态。
  1. 库(或框架)与设计模式有什么相似/不同之处?
它们不是设计模式。它们提供了可以直接在应用程序中使用的实现。但是他们可以在那些实现中使用模式的概念。

第一部分:四种模式

第二部分:附加设计模式

第三部分:关于设计模式的最后讨论

posted @ 2024-08-06 16:38  绝不原创的飞龙  阅读(3)  评论(0编辑  收藏  举报