访问者模式 - 重新解释






4.41/5 (9投票s)
访问者模式将业务逻辑与其操作的数据结构分离开,允许在不修改现有数据结构的情况下为其添加新操作。
引言
在这里,我们将了解访问者设计模式。本文将详细解释
- 它是什么?
- 它为什么存在?
- 它是如何工作的?
为什么写这篇文章? 好了,网上有很多关于这个模式的文章,但大多数都未能描述其基本原理。网上找到的大部分解释或实现都让人对其真实世界中的用法感到困惑。所以,我试图弥补这个差距,并尽最大努力阐述该模式所针对的问题,以及它是如何解决这些问题的。本文将帮助开发者不仅学习实现,还能理解核心概念,以便他们能够识别出可以应用此模式的实际场景。在本文结束时,我希望您能够自己描述访问者模式的用途和好处。
背景
总的来说,访问者模式被认为是设计模式中最复杂的一种,因为其解决问题的性质。我们将了解它是什么,它是如何工作的,以及如何在实际场景中实现它。
我们每个人可能都遇到过这种情况:有一个复杂的类层次结构,我们想在不修改其原始代码的情况下添加或修改其行为。“复杂”这个词我用在这里是因为在层次结构中,每个对象都是不同类型的,而我们想添加的操作将针对特定类型的对象运行,即不同的操作针对不同类型的对象。坦白说,大多数时候我们会进行类型/实例检查,然后通过 If-else 语句来选择操作。这种方法有其自身的局限性,当然,也不是面向对象的方式。为了以面向对象的方式解决这类问题——访问者模式应运而生。
这是什么?
访问者模式的标准定义(根据 G.O.F)是:
表示要在对象结构中的元素上执行的操作。访问者允许您在不更改其操作的元素类的情况下定义新操作。
基本上,访问者模式有两个重要方面:
- 有一个迭代机制,它知道如何在对象层次结构上进行迭代。它对层次结构中的对象的行为一无所知。
- 需要实现的新行为对迭代机制一无所知,它们不知道如何迭代对象层次结构。
这两个方面是相互独立的,不应混淆。所以,这一切都与被称为 **单一职责原则** 的 OOP 原则有关。
持有新行为的类通常被称为 **访问者**。
在访问者模式中,关键参与者是:
- 访问者 (Visitor) - 一个定义 Visit 操作的接口。这是访问者模式的核心。它为对象结构中的每种 **具体元素 (Concreate Element)** 定义一个 **Visit** 操作。
- 具体访问者 (ConcreateVisitor) - 实现 **访问者** 接口中定义的操作。
- 元素基类 (ElementBase):这是一个抽象类/接口,定义了以访问者为参数的 **Accept** 操作。
- 具体元素 (ConcreateElement) - 这些类型实现 Element 接口的 Accept 方法。
- 对象结构 (Object Structure) - 它作为一个集合、列表或其他可枚举的结构来保存数据结构的所有元素,供访问者使用。它为所有访问者提供访问其元素的接口。这些元素包括一个名为“Accept”的方法。然后对集合进行枚举。
关键概念是创建一个功能有限的数据模型,以及一组具有特定功能的访问者来操作该数据。该模式允许数据结构的每个元素被访问者访问,通过将对象作为参数传递给访问者方法。将算法与其数据模型分离的关键好处是能够轻松添加新行为。数据模型类是通过一个名为“Visit”的通用方法创建的,该方法可以在运行时接受访问者对象。然后可以创建不同的访问者对象并将其传递给该方法,然后该方法通过将自身作为参数传递来回调访问者方法。
向对象层次结构添加新类型需要修改所有访问者,这应该被视为一种优势,因为它肯定会迫使我们在所有保留了类型特定代码的地方都添加新类型。基本上,它不会让你忘记这一点。
访问者模式仅在以下情况有用:
- 您想实现的接口相当静态,并且变化不大。
- 所有类型都是预先已知的,即在设计时必须知道所有对象。
为什么存在?
新的行为不会直接集成到对象中,因为这需要更改源代码,而且由于将来可能需要实现许多行为,所以我们不能每次都去更改对象的原始代码。因此,行为被分离到新的对象中。所以,每当我们想为对象层次结构引入新行为时,我们只需为其创建一个新对象,并且这些新行为将通过多态在运行时应用于对象。因此,这是扩展对象功能而无需修改其类代码的一种简单而简洁的解决方案。
此模式允许我们在不修改其原始源代码的情况下为对象添加新行为,方法是将新行为分离到单独的类中,然后动态地将这些行为应用于其各自的对象。
访问者实现了以下设计原则:
- 关注点分离 (Separation of Concern) - 访问者模式促进了这一原则,多个方面/关注点被分离到多个其他类中,因为它鼓励更清晰的代码和代码的可重用性,并且代码也更易于测试。
- 单一职责原则 (Single Responsibility Principle) - 访问者模式也强制执行此原则。一个对象应该只有一种职责。不相关的行为必须与它分离到另一个类。
- 开闭原则 (Open Close Principle) - 访问者模式也遵循此原则,因为如果我们想扩展对象的行为,则不会修改原始源代码。访问者模式为我们提供了一种将其分离到另一个类并在运行时为对象应用这些操作的机制。
访问者实现的好处是:
- 将数据结构的逻辑代码与其本身分离开。创建单独的访问者对象来实现此类行为。
- 它解决了 **双重分派 (Double Dispatch)** 问题,该问题很少遇到但影响很大。
总之,如果您想将某些逻辑代码与您用作输入的元素解耦,那么访问者可能是最佳选择。
注意:为了使本文简短,我将不关注双重分派问题。一篇很好的文章可以在 这里 找到。
如何实现?
到目前为止,我们已经理解了访问者模式的“是什么”和“为什么”。现在是时候编写一些代码了。
让我们从一个场景开始。我们将为机场构建一个税务计算系统。该系统用于计算进口产品的税费/进口关税。系统将根据某些规则计算进口关税。
注意:为简单起见,假设只能进口三种产品,即 **书 (Book)**、**汽车 (Car)**、**葡萄酒 (Wine)**。
目前,航空公司应用程序仅提供一个类,即乘客只能购买 **普通 (Normal)** 舱机票,但将来会扩展到 **企业 (Corporate)** 和 **商务 (Executive)** 舱。下表显示了普通舱的税收规则……
Type of Passenger Book Car Wine ================================================ Normal 10% 30% 32%
上表显示了我们的应用程序将据此计算税费/关税的规则。当我们查看规则时,我们会发现不同类型的进口商品有不同的税率百分比,即对于普通舱乘客,对 **书 (Book)** 适用 10% 的税,对 **汽车 (Car)** 适用 30% 的税,对 **葡萄酒 (Wine)** 收取 32% 的税,并且会对其他舱位的乘客收取不同的税。
首先,我们将定义一个名为 **IVisitable** 的接口。它将定义一个单独的 **Accept** 方法,该方法将接受一个 **IVisitor** 类型的参数。该接口将作为产品列表中所有类型的基类。所有类型,如书、汽车和葡萄酒(在我们的示例中),都将实现此类型。
/// <summary> /// Define Visitable Interface.This is to enforce Visit method for all items in product. /// </summary> internal interface IVisitable { void Accept(IVisitor visit); }
让我们为三种产品类型创建类型,即书、汽车和葡萄酒。所有类型都将实现 **IVisitable** 接口,这使得这些具体类可以定义 Accept 方法。这个 **Accept** 方法只是接受一个 **IVisitor** 类型的参数,并回调访问者类中定义的一个特定方法。因此,访问者方法由这个 **Accept** 方法选择。
注意 - 在下面的示例代码中,我同时包含了接口和抽象类。在这个简单的示例中,我们不需要两者,即我们可以通过其中任何一个来运行此示例,但我希望同时使用它们,因为在实际场景中,抽象类会存在以保留层次结构中的一些共同行为,而接口将用于强制类执行某些规则。所以,我同时使用了两者来展示这种情况。
#region "Structure Implementations" /// <summary> /// Define base class for all items in products to share some common state or behaviors. /// Thic class implement IVisitable,so it allows products to be Visitable. /// </summary> internal abstract class Product : IVisitable { public int Price { get; set; } public abstract void Accept(IVisitor visit); } /// <summary> /// Define Book Class which is of Product type. /// </summary> internal class Book : Product { // Book specific data public Book(int price) { this.Price = price; } public override void Accept(IVisitor visitor) { visitor.Visit(this); } } /// <summary> /// Define Car Class which is of Product type. /// </summary> internal class Car : Product { // Car specific data public Car(int price) { this.Price = price; } public override void Accept(IVisitor visitor) { visitor.Visit(this); } } /// <summary> /// Define Wine Class which is of Product type. /// </summary> internal class Wine : Product { // Wine specific data public Wine(int price) { this.Price = price; } public override void Accept(IVisitor visitor) { visitor.Visit(this); } } #endregion "Structure Implementations"
让我们创建一个访问者接口。它将为产品中的所有类型的项目持有一个访问方法,即目前我们的应用程序只支持三种项目:**书 (Book)**、**汽车 (Car)** 和 **葡萄酒 (Wine)**,所以每种类型只有三种方法。
/// <summary> /// Define basic Visitor Interface. /// </summary> internal interface IVisitor { void Visit(Book book); void Visit(Car car); void Visit(Wine wine); }
现在,我们将创建具体访问者类 **BasicPriceVisitor**,它将实现上述定义的接口,即 **IVisitor**。因为它为所有类型都有方法,所以这个类可以为每种类型提供自己的实现。在当前的示例中,我们正在计算每种类型的关税,因为每种类型都有不同的税率百分比。
#region "Visitor Implementation" /// <summary> /// Define Visitor of Basic Tax Calculator. /// </summary> internal class BasicPriceVisitor : IVisitor { public int taxToPay { get; private set; } public int totalPrice { get; private set; } public void Visit(Book book) { var calculatedTax = (book.Price * 10) / 100; totalPrice += book.Price + calculatedTax; taxToPay += calculatedTax; } public void Visit(Car car) { var calculatedTax = (car.Price * 30) / 100; totalPrice += car.Price + calculatedTax; taxToPay += calculatedTax; } public void Visit(Wine wine) { var calculatedTax = (wine.Price * 32) / 100; totalPrice += wine.Price + calculatedTax; taxToPay += calculatedTax; } } #endregion "Visitor Implementation"
如上所示,在每种类型的内部方法中,我们都根据类型计算了关税,从而为每种类型实现了不同的税收规则。
现在是时候看看它的实际应用了。我们创建了一个产品列表,然后创建了一个 **BasicPriceVisitor** 对象。现在,我们只需遍历产品列表并将 **BasicPriceVisitor** 实例传递给每个项目。
static void Main(string[] args) { Program.ShowHeader("Visitor Pattern"); List<Product> products = new List<Product> { new Book(200),new Book(205),new Book(303),new Wine(706) }; ShowTitle("Basic Price calculation"); BasicPriceVisitor pricevisitor = new BasicPriceVisitor(); products.ForEach(x => { x.Accept(pricevisitor); }); Console.WriteLine(""); }
执行后,将在命令行中显示以下结果:
如何扩展?
一段时间后,航空公司决定引入两个新的乘客类别,并享有不同的税收优惠,例如 **企业 (Corporate)** 和 **商务 (Executive)** 舱。新执行舱适用的进口关税如下。
Type of Passenger Book Car Wine ==================================================== Corporate Offer 7% 20% 20% Executive Offer 5% 10% 10%
为了满足这个新需求,我们将创建两个不同的类,比如 **CorporateOfferVisitor** 和 **ExecutiveOfferVisitor**。这两个类都将实现 **IVisitor** 接口,因为它提供了一种以不同方式处理不同类型项目的方法。**CorporateOfferVisitor** 类为书、汽车和葡萄酒提供了不同的税收规则,同样 **ExecutiveOfferVisitor** 提供不同的关税百分比。请仔细查看下面的代码……
#region "Visitor Implementation" /// <summary> /// Define Visitor of Corporate Tax Calculator. /// </summary> internal class CorporateOfferVisitor : IVisitor { public int taxToPay { get; private set; } public int totalPrice { get; private set; } public void Visit(Book book) { var calculatedTax = (book.Price * 7) / 100; totalPrice += book.Price + calculatedTax; taxToPay += calculatedTax; } public void Visit(Car car) { var calculatedTax = (car.Price * 20) / 100; totalPrice += car.Price + calculatedTax; taxToPay += calculatedTax; } public void Visit(Wine wine) { var calculatedTax = (wine.Price * 20) / 100; totalPrice += wine.Price + calculatedTax; taxToPay += calculatedTax; } } /// <summary> /// Define Visitor of Executive Tax Calculator. /// </summary> internal class ExecutiveOfferVisitor : IVisitor { public int taxToPay { get; private set; } public int totalPrice { get; set; } public void Visit(Book book) { var calculatedTax = (book.Price * 5) / 100; ; totalPrice += book.Price + calculatedTax; taxToPay += calculatedTax; } public void Visit(Car car) { var calculatedTax = (car.Price * 10) / 100; totalPrice += car.Price + calculatedTax; taxToPay += calculatedTax; } public void Visit(Wine wine) { var calculatedTax = (wine.Price * 10) / 100; totalPrice += wine.Price + calculatedTax; taxToPay += calculatedTax; } } #endregion "Visitor Implementation"
让我们在程序中使用上述新的访问者。这些新的访问者将以与我们以前的访问者 **BasicPriceVisitor** 相同的方式使用,即我们将这些访问者传递给列表中的每个项目。
static void Main(string[] args) { Program.ShowHeader("Visitor Pattern"); List<Product> products = new List<Product> { new Book(200),new Book(205),new Book(303),new Wine(706) }; ShowTitle("Basic Price calculation"); BasicPriceVisitor pricevisitor = new BasicPriceVisitor(); products.ForEach(x => { x.Accept(pricevisitor); }); Console.WriteLine("Total Tax paid - Rs {0}.", pricevisitor.taxToPay); Console.WriteLine("Basic Price Calculation - Rs {0}.", pricevisitor.totalPrice); ShowTitle("Corporate Price calculation"); CorporateOfferVisitor offervisitor = new CorporateOfferVisitor(); products.ForEach(x => { x.Accept(offervisitor); }); Console.WriteLine("Total Tax paid - Rs {0}.", offervisitor.taxToPay); Console.WriteLine("Corporate Price Calculation - Rs {0}.", offervisitor.totalPrice); Console.WriteLine(""); ShowTitle("Executive Offer Price calculation"); ExecutiveOfferVisitor executiveOfferVisitor = new ExecutiveOfferVisitor(); products.ForEach(x => { x.Accept(executiveOfferVisitor); }); Console.WriteLine("Total Tax paid - Rs {0}.", executiveOfferVisitor.taxToPay); Console.WriteLine("Basic Price Calculation - Rs {0}.", executiveOfferVisitor.totalPrice); Console.WriteLine(""); }
因此,我们看到了如何轻松地扩展我们的示例以适应两个新功能,而无需编辑现有代码或任何现有类型,如书、汽车或葡萄酒。这就是访问者模式的魅力。整个想法是在单独类的某个方法中定义行为,并通过回调来调用它。
关注点
访问者对象表示要在对象结构的元素上执行的操作。
当以下情况时使用访问者模式:
- 我们想对层次结构或集合中分组的不同类型的对象执行类似的操作。
- 我们想将不同的、不相关的行为与类型类(例如上例中的 Car)分离到另一个类中,并希望动态地切换行为。
- 我们拥有稳定且不太可能改变的对象层次结构,但未来添加新操作的可能性很大。由于访问者模式允许我们将操作与对象结构分离开,因此现在可以轻松地以访问者的形式添加新操作。只要对象结构保持不变,这将起作用。
当以下情况时不应使用:
- 访问者模式要求在设计时就知道访问方法中的参数和返回类型。因此,对于频繁添加类型的场景,此模式不适用,因为一旦引入了新类型,所有访问者都必须进行相应更改。
- 行为是特定于类型的,而不是针对整个层次结构的。此类行为不应通过访问者实现,因为访问者用于定义将应用于整个层次结构的行为(访问者)。