الگوی طراحی بازدیدکننده (Visitor) در زبان C#

توضیح دوات: برای راهنمایی و اطلاعات بیشتر در مورد نمودار کلاس به این مقاله مراجعه کنید.

الگوی طراحی Visitor (بازدیدکننده) یک الگوی رفتاری است که به ما اجازه می‌دهد الگوریتم‌ها را از موجودیت‌هایی که روی آن‌ها عمل می‌کنند جدا کنیم.

الگوی بازدیدکننده به ما اجازه می‌دهد روی اشیاء عملیات انجام دهیم، با نمایش آن عملیات به‌عنوان یک شیء مستقل. بدین ترتیب، می‌توانیم روی این اشیاء کار کنیم بدون اینکه نیازی به تغییر تعاریف یا عملکرد آن‌ها باشد.

شما می‌توانید کد نمونه مربوط به این پست را در گیت‌هاب پیدا کنید.

درک مسئله

تصور کنید که در حال کار روی یک برنامه هستیم که اطلاعات جغرافیایی به‌صورت یک گراف بزرگ سازماندهی شده‌اند. این گراف شامل انواع مختلفی از گره‌ها (Nodes) است. بعضی از این گره‌ها نشان‌دهنده موجودیت‌های پیچیده مانند شهرها هستند و برخی دیگر موجودیت‌های جزئی‌تر مانند فروشگاه‌ها، کسب‌وکارها و مناطق دیدنی را نمایش می‌دهند. اگر بین دو شیء واقعی یک مسیر ارتباطی وجود داشته باشد، گره‌های گراف نیز به هم متصل می‌شوند. در ساختار کد، هر نوع گره توسط کلاس مخصوص خود نمایش داده می‌شود.

گراف بازدیدکننده

اکنون باید مکانیزمی برای ذخیره گراف به فرمت XML پیاده‌سازی کنیم. در ابتدا این کار ساده به نظر می‌رسید. می‌توانیم یک متد export (صادرات) به هر کلاس گره اضافه کنیم و سپس با استفاده از بازگشت (Recursion)، به هر گره گراف مراجعه کرده و متد export را اجرا کنیم. این راه‌حل ساده و زیبا است؛ زیرا به لطف چندریختی (Polymorphism)، کدی که متد export را صدا می‌زند به کلاس‌های خاص گره‌ها وابسته نیست.

متأسفانه، معمار سیستم اجازه تغییر کلاس‌های موجود را به ما نمی‌دهد. کدها در حال حاضر در محیط تولید (Production) قرار دارند و هرگونه باگ احتمالی می‌تواند برنامه را دچار مشکل کند.

گراف بازدیدکننده

علاوه بر این، منطقی نیست که کدهای مربوط به ذخیره XML در کلاس‌های گره قرار بگیرند. وظیفه اصلی این کلاس‌ها کار با داده‌های جغرافیایی (Geodata) است و اضافه کردن قابلیت ذخیره XML به آن‌ها بی‌ربط و غیرمنطقی به نظر می‌رسد.

دلیل دیگری برای رد این پیشنهاد وجود دارد. به احتمال زیاد، پس از پیاده‌سازی این ویژگی، آقای اسمیتِرز از بخش بازاریابی از ما خواهد خواست که قابلیت صادرات به یک فرمت متفاوت را فراهم کنیم، یا حتی درخواست‌های عجیبی ارائه دهد. این موضوع ما را مجبور می‌کند که کلاس‌های ارزشمند گره‌ها را دوباره تغییر دهیم.

راه‌حل با استفاده از الگوی طراحی Visitor

الگوی بازدیدکننده پیشنهاد می‌کند که رفتار جدید (مانند ذخیره XML) را در یک کلاس جداگانه به نام Visitor قرار دهیم، به‌جای اینکه سعی کنیم آن را در کلاس‌های موجود ادغام کنیم. شیء اصلی که باید این رفتار را انجام دهد، به‌عنوان یک آرگومان به یکی از متدهای کلاس بازدیدکننده ارسال می‌شود و این متد به تمام داده‌های لازم درون شیء دسترسی پیدا می‌کند.

در مورد ما، پیاده‌سازی واقعی احتمالاً در کلاس‌های مختلف گره متفاوت خواهد بود. بنابراین، کلاس بازدیدکننده ممکن است نه یک، بلکه مجموعه‌ای از متدها را تعریف کند که هر کدام می‌توانند آرگومان‌هایی با انواع مختلف بپذیرند.

				
					public class ExportVisitor : IVisitor
{
    public string ExportCity(City city) {...}
    public string ExportRestaurant(Restaurant restaurant) {...}
    public string ExportSightseeing(Sightseeing sightseeing) {...}
    //...
}
				
			

مشکل اکنون این است که چگونه باید کل گراف را پردازش کنیم. این متدها امضاهای متفاوتی دارند، بنابراین نمی‌توانیم از چندریختی (Polymorphism) استفاده کنیم. برای انتخاب متدی مناسب که بتواند شیء مورد نظر را پردازش کند، باید کلاس آن شیء را بررسی کنیم.

				
					foreach(Node node in graph)
{
    if (node is City)
        visitor.ExportCity((City)node);
    if (node is Restaurant)
        visitor.ExportRestaurant((Restaurant)node);
    //...
}

				
			

چرا از بیش‌بارگذاری متد (Method Overloading) استفاده نمی‌کنیم؟ می‌توانیم به تمام متدها نام یکسانی بدهیم، حتی اگر هر کدام مجموعه پارامترهای متفاوتی داشته باشند. به هر حال، C# از قابلیت بیش‌بارگذاری متد پشتیبانی می‌کند.

متأسفانه، این روش به ما کمکی نمی‌کند. از آنجایی که کلاس دقیق گره (Node) از قبل مشخص نیست، کامپایلر نمی‌تواند متد صحیح را برای اجرا تشخیص دهد. در نتیجه، متدی که پارامتری از نوع کلاس پایه Node می‌گیرد، به‌طور پیش‌فرض انتخاب می‌شود.

با این حال، الگوی طراحی بازدیدکننده (Visitor) این مشکل را برطرف می‌کند. این الگو از تکنیکی به نام Double Dispatch استفاده می‌کند که به ما کمک می‌کند متد مناسب را بر روی یک شیء بدون استفاده از شرط‌های پیچیده اجرا کنیم. به‌جای اینکه مشتری متد صحیح را برای فراخوانی انتخاب کند، این انتخاب به بازدیدکننده (Visitor) به‌عنوان آرگومان واگذار می‌شود. از آنجایی که اشیاء از کلاس خود آگاه هستند، می‌توانند متد مناسب را برای بازدیدکننده انتخاب کنند.

				
					foreach(Node node in graph)
    node.Accept(visitor);
//...

public class City 
{
    public string Accept(Visitor v)
    {
        v.ExportCity(this);
        //...
    }
}

public class Restaurant
{
    public string Accept(Visitor v)
    {
        v.ExportRestaurant(this);
        //...
    }
}
				
			

اکنون، اگر یک رابط مشترک برای همه بازدیدکنندگان (Visitors) استخراج کنیم، تمام گره‌های موجود می‌توانند با هر بازدیدکننده‌ای که در برنامه معرفی می‌کنیم، کار کنند. اگر بخواهیم رفتار جدیدی مرتبط با گره‌ها اضافه کنیم، تنها کاری که باید انجام دهیم پیاده‌سازی یک کلاس بازدیدکننده‌ی جدید است.

ساختار الگوی طراحی بازدیدکننده

در پیاده‌سازی پایه‌ای الگوی طراحی بازدیدکننده، پنج مشارکت‌کننده اصلی وجود دارد:

نمودار کلاس الگوی طراحی visitor

  1. Visitor (بازدیدکننده):
    رابط (Interface) بازدیدکننده مجموعه‌ای از متدهای بازدید (Visiting) را تعریف می‌کند که می‌توانند عناصر خاصی از یک ساختار شیء را به‌عنوان آرگومان بپذیرند. این متدها ممکن است نام یکسانی داشته باشند، اما نوع پارامترهای آن‌ها باید متفاوت باشد.

  2. Concrete Visitor (بازدیدکننده‌ی مشخص):
    هر بازدیدکننده‌ی مشخص نسخه‌های مختلفی از همان رفتار را پیاده‌سازی می‌کند که برای کلاس‌های عنصر خاص سفارشی شده‌اند.

  3. Element (عنصر):
    رابط عنصر یک متد برای پذیرفتن بازدیدکنندگان (accept) تعریف می‌کند. این متد باید یک پارامتر داشته باشد که با نوع رابط بازدیدکننده اعلام شود.

  4. Concrete Element (عنصر مشخص):
    هر عنصر مشخص باید متد accept را پیاده‌سازی کند. هدف این متد هدایت فراخوانی به متد مناسب بازدیدکننده است که مربوط به کلاس عنصر فعلی می‌باشد. توجه داشته باشید که اگر کلاس عنصر پایه این متد را پیاده‌سازی کند، تمام زیرکلاس‌ها باید همچنان این متد را بازنویسی کرده و متد مناسب روی شیء بازدیدکننده را صدا بزنند.

  5. Client (مشتری):
    مشتری معمولاً نماینده یک مجموعه (Collection) یا یک شیء پیچیده دیگر (مثلاً درخت مرکب Composite Tree) است. معمولاً مشتری از تمام کلاس‌های عنصر مشخص آگاه نیست، زیرا با اشیاء موجود در مجموعه از طریق یک رابط انتزاعی کار می‌کند.

مثال: پیاده‌سازی الگوی بازدیدکننده برای اشکال هندسی

برای نمایش نحوه کار الگوی بازدیدکننده، یک برنامه ایجاد خواهیم کرد که اشکال هندسی را می‌سازد و آن‌ها را به فرمت XML صادر می‌کند.

اولین گام، ایجاد شرکت‌کننده‌ی Element است که توسط رابط IShape نمایش داده می‌شود.

				
					using Visitor.Visitor;

namespace Visitor.Shapes
{
    /// <summary>
    /// Common shape interface
    /// </summary>
    public interface IShape
    {
        public void Move(int x, int y);
        public void Draw();
        public string Accept(IVisitor visitor);
    }
}

				
			

ما همچنین به عناصر مشخص (Concrete Elements) نیاز داریم که اشکال مختلف پشتیبانی‌شده توسط برنامه را نمایش دهند. توجه داشته باشید که هر عنصر یک IVisitor را می‌پذیرد و سپس متد مناسب بازدیدکننده را فراخوانی می‌کند. این فرآیند به نام Double Dispatch (اعزام دوگانه) شناخته می‌شود که در ادامه به‌طور مفصل درباره آن بحث خواهیم کرد.

				
					using Visitor.Visitor;

namespace Visitor.Shapes
{
    public class Dot : IShape
    {
        public Guid Id { get; set; }
        public int X { get; set; }
        public int Y { get; set; }

        public Dot()
        {
        }

        public Dot(Guid id, int x, int y)
        {
            Id = id;
            X = x;
            Y = y;
        }

        public string Accept(IVisitor visitor)
        {
            return visitor.VisitDot(this);
        }

        public void Draw()
        {
            Console.WriteLine($"Drawing dot-{Id} at ({X},{Y})");
        }

        public void Move(int x, int y)
        {
            X += x;
            Y += y;
        }
    }
}
				
			
				
					using Visitor.Visitor;

namespace Visitor.Shapes
{
    public class Circle : IShape
    {
        public Guid Id { get; set; }
        public int X { get; set; }
        public int Y { get; set; }
        public int Radius { get; }

        public Circle(Guid id, int x, int y, int radius)
        {
            Id = id;
            X = x;
            Y = y;
            Radius = radius;
        }

        public new string Accept(IVisitor visitor)
        {
            return visitor.VisitCircle(this);
        }

        public void Move(int x, int y)
        {
            X += x;
            Y += y;
        }

        public void Draw()
        {
            Console.WriteLine($"Drawing circle-{Id} at ({X},{Y}) with radius {Radius}");
        }
    }
}
				
			
				
					using Visitor.Visitor;

namespace Visitor.Shapes
{
    public class Rectangle : IShape
    {
        public Guid Id { get; set; }

        public int X { get; set; }
        public int Y { get; set; }

        public int Width { get; set; }
        public int Height { get; set; }

        public Rectangle(Guid id, int x, int y, int width, int height)
        {
            Id = id;
            X = x;
            Y = y;
            Width = width;
            Height = height;
        }

        public string Accept(IVisitor visitor)
        {
            return visitor.VisitRectangle(this);
        }

        public void Draw()
        {
            Console.WriteLine($"Drawing rectangle-{Id} at ({X},{Y}) with width {Width} and height {Height}");
        }

        public void Move(int x, int y)
        {
            X += x;
            Y += y;
        }
    }
}
				
			

علاوه بر این کلاس ComplexShape را که نماینده اشکال مرکب است اضافه می‌کنیم.

				
					using Visitor.Visitor;

namespace Visitor.Shapes
{
    public class ComplexShape : IShape
    {
        public Guid Id { get; set; }
        public List<IShape> children = new();

        public int X { get; set; }
        public int Y { get; set; }

        public ComplexShape(Guid id)
        {
            Id = id;
        }

        public void Add(IShape shape)
        {
            children.Add(shape);
        }

        public string Accept(IVisitor visitor)
        {
            return visitor.VisitComplex(this);
        }

        public void Draw()
        {
            Console.WriteLine($"Drawing complex-{Id} at ({X},{Y})");
        }

        public void Move(int x, int y)
        {
            X += x;
            Y += y;
        }
    }
}
				
			

حالا می‌توانیم شرکت‌کننده بازدیدکننده را تعریف کنیم که رابطی برای تعریف انواع مختلف متدهای ویزیت خواهد بود: هر متد ویزیت برای یک المان خاص.

				
					using Visitor.Shapes;

namespace Visitor.Visitor
{
    public interface IVisitor
    {
        public string VisitDot(Dot dot);
        public string VisitCircle(Circle circle);
        public string VisitRectangle(Rectangle rectangle);
        public string VisitComplex(ComplexShape complexShape);
    }
}
				
			

در نهایت، ما بازدیدکننده مشخص (Concrete Visitor) خود را پیاده‌سازی خواهیم کرد. در این مثال، کلاس ما XMLExportVisitor خواهد بود. این کلاس متدهای Visit از رابط IVisitor را پیاده‌سازی می‌کند و برای هر عنصر، المان‌های XML ایجاد خواهد کرد:

				
					using System.Text;
using System.Xml.Linq;
using Visitor.Shapes;

namespace Visitor.Visitor
{
    public class XMLExportVisitor : IVisitor
    {
        public string Export(params IShape[] shapes)
        {
            StringBuilder builder = new StringBuilder();
            builder.AppendLine("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
            builder.AppendLine("<shapes>");

            foreach (IShape shape in shapes)
                builder.AppendLine(shape.Accept(this));

            builder.AppendLine("</shapes>");
            return XMLFormatter(builder.ToString());
        }

        public string VisitCircle(Circle circle)
        {
            return $@"<circle> 
                          <id>{circle.Id}</id> 
                          <x>{circle.X}</x> 
                          <y>{circle.Y}</y> 
                          <radius>{circle.Radius}</radius> 
                      </circle>";
        }

        public string VisitComplex(ComplexShape complexShape)
        {
            return $@"<complex> 
                          <id>{complexShape.Id}</id> 
                          {VisitChildren(complexShape)}
                      </complex>";

        }

        public string VisitDot(Dot dot)
        {
            return $@"<dot> 
                      <id>{dot.Id}</id> 
                      <x>{dot.X}</x> 
                      <y>{dot.Y}</y> 
                      </dot>";
        }

        public string VisitRectangle(Rectangle rectangle)
        {
            return $@"<rectangle> 
                          <id>{rectangle.Id}</id> 
                          <x>{rectangle.X}</x> 
                          <y>{rectangle.Y}</y> 
                          <width>{rectangle.Width}</width> 
                          <height>{rectangle.Height}</height> 
                      </rectangle>";
        }

        private string VisitChildren(ComplexShape shape)
        {
            StringBuilder stringBuilder = new StringBuilder();

            foreach (IShape child in shape.children)
            {
                string childXML = child.Accept(this);
                stringBuilder.AppendLine(childXML);
            }

            return stringBuilder.ToString();
        }

        private string XMLFormatter(string xml)
        {
            XDocument doc = XDocument.Parse(xml);
            return doc.ToString();
        }
    }
}
				
			

برای جمع‌بندی و کنار هم قرار دادن تمام این موارد، در متد Main() خود چند شکل تعریف خواهیم کرد و سپس از XMLExportVisitor برای ذخیره کردن آن‌ها به فرمت XML استفاده می‌کنیم:

				
					using Visitor.Shapes;
using Visitor.Visitor;

public class Program
{
    public static void Main(string[] args)
    {
        Dot dot = new Dot(Guid.NewGuid(), 10, 12);
        Circle circle = new Circle(Guid.NewGuid(), 23, 15, 21);
        Rectangle rectangle = new Rectangle(Guid.NewGuid(), 10, 17, 20, 32);

        ComplexShape complex = new ComplexShape(Guid.NewGuid());
        complex.Add(dot);
        complex.Add(circle);
        complex.Add(rectangle);

        ComplexShape complexShapeDot = new ComplexShape(Guid.NewGuid());
        complexShapeDot.Add(dot);
        complex.Add(complexShapeDot);

        Export(dot, circle, complex);

        Console.ReadKey();
    }

    private static void Export(params IShape[] shapes)
    {
        XMLExportVisitor xmlVisitor = new XMLExportVisitor();
        Console.WriteLine(xmlVisitor.Export(shapes));
    }
}
				
			

اگر برنامه را اجرا کنیم اشکال در قالب XML ذخیره خواهند شد.

اعزام دوگانه و الگوی بازدیدکننده

اعزام دوگانه (Double Dispatch) یک تکنیک است که می‌توانیم از آن برای کنترل نحوه‌ی جریان ارتباط بین دو شیء استفاده کنیم.

فرض کنید در حال ساخت یک کاوشگر کشاورزی هستیم که به‌طور منظم به مکان‌های مختلف سفر می‌کند و ترکیب خاک را بررسی می‌کند. با این حال، آزمایش ترکیب خاک در مکان‌های مختلف متفاوت است، به دلیل تفاوت در نوع خاکی که در هر مکان یافت می‌شود.

برای سادگی، بیایید ۳ نوع خاک را مدل‌سازی کنیم:

				
					namespace VisitorDoubleDispatch.SingleDispatch.Soils
{
    public interface ISoil
    {
        public void DisplayName();
    }
}

namespace VisitorDoubleDispatch.SingleDispatch.Soils
{
    public class Loam : ISoil
    {
        public void DisplayName()
        {
            Console.WriteLine("Loam");
        }
    }
}

namespace VisitorDoubleDispatch.SingleDispatch.Soils
{
    public class Peat : ISoil
    {
        public void DisplayName()
        {
            Console.WriteLine("Peat");
        }
    }
}

namespace VisitorDoubleDispatch.SingleDispatch.Soils
{
    public class Podzol : ISoil
    {
        public void DisplayName()
        {
            Console.WriteLine("Podzol");
        }
    }
}
				
			

در مرحله بعد متدهای کاوشگر خاک را پیاده‌سازی می‌کنیم:

				
					using VisitorDoubleDispatch.SingleDispatch.Soils;

namespace VisitorDoubleDispatch.SingleDispatch.Probe
{
    public interface IProbe
    {
        public void Visit(Loam loam);
        public void Visit(Peat peat);
        public void Visit(Podzol podzol);
        public void Visit(ISoil soil);
    }
}

				
			
				
					using VisitorDoubleDispatch.SingleDispatch.Soils;

namespace VisitorDoubleDispatch.SingleDispatch.Probe
{
    public class SoilProbe : IProbe
    {
        public void Visit(Loam loam)
        {
            Console.WriteLine("Deploying tools specific to loam");
        }

        public void Visit(Peat peat)
        {
            Console.WriteLine("Deploying tools specific to peat");
        }

        public void Visit(Podzol podzol)
        {
            Console.WriteLine("Deploying tools specific to podzol");
        }

        public void Visit(ISoil soil)
        {
            Console.WriteLine("Cannot probe unknown soil");
        }
    }
}

				
			

حالا می‌توانیم از کاوشگر استفاده کنیم.

				
					using VisitorDoubleDispatch.SingleDispatch.Probe;
using VisitorDoubleDispatch.SingleDispatch.Soils;

namespace VisitorDoubleDispatch.SingleDispatch
{
    public class SingleDispatchRunner
    {
        public static void Run()
        {
            ISoil podzol = new Podzol();
            ISoil peat = new Peat();
            ISoil loam = new Loam();

            IProbe probe = new SoilExplorer();

            List<ISoil> soilsToVisit = new List<ISoil>();

            soilsToVisit.Add(podzol);
            soilsToVisit.Add(peat);
            soilsToVisit.Add(loam);

            foreach (ISoil soil in soilsToVisit)
                probe.Visit(soil);
        }
    }
}
				
			

مثل یک کامپایلر فکر کردن

بیایید برای لحظه‌ای وانمود کنیم که کامپایلر هستیم و می‌خواهیم کد زیر را کامپایل کنیم:

				
					public void DisplaySoilName(ISoil soil)
{
    soil.DisplayName();
}
				
			

متد DisplayName() در رابط ISoil تعریف شده است. اما صبر کنید، سه کلاس این رابط را پیاده‌سازی می‌کنند. آیا می‌توانیم به‌طور ایمن تصمیم بگیریم که کدام یک از این پیاده‌سازی‌ها را در اینجا فراخوانی کنیم؟ به نظر نمی‌رسد. تنها راه برای اطمینان این است که برنامه را اجرا کنیم و کلاس شیء ارسال‌شده به متد را بررسی کنیم. تنها چیزی که می‌دانیم این است که شیء ارسالی یک پیاده‌سازی از متد DisplayName() خواهد داشت.

بنابراین، کد کامپایل‌شده‌ی نهایی کلاس شیء ارسال‌شده به پارامتر soil را بررسی کرده و پیاده‌سازی DisplayName مربوط به کلاس مناسب را انتخاب خواهد کرد.

این فرآیند باندینگ دیرهنگام (Late Binding) یا باندینگ پویا (Dynamic Binding) نامیده می‌شود. «دیرهنگام» زیرا شیء و پیاده‌سازی‌های آن در زمان اجرا به هم متصل می‌شوند. «پویا» زیرا هر شیء جدید ممکن است نیاز داشته باشد به یک پیاده‌سازی متفاوت متصل شود.

حالا بیایید کد زیر را کامپایل کنیم:

				
					IProbe probe = new SoilExplorer();

foreach (ISoil soil in soilsToVisit)
    probe.VisitSoil(soil);
				
			

همه‌چیز با خط اول واضح است: کلاس SoilExplorer سازنده‌ی سفارشی ندارد، بنابراین فقط یک شیء را نمونه‌سازی می‌کنیم. اما درباره‌ی فراخوانی متد VisitSoil چطور؟ کلاس SoilExplorer چهار متد با همین نام دارد که با نوع پارامترهایشان تفاوت دارند. کدام یک باید فراخوانی شود؟ به نظر می‌رسد که در اینجا نیز به باندینگ پویا نیاز داریم.

اما یک مشکل دیگر وجود دارد. اگر کلاسی از نوع خاک (Soil) وجود داشته باشد که متد مناسبی در کلاس SoilExplorer برای آن تعریف نشده باشد چه اتفاقی می‌افتد؟ به‌عنوان مثال، خاک Clay. کامپایلر نمی‌تواند تضمین کند که متد سربارگذاری‌شده‌ی درستی وجود دارد. این وضعیت مبهمی ایجاد می‌کند که کامپایلر نمی‌تواند آن را بپذیرد. بنابراین، تیم‌های توسعه‌ی کامپایلر از یک مسیر امن استفاده می‌کنند و برای متدهای سربارگذاری‌شده از باندینگ زودهنگام (Early Binding) استفاده می‌کنند.

این فرآیند باندینگ زودهنگام یا ایستا (Static Binding) نامیده می‌شود. «زودهنگام» زیرا در زمان کامپایل قبل از اجرای برنامه اتفاق می‌افتد. «ایستا» زیرا در زمان اجرا نمی‌تواند تغییر کند.

حالا به مثال خودمان برگردیم. ما مطمئن هستیم که آرگومان ورودی یکی از سلسله‌مراتب Soil خواهد بود: یا کلاس ISoil یا یکی از زیرکلاس‌های آن. همچنین می‌دانیم که کلاس SoilExplorer پیاده‌سازی پایه‌ای از متد VisitSoil دارد که از رابط ISoil پشتیبانی می‌کند:VisitSoil(ISoil soil)

این تنها پیاده‌سازی‌ای است که می‌توان با اطمینان آن را به کد مشخصی لینک کرد بدون اینکه ابهام ایجاد شود. به همین دلیل، حتی اگر یک شیء از نوع Podzol به متد VisitSoil ارسال کنیم، برنامه همچنان متد VisitSoil(ISoil soil) را فراخوانی خواهد کرد.

اعزام دوگانه (Double Dispatch)

اعزام دوگانه تکنیکی است که اجازه می‌دهد از بایندینگ پویا در کنار متدهای سربارگذاری‌شده استفاده کنیم.

ما می‌توانیم سطح دوم از چندریختی (Polymorphism) را با استفاده از الگوی Visitor شبیه‌سازی کنیم.

ابتدا باید متد Accept را در کلاس‌های ISoil پیاده‌سازی کنیم. هدف از متد Accept این است که به IProbe اطلاعات بیشتری بدهد درباره اینکه کدام نوع از ISoil باید با استفاده از this بازدید شود.

				
					using VisitorDoubleDispatch.DoubleDispatch.Probes;

namespace VisitorDoubleDispatch.DoubleDispatch.Soils
{
    public interface ISoil
    {
        public void Accept(IProbe probe);
        public void DisplayName();
    }
}
				
			
				
					using VisitorDoubleDispatch.DoubleDispatch.Probes;

namespace VisitorDoubleDispatch.DoubleDispatch.Soils
{
    public class Loam : ISoil
    {
        public void Accept(IProbe probe)
        {
            probe.Visit(this);
        }

        public void DisplayName()
        {
            Console.WriteLine("Loam");
        }
    }
}

using VisitorDoubleDispatch.DoubleDispatch.Probes;

namespace VisitorDoubleDispatch.DoubleDispatch.Soils
{
    public class Peat : ISoil
    {
        public void Accept(IProbe probe)
        {
            probe.Visit(this);
        }

        public void DisplayName()
        {
            Console.WriteLine("Peat");
        }
    }
}

using VisitorDoubleDispatch.DoubleDispatch.Probes;

namespace VisitorDoubleDispatch.DoubleDispatch.Soils
{
    public class Podzol : ISoil
    {
        public void Accept(IProbe probe)
        {
            probe.Visit(this);
        }

        public void DisplayName()
        {
            Console.WriteLine("Podzol");
        }
    }
}
				
			

سپس می‌توانیم از متد ISoil.Accept برای بررسی انواع مختلف خاک استفاده کنیم. توجه داشته باشید که متد Accept یک لایه اضافی از غیرمستقیم‌سازی (Indirection) اضافه می‌کند. این متد به‌طور صریح به Probe اعلام می‌کند که کدام پیاده‌سازی از ISoil را باید بازدید کند.

				
					using VisitorDoubleDispatch.DoubleDispatch.Probes;
using VisitorDoubleDispatch.DoubleDispatch.Soils;

namespace VisitorDoubleDispatch.DoubleDispatch
{
    public class DoubleDispatchRunner
    {
        public static void Run()
        {
            ISoil loam = new Loam();
            ISoil peat = new Peat();
            ISoil podzol = new Podzol();

            IProbe probe = new SoilProbe();

            List<ISoil> soilsToBeVisited = new();

            soilsToBeVisited.Add(loam);
            soilsToBeVisited.Add(peat);
            soilsToBeVisited.Add(podzol);

            foreach (ISoil soil in soilsToBeVisited)
                soil.Accept(probe);
        }
    }
}
				
			

ما به‌طور مؤثر فراخوانی اولیه‌ی probe.Visit(soil) را به دو لایه‌ی مجزا تقسیم کرده‌ایم. فراخوانی soil.Accept(probe) از چندریختی (Polymorphism) برای تشخیص پیاده‌سازی مربوط به ISoil استفاده می‌کند، و فراخوانی probe.Visit(this) به کامپایلر اطلاع می‌دهد که کدام کلاس خاص را باید به آن متصل کند.

مزایا و معایب الگوی طراحی بازدیدکننده (Visitor)

مزایا

  • می‌توانیم یک رفتار جدید معرفی کنیم که با اشیاء کلاس‌های مختلف کار کند بدون آن‌که این کلاس‌ها تغییر کنند، بنابراین از اصل باز/بسته (Open/Closed Principle) پیروی می‌کنیم.
  • می‌توانیم نسخه‌های مختلف یک رفتار را به یک کلاس منتقل کنیم، بنابراین از اصل مسئولیت یگانه (Single Responsibility Principle) پیروی می‌کنیم.
  • یک شیء بازدیدکننده می‌تواند هنگام کار با اشیاء مختلف اطلاعات مفیدی جمع‌آوری کند. این ویژگی می‌تواند در مواقعی مفید باشد که می‌خواهیم در یک ساختار پیچیده مثل یک درخت اشیاء پیمایش کرده و بازدیدکننده را روی هر شیء این ساختار اعمال کنیم.

معایب

  • ممکن است بازدیدکنندگان نتوانند به فیلدها و متدهای خصوصی عناصر مورد نظرشان دسترسی داشته باشند.
  • هر بار که کلاسی به سلسله‌مراتب عناصر اضافه یا از آن حذف شود، باید تمام بازدیدکنندگان (Visitors) را به‌روزرسانی کنیم.

ارتباط با سایر الگوهای طراحی

  • الگوی طراحی بازدیدکننده را می‌توان به‌عنوان نسخه‌ای قدرتمندتر از الگوی طراحی فرمان (Command) در نظر گرفت. اشیاء آن می‌توانند عملیات‌هایی را روی اشیاء مختلف از کلاس‌های متفاوت اجرا کنند.
  • می‌توان از الگوی طراحی بازدیدکننده برای اجرای عملیات بر کل درخت مرکب (Composite) استفاده کرد.
  • می‌توان از الگوی طراحی بازدیدکننده همراه با الگوی تکرارکننده (Iterator) برای پیمایش یک ساختار داده‌ای پیچیده و اجرای عملیات بر عناصر آن، حتی اگر این عناصر متعلق به کلاس‌های متفاوت باشند، استفاده کرد.

نتیجه‌گیری

در این مقاله، الگوی طراحی بازدیدکننده را بررسی کردیم، زمان استفاده از آن و مزایا و معایب این الگوی طراحی را تحلیل کردیم. سپس تکنیک Double Dispatch را معرفی کردیم و نحوه‌ی پیاده‌سازی آن با استفاده از الگوی بازدیدکننده را دیدیم. در نهایت، ارتباط الگوی بازدیدکننده با سایر الگوهای طراحی کلاسیک را بررسی کردیم.

الگوی طراحی بازدیدکننده در بسیاری از موارد مفید و انعطاف‌پذیر است، به شرطی که به درستی از آن استفاده شود. با این حال، باید توجه داشت که الگوی بازدیدکننده، به همراه سایر الگوهای طراحی معرفی‌شده توسط Gang of Four، راه‌حل نهایی و بی‌نقصی برای طراحی نرم‌افزار نیست. انتخاب زمان و مکان مناسب برای استفاده از این الگوها بر عهده‌ی مهندسان نرم‌افزار است. در نهایت، این الگوها زمانی مفید هستند که به‌عنوان ابزار دقیق به کار روند، نه به‌عنوان چکش سنگین.

©دوات با هدف دسترس‌پذیر کردن دانش انگلیسی در حوزه صنعت نرم‌افزار وجود آمده است. در این راستا از هوش مصنوعی برای ترجمه گلچینی از مقالات مطرح و معتبر استفاده می‌شود. با ما در تماس باشید و انتقادات و پیشنهادات خود را از طریق صفحه «تماس با ما» در میان بگذارید.