الگوی طراحی Prototype یک الگوی ایجادی است که به ما امکان میدهد اشیای موجود را کپی کنیم، بدون اینکه کد ما وابسته به کلاسهای ما شود.
روش معمول برای درک این الگو این است که تصور کنیم چگونه ممکن است طیف رنگ را مدلسازی کنیم. چیزی حدود ۱۰ میلیون رنگ قابل مشاهده وجود دارد، بنابراین مدلسازی آنها بهصورت کلاسهای جداگانه (مانند Red
، LightMauve
، Skobeloff
، Mac and Cheese
، Zomp
) بسیار غیرعملی خواهد بود.
با این حال، رنگ، هرچه که باشد، یک رنگ است؛ همه رنگها خصوصیات مشابهی دارند، هرچند مقادیر آنها متفاوت است. اگر نیاز داشتیم تعداد زیادی نمونه از رنگها ایجاد کنیم، میتوانستیم با استفاده از الگوی طراحی Prototype این کار را انجام دهیم.
مفهومسازی مسئله
فرض کنید شیئی داریم که میخواهیم کپی دقیقی از آن بسازیم. چگونه میتوانیم این کار را انجام دهیم؟ ابتدا باید یک شیء جدید از همان کلاس بسازیم. سپس باید از میان تمام فیلدهای نمونه اصلی عبور کنیم و مقادیر آنها را به نمونه جدید کپی کنیم.
عالی به نظر میرسد. اما مشکلی وجود دارد. همه اشیا نمیتوانند به این روش کپی شوند، زیرا برخی از فیلدهای شیء ممکن است خصوصی باشند و از بیرون شیء قابل مشاهده نباشند.
مشکل دیگری که در این روش وجود دارد، این است که برای ساختن یک نسخه کپی، باید کلاس شیء را بدانیم. این وابستگی کد ما را به کلاس خاصی وابسته میکند. اگر این وابستگی اضافی شما را نگران نمیکند، نکته دیگری هم وجود دارد: اغلب فقط رابطی که شیء پیادهسازی کرده را میشناسیم و نه کلاس واقعی آن.
چگونه Prototype این مشکلات را حل میکند؟
الگوی Prototype فرآیند کپی کردن را به اشیایی که باید کپی شوند واگذار میکند. این الگو یک رابط مشترک برای تمام اشیایی که از کپی کردن پشتیبانی میکنند، اعلام میکند. این رابط به ما امکان میدهد بدون نیاز به وابسته کردن کد خود به کلاسهای شیء، یک نمونه را کپی کنیم. این رابط معمولاً تنها شامل یک متد است: Clone
.
پیادهسازی متد Clone
در همه کلاسها تقریباً مشابه است. این متد یک نمونه جدید از کلاس فعلی ایجاد میکند. سپس تمام مقادیر نمونه قدیمی را به نمونه جدید منتقل میکند.
شیئی که از کپی کردن پشتیبانی میکند، پروتوتایپ نامیده میشود. هنگامی که اشیای ما دارای دهها فیلد و صدها پیکربندی ممکن هستند، کپی کردن آنها ممکن است جایگزین مناسبی برای زیرکلاسبندی باشد. به این صورت کار میکند: ما مجموعهای از اشیای از پیش پیکربندیشده ایجاد میکنیم. وقتی به شیئی با پیکربندی خاص نیاز داریم، بهجای ساختن یک نمونه جدید از ابتدا، فقط یک پروتوتایپ را کپی میکنیم.
ساختار الگوی طراحی Prototype
الگوی Prototype دو شکل رایج دارد: پیادهسازی پایه Prototype و پیادهسازی رجیستری Prototype.
پیادهسازی پایه Prototype
در این پیادهسازی، الگوی Prototype دارای ۳ شرکتکننده است:
- Prototype: رابط Prototype متدهای مربوط به کپی کردن را اعلام میکند. در اکثر موارد، این متد فقط یک متد
Clone
است، اما ممکن است بسته به زمینه پیچیدهتر باشد. - Concrete Prototype: متد Clone توسط کلاس Concrete Prototype پیادهسازی میشود. این متد ممکن است سناریوهای خاصی از فرآیند کپی کردن را مدیریت کند، مانند کپی کردن اشیای مرتبط، باز کردن وابستگیهای بازگشتی و غیره، علاوه بر انتقال محتوای شیء اصلی به نمونه کپی.
- Client: کلاینت میتواند کپی هر شیئی را که رابط پروتوتایپ را پیادهسازی میکند، تولید کند.
پیادهسازی رجیستری Prototype
در پیادهسازی رجیستری، الگوی Prototype دارای ۴ شرکتکننده است:
- Prototype: رابط Prototype متدهای مربوط به کپی کردن را اعلام میکند. همانند پیادهسازی پایه، معمولاً این یک متد
Clone
است. - Concrete Prototype: این متد توسط کلاس Concrete Prototype پیادهسازی میشود و فرآیند کپی کردن را مدیریت میکند.
- Prototype Registry: رجیستری Prototype یافتن پروتوتایپهای پرکاربرد را آسان میکند. این رجیستری مجموعهای از اشیای از پیش ساختهشده آماده برای کپی را نگهداری میکند. یک نگاشت هش به نامها سادهترین نوع رجیستری Prototype است. اگر به پارامترهای جستجوی بیشتری نیاز دارید، میتوانید نسخهای پیچیدهتر از رجیستری را ایجاد کنید.
- Client: کلاینت میتواند کپی هر شیئی را که رابط پروتوتایپ را پیادهسازی میکند، تولید کند.
برای نشان دادن نحوه عملکرد الگوی Prototype، سیستمی برای تولید اشکال مختلف ایجاد میکنیم.
ابتدا یک کلاس انتزاعی به نام AbstractShape (شرکتکننده Prototype) ایجاد میکنیم تا یک شکل را نمایش دهد. سپس یک متد به نام Clone تعریف میکنیم که یک نمونه از این کلاس میتواند از آن برای کپی کردن خود استفاده کند.
///
/// The Prototype abstract class
///
public abstract class Shape
{
public int x;
public int y;
public string color;
public Shape()
{
}
public Shape(Shape target)
{
if (target != null)
{
this.x = target.x;
this.y = target.y;
this.color = target.color;
}
}
public abstract Shape clone();
public override bool Equals(object object2)
{
if (!(object2 is Shape)) return false;
Shape shape2 = (Shape)object2;
return shape2.x == x && shape2.y == y && object.Equals(shape2.color, color);
}
}
حالا که کلاس انتزاعی خود را داریم، نمونههای اولیه عملی (Concrete Prototypes) را پیادهسازی خواهیم کرد. نمونه اولیه عملی اول یک دایره ایجاد میکند. این نمونه وظیفه کپیسازی و مقایسه برابری را بر عهده خواهد داشت.
public class Circle : Shape
{
public int Radius;
public Circle()
{
}
public Circle(Circle target) : base(target)
{
if (target != null)
{
this.Radius = target.Radius;
}
}
public override Shape clone()
{
return new Circle(this);
}
public override bool Equals(object object2)
{
if (!(object2 is Circle shape2) || !base.Equals(shape2)) return false;
return shape2.Radius == Radius;
}
public override int GetHashCode()
{
var hashCode = base.GetHashCode();
hashCode = hashCode * 31 + Radius.GetHashCode();
return hashCode;
}
}
نمونه اولیه دوم یک مستطیل ایجاد خواهد کرد (چه ایدهی اصیلی!). این نمونه نیز وظیفه کپیسازی و مقایسه برابری را بر عهده خواهد داشت.
public class Rectangle : Shape
{
public int Width;
public int Height;
public Rectangle()
{
}
public Rectangle(Rectangle target) : base(target)
{
if (target != null)
{
this.Width = target.Width;
this.Height = target.Height;
}
}
public override Shape clone()
{
return new Rectangle(this);
}
public override bool Equals(object object2)
{
if (!(object2 is Rectangle) || !base.Equals(object2)) return false;
Rectangle shape2 = (Rectangle)object2;
return shape2.Width == Width && shape2.Height == Height;
}
public override int GetHashCode()
{
int hashCode = base.GetHashCode();
hashCode = hashCode * 31 + Width.GetHashCode();
hashCode = hashCode * 31 + Height.GetHashCode();
return hashCode;
}
}
حالا باید تعدادی شکل ایجاد کنیم.
در متد Main()
ما (که بهعنوان شرکتکننده Client نیز عمل میکند)، میتوانیم این کار را انجام دهیم، به این صورت که نمونه اولیه (Prototype) را ایجاد کرده و سپس آن را کپی کنیم:
public static void Main(string[] args)
{
List shapes = new List();
List shapesCopy = new List();
Circle circle = new Circle();
circle.x = 10;
circle.y = 20;
circle.Radius = 15;
circle.color = "red";
shapes.Add(circle);
Circle anotherCircle = (Circle)circle.clone();
shapes.Add(anotherCircle);
Rectangle rectangle = new Rectangle();
rectangle.Width = 10;
rectangle.Height = 20;
rectangle.color = "blue";
shapes.Add(rectangle);
cloneAndCompare(shapes, shapesCopy);
}
private static void cloneAndCompare(List shapes, List shapesCopy)
{
shapesCopy.AddRange(shapes.Select(shape => shape.clone()));
for (var i = 0; i < shapes.Count; i++)
{
if (shapes[i] != shapesCopy[i])
{
Console.WriteLine(i + ": Shapes are different objects (Exquisite!)");
if (shapes[i].Equals(shapesCopy[i]))
{
Console.WriteLine(i + ": And they are identical (Charming!)");
}
else
{
Console.WriteLine(i + ": But they are not identical (Obscene!)");
}
}
else
{
Console.WriteLine(i + ": Shape objects are the same (Indecorous!)");
}
}
}
اگر برنامه خود را اجرا کنیم، خروجی زیر را دریافت خواهیم کرد:
پروتوتایپ با استفاده از رجیستری (Prototype Registry)
ما میتوانیم یک سیستم ثبت مرکزی نمونه اولیه (یا کارخانهای) ایجاد کنیم که شامل یک کتابخانه از اشیای نمونه اولیه از پیش تعریفشده باشد. سپس میتوانیم اشیای جدید را از این کارخانه با ارائه نام یا سایر پارامترها دریافت کنیم. سازنده، یک نمونه اولیه مناسب را جستجو کرده، آن را کپی میکند و یک نسخه مشابه برای ما ارسال میکند.
using PrototypeRegistry.Registry;
using PrototypeRegistry.Shapes;
public class Program {
public static void Main(string[] args)
{
BundledShapeRegistry cache = new BundledShapeRegistry();
Shape shape1 = cache.Get("Big green circle");
Shape shape2 = cache.Get("Medium blue rectangle");
Shape shape3 = cache.Get("Medium blue rectangle");
if (shape1 != shape2 && !shape1.Equals(shape2))
{
Console.WriteLine("Big green circle != Medium blue rectangle (yay!)");
}
else
{
Console.WriteLine("Big green circle == Medium blue rectangle (booo!)");
}
if (shape2 != shape3)
{
Console.WriteLine("Medium blue rectangles are two different objects (yay!)");
if (shape2.Equals(shape3))
{
Console.WriteLine("And they are identical (yay!)");
}
else
{
Console.WriteLine("But they are not identical (booo!)");
}
}
else
{
Console.WriteLine("Rectangle objects are the same (booo!)");
}
}
}
اگر برنامه خود را اجرا کنیم، نتیجه زیر را خواهیم دید:
مزایا و معایب الگوی طراحی Prototype
مزایا
- میتوانیم اشیاء را بدون وابستگی به کلاسهای واقعی آنها کپی کنیم.
- میتوانیم از کدهای تکراری برای مقداردهی اولیه اشیاء خلاص شویم و به جای آن از پروتوتایپهای از پیش ساختهشده استفاده کنیم.
- امکان تولید اشیاء پیچیده بهصورت راحتتر وجود دارد.
- این الگو یک جایگزین برای ارثبری هنگام کار با تنظیمات از پیش تعیینشده برای اشیاء پیچیده است.
معایب
- کپی کردن اشیاء پیچیده که رفرنس بازگشتی (Circular References) دارند، میتواند بسیار دشوار باشد.
ارتباط Prototype با سایر الگوهای طراحی
بسیاری از طراحیها با استفاده از الگوی Factory Method آغاز میشوند (که سادهتر و قابل تنظیمتر با استفاده از زیرکلاسها است) و سپس به سمت الگوهای Abstract Factory، Prototype یا Builder پیش میروند (که انعطافپذیرتر اما پیچیدهتر هستند).
کلاسهای Abstract Factory معمولاً بر اساس مجموعهای از Factory Methods ساخته میشوند. با این حال، متدهای این کلاسها میتوانند با استفاده از Prototype نیز پیادهسازی شوند.
اگر نیاز به ذخیره نسخههایی از دستورات (Commands) در تاریخچه دارید، Prototype میتواند مفید باشد.
طراحیهایی که به شدت به الگوهای Composite و Decorator متکی هستند، اغلب از Prototype نیز بهرهمند میشوند. با استفاده از این الگو، میتوانید ساختارهای پیچیده را کپی کنید بهجای اینکه آنها را از ابتدا بازسازی کنید.
از آنجا که Prototype به ارثبری وابسته نیست، معایب مربوط به ارثبری را ندارد. از سوی دیگر، الگوی Prototype نیازمند مقداردهی اولیه پیچیده برای شیء کپیشده است. روش Factory Method که مبتنی بر ارثبری است، نیازی به این مرحله ندارد.
در برخی موارد، Prototype میتواند جایگزین سادهتری برای الگوی Memento باشد. این روش زمانی کارآمد است که شیء موردنظر برای ذخیرهسازی حالت (State) ساده باشد و ارتباطی با منابع دیگر نداشته باشد، یا اگر این ارتباطها بهسادگی قابل بازسازی باشند.
Singletonها ممکن است برای پیادهسازی الگوهای Abstract Factory، Builder و Prototype استفاده شوند.
نتیجهگیری
در این مقاله، الگوی طراحی Prototype را بررسی کردیم، اینکه چه زمانی از آن استفاده کنیم و مزایا و معایب استفاده از این الگو چیست. همچنین موارد استفادهای برای این الگو ارائه کردیم و توضیح دادیم چگونه Prototype با سایر الگوهای طراحی کلاسیک ارتباط دارد.
نکته مهم این است که الگوی Prototype، همانند سایر الگوهای طراحی ارائهشده توسط Gang of Four، یک راهحل جهانی یا بیچونوچرا برای طراحی نرمافزار نیست. در نهایت این وظیفه مهندسان است که تصمیم بگیرند چه زمانی از یک الگوی خاص استفاده کنند. بههرحال، این الگوها زمانی مفید هستند که بهعنوان یک ابزار دقیق استفاده شوند، نه یک ابزار سنگین و بیهدف.