意圖
實現通過統一的接口訪問不同類型元素的操作,並且通過這個接口可以增加新的操作而不改變元素的類。
場景
想不出什麼好例子,我們在組合模式的那個例子上進行修改吧。我們知道,無論是游戲大區、游戲服務器還是游戲的服務都是一個元素,只不過它們的層次不一樣。對於這樣的層次結構,我們使用了組合模式來統一各層的接口,這樣對游戲大區的操作和對游戲服務器的操作對調用方來說沒有什麼兩樣。在現實中,組合模式的運用往往沒有這麼順利:
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 第三個缺點是需要實現考慮到這樣的需求並且提前設置接受訪問者的方法。