首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> C# >> 關於C#

C# 設計模式無廢話系列(二十一):Visitor 訪問者模式

     編輯:關於C#

意圖

提供統一介面訪問不同類型元素,並能在不修改元素類的前提下新增操作。

適用場景

承接 Composite 模式的遊戲伺服器架構範例:遊戲大區、伺服器、服務均為元素,層次不同。Composite 統一了介面,但實務上常遇到:

  • 元素需新增操作 → 抽象元素需加介面 → 幾乎所有具體元素皆需修改。
  • 元素無統一介面,且分支節點包含多種葉節點 → 分支需依葉節點類型呼叫不同方法。
  • 葉節點介面頻繁變動 → 操作葉節點的分支節點也須跟著改。

Visitor 模式可解決上述問題。

完整範例程式碼

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) { gameService.StartGameService(this); } // A9
        public void Visit(GameServer gameServer) { gameServer.StartGameServer(this); }     // A6
        public void Visit(GameArea gameArea)   { gameArea.StartGameArea(this); }           // A3
    }

    class StopGameVisitor : IVisitor, IGameServiceVisitor, IGameServerVisitor, IGameAreaVisitor
    {
        public void Visit(GameService gameService)
        {
            Console.WriteLine($"{gameService.Name} stopped"); // B7
        }
        public void Visit(GameServer gameServer)
        {
            Console.WriteLine($"=============Stopping the whole {gameServer.Name}============="); // B5
            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)
        {
            Console.WriteLine($"=============Stopping the whole {gameArea.Name}============="); // B3
            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 gs in gameServer)
                if (gs.ServiceType == 1) playerCount += gs.PlayerCount;
            Console.WriteLine($"=============Player Count on {gameServer.Name} is: {playerCount}");
        }
        public void Visit(GameArea gameArea)
        {
            int playerCount = 0;
            foreach (GameServer gs in gameArea)
                foreach (GameService s in gs)
                    if (s.ServiceType == 1) playerCount += s.PlayerCount;
            Console.WriteLine($"=============Player Count on {gameArea.Name} is: {playerCount}");
        }
    }

    abstract class Element
    {
        protected string name;
        public string Name => 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>
    {
        public int ServiceType { get; set; }
        public string ServiceName { get; private set; }
        public int PlayerCount { get; set; }

        public GameService(string name, int serviceType, string serviceName) : base(name)
        { ServiceType = serviceType; ServiceName = serviceName; }
        public GameService(string name, int serviceType, string serviceName, int playerCount) : this(name, serviceType, serviceName)
        { PlayerCount = playerCount; }

        public override void Add(Element element)    => throw new ApplicationException("Leaf cannot add");
        public override void Remove(Element element) => throw new ApplicationException("Leaf cannot remove");
        public override void Accept(IVisitor visitor)
        {
            var v = visitor as IGameServiceVisitor;
            v?.Visit(this); // A8, B6
        }
        public int CompareTo(GameService other) => other.ServiceType.CompareTo(ServiceType);
        public void StartGameService(IVisitor visitor) => Console.WriteLine($"{Name} started"); // A10
    }

    class GameServer : Element
    {
        public string ServerIP { get; }
        public List<GameService> ServiceList { get; } = new List<GameService>();

        public GameServer(string name, string serverIP) : base(name) { 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)
        {
            var v = visitor as IGameServerVisitor;
            v?.Visit(this); // A5, B5
        }
        public IEnumerator<GameService> GetEnumerator()
        {
            foreach (var s in ServiceList) yield return s;
        }
        public void StartGameServer(IVisitor visitor)
        {
            var v = visitor as IGameServerVisitor;
            if (v != null)
            {
                ServiceList.Sort();
                Console.WriteLine($"=============Starting the whole {Name}============="); // A7
                foreach (Element s in ServiceList) s.Accept(visitor);
                Console.WriteLine($"=============The whole {Name} started=============");
            }
        }
    }

    class GameArea : Element
    {
        public List<GameServer> ServerList { get; } = new List<GameServer>();
        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)
        {
            var v = visitor as IGameAreaVisitor;
            v?.Visit(this); // A2, B2
        }
        public IEnumerator<GameServer> GetEnumerator()
        {
            foreach (var s in ServerList) yield return s;
        }
        public void StartGameArea(IVisitor visitor)
        {
            var v = visitor as IGameAreaVisitor;
            if (v != null)
            {
                Console.WriteLine($"=============Starting the whole {Name}============="); // A4
                foreach (Element s in ServerList) s.Accept(visitor);
                Console.WriteLine($"=============The whole {Name} started=============");
            }
        }
    }
}

程式碼執行結果

Visitor 模式執行結果

程式碼深度解析

  • IVisitor:空介面,作為所有 Visitor 的抽象基底,層次對等 Element。
  • IGameServiceVisitor / IGameServerVisitor / IGameAreaVisitor:定義各層級元素的訪問操作。刻意拆分為多個小介面,因為某 Visitor 可能只針對部分層級或特定類型元素。
  • StartGameVisitor / StopGameVisitor / QueryPlayerCountVisitor:具體訪問者,依需求實作所需介面。雖同為 Visitor,目的各異。
  • Element:抽象構件,提供一致介面,並定義 Accept(IVisitor) 接受訪問者。
  • GameService / GameServer / GameArea:具體元素。組合視角下,GameServer、GameArea 為分支,GameService 為葉節點。Accept() 中以型別轉換確認訪問者類型,正確則呼叫對應 Visit()。
  • StartGameVisitor 統一了啟動介面:原本 GameService/GameServer/GameArea 啟動方法各異,導致高層耦合低層具體方法。引入 Visitor 後,高層僅耦合 Visitor 統一方法,由 Visitor 適配各具體啟動邏輯(可參考程式碼中 A 開頭註解追蹤流程)。
  • StopGameVisitor 體現 Visitor 核心價值——為元素結構新增操作而無需修改元素類別,得益於統一 Accept 與雙重分派機制(參考 B 開頭註解)。
  • 介面拆分策略:頂層空介面 + 功能介面,使具體 Visitor 可選擇性實作,新增元素層級時擴充性較佳。QueryPlayerCountVisitor 僅實作 Server 與 Area 訪問,不需理會 Service。
  • Visitor 亦簡化異質集合遍歷,符合開閉原則。本例因 GameServer 子元素僅有 GameService,未展現此優勢。
  • Visitor 結構多變,核心永遠是雙重分派與統一 Accept。效果有時類似 Adapter、Decorator,常與 Iterator、Composite 協作。

何時採用

  • 對象結構包含多類型,無統一介面,但希望統一操作。
  • 需為結構中的類型新增操作,且不想修改既有程式碼。
  • 結構中某些類型耦合且實作常變動。
  • 需對同層或跨層不同類型進行迭代操作。

Visitor 主要針對有層次關係的對象結構,且需在設計階段預留 Accept 介面。

實作要點

  • 每個元素必須提供 Accept() 接受訪問者。
  • 兩次多態分派:確定訪問者型別、確定訪問者中的方法。
  • 若同層級有多種元素類型,可引入 ObjectStructure 角色封裝管理。

注意事項

  • 難以擴充對象結構(新增元素類型需修改所有 Visitor),但可透過介面拆分緩解。
  • 需過度暴露對象內部結構,否則 Visitor 難以實質操作。
  • 需預見需求並提前在元素中置入 Accept 方法。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved