(原创)无废话C#设计模式之二十一:Visitor
无废话C#设计模式之二十一:Visitor
意图
实现通过统一的接口访问不同类型元素的操作,并且通过这个接口可以增加新的操作而不改变元素的类。
场景
想不出什么好例子,我们在组合模式的那个例子上进行修改吧。我们知道,无论是游戏大区、游戏服务器还是游戏的服务都是一个元素,只不过它们的层次不一样。对于这样的层次结构,我们使用了组合模式来统一各层的接口,这样对游戏大区的操作和对游戏服务器的操作对调用方来说没有什么两样。在现实中,组合模式的运用往往没有这么顺利:
l 如果元素需要增加新的操作,那么势必需要在抽象元素中增加接口,这个时候的改动就非常大了,几乎每一个具体元素都需要修改。
l 如果元素并没有统一的接口,并且树枝角色中有多种树叶角色,那么树枝角色势必需要根据树叶类型来调用不同的方法。
l 如果树叶角色的接口经常发生变动,那么一旦发生变动操作树叶的树枝角色也需要发生修改。
访问者模式可以解决这些问题。
示例代码
using System; using System.Collections; using System.Collections.Generic; using System.Text; namespace VisitorExample { class Program { static void Main(string[] args) { Element server1 = new GameServer("GS1", "192.168.0.1"); server1.Add(new GameService("Lobby1", 1, "S5Lobby1", 100)); server1.Add(new GameService("Lobby2", 1, "S5Lobby2", 200)); server1.Add(new GameService("Gate1", 2, "S5Gate1")); server1.Add(new GameService("DataExchange1", 3, "S5DataExchange1")); server1.Add(new GameService("Rank1", 4, "S5Rank1")); server1.Add(new GameService("Log1", 5, "S5Log1")); Element server2 = new GameServer("GS2", "192.168.0.2"); server2.Add(new GameService("Lobby3", 1, "S5Lobby3", 150)); server2.Add(new GameService("Lobby4", 1, "S5Lobby4", 250)); server2.Add(new GameService("Gate2", 2, "S5Gate2")); server2.Add(new GameService("DataExchange2", 3, "S5DataExchange1")); server2.Add(new GameService("Rank2", 4, "S5Rank2")); server2.Add(new GameService("Log2", 5, "S5Log2")); Element area = new GameArea("电信区"); area.Add(server1); area.Add(server2); area.Accept(new StartGameVisitor()); //A1 area.Accept(new StopGameVisitor()); //B1 server1.Accept(new QueryPlayerCountVisitor()); server2.Accept(new QueryPlayerCountVisitor()); area.Accept(new QueryPlayerCountVisitor()); } } interface IVisitor { } interface IGameServiceVisitor { void Visit(GameService gameService); } interface IGameServerVisitor { void Visit(GameServer gameServer); } interface IGameAreaVisitor { void Visit(GameArea gameArea); } class StartGameVisitor : IVisitor, IGameServiceVisitor, IGameServerVisitor, IGameAreaVisitor { public void Visit(GameService gameService) { //A9 gameService.StartGameService(this); } public void Visit(GameServer gameServer) { //A6 gameServer.StartGameServer(this); } public void Visit(GameArea gameArea) { //A3 gameArea.StartGameArea(this); } } class StopGameVisitor : IVisitor, IGameServiceVisitor, IGameServerVisitor, IGameAreaVisitor { public void Visit(GameService gameService) { //B7 Console.WriteLine(string.Format("{0} stopped", gameService.Name)); } public void Visit(GameServer gameServer) { //B5 Console.WriteLine("=============Stopping the whole " + gameServer.Name + "============="); for (int i = gameServer.ServiceList.Count - 1; i >= 0; i--) { gameServer.ServiceList[i].Accept(this); } Console.WriteLine("=============The whole " + gameServer.Name + " stopped============="); } public void Visit(GameArea gameArea) { //B3 Console.WriteLine("=============Stopping the whole " + gameArea.Name + "============="); foreach (GameServer element in gameArea.ServerList) { element.Accept(this); } Console.WriteLine("=============The whole " + gameArea.Name + " stopped============="); } } class QueryPlayerCountVisitor : IVisitor, IGameServerVisitor, IGameAreaVisitor { public void Visit(GameServer gameServer) { int playerCount = 0; foreach (GameService gameService in gameServer) { if (gameService.ServiceType == 1) playerCount += gameService.PlayerCount; } Console.WriteLine("=============Player Count on " + gameServer.Name + " is:" + playerCount); } public void Visit(GameArea gameArea) { int playerCount = 0; foreach (GameServer gameServer in gameArea) { foreach (GameService gameService in gameServer) { if (gameService.ServiceType == 1) playerCount += gameService.PlayerCount; } } Console.WriteLine("=============Player Count on " + gameArea.Name + " is:" + playerCount); } } abstract class Element { protected string name; public string Name { get { return name; } } public Element(string name) { this.name = name; } public abstract void Add(Element element); public abstract void Remove(Element element); public abstract void Accept(IVisitor visitor); } class GameService : Element, IComparable<GameService> { private int serviceType; public int ServiceType { get { return serviceType; } set { serviceType = value; } } private string serviceName; private int playerCount; public int PlayerCount { get { return playerCount; } set { playerCount = value; } } public GameService(string name, int serviceType, string serviceName) : base(name) { this.serviceName = serviceName; this.serviceType = serviceType; } public GameService(string name, int serviceType, string serviceName, int playerCount) : base(name) { this.serviceName = serviceName; this.serviceType = serviceType; this.playerCount = playerCount; } public override void Add(Element element) { throw new ApplicationException("xxx"); } public override void Remove(Element element) { throw new ApplicationException("xxx"); } public override void Accept(IVisitor visitor) { //A8,B6 IGameServiceVisitor gameServiceVisitor = visitor as IGameServiceVisitor; if (gameServiceVisitor != null) gameServiceVisitor.Visit(this); } public int CompareTo(GameService other) { return other.serviceType.CompareTo(serviceType); } public void StartGameService(IVisitor visitor) { //A10 Console.WriteLine(string.Format("{0} started", name)); } } class GameServer : Element { private string serverIP; private List<GameService> serviceList = new List<GameService>(); public List<GameService> ServiceList { get { return serviceList; } } public GameServer(string name, string serverIP) : base(name) { this.serverIP = serverIP; } public override void Add(Element element) { serviceList.Add((GameService)element); } public override void Remove(Element element) { serviceList.Remove((GameService)element); } public override void Accept(IVisitor visitor) { //A5,B5 IGameServerVisitor gameServerVisitor = visitor as IGameServerVisitor; if (gameServerVisitor != null) gameServerVisitor.Visit(this); } public IEnumerator<GameService> GetEnumerator() { foreach (GameService gameService in serviceList) yield return gameService; } public void StartGameServer(IVisitor visitor) { //A7 IGameServerVisitor gameServerVisitor = visitor as IGameServerVisitor; if (gameServerVisitor != null) { serviceList.Sort(); Console.WriteLine("=============Starting the whole " + name + "============="); foreach (Element gameService in serviceList) { gameService.Accept(visitor); } Console.WriteLine("=============The whole " + name + " started============="); } } } class GameArea : Element { private List<GameServer> serverList = new List<GameServer>(); public List<GameServer> ServerList { get { return serverList; } } public GameArea(string name) : base(name) { } public override void Add(Element element) { serverList.Add((GameServer)element); } public override void Remove(Element element) { serverList.Remove((GameServer)element); } public override void Accept(IVisitor visitor) { //A2,B2 IGameAreaVisitor gameAreaVisitor = visitor as IGameAreaVisitor; if (gameAreaVisitor != null) gameAreaVisitor.Visit(this); } public IEnumerator<GameServer> GetEnumerator() { foreach (GameServer gameServer in serverList) yield return gameServer; } public void StartGameArea(IVisitor visitor) { //A4 IGameAreaVisitor gameAreaVisitor = visitor as IGameAreaVisitor; if (gameAreaVisitor != null) { Console.WriteLine("=============Starting the whole " + name + "============="); foreach (Element gameServer in serverList) { gameServer.Accept(visitor); } Console.WriteLine("=============The whole " + name + " started============="); } } } } |
代码执行结果如下图:
代码说明
代码从组合模式的例子修改过来,有一点乱,一点一点来分析:
l IVisitor是一个空接口,目的是为了抽象所有的访问者,抽象层次相当于Element。
l IGameServerVisitor、IGameServiceVisitor以及IGameAreaVisitor定义了访问者的访问操作(操作接口)。在这里,我们并没有把它们合并为一个接口,因为访问者可能仅针对一部分元素,或一个层次的元素,不一定都针对所有元素。
l StartGameVisitor、StopGameVisitor以及QueryPlayCountVisitor是具体的访问者。它们根据自己的需求实现不同的操作接口,虽然都是访问者但是它们的目的不太一样,见下。
l Element类型就是抽象构件(抽象元素),它给组合对象以及单个对象提供了一个一致的接口,使得它们都能有一致的行为。唯一和组合模式不同的是,在这里定义了一个接受访问者的接口。
l GameService、GameServer以及GameArea都是具体的元素。从组合角度来看,GameServer和GameArea是树枝,GameService是树叶。看一下Accept()方法,对于具体元素来说,它应该明确接受特定层次的访问者,如果类型转换正确的话,那么调用访问者的访问方法。
l 这个例子中的StartGameVisitor的目的是统一元素的接口。注意到GameService、GameServer以及GameArea中开启服务的方法都不同(并没有实现统一的接口),这样的话,高层的元素需要直接耦合低层元素的某个方法。通过访问者,我们使得它们直接和访问者的统一方法耦合,由访问者再适配不同的方法。可以从代码中以A打头的数字看出开启服务的整个流程。
l 这个例子中的StopGameVisitor体现了访问者模式最主要的作用,那就是为元素增加新的操作。这得益于访问者统一Accept接口以及双重分派的机制。可以从代码中以B打头的数字看出关闭服务的整个流程。
l 由于本例中把Visitor的接口按照功能分成了小接口,并且还有一个抽象顶层的空接口。这样,我们就可以为具体访问者实现需要的接口,并且为层次中增加元素也变得不是那么困难。从QueryPlayerCountVisitor中可以看到,具体访问者可以某些元素的新操作,而无需实现所有元素的新操作。
l 访问者模式还有一个应用是使不同类型元素组成的集合的遍历访问变得简单、符合开闭原则。由于GameServer的子元素一般只有GameService而不会有GameService和GameAgent,所以本例没有体现这样的应用。
l 访问者模式的结构比较复杂,变化也比较多。一般不管怎么样变化,主要还是依靠访问者模式双重分派和统一的Accept接口来实现。访问者模式的效果有的时候也类似适配器、装饰模式等,由于往往用于操作集合所以一般也通常会和迭代器、组合模式一起使用。
何时采用
访问者模式适用下面的情况:
l 对象结构中包含很多类型,这些类型没有统一的接口,而我们又希望使得对象的操作进行统一。
l 希望为对象结构中的类型新增操作,并且不希望改变原有的代码。
l 对象结构中的一些类型之间发生耦合,而它们的实现又经常会发生变动。
l 针对结构中同一层次的不同类型甚至是不同层次的类型进行迭代。
从这里可以看出,访问者模式的主要适用还是针对对象结构(往往有层次关系),并且需要在设计的时候实现为各类型预留接受访问者的接口。
实现要点
l 每个元素都需要设置Accept()方法来接受访问者。
l 两次多态分发,确定访问者以及访问者中的方法。
l 如果一个结构层次中有多个类型的元素,那么可以通过一个ObjectStructure的角色进行封装。
注意事项
l 访问者模式一个主要的缺点就是难以扩展对象结构,其实,这点是可以通过一些变化进行化解的。
l 访问者模式第二个缺点是需要过多暴露对象的内部元素,否则访问者难以对对象进行实质性的操作。
l 第三个缺点是需要实现考虑到这样的需求并且提前设置接受访问者的方法。