65.9K
CodeProject 正在变化。 阅读更多。
Home

设计模式入门(第 1 部分)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.60/5 (4投票s)

2011年5月19日

CPOL

11分钟阅读

viewsIcon

18114

本文是设计模式系列文章的第一部分,旨在解释设计模式的基本概念。

先决条件

如果您对面向对象编程及其概念,如封装、多态和继承有大致的了解,那会更好。如果您没有这些知识,那么本文可能会显得有些混乱。如果确实如此,请留言,我将尽力解释。我这里提供的所有示例代码都将遵循Java语法。

什么是设计模式

来自维基百科 -

在建筑和计算机科学中,设计模式是一种正式记录特定专业领域设计问题解决方案的方式。

从上面两行你理解了什么?

如果你问我,我会说——**什么都没懂**。这就是所有设计模式初学者都会遇到的问题。那些教授这些知识的人和网站似乎用一种很难让初学者理解的方式来谈论这些东西。与其追求这些**深奥**的定义,不如通过观察设计模式的应用来学习它们是什么。

你知道什么是对象吗

简单来说,对象就是具有以下特征的东西:

  • 数据(或者说属性/特性/状态)
  • 对数据进行操作的操作(也称为方法)
对象通过其数据执行某些操作。当对象收到来自另一个对象的请求时,它会执行一个操作。请求对象被称为第一个对象的*客户端*。您可以通过调用其适当参数的方法来请求对象的操作。作为回报,该操作会为您完成一些工作,并可能返回一个响应。

说得够多了。我们来看一些代码。

在创建对象之前,您需要让编译器知道该对象将包含哪些数据和操作。您可以通过声明一个*类*来实现,如下所示:

public class Person
{
    protected string name;                                       // Data

    public  person(){}                                           // Constructors, a special type of operation

    public string getName()
	{                                     // Operation
        return this.name;
    }

    public void setName(string name)
	{
        string escapedName = EscapeString.escape(name);          // Here EscapeString is another object whose
                                                                 // service is being requested by this Person
                                                                 // class. So Person is a client of EscapeString
        this.name = name;
    }
 }

您可以将类看作是编译器的蓝图,它描述了对象将包含哪些数据和操作。

现在,要创建一个对象,您可以使用以下语法:

Person aPerson = new Person();

再次强调,本文**不适用于面向对象编程的绝对初学者**。本文提供了这些概念的不同视角。您应该从Java官方网站提供的这篇精彩文章中获取基本概念。

请求是让对象执行操作的唯一方式。操作是改变对象内部数据的唯一方式。由于这些限制,对象的内部状态被称为*封装*。

认识封装

封装意味着对象包含的数据无法直接从对象外部访问,并且其表示形式在对象外部是不可见的。这是开发松耦合软件组件的好方法。

上面两行到底是什么意思?

这意味着您无法直接访问 Person 对象的 `name` 属性。如果您需要获取该值,则必须通过 `getName` 方法请求它。这样做为对象的设计者提供了很大的灵活性,即他可以将变量名更改为 `myname`,而这仍然不会导致对象外部的任何代码发生更改。设计者甚至可以选择将名称值存储在 Java Map 或数组中,而不会遇到太多麻烦。

设计模式是否帮助我们维护封装

是的,确实如此!有很多设计模式专门用于此目的。其中之一是策略模式。它帮助我们将复杂的数据结构、算法、变化的行为封装到它们自己的独立类中,并保护系统的其余部分免受它们可能发生的任何更改的影响。因此,我们可以说——

当我们以真正的面向对象方式编写应用程序时,设计模式有助于我们维护封装。

这是设计模式最重要的用途之一。

我们如何确定什么应该是一个对象

  • 你可以写一个问题陈述,找出名词和动词,然后创建相应的类和操作。
  • 或者你可以关注系统中的协作和职责。
  • 或者你可以对现实世界进行建模,并将分析中发现的对象转化为设计。

你从上面三点理解了什么?

如果你不理解,请不要担心。这些属于一个更广泛的话题,称为*系统分析与设计*,我将在以后介绍。

您可能想知道设计模式是否能帮助您确定系统中应该有哪些对象,您会很高兴知道它们确实能。有一个模式叫做门面模式,它描述了如何将子系统表示为对象。还有另一个模式叫做享元模式,它描述了如何以最细粒度支持大量对象。

所以我们可以说——

设计模式帮助我们决定系统中应该有哪些对象。

这是设计模式的另一个重要用途。

方法签名、对象接口、类型、子类型和超类型

对象声明的每个操作都指定了操作的名称、作为参数的对象以及操作的返回值。这被称为操作的签名。不要将其与不包括方法返回类型的方法签名混淆。

让我们考虑前面讨论的 Person 对象。其 `getName` 操作的方法签名是:

string getName()

由对象操作定义的所有签名集合称为对象的接口(注意,这与我们编程语言的*接口*构造不同)。有一点您应该注意。有些面向对象编程语言允许我们声明无法从对象外部访问的方法(例如,在Java中使用*private*,*protected*关键字)。这些方法将不会包含在对象接口中,因为您无法从对象外部向该对象请求这些操作。

如果考虑 Person 对象,那么它的接口是 -

{void setName(string name), string getName()}

对象的接口描述了可以发送给对象的完整请求集。任何与对象接口中的签名匹配的请求都可以发送给对象。

类型是用于表示特定接口的名称。如果一个对象接受接口 X 中定义的所有操作请求,我们就说它具有类型 X。一个对象可以有多种类型,并且截然不同的对象可以共享一种类型。

举例来说,我们可以说我们的 Person 对象定义的接口类型是 Person。

接口可以包含其他接口作为子集。我们说一个类型是另一个类型的子类型,如果它的接口包含其超类型的接口。

让我们考虑一个例子。假设我有一个名为 Man 的类,其接口如下:

{void setName(string name), string getName(), void playCricket(), void playKabadi()}

这个对象的接口包含了 Person 对象接口中定义的所有方法签名。因此,在这种情况下,我们可以说 Man 是 Person 的子类型,而 Person 是 Man 的超类型。我们还可以说 Man 对象的接口*继承*了 Person 对象的接口。

我为什么要谈论这些东西?

因为接口在面向对象系统中是基础。对象只能通过它们的接口来了解。不通过接口,就无法了解任何关于对象的信息,也无法让它做任何事情。然而,对象的接口并没有说明其实现。不同的对象可以自由地以不同的方式实现请求。这意味着两个具有完全不同实现的对象可以拥有相同的接口。

让我用一个例子来澄清最后一点。考虑 Man 对象的以下定义:

public class Man extends Person
{  
	public Man(){}
    
	public string getName()
	{
		return EscapeString.escape(this.name);
	}
    
	public void setName(string name)
	{
		this.name = name;
	}
    
	public void playCricket()
	{
		System.out.Print("I am playing cricket");
	}
    
	public void playKabadi()
	{
		System.out.Print("I am playing kabadi");
	}
 }

这个例子使用*继承*来确保 Man 对象拥有 Person 对象的接口作为其子类型。您可以通过阅读 Sun Java 的文章了解更多关于继承的内容。我只想说的是——由于我们是从 Person 类继承 Man 类,那么构成其对象接口的所有方法也将包含在 Man 类中。Man 类通过*覆盖*这些方法的定义/实现,为这些方法提供了不同的实现。这就是我们本文所关心的全部内容。

对于那些觉得这个例子令人困惑的人,我想说的是,对象接口和Java/C#语言的接口不是一回事,尽管它们有很多共同之处。对象接口是所有可以从对象外部调用的方法签名的集合;而C#/Java中的接口定义了某个对象可以实现的方法签名集合,从而将这些签名包含在其对象接口中。从这个意义上说,你可以说C#/Java中的接口构造定义了一个对象接口,但这并不是唯一的方法。在这些语言中,我们可以通过以下两种方式之一确保一个对象具有特定的对象接口:

  • 通过从另一个对象扩展/继承。这样,被继承的对象的对象接口将包含在这个对象中。
  • 通过使用这些语言提供的`interface`关键字声明一个接口(即方法签名的集合),并实现该接口。

现在您可能会想说,这难道不是一回事吗?我会说,不是,因为一个没有实现任何编程语言接口的对象仍然有一个对象接口,它是所有可以从该对象外部调用的方法的签名集合。

请在继续之前仔细思考这个区别。

根据我们对接口、类型和子类型的定义,我们可以说 Man 类也具有 Person 类型,因为它接受 Person 接口类型中定义的所有操作。它的超类型是 Person,它是 Person 的子类型。

现在,看看 `getName` 和 `setName` 的实现。它们与 Person 的实现完全不同,尽管它们具有共同的接口。

由于这些对象具有共同的接口,因此当您只处理该共同接口时,可以使用一个对象来代替另一个对象。让我用另一个例子来澄清这一点。考虑某个类的以下方法:

public string returnProcessedName(Person p)
{
    this.nameProcessor = p;                     // nameProcessor is a member variable of CertainClass whose type
                                                // is Person. You can use a setter method for setting up appropriate name
                                                // processor, but hey, this is just an example ;-) .
    return this.nameProcessor.getName();
}

现在我可以这样调用这个方法:

Person newPerson = new Person();
CertainClass c = new CertainClass();

newPerson.setName("Stupid");
string processedName = c.returnProcessedName(newPerson);

或者这样:

Man newMan = new Man();
CertainClass c = new CertainClass();

newMan.setName("Stupid");
string processedName = c.returnProcessedName(newMan);

这段代码片段也是有效的。为什么?因为 Person 和 Man 对象都拥有一个共同的接口,我们之前将其命名为 Person。因此,当您处理该接口时,可以使用一个对象来代替另一个对象。如果您将 Person 对象的引用传递给 `returnProcessedName` 方法,它将调用该类或对象提供的 `getName` 实现。同样,如果您传递 Man 对象的引用,则将调用其实现。因此,我们可以说 `getName` 的哪个实现将被调用将在运行时确定,即当应用程序运行时,而不是在编译时,即当应用程序编译时。这种请求与其实现之一的运行时关联称为*动态绑定*。

动态绑定意味着发出请求不会将您束缚于特定的实现,直到运行时。因此,您可以编写期望具有特定接口的程序,知道任何具有正确接口的对象都将接受该请求。

这个特性非常、非常重要,因为*多态性*依赖于它。

所以,我们偶然发现了多态性

动态绑定允许您在运行时用具有相同接口的对象相互替换。这种可替换性称为*多态性*。它让客户端对象对其他对象的假设很少,除了支持特定接口。因此,它简化了客户端的定义,解耦了对象,并允许它们在运行时改变彼此之间的关系。这些可互换的类被认为表现出*多态行为*。

让我们结合上一个例子来讨论这个问题。

在我们的上一个例子中,`CertainClass` 是 Person 接口的客户端。由于 Person 对象和 Man 对象都具有此接口,我们能够在这两个对象中使用 `CertainClass`。客户端类甚至不必知道它正在提供哪个实现,只要该实现或对象具有此 Person 接口即可。这才是`多态性`的真正含义,并且这个客户端对象`CertainClass`被认为显示**多态行为**,因为它在运行时可以通过简单地在 `Person` 和 `Man` 类的实例之间切换来获得两种不同的行为,如下所示:

Person newPerson = new Person();
CertainClass c = new CertainClass();

newPerson.setName("Stupid");
string processedName = c.returnProcessedName(newPerson);
System.out.println("Current name: " + processedName);

Man newMan = new Man();
newMan.setName("Stupid");
processedName = c.returnProcessedName(newMan);           // Now you will get name processed in a different
                                                         // way, because implementation is different!
System.out.println("Current name: " + processedName);

这极大地降低了适应应用程序变化的复杂性,因为如果您使用多态性设计应用程序,您可以将此接口的任何实现插入到您的应用程序中,而无需测试和重新测试您的应用程序,也无需破坏任何现有代码。

设计模式是否帮助我们实现多态性

是的,确实如此!所以我们发现了设计模式的另一个用途——

设计模式通过帮助我们在应用程序中维护真正的多态性,使我们能够设计出灵活的应用程序

本文到此为止。我很快会写本文的第二部分。在此之前,祝您愉快 :-) 。

© . All rights reserved.